summaryrefslogtreecommitdiff
path: root/extlib
diff options
context:
space:
mode:
Diffstat (limited to 'extlib')
-rw-r--r--extlib/Apache2.0.txt202
-rw-r--r--extlib/Auth/OpenID.php552
-rw-r--r--extlib/Auth/OpenID/AX.php1023
-rw-r--r--extlib/Auth/OpenID/Association.php613
-rw-r--r--extlib/Auth/OpenID/BigMath.php471
-rw-r--r--extlib/Auth/OpenID/Consumer.php2229
-rw-r--r--extlib/Auth/OpenID/CryptUtil.php109
-rw-r--r--extlib/Auth/OpenID/DatabaseConnection.php131
-rw-r--r--extlib/Auth/OpenID/DiffieHellman.php113
-rw-r--r--extlib/Auth/OpenID/Discover.php548
-rw-r--r--extlib/Auth/OpenID/DumbStore.php100
-rw-r--r--extlib/Auth/OpenID/Extension.php62
-rw-r--r--extlib/Auth/OpenID/FileStore.php618
-rw-r--r--extlib/Auth/OpenID/HMAC.php99
-rw-r--r--extlib/Auth/OpenID/Interface.php197
-rw-r--r--extlib/Auth/OpenID/KVForm.php112
-rw-r--r--extlib/Auth/OpenID/MemcachedStore.php208
-rw-r--r--extlib/Auth/OpenID/Message.php915
-rw-r--r--extlib/Auth/OpenID/MySQLStore.php78
-rw-r--r--extlib/Auth/OpenID/Nonce.php109
-rw-r--r--extlib/Auth/OpenID/PAPE.php301
-rw-r--r--extlib/Auth/OpenID/Parse.php352
-rw-r--r--extlib/Auth/OpenID/PostgreSQLStore.php113
-rw-r--r--extlib/Auth/OpenID/SQLStore.php569
-rw-r--r--extlib/Auth/OpenID/SQLiteStore.php71
-rw-r--r--extlib/Auth/OpenID/SReg.php521
-rw-r--r--extlib/Auth/OpenID/Server.php1760
-rw-r--r--extlib/Auth/OpenID/ServerRequest.php37
-rw-r--r--extlib/Auth/OpenID/TrustRoot.php462
-rw-r--r--extlib/Auth/OpenID/URINorm.php249
-rw-r--r--extlib/Auth/Yadis/HTTPFetcher.php147
-rw-r--r--extlib/Auth/Yadis/Manager.php529
-rw-r--r--extlib/Auth/Yadis/Misc.php59
-rw-r--r--extlib/Auth/Yadis/ParanoidHTTPFetcher.php228
-rw-r--r--extlib/Auth/Yadis/ParseHTML.php259
-rw-r--r--extlib/Auth/Yadis/PlainHTTPFetcher.php251
-rw-r--r--extlib/Auth/Yadis/XML.php374
-rw-r--r--extlib/Auth/Yadis/XRDS.php478
-rw-r--r--extlib/Auth/Yadis/XRI.php234
-rw-r--r--extlib/Auth/Yadis/XRIRes.php72
-rw-r--r--extlib/Auth/Yadis/Yadis.php382
-rw-r--r--extlib/DB.php1489
-rw-r--r--extlib/DB/DataObject.php4152
-rw-r--r--extlib/DB/DataObject/Cast.php546
-rw-r--r--extlib/DB/DataObject/Error.php53
-rw-r--r--extlib/DB/DataObject/Generator.php1553
-rwxr-xr-xextlib/DB/DataObject/createTables.php59
-rw-r--r--extlib/DB/common.php2262
-rw-r--r--extlib/DB/dbase.php510
-rw-r--r--extlib/DB/fbsql.php769
-rw-r--r--extlib/DB/ibase.php1082
-rw-r--r--extlib/DB/ifx.php683
-rw-r--r--extlib/DB/msql.php831
-rw-r--r--extlib/DB/mssql.php963
-rw-r--r--extlib/DB/mysql.php1045
-rw-r--r--extlib/DB/mysqli.php1092
-rw-r--r--extlib/DB/oci8.php1156
-rw-r--r--extlib/DB/odbc.php883
-rw-r--r--extlib/DB/pgsql.php1135
-rw-r--r--extlib/DB/sqlite.php960
-rw-r--r--extlib/DB/storage.php506
-rw-r--r--extlib/DB/sybase.php942
-rw-r--r--extlib/Mail.php238
-rw-r--r--extlib/Mail/RFC822.php940
-rw-r--r--extlib/Mail/mail.php143
-rw-r--r--extlib/Mail/mock.php119
-rw-r--r--extlib/Mail/null.php60
-rw-r--r--extlib/Mail/sendmail.php170
-rw-r--r--extlib/Mail/smtp.php407
-rw-r--r--extlib/Mail/smtpmx.php478
-rw-r--r--extlib/Net/SMTP.php1082
-rw-r--r--extlib/Net/Socket.php592
-rw-r--r--extlib/OAuth.php755
-rw-r--r--extlib/OAuth_LICENSE.txt22
-rw-r--r--extlib/PEAR.php1118
-rw-r--r--extlib/PHP_License_2_02.txt75
-rw-r--r--extlib/PHP_License_3.01.txt68
-rw-r--r--extlib/PHP_Markdown_License.text36
-rw-r--r--extlib/Validate.php1051
-rw-r--r--extlib/XMPPHP/BOSH.php188
-rw-r--r--extlib/XMPPHP/Exception.php41
-rw-r--r--extlib/XMPPHP/Log.php119
-rw-r--r--extlib/XMPPHP/Roster.php163
-rw-r--r--extlib/XMPPHP/XMLObj.php158
-rw-r--r--extlib/XMPPHP/XMLStream.php763
-rw-r--r--extlib/XMPPHP/XMPP.php423
-rw-r--r--extlib/XMPPHP/XMPP_Old.php114
-rw-r--r--extlib/facebook/facebook.php499
-rw-r--r--extlib/facebook/facebook_desktop.php104
-rw-r--r--extlib/facebook/facebookapi_php5_restlib.php2632
-rw-r--r--extlib/facebook/jsonwrapper/JSON/JSON.php806
-rw-r--r--extlib/facebook/jsonwrapper/JSON/LICENSE21
-rw-r--r--extlib/facebook/jsonwrapper/jsonwrapper.php6
-rw-r--r--extlib/facebook/jsonwrapper/jsonwrapper_inner.php23
-rw-r--r--extlib/get_temp_dir.php14
-rw-r--r--extlib/gpl-2.0.txt339
-rw-r--r--extlib/markdown.php1710
97 files changed, 54085 insertions, 0 deletions
diff --git a/extlib/Apache2.0.txt b/extlib/Apache2.0.txt
new file mode 100644
index 000000000..d64569567
--- /dev/null
+++ b/extlib/Apache2.0.txt
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/extlib/Auth/OpenID.php b/extlib/Auth/OpenID.php
new file mode 100644
index 000000000..6556b5b01
--- /dev/null
+++ b/extlib/Auth/OpenID.php
@@ -0,0 +1,552 @@
+<?php
+
+/**
+ * This is the PHP OpenID library by JanRain, Inc.
+ *
+ * This module contains core utility functionality used by the
+ * library. See Consumer.php and Server.php for the consumer and
+ * server implementations.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005-2008 Janrain, Inc.
+ * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
+ */
+
+/**
+ * The library version string
+ */
+define('Auth_OpenID_VERSION', '2.1.2');
+
+/**
+ * Require the fetcher code.
+ */
+require_once "Auth/Yadis/PlainHTTPFetcher.php";
+require_once "Auth/Yadis/ParanoidHTTPFetcher.php";
+require_once "Auth/OpenID/BigMath.php";
+require_once "Auth/OpenID/URINorm.php";
+
+/**
+ * Status code returned by the server when the only option is to show
+ * an error page, since we do not have enough information to redirect
+ * back to the consumer. The associated value is an error message that
+ * should be displayed on an HTML error page.
+ *
+ * @see Auth_OpenID_Server
+ */
+define('Auth_OpenID_LOCAL_ERROR', 'local_error');
+
+/**
+ * Status code returned when there is an error to return in key-value
+ * form to the consumer. The caller should return a 400 Bad Request
+ * response with content-type text/plain and the value as the body.
+ *
+ * @see Auth_OpenID_Server
+ */
+define('Auth_OpenID_REMOTE_ERROR', 'remote_error');
+
+/**
+ * Status code returned when there is a key-value form OK response to
+ * the consumer. The value associated with this code is the
+ * response. The caller should return a 200 OK response with
+ * content-type text/plain and the value as the body.
+ *
+ * @see Auth_OpenID_Server
+ */
+define('Auth_OpenID_REMOTE_OK', 'remote_ok');
+
+/**
+ * Status code returned when there is a redirect back to the
+ * consumer. The value is the URL to redirect back to. The caller
+ * should return a 302 Found redirect with a Location: header
+ * containing the URL.
+ *
+ * @see Auth_OpenID_Server
+ */
+define('Auth_OpenID_REDIRECT', 'redirect');
+
+/**
+ * Status code returned when the caller needs to authenticate the
+ * user. The associated value is a {@link Auth_OpenID_ServerRequest}
+ * object that can be used to complete the authentication. If the user
+ * has taken some authentication action, use the retry() method of the
+ * {@link Auth_OpenID_ServerRequest} object to complete the request.
+ *
+ * @see Auth_OpenID_Server
+ */
+define('Auth_OpenID_DO_AUTH', 'do_auth');
+
+/**
+ * Status code returned when there were no OpenID arguments
+ * passed. This code indicates that the caller should return a 200 OK
+ * response and display an HTML page that says that this is an OpenID
+ * server endpoint.
+ *
+ * @see Auth_OpenID_Server
+ */
+define('Auth_OpenID_DO_ABOUT', 'do_about');
+
+/**
+ * Defines for regexes and format checking.
+ */
+define('Auth_OpenID_letters',
+ "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ");
+
+define('Auth_OpenID_digits',
+ "0123456789");
+
+define('Auth_OpenID_punct',
+ "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~");
+
+if (Auth_OpenID_getMathLib() === null) {
+ Auth_OpenID_setNoMathSupport();
+}
+
+/**
+ * The OpenID utility function class.
+ *
+ * @package OpenID
+ * @access private
+ */
+class Auth_OpenID {
+
+ /**
+ * Return true if $thing is an Auth_OpenID_FailureResponse object;
+ * false if not.
+ *
+ * @access private
+ */
+ function isFailure($thing)
+ {
+ return is_a($thing, 'Auth_OpenID_FailureResponse');
+ }
+
+ /**
+ * Gets the query data from the server environment based on the
+ * request method used. If GET was used, this looks at
+ * $_SERVER['QUERY_STRING'] directly. If POST was used, this
+ * fetches data from the special php://input file stream.
+ *
+ * Returns an associative array of the query arguments.
+ *
+ * Skips invalid key/value pairs (i.e. keys with no '=value'
+ * portion).
+ *
+ * Returns an empty array if neither GET nor POST was used, or if
+ * POST was used but php://input cannot be opened.
+ *
+ * @access private
+ */
+ function getQuery($query_str=null)
+ {
+ $data = array();
+
+ if ($query_str !== null) {
+ $data = Auth_OpenID::params_from_string($query_str);
+ } else if (!array_key_exists('REQUEST_METHOD', $_SERVER)) {
+ // Do nothing.
+ } else {
+ // XXX HACK FIXME HORRIBLE.
+ //
+ // POSTing to a URL with query parameters is acceptable, but
+ // we don't have a clean way to distinguish those parameters
+ // when we need to do things like return_to verification
+ // which only want to look at one kind of parameter. We're
+ // going to emulate the behavior of some other environments
+ // by defaulting to GET and overwriting with POST if POST
+ // data is available.
+ $data = Auth_OpenID::params_from_string($_SERVER['QUERY_STRING']);
+
+ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ $str = file_get_contents('php://input');
+
+ if ($str === false) {
+ $post = array();
+ } else {
+ $post = Auth_OpenID::params_from_string($str);
+ }
+
+ $data = array_merge($data, $post);
+ }
+ }
+
+ return $data;
+ }
+
+ function params_from_string($str)
+ {
+ $chunks = explode("&", $str);
+
+ $data = array();
+ foreach ($chunks as $chunk) {
+ $parts = explode("=", $chunk, 2);
+
+ if (count($parts) != 2) {
+ continue;
+ }
+
+ list($k, $v) = $parts;
+ $data[$k] = urldecode($v);
+ }
+
+ return $data;
+ }
+
+ /**
+ * Create dir_name as a directory if it does not exist. If it
+ * exists, make sure that it is, in fact, a directory. Returns
+ * true if the operation succeeded; false if not.
+ *
+ * @access private
+ */
+ function ensureDir($dir_name)
+ {
+ if (is_dir($dir_name) || @mkdir($dir_name)) {
+ return true;
+ } else {
+ $parent_dir = dirname($dir_name);
+
+ // Terminal case; there is no parent directory to create.
+ if ($parent_dir == $dir_name) {
+ return true;
+ }
+
+ return (Auth_OpenID::ensureDir($parent_dir) && @mkdir($dir_name));
+ }
+ }
+
+ /**
+ * Adds a string prefix to all values of an array. Returns a new
+ * array containing the prefixed values.
+ *
+ * @access private
+ */
+ function addPrefix($values, $prefix)
+ {
+ $new_values = array();
+ foreach ($values as $s) {
+ $new_values[] = $prefix . $s;
+ }
+ return $new_values;
+ }
+
+ /**
+ * Convenience function for getting array values. Given an array
+ * $arr and a key $key, get the corresponding value from the array
+ * or return $default if the key is absent.
+ *
+ * @access private
+ */
+ function arrayGet($arr, $key, $fallback = null)
+ {
+ if (is_array($arr)) {
+ if (array_key_exists($key, $arr)) {
+ return $arr[$key];
+ } else {
+ return $fallback;
+ }
+ } else {
+ trigger_error("Auth_OpenID::arrayGet (key = ".$key.") expected " .
+ "array as first parameter, got " .
+ gettype($arr), E_USER_WARNING);
+
+ return false;
+ }
+ }
+
+ /**
+ * Replacement for PHP's broken parse_str.
+ */
+ function parse_str($query)
+ {
+ if ($query === null) {
+ return null;
+ }
+
+ $parts = explode('&', $query);
+
+ $new_parts = array();
+ for ($i = 0; $i < count($parts); $i++) {
+ $pair = explode('=', $parts[$i]);
+
+ if (count($pair) != 2) {
+ continue;
+ }
+
+ list($key, $value) = $pair;
+ $new_parts[$key] = urldecode($value);
+ }
+
+ return $new_parts;
+ }
+
+ /**
+ * Implements the PHP 5 'http_build_query' functionality.
+ *
+ * @access private
+ * @param array $data Either an array key/value pairs or an array
+ * of arrays, each of which holding two values: a key and a value,
+ * sequentially.
+ * @return string $result The result of url-encoding the key/value
+ * pairs from $data into a URL query string
+ * (e.g. "username=bob&id=56").
+ */
+ function httpBuildQuery($data)
+ {
+ $pairs = array();
+ foreach ($data as $key => $value) {
+ if (is_array($value)) {
+ $pairs[] = urlencode($value[0])."=".urlencode($value[1]);
+ } else {
+ $pairs[] = urlencode($key)."=".urlencode($value);
+ }
+ }
+ return implode("&", $pairs);
+ }
+
+ /**
+ * "Appends" query arguments onto a URL. The URL may or may not
+ * already have arguments (following a question mark).
+ *
+ * @access private
+ * @param string $url A URL, which may or may not already have
+ * arguments.
+ * @param array $args Either an array key/value pairs or an array of
+ * arrays, each of which holding two values: a key and a value,
+ * sequentially. If $args is an ordinary key/value array, the
+ * parameters will be added to the URL in sorted alphabetical order;
+ * if $args is an array of arrays, their order will be preserved.
+ * @return string $url The original URL with the new parameters added.
+ *
+ */
+ function appendArgs($url, $args)
+ {
+ if (count($args) == 0) {
+ return $url;
+ }
+
+ // Non-empty array; if it is an array of arrays, use
+ // multisort; otherwise use sort.
+ if (array_key_exists(0, $args) &&
+ is_array($args[0])) {
+ // Do nothing here.
+ } else {
+ $keys = array_keys($args);
+ sort($keys);
+ $new_args = array();
+ foreach ($keys as $key) {
+ $new_args[] = array($key, $args[$key]);
+ }
+ $args = $new_args;
+ }
+
+ $sep = '?';
+ if (strpos($url, '?') !== false) {
+ $sep = '&';
+ }
+
+ return $url . $sep . Auth_OpenID::httpBuildQuery($args);
+ }
+
+ /**
+ * Implements python's urlunparse, which is not available in PHP.
+ * Given the specified components of a URL, this function rebuilds
+ * and returns the URL.
+ *
+ * @access private
+ * @param string $scheme The scheme (e.g. 'http'). Defaults to 'http'.
+ * @param string $host The host. Required.
+ * @param string $port The port.
+ * @param string $path The path.
+ * @param string $query The query.
+ * @param string $fragment The fragment.
+ * @return string $url The URL resulting from assembling the
+ * specified components.
+ */
+ function urlunparse($scheme, $host, $port = null, $path = '/',
+ $query = '', $fragment = '')
+ {
+
+ if (!$scheme) {
+ $scheme = 'http';
+ }
+
+ if (!$host) {
+ return false;
+ }
+
+ if (!$path) {
+ $path = '';
+ }
+
+ $result = $scheme . "://" . $host;
+
+ if ($port) {
+ $result .= ":" . $port;
+ }
+
+ $result .= $path;
+
+ if ($query) {
+ $result .= "?" . $query;
+ }
+
+ if ($fragment) {
+ $result .= "#" . $fragment;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Given a URL, this "normalizes" it by adding a trailing slash
+ * and / or a leading http:// scheme where necessary. Returns
+ * null if the original URL is malformed and cannot be normalized.
+ *
+ * @access private
+ * @param string $url The URL to be normalized.
+ * @return mixed $new_url The URL after normalization, or null if
+ * $url was malformed.
+ */
+ function normalizeUrl($url)
+ {
+ @$parsed = parse_url($url);
+
+ if (!$parsed) {
+ return null;
+ }
+
+ if (isset($parsed['scheme']) &&
+ isset($parsed['host'])) {
+ $scheme = strtolower($parsed['scheme']);
+ if (!in_array($scheme, array('http', 'https'))) {
+ return null;
+ }
+ } else {
+ $url = 'http://' . $url;
+ }
+
+ $normalized = Auth_OpenID_urinorm($url);
+ if ($normalized === null) {
+ return null;
+ }
+ list($defragged, $frag) = Auth_OpenID::urldefrag($normalized);
+ return $defragged;
+ }
+
+ /**
+ * Replacement (wrapper) for PHP's intval() because it's broken.
+ *
+ * @access private
+ */
+ function intval($value)
+ {
+ $re = "/^\\d+$/";
+
+ if (!preg_match($re, $value)) {
+ return false;
+ }
+
+ return intval($value);
+ }
+
+ /**
+ * Count the number of bytes in a string independently of
+ * multibyte support conditions.
+ *
+ * @param string $str The string of bytes to count.
+ * @return int The number of bytes in $str.
+ */
+ function bytes($str)
+ {
+ return strlen(bin2hex($str)) / 2;
+ }
+
+ /**
+ * Get the bytes in a string independently of multibyte support
+ * conditions.
+ */
+ function toBytes($str)
+ {
+ $hex = bin2hex($str);
+
+ if (!$hex) {
+ return array();
+ }
+
+ $b = array();
+ for ($i = 0; $i < strlen($hex); $i += 2) {
+ $b[] = chr(base_convert(substr($hex, $i, 2), 16, 10));
+ }
+
+ return $b;
+ }
+
+ function urldefrag($url)
+ {
+ $parts = explode("#", $url, 2);
+
+ if (count($parts) == 1) {
+ return array($parts[0], "");
+ } else {
+ return $parts;
+ }
+ }
+
+ function filter($callback, &$sequence)
+ {
+ $result = array();
+
+ foreach ($sequence as $item) {
+ if (call_user_func_array($callback, array($item))) {
+ $result[] = $item;
+ }
+ }
+
+ return $result;
+ }
+
+ function update(&$dest, &$src)
+ {
+ foreach ($src as $k => $v) {
+ $dest[$k] = $v;
+ }
+ }
+
+ /**
+ * Wrap PHP's standard error_log functionality. Use this to
+ * perform all logging. It will interpolate any additional
+ * arguments into the format string before logging.
+ *
+ * @param string $format_string The sprintf format for the message
+ */
+ function log($format_string)
+ {
+ $args = func_get_args();
+ $message = call_user_func_array('sprintf', $args);
+ error_log($message);
+ }
+
+ function autoSubmitHTML($form, $title="OpenId transaction in progress")
+ {
+ return("<html>".
+ "<head><title>".
+ $title .
+ "</title></head>".
+ "<body onload='document.forms[0].submit();'>".
+ $form .
+ "<script>".
+ "var elements = document.forms[0].elements;".
+ "for (var i = 0; i < elements.length; i++) {".
+ " elements[i].style.display = \"none\";".
+ "}".
+ "</script>".
+ "</body>".
+ "</html>");
+ }
+}
+?>
diff --git a/extlib/Auth/OpenID/AX.php b/extlib/Auth/OpenID/AX.php
new file mode 100644
index 000000000..4a617ae30
--- /dev/null
+++ b/extlib/Auth/OpenID/AX.php
@@ -0,0 +1,1023 @@
+<?php
+
+/**
+ * Implements the OpenID attribute exchange specification, version 1.0
+ * as of svn revision 370 from openid.net svn.
+ *
+ * @package OpenID
+ */
+
+/**
+ * Require utility classes and functions for the consumer.
+ */
+require_once "Auth/OpenID/Extension.php";
+require_once "Auth/OpenID/Message.php";
+require_once "Auth/OpenID/TrustRoot.php";
+
+define('Auth_OpenID_AX_NS_URI',
+ 'http://openid.net/srv/ax/1.0');
+
+// Use this as the 'count' value for an attribute in a FetchRequest to
+// ask for as many values as the OP can provide.
+define('Auth_OpenID_AX_UNLIMITED_VALUES', 'unlimited');
+
+// Minimum supported alias length in characters. Here for
+// completeness.
+define('Auth_OpenID_AX_MINIMUM_SUPPORTED_ALIAS_LENGTH', 32);
+
+/**
+ * AX utility class.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_AX {
+ /**
+ * @param mixed $thing Any object which may be an
+ * Auth_OpenID_AX_Error object.
+ *
+ * @return bool true if $thing is an Auth_OpenID_AX_Error; false
+ * if not.
+ */
+ function isError($thing)
+ {
+ return is_a($thing, 'Auth_OpenID_AX_Error');
+ }
+}
+
+/**
+ * Check an alias for invalid characters; raise AXError if any are
+ * found. Return None if the alias is valid.
+ */
+function Auth_OpenID_AX_checkAlias($alias)
+{
+ if (strpos($alias, ',') !== false) {
+ return new Auth_OpenID_AX_Error(sprintf(
+ "Alias %s must not contain comma", $alias));
+ }
+ if (strpos($alias, '.') !== false) {
+ return new Auth_OpenID_AX_Error(sprintf(
+ "Alias %s must not contain period", $alias));
+ }
+
+ return true;
+}
+
+/**
+ * Results from data that does not meet the attribute exchange 1.0
+ * specification
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_AX_Error {
+ function Auth_OpenID_AX_Error($message=null)
+ {
+ $this->message = $message;
+ }
+}
+
+/**
+ * Abstract class containing common code for attribute exchange
+ * messages.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_AX_Message extends Auth_OpenID_Extension {
+ /**
+ * ns_alias: The preferred namespace alias for attribute exchange
+ * messages
+ */
+ var $ns_alias = 'ax';
+
+ /**
+ * mode: The type of this attribute exchange message. This must be
+ * overridden in subclasses.
+ */
+ var $mode = null;
+
+ var $ns_uri = Auth_OpenID_AX_NS_URI;
+
+ /**
+ * Return Auth_OpenID_AX_Error if the mode in the attribute
+ * exchange arguments does not match what is expected for this
+ * class; true otherwise.
+ *
+ * @access private
+ */
+ function _checkMode($ax_args)
+ {
+ $mode = Auth_OpenID::arrayGet($ax_args, 'mode');
+ if ($mode != $this->mode) {
+ return new Auth_OpenID_AX_Error(
+ sprintf(
+ "Expected mode '%s'; got '%s'",
+ $this->mode, $mode));
+ }
+
+ return true;
+ }
+
+ /**
+ * Return a set of attribute exchange arguments containing the
+ * basic information that must be in every attribute exchange
+ * message.
+ *
+ * @access private
+ */
+ function _newArgs()
+ {
+ return array('mode' => $this->mode);
+ }
+}
+
+/**
+ * Represents a single attribute in an attribute exchange
+ * request. This should be added to an AXRequest object in order to
+ * request the attribute.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_AX_AttrInfo {
+ /**
+ * Construct an attribute information object. Do not call this
+ * directly; call make(...) instead.
+ *
+ * @param string $type_uri The type URI for this attribute.
+ *
+ * @param int $count The number of values of this type to request.
+ *
+ * @param bool $required Whether the attribute will be marked as
+ * required in the request.
+ *
+ * @param string $alias The name that should be given to this
+ * attribute in the request.
+ */
+ function Auth_OpenID_AX_AttrInfo($type_uri, $count, $required,
+ $alias)
+ {
+ /**
+ * required: Whether the attribute will be marked as required
+ * when presented to the subject of the attribute exchange
+ * request.
+ */
+ $this->required = $required;
+
+ /**
+ * count: How many values of this type to request from the
+ * subject. Defaults to one.
+ */
+ $this->count = $count;
+
+ /**
+ * type_uri: The identifier that determines what the attribute
+ * represents and how it is serialized. For example, one type
+ * URI representing dates could represent a Unix timestamp in
+ * base 10 and another could represent a human-readable
+ * string.
+ */
+ $this->type_uri = $type_uri;
+
+ /**
+ * alias: The name that should be given to this attribute in
+ * the request. If it is not supplied, a generic name will be
+ * assigned. For example, if you want to call a Unix timestamp
+ * value 'tstamp', set its alias to that value. If two
+ * attributes in the same message request to use the same
+ * alias, the request will fail to be generated.
+ */
+ $this->alias = $alias;
+ }
+
+ /**
+ * Construct an attribute information object. For parameter
+ * details, see the constructor.
+ */
+ function make($type_uri, $count=1, $required=false,
+ $alias=null)
+ {
+ if ($alias !== null) {
+ $result = Auth_OpenID_AX_checkAlias($alias);
+
+ if (Auth_OpenID_AX::isError($result)) {
+ return $result;
+ }
+ }
+
+ return new Auth_OpenID_AX_AttrInfo($type_uri, $count, $required,
+ $alias);
+ }
+
+ /**
+ * When processing a request for this attribute, the OP should
+ * call this method to determine whether all available attribute
+ * values were requested. If self.count == UNLIMITED_VALUES, this
+ * returns True. Otherwise this returns False, in which case
+ * self.count is an integer.
+ */
+ function wantsUnlimitedValues()
+ {
+ return $this->count === Auth_OpenID_AX_UNLIMITED_VALUES;
+ }
+}
+
+/**
+ * Given a namespace mapping and a string containing a comma-separated
+ * list of namespace aliases, return a list of type URIs that
+ * correspond to those aliases.
+ *
+ * @param $namespace_map The mapping from namespace URI to alias
+ * @param $alias_list_s The string containing the comma-separated
+ * list of aliases. May also be None for convenience.
+ *
+ * @return $seq The list of namespace URIs that corresponds to the
+ * supplied list of aliases. If the string was zero-length or None, an
+ * empty list will be returned.
+ *
+ * return null If an alias is present in the list of aliases but
+ * is not present in the namespace map.
+ */
+function Auth_OpenID_AX_toTypeURIs(&$namespace_map, $alias_list_s)
+{
+ $uris = array();
+
+ if ($alias_list_s) {
+ foreach (explode(',', $alias_list_s) as $alias) {
+ $type_uri = $namespace_map->getNamespaceURI($alias);
+ if ($type_uri === null) {
+ // raise KeyError(
+ // 'No type is defined for attribute name %r' % (alias,))
+ return new Auth_OpenID_AX_Error(
+ sprintf('No type is defined for attribute name %s',
+ $alias)
+ );
+ } else {
+ $uris[] = $type_uri;
+ }
+ }
+ }
+
+ return $uris;
+}
+
+/**
+ * An attribute exchange 'fetch_request' message. This message is sent
+ * by a relying party when it wishes to obtain attributes about the
+ * subject of an OpenID authentication request.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_AX_FetchRequest extends Auth_OpenID_AX_Message {
+
+ var $mode = 'fetch_request';
+
+ function Auth_OpenID_AX_FetchRequest($update_url=null)
+ {
+ /**
+ * requested_attributes: The attributes that have been
+ * requested thus far, indexed by the type URI.
+ */
+ $this->requested_attributes = array();
+
+ /**
+ * update_url: A URL that will accept responses for this
+ * attribute exchange request, even in the absence of the user
+ * who made this request.
+ */
+ $this->update_url = $update_url;
+ }
+
+ /**
+ * Add an attribute to this attribute exchange request.
+ *
+ * @param attribute: The attribute that is being requested
+ * @return true on success, false when the requested attribute is
+ * already present in this fetch request.
+ */
+ function add($attribute)
+ {
+ if ($this->contains($attribute->type_uri)) {
+ return new Auth_OpenID_AX_Error(
+ sprintf("The attribute %s has already been requested",
+ $attribute->type_uri));
+ }
+
+ $this->requested_attributes[$attribute->type_uri] = $attribute;
+
+ return true;
+ }
+
+ /**
+ * Get the serialized form of this attribute fetch request.
+ *
+ * @returns Auth_OpenID_AX_FetchRequest The fetch request message parameters
+ */
+ function getExtensionArgs()
+ {
+ $aliases = new Auth_OpenID_NamespaceMap();
+
+ $required = array();
+ $if_available = array();
+
+ $ax_args = $this->_newArgs();
+
+ foreach ($this->requested_attributes as $type_uri => $attribute) {
+ if ($attribute->alias === null) {
+ $alias = $aliases->add($type_uri);
+ } else {
+ $alias = $aliases->addAlias($type_uri, $attribute->alias);
+
+ if ($alias === null) {
+ return new Auth_OpenID_AX_Error(
+ sprintf("Could not add alias %s for URI %s",
+ $attribute->alias, $type_uri
+ ));
+ }
+ }
+
+ if ($attribute->required) {
+ $required[] = $alias;
+ } else {
+ $if_available[] = $alias;
+ }
+
+ if ($attribute->count != 1) {
+ $ax_args['count.' . $alias] = strval($attribute->count);
+ }
+
+ $ax_args['type.' . $alias] = $type_uri;
+ }
+
+ if ($required) {
+ $ax_args['required'] = implode(',', $required);
+ }
+
+ if ($if_available) {
+ $ax_args['if_available'] = implode(',', $if_available);
+ }
+
+ return $ax_args;
+ }
+
+ /**
+ * Get the type URIs for all attributes that have been marked as
+ * required.
+ *
+ * @return A list of the type URIs for attributes that have been
+ * marked as required.
+ */
+ function getRequiredAttrs()
+ {
+ $required = array();
+ foreach ($this->requested_attributes as $type_uri => $attribute) {
+ if ($attribute->required) {
+ $required[] = $type_uri;
+ }
+ }
+
+ return $required;
+ }
+
+ /**
+ * Extract a FetchRequest from an OpenID message
+ *
+ * @param request: The OpenID request containing the attribute
+ * fetch request
+ *
+ * @returns mixed An Auth_OpenID_AX_Error or the
+ * Auth_OpenID_AX_FetchRequest extracted from the request message if
+ * successful
+ */
+ function &fromOpenIDRequest($request)
+ {
+ $m = $request->message;
+ $obj = new Auth_OpenID_AX_FetchRequest();
+ $ax_args = $m->getArgs($obj->ns_uri);
+
+ $result = $obj->parseExtensionArgs($ax_args);
+
+ if (Auth_OpenID_AX::isError($result)) {
+ return $result;
+ }
+
+ if ($obj->update_url) {
+ // Update URL must match the openid.realm of the
+ // underlying OpenID 2 message.
+ $realm = $m->getArg(Auth_OpenID_OPENID_NS, 'realm',
+ $m->getArg(
+ Auth_OpenID_OPENID_NS,
+ 'return_to'));
+
+ if (!$realm) {
+ $obj = new Auth_OpenID_AX_Error(
+ sprintf("Cannot validate update_url %s " .
+ "against absent realm", $obj->update_url));
+ } else if (!Auth_OpenID_TrustRoot::match($realm,
+ $obj->update_url)) {
+ $obj = new Auth_OpenID_AX_Error(
+ sprintf("Update URL %s failed validation against realm %s",
+ $obj->update_url, $realm));
+ }
+ }
+
+ return $obj;
+ }
+
+ /**
+ * Given attribute exchange arguments, populate this FetchRequest.
+ *
+ * @return $result Auth_OpenID_AX_Error if the data to be parsed
+ * does not follow the attribute exchange specification. At least
+ * when 'if_available' or 'required' is not specified for a
+ * particular attribute type. Returns true otherwise.
+ */
+ function parseExtensionArgs($ax_args)
+ {
+ $result = $this->_checkMode($ax_args);
+ if (Auth_OpenID_AX::isError($result)) {
+ return $result;
+ }
+
+ $aliases = new Auth_OpenID_NamespaceMap();
+
+ foreach ($ax_args as $key => $value) {
+ if (strpos($key, 'type.') === 0) {
+ $alias = substr($key, 5);
+ $type_uri = $value;
+
+ $alias = $aliases->addAlias($type_uri, $alias);
+
+ if ($alias === null) {
+ return new Auth_OpenID_AX_Error(
+ sprintf("Could not add alias %s for URI %s",
+ $alias, $type_uri)
+ );
+ }
+
+ $count_s = Auth_OpenID::arrayGet($ax_args, 'count.' . $alias);
+ if ($count_s) {
+ $count = Auth_OpenID::intval($count_s);
+ if (($count === false) &&
+ ($count_s === Auth_OpenID_AX_UNLIMITED_VALUES)) {
+ $count = $count_s;
+ }
+ } else {
+ $count = 1;
+ }
+
+ if ($count === false) {
+ return new Auth_OpenID_AX_Error(
+ sprintf("Integer value expected for %s, got %s",
+ 'count.' . $alias, $count_s));
+ }
+
+ $attrinfo = Auth_OpenID_AX_AttrInfo::make($type_uri, $count,
+ false, $alias);
+
+ if (Auth_OpenID_AX::isError($attrinfo)) {
+ return $attrinfo;
+ }
+
+ $this->add($attrinfo);
+ }
+ }
+
+ $required = Auth_OpenID_AX_toTypeURIs($aliases,
+ Auth_OpenID::arrayGet($ax_args, 'required'));
+
+ foreach ($required as $type_uri) {
+ $attrib =& $this->requested_attributes[$type_uri];
+ $attrib->required = true;
+ }
+
+ $if_available = Auth_OpenID_AX_toTypeURIs($aliases,
+ Auth_OpenID::arrayGet($ax_args, 'if_available'));
+
+ $all_type_uris = array_merge($required, $if_available);
+
+ foreach ($aliases->iterNamespaceURIs() as $type_uri) {
+ if (!in_array($type_uri, $all_type_uris)) {
+ return new Auth_OpenID_AX_Error(
+ sprintf('Type URI %s was in the request but not ' .
+ 'present in "required" or "if_available"',
+ $type_uri));
+
+ }
+ }
+
+ $this->update_url = Auth_OpenID::arrayGet($ax_args, 'update_url');
+
+ return true;
+ }
+
+ /**
+ * Iterate over the AttrInfo objects that are contained in this
+ * fetch_request.
+ */
+ function iterAttrs()
+ {
+ return array_values($this->requested_attributes);
+ }
+
+ function iterTypes()
+ {
+ return array_keys($this->requested_attributes);
+ }
+
+ /**
+ * Is the given type URI present in this fetch_request?
+ */
+ function contains($type_uri)
+ {
+ return in_array($type_uri, $this->iterTypes());
+ }
+}
+
+/**
+ * An abstract class that implements a message that has attribute keys
+ * and values. It contains the common code between fetch_response and
+ * store_request.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_AX_KeyValueMessage extends Auth_OpenID_AX_Message {
+
+ function Auth_OpenID_AX_KeyValueMessage()
+ {
+ $this->data = array();
+ }
+
+ /**
+ * Add a single value for the given attribute type to the
+ * message. If there are already values specified for this type,
+ * this value will be sent in addition to the values already
+ * specified.
+ *
+ * @param type_uri: The URI for the attribute
+ * @param value: The value to add to the response to the relying
+ * party for this attribute
+ * @return null
+ */
+ function addValue($type_uri, $value)
+ {
+ if (!array_key_exists($type_uri, $this->data)) {
+ $this->data[$type_uri] = array();
+ }
+
+ $values =& $this->data[$type_uri];
+ $values[] = $value;
+ }
+
+ /**
+ * Set the values for the given attribute type. This replaces any
+ * values that have already been set for this attribute.
+ *
+ * @param type_uri: The URI for the attribute
+ * @param values: A list of values to send for this attribute.
+ */
+ function setValues($type_uri, &$values)
+ {
+ $this->data[$type_uri] =& $values;
+ }
+
+ /**
+ * Get the extension arguments for the key/value pairs contained
+ * in this message.
+ *
+ * @param aliases: An alias mapping. Set to None if you don't care
+ * about the aliases for this request.
+ *
+ * @access private
+ */
+ function _getExtensionKVArgs(&$aliases)
+ {
+ if ($aliases === null) {
+ $aliases = new Auth_OpenID_NamespaceMap();
+ }
+
+ $ax_args = array();
+
+ foreach ($this->data as $type_uri => $values) {
+ $alias = $aliases->add($type_uri);
+
+ $ax_args['type.' . $alias] = $type_uri;
+ $ax_args['count.' . $alias] = strval(count($values));
+
+ foreach ($values as $i => $value) {
+ $key = sprintf('value.%s.%d', $alias, $i + 1);
+ $ax_args[$key] = $value;
+ }
+ }
+
+ return $ax_args;
+ }
+
+ /**
+ * Parse attribute exchange key/value arguments into this object.
+ *
+ * @param ax_args: The attribute exchange fetch_response
+ * arguments, with namespacing removed.
+ *
+ * @return Auth_OpenID_AX_Error or true
+ */
+ function parseExtensionArgs($ax_args)
+ {
+ $result = $this->_checkMode($ax_args);
+ if (Auth_OpenID_AX::isError($result)) {
+ return $result;
+ }
+
+ $aliases = new Auth_OpenID_NamespaceMap();
+
+ foreach ($ax_args as $key => $value) {
+ if (strpos($key, 'type.') === 0) {
+ $type_uri = $value;
+ $alias = substr($key, 5);
+
+ $result = Auth_OpenID_AX_checkAlias($alias);
+
+ if (Auth_OpenID_AX::isError($result)) {
+ return $result;
+ }
+
+ $alias = $aliases->addAlias($type_uri, $alias);
+
+ if ($alias === null) {
+ return new Auth_OpenID_AX_Error(
+ sprintf("Could not add alias %s for URI %s",
+ $alias, $type_uri)
+ );
+ }
+ }
+ }
+
+ foreach ($aliases->iteritems() as $pair) {
+ list($type_uri, $alias) = $pair;
+
+ if (array_key_exists('count.' . $alias, $ax_args)) {
+
+ $count_key = 'count.' . $alias;
+ $count_s = $ax_args[$count_key];
+
+ $count = Auth_OpenID::intval($count_s);
+
+ if ($count === false) {
+ return new Auth_OpenID_AX_Error(
+ sprintf("Integer value expected for %s, got %s",
+ 'count. %s' . $alias, $count_s,
+ Auth_OpenID_AX_UNLIMITED_VALUES)
+ );
+ }
+
+ $values = array();
+ for ($i = 1; $i < $count + 1; $i++) {
+ $value_key = sprintf('value.%s.%d', $alias, $i);
+
+ if (!array_key_exists($value_key, $ax_args)) {
+ return new Auth_OpenID_AX_Error(
+ sprintf(
+ "No value found for key %s",
+ $value_key));
+ }
+
+ $value = $ax_args[$value_key];
+ $values[] = $value;
+ }
+ } else {
+ $key = 'value.' . $alias;
+
+ if (!array_key_exists($key, $ax_args)) {
+ return new Auth_OpenID_AX_Error(
+ sprintf(
+ "No value found for key %s",
+ $key));
+ }
+
+ $value = $ax_args['value.' . $alias];
+
+ if ($value == '') {
+ $values = array();
+ } else {
+ $values = array($value);
+ }
+ }
+
+ $this->data[$type_uri] = $values;
+ }
+
+ return true;
+ }
+
+ /**
+ * Get a single value for an attribute. If no value was sent for
+ * this attribute, use the supplied default. If there is more than
+ * one value for this attribute, this method will fail.
+ *
+ * @param type_uri: The URI for the attribute
+ * @param default: The value to return if the attribute was not
+ * sent in the fetch_response.
+ *
+ * @return $value Auth_OpenID_AX_Error on failure or the value of
+ * the attribute in the fetch_response message, or the default
+ * supplied
+ */
+ function getSingle($type_uri, $default=null)
+ {
+ $values = Auth_OpenID::arrayGet($this->data, $type_uri);
+ if (!$values) {
+ return $default;
+ } else if (count($values) == 1) {
+ return $values[0];
+ } else {
+ return new Auth_OpenID_AX_Error(
+ sprintf('More than one value present for %s',
+ $type_uri)
+ );
+ }
+ }
+
+ /**
+ * Get the list of values for this attribute in the
+ * fetch_response.
+ *
+ * XXX: what to do if the values are not present? default
+ * parameter? this is funny because it's always supposed to return
+ * a list, so the default may break that, though it's provided by
+ * the user's code, so it might be okay. If no default is
+ * supplied, should the return be None or []?
+ *
+ * @param type_uri: The URI of the attribute
+ *
+ * @return $values The list of values for this attribute in the
+ * response. May be an empty list. If the attribute was not sent
+ * in the response, returns Auth_OpenID_AX_Error.
+ */
+ function get($type_uri)
+ {
+ if (array_key_exists($type_uri, $this->data)) {
+ return $this->data[$type_uri];
+ } else {
+ return new Auth_OpenID_AX_Error(
+ sprintf("Type URI %s not found in response",
+ $type_uri)
+ );
+ }
+ }
+
+ /**
+ * Get the number of responses for a particular attribute in this
+ * fetch_response message.
+ *
+ * @param type_uri: The URI of the attribute
+ *
+ * @returns int The number of values sent for this attribute. If
+ * the attribute was not sent in the response, returns
+ * Auth_OpenID_AX_Error.
+ */
+ function count($type_uri)
+ {
+ if (array_key_exists($type_uri, $this->data)) {
+ return count($this->get($type_uri));
+ } else {
+ return new Auth_OpenID_AX_Error(
+ sprintf("Type URI %s not found in response",
+ $type_uri)
+ );
+ }
+ }
+}
+
+/**
+ * A fetch_response attribute exchange message.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_AX_FetchResponse extends Auth_OpenID_AX_KeyValueMessage {
+ var $mode = 'fetch_response';
+
+ function Auth_OpenID_AX_FetchResponse($update_url=null)
+ {
+ $this->Auth_OpenID_AX_KeyValueMessage();
+ $this->update_url = $update_url;
+ }
+
+ /**
+ * Serialize this object into arguments in the attribute exchange
+ * namespace
+ *
+ * @return $args The dictionary of unqualified attribute exchange
+ * arguments that represent this fetch_response, or
+ * Auth_OpenID_AX_Error on error.
+ */
+ function getExtensionArgs($request=null)
+ {
+ $aliases = new Auth_OpenID_NamespaceMap();
+
+ $zero_value_types = array();
+
+ if ($request !== null) {
+ // Validate the data in the context of the request (the
+ // same attributes should be present in each, and the
+ // counts in the response must be no more than the counts
+ // in the request)
+
+ foreach ($this->data as $type_uri => $unused) {
+ if (!$request->contains($type_uri)) {
+ return new Auth_OpenID_AX_Error(
+ sprintf("Response attribute not present in request: %s",
+ $type_uri)
+ );
+ }
+ }
+
+ foreach ($request->iterAttrs() as $attr_info) {
+ // Copy the aliases from the request so that reading
+ // the response in light of the request is easier
+ if ($attr_info->alias === null) {
+ $aliases->add($attr_info->type_uri);
+ } else {
+ $alias = $aliases->addAlias($attr_info->type_uri,
+ $attr_info->alias);
+
+ if ($alias === null) {
+ return new Auth_OpenID_AX_Error(
+ sprintf("Could not add alias %s for URI %s",
+ $attr_info->alias, $attr_info->type_uri)
+ );
+ }
+ }
+
+ if (array_key_exists($attr_info->type_uri, $this->data)) {
+ $values = $this->data[$attr_info->type_uri];
+ } else {
+ $values = array();
+ $zero_value_types[] = $attr_info;
+ }
+
+ if (($attr_info->count != Auth_OpenID_AX_UNLIMITED_VALUES) &&
+ ($attr_info->count < count($values))) {
+ return new Auth_OpenID_AX_Error(
+ sprintf("More than the number of requested values " .
+ "were specified for %s",
+ $attr_info->type_uri)
+ );
+ }
+ }
+ }
+
+ $kv_args = $this->_getExtensionKVArgs($aliases);
+
+ // Add the KV args into the response with the args that are
+ // unique to the fetch_response
+ $ax_args = $this->_newArgs();
+
+ // For each requested attribute, put its type/alias and count
+ // into the response even if no data were returned.
+ foreach ($zero_value_types as $attr_info) {
+ $alias = $aliases->getAlias($attr_info->type_uri);
+ $kv_args['type.' . $alias] = $attr_info->type_uri;
+ $kv_args['count.' . $alias] = '0';
+ }
+
+ $update_url = null;
+ if ($request) {
+ $update_url = $request->update_url;
+ } else {
+ $update_url = $this->update_url;
+ }
+
+ if ($update_url) {
+ $ax_args['update_url'] = $update_url;
+ }
+
+ Auth_OpenID::update(&$ax_args, $kv_args);
+
+ return $ax_args;
+ }
+
+ /**
+ * @return $result Auth_OpenID_AX_Error on failure or true on
+ * success.
+ */
+ function parseExtensionArgs($ax_args)
+ {
+ $result = parent::parseExtensionArgs($ax_args);
+
+ if (Auth_OpenID_AX::isError($result)) {
+ return $result;
+ }
+
+ $this->update_url = Auth_OpenID::arrayGet($ax_args, 'update_url');
+
+ return true;
+ }
+
+ /**
+ * Construct a FetchResponse object from an OpenID library
+ * SuccessResponse object.
+ *
+ * @param success_response: A successful id_res response object
+ *
+ * @param signed: Whether non-signed args should be processsed. If
+ * True (the default), only signed arguments will be processsed.
+ *
+ * @return $response A FetchResponse containing the data from the
+ * OpenID message
+ */
+ function fromSuccessResponse($success_response, $signed=true)
+ {
+ $obj = new Auth_OpenID_AX_FetchResponse();
+ if ($signed) {
+ $ax_args = $success_response->getSignedNS($obj->ns_uri);
+ } else {
+ $ax_args = $success_response->message->getArgs($obj->ns_uri);
+ }
+ if ($ax_args === null || Auth_OpenID::isFailure($ax_args) ||
+ sizeof($ax_args) == 0) {
+ return null;
+ }
+
+ $result = $obj->parseExtensionArgs($ax_args);
+ if (Auth_OpenID_AX::isError($result)) {
+ #XXX log me
+ return null;
+ }
+ return $obj;
+ }
+}
+
+/**
+ * A store request attribute exchange message representation.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_AX_StoreRequest extends Auth_OpenID_AX_KeyValueMessage {
+ var $mode = 'store_request';
+
+ /**
+ * @param array $aliases The namespace aliases to use when making
+ * this store response. Leave as None to use defaults.
+ */
+ function getExtensionArgs($aliases=null)
+ {
+ $ax_args = $this->_newArgs();
+ $kv_args = $this->_getExtensionKVArgs($aliases);
+ Auth_OpenID::update(&$ax_args, $kv_args);
+ return $ax_args;
+ }
+}
+
+/**
+ * An indication that the store request was processed along with this
+ * OpenID transaction. Use make(), NOT the constructor, to create
+ * response objects.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_AX_StoreResponse extends Auth_OpenID_AX_Message {
+ var $SUCCESS_MODE = 'store_response_success';
+ var $FAILURE_MODE = 'store_response_failure';
+
+ /**
+ * Returns Auth_OpenID_AX_Error on error or an
+ * Auth_OpenID_AX_StoreResponse object on success.
+ */
+ function &make($succeeded=true, $error_message=null)
+ {
+ if (($succeeded) && ($error_message !== null)) {
+ return new Auth_OpenID_AX_Error('An error message may only be '.
+ 'included in a failing fetch response');
+ }
+
+ return new Auth_OpenID_AX_StoreResponse($succeeded, $error_message);
+ }
+
+ function Auth_OpenID_AX_StoreResponse($succeeded=true, $error_message=null)
+ {
+ if ($succeeded) {
+ $this->mode = $this->SUCCESS_MODE;
+ } else {
+ $this->mode = $this->FAILURE_MODE;
+ }
+
+ $this->error_message = $error_message;
+ }
+
+ /**
+ * Was this response a success response?
+ */
+ function succeeded()
+ {
+ return $this->mode == $this->SUCCESS_MODE;
+ }
+
+ function getExtensionArgs()
+ {
+ $ax_args = $this->_newArgs();
+ if ((!$this->succeeded()) && $this->error_message) {
+ $ax_args['error'] = $this->error_message;
+ }
+
+ return $ax_args;
+ }
+}
+
+?> \ No newline at end of file
diff --git a/extlib/Auth/OpenID/Association.php b/extlib/Auth/OpenID/Association.php
new file mode 100644
index 000000000..37ce0cbf4
--- /dev/null
+++ b/extlib/Auth/OpenID/Association.php
@@ -0,0 +1,613 @@
+<?php
+
+/**
+ * This module contains code for dealing with associations between
+ * consumers and servers.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005-2008 Janrain, Inc.
+ * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
+ */
+
+/**
+ * @access private
+ */
+require_once 'Auth/OpenID/CryptUtil.php';
+
+/**
+ * @access private
+ */
+require_once 'Auth/OpenID/KVForm.php';
+
+/**
+ * @access private
+ */
+require_once 'Auth/OpenID/HMAC.php';
+
+/**
+ * This class represents an association between a server and a
+ * consumer. In general, users of this library will never see
+ * instances of this object. The only exception is if you implement a
+ * custom {@link Auth_OpenID_OpenIDStore}.
+ *
+ * If you do implement such a store, it will need to store the values
+ * of the handle, secret, issued, lifetime, and assoc_type instance
+ * variables.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_Association {
+
+ /**
+ * This is a HMAC-SHA1 specific value.
+ *
+ * @access private
+ */
+ var $SIG_LENGTH = 20;
+
+ /**
+ * The ordering and name of keys as stored by serialize.
+ *
+ * @access private
+ */
+ var $assoc_keys = array(
+ 'version',
+ 'handle',
+ 'secret',
+ 'issued',
+ 'lifetime',
+ 'assoc_type'
+ );
+
+ var $_macs = array(
+ 'HMAC-SHA1' => 'Auth_OpenID_HMACSHA1',
+ 'HMAC-SHA256' => 'Auth_OpenID_HMACSHA256'
+ );
+
+ /**
+ * This is an alternate constructor (factory method) used by the
+ * OpenID consumer library to create associations. OpenID store
+ * implementations shouldn't use this constructor.
+ *
+ * @access private
+ *
+ * @param integer $expires_in This is the amount of time this
+ * association is good for, measured in seconds since the
+ * association was issued.
+ *
+ * @param string $handle This is the handle the server gave this
+ * association.
+ *
+ * @param string secret This is the shared secret the server
+ * generated for this association.
+ *
+ * @param assoc_type This is the type of association this
+ * instance represents. The only valid values of this field at
+ * this time is 'HMAC-SHA1' and 'HMAC-SHA256', but new types may
+ * be defined in the future.
+ *
+ * @return association An {@link Auth_OpenID_Association}
+ * instance.
+ */
+ function fromExpiresIn($expires_in, $handle, $secret, $assoc_type)
+ {
+ $issued = time();
+ $lifetime = $expires_in;
+ return new Auth_OpenID_Association($handle, $secret,
+ $issued, $lifetime, $assoc_type);
+ }
+
+ /**
+ * This is the standard constructor for creating an association.
+ * The library should create all of the necessary associations, so
+ * this constructor is not part of the external API.
+ *
+ * @access private
+ *
+ * @param string $handle This is the handle the server gave this
+ * association.
+ *
+ * @param string $secret This is the shared secret the server
+ * generated for this association.
+ *
+ * @param integer $issued This is the time this association was
+ * issued, in seconds since 00:00 GMT, January 1, 1970. (ie, a
+ * unix timestamp)
+ *
+ * @param integer $lifetime This is the amount of time this
+ * association is good for, measured in seconds since the
+ * association was issued.
+ *
+ * @param string $assoc_type This is the type of association this
+ * instance represents. The only valid values of this field at
+ * this time is 'HMAC-SHA1' and 'HMAC-SHA256', but new types may
+ * be defined in the future.
+ */
+ function Auth_OpenID_Association(
+ $handle, $secret, $issued, $lifetime, $assoc_type)
+ {
+ if (!in_array($assoc_type,
+ Auth_OpenID_getSupportedAssociationTypes())) {
+ $fmt = 'Unsupported association type (%s)';
+ trigger_error(sprintf($fmt, $assoc_type), E_USER_ERROR);
+ }
+
+ $this->handle = $handle;
+ $this->secret = $secret;
+ $this->issued = $issued;
+ $this->lifetime = $lifetime;
+ $this->assoc_type = $assoc_type;
+ }
+
+ /**
+ * This returns the number of seconds this association is still
+ * valid for, or 0 if the association is no longer valid.
+ *
+ * @return integer $seconds The number of seconds this association
+ * is still valid for, or 0 if the association is no longer valid.
+ */
+ function getExpiresIn($now = null)
+ {
+ if ($now == null) {
+ $now = time();
+ }
+
+ return max(0, $this->issued + $this->lifetime - $now);
+ }
+
+ /**
+ * This checks to see if two {@link Auth_OpenID_Association}
+ * instances represent the same association.
+ *
+ * @return bool $result true if the two instances represent the
+ * same association, false otherwise.
+ */
+ function equal($other)
+ {
+ return ((gettype($this) == gettype($other))
+ && ($this->handle == $other->handle)
+ && ($this->secret == $other->secret)
+ && ($this->issued == $other->issued)
+ && ($this->lifetime == $other->lifetime)
+ && ($this->assoc_type == $other->assoc_type));
+ }
+
+ /**
+ * Convert an association to KV form.
+ *
+ * @return string $result String in KV form suitable for
+ * deserialization by deserialize.
+ */
+ function serialize()
+ {
+ $data = array(
+ 'version' => '2',
+ 'handle' => $this->handle,
+ 'secret' => base64_encode($this->secret),
+ 'issued' => strval(intval($this->issued)),
+ 'lifetime' => strval(intval($this->lifetime)),
+ 'assoc_type' => $this->assoc_type
+ );
+
+ assert(array_keys($data) == $this->assoc_keys);
+
+ return Auth_OpenID_KVForm::fromArray($data, $strict = true);
+ }
+
+ /**
+ * Parse an association as stored by serialize(). This is the
+ * inverse of serialize.
+ *
+ * @param string $assoc_s Association as serialized by serialize()
+ * @return Auth_OpenID_Association $result instance of this class
+ */
+ function deserialize($class_name, $assoc_s)
+ {
+ $pairs = Auth_OpenID_KVForm::toArray($assoc_s, $strict = true);
+ $keys = array();
+ $values = array();
+ foreach ($pairs as $key => $value) {
+ if (is_array($value)) {
+ list($key, $value) = $value;
+ }
+ $keys[] = $key;
+ $values[] = $value;
+ }
+
+ $class_vars = get_class_vars($class_name);
+ $class_assoc_keys = $class_vars['assoc_keys'];
+
+ sort($keys);
+ sort($class_assoc_keys);
+
+ if ($keys != $class_assoc_keys) {
+ trigger_error('Unexpected key values: ' . var_export($keys, true),
+ E_USER_WARNING);
+ return null;
+ }
+
+ $version = $pairs['version'];
+ $handle = $pairs['handle'];
+ $secret = $pairs['secret'];
+ $issued = $pairs['issued'];
+ $lifetime = $pairs['lifetime'];
+ $assoc_type = $pairs['assoc_type'];
+
+ if ($version != '2') {
+ trigger_error('Unknown version: ' . $version, E_USER_WARNING);
+ return null;
+ }
+
+ $issued = intval($issued);
+ $lifetime = intval($lifetime);
+ $secret = base64_decode($secret);
+
+ return new $class_name(
+ $handle, $secret, $issued, $lifetime, $assoc_type);
+ }
+
+ /**
+ * Generate a signature for a sequence of (key, value) pairs
+ *
+ * @access private
+ * @param array $pairs The pairs to sign, in order. This is an
+ * array of two-tuples.
+ * @return string $signature The binary signature of this sequence
+ * of pairs
+ */
+ function sign($pairs)
+ {
+ $kv = Auth_OpenID_KVForm::fromArray($pairs);
+
+ /* Invalid association types should be caught at constructor */
+ $callback = $this->_macs[$this->assoc_type];
+
+ return call_user_func_array($callback, array($this->secret, $kv));
+ }
+
+ /**
+ * Generate a signature for some fields in a dictionary
+ *
+ * @access private
+ * @param array $fields The fields to sign, in order; this is an
+ * array of strings.
+ * @param array $data Dictionary of values to sign (an array of
+ * string => string pairs).
+ * @return string $signature The signature, base64 encoded
+ */
+ function signMessage($message)
+ {
+ if ($message->hasKey(Auth_OpenID_OPENID_NS, 'sig') ||
+ $message->hasKey(Auth_OpenID_OPENID_NS, 'signed')) {
+ // Already has a sig
+ return null;
+ }
+
+ $extant_handle = $message->getArg(Auth_OpenID_OPENID_NS,
+ 'assoc_handle');
+
+ if ($extant_handle && ($extant_handle != $this->handle)) {
+ // raise ValueError("Message has a different association handle")
+ return null;
+ }
+
+ $signed_message = $message;
+ $signed_message->setArg(Auth_OpenID_OPENID_NS, 'assoc_handle',
+ $this->handle);
+
+ $message_keys = array_keys($signed_message->toPostArgs());
+ $signed_list = array();
+ $signed_prefix = 'openid.';
+
+ foreach ($message_keys as $k) {
+ if (strpos($k, $signed_prefix) === 0) {
+ $signed_list[] = substr($k, strlen($signed_prefix));
+ }
+ }
+
+ $signed_list[] = 'signed';
+ sort($signed_list);
+
+ $signed_message->setArg(Auth_OpenID_OPENID_NS, 'signed',
+ implode(',', $signed_list));
+ $sig = $this->getMessageSignature($signed_message);
+ $signed_message->setArg(Auth_OpenID_OPENID_NS, 'sig', $sig);
+ return $signed_message;
+ }
+
+ /**
+ * Given a {@link Auth_OpenID_Message}, return the key/value pairs
+ * to be signed according to the signed list in the message. If
+ * the message lacks a signed list, return null.
+ *
+ * @access private
+ */
+ function _makePairs(&$message)
+ {
+ $signed = $message->getArg(Auth_OpenID_OPENID_NS, 'signed');
+ if (!$signed || Auth_OpenID::isFailure($signed)) {
+ // raise ValueError('Message has no signed list: %s' % (message,))
+ return null;
+ }
+
+ $signed_list = explode(',', $signed);
+ $pairs = array();
+ $data = $message->toPostArgs();
+ foreach ($signed_list as $field) {
+ $pairs[] = array($field, Auth_OpenID::arrayGet($data,
+ 'openid.' .
+ $field, ''));
+ }
+ return $pairs;
+ }
+
+ /**
+ * Given an {@link Auth_OpenID_Message}, return the signature for
+ * the signed list in the message.
+ *
+ * @access private
+ */
+ function getMessageSignature(&$message)
+ {
+ $pairs = $this->_makePairs($message);
+ return base64_encode($this->sign($pairs));
+ }
+
+ /**
+ * Confirm that the signature of these fields matches the
+ * signature contained in the data.
+ *
+ * @access private
+ */
+ function checkMessageSignature(&$message)
+ {
+ $sig = $message->getArg(Auth_OpenID_OPENID_NS,
+ 'sig');
+
+ if (!$sig || Auth_OpenID::isFailure($sig)) {
+ return false;
+ }
+
+ $calculated_sig = $this->getMessageSignature($message);
+ return $calculated_sig == $sig;
+ }
+}
+
+function Auth_OpenID_getSecretSize($assoc_type)
+{
+ if ($assoc_type == 'HMAC-SHA1') {
+ return 20;
+ } else if ($assoc_type == 'HMAC-SHA256') {
+ return 32;
+ } else {
+ return null;
+ }
+}
+
+function Auth_OpenID_getAllAssociationTypes()
+{
+ return array('HMAC-SHA1', 'HMAC-SHA256');
+}
+
+function Auth_OpenID_getSupportedAssociationTypes()
+{
+ $a = array('HMAC-SHA1');
+
+ if (Auth_OpenID_HMACSHA256_SUPPORTED) {
+ $a[] = 'HMAC-SHA256';
+ }
+
+ return $a;
+}
+
+function Auth_OpenID_getSessionTypes($assoc_type)
+{
+ $assoc_to_session = array(
+ 'HMAC-SHA1' => array('DH-SHA1', 'no-encryption'));
+
+ if (Auth_OpenID_HMACSHA256_SUPPORTED) {
+ $assoc_to_session['HMAC-SHA256'] =
+ array('DH-SHA256', 'no-encryption');
+ }
+
+ return Auth_OpenID::arrayGet($assoc_to_session, $assoc_type, array());
+}
+
+function Auth_OpenID_checkSessionType($assoc_type, $session_type)
+{
+ if (!in_array($session_type,
+ Auth_OpenID_getSessionTypes($assoc_type))) {
+ return false;
+ }
+
+ return true;
+}
+
+function Auth_OpenID_getDefaultAssociationOrder()
+{
+ $order = array();
+
+ if (!Auth_OpenID_noMathSupport()) {
+ $order[] = array('HMAC-SHA1', 'DH-SHA1');
+
+ if (Auth_OpenID_HMACSHA256_SUPPORTED) {
+ $order[] = array('HMAC-SHA256', 'DH-SHA256');
+ }
+ }
+
+ $order[] = array('HMAC-SHA1', 'no-encryption');
+
+ if (Auth_OpenID_HMACSHA256_SUPPORTED) {
+ $order[] = array('HMAC-SHA256', 'no-encryption');
+ }
+
+ return $order;
+}
+
+function Auth_OpenID_getOnlyEncryptedOrder()
+{
+ $result = array();
+
+ foreach (Auth_OpenID_getDefaultAssociationOrder() as $pair) {
+ list($assoc, $session) = $pair;
+
+ if ($session != 'no-encryption') {
+ if (Auth_OpenID_HMACSHA256_SUPPORTED &&
+ ($assoc == 'HMAC-SHA256')) {
+ $result[] = $pair;
+ } else if ($assoc != 'HMAC-SHA256') {
+ $result[] = $pair;
+ }
+ }
+ }
+
+ return $result;
+}
+
+function &Auth_OpenID_getDefaultNegotiator()
+{
+ $x = new Auth_OpenID_SessionNegotiator(
+ Auth_OpenID_getDefaultAssociationOrder());
+ return $x;
+}
+
+function &Auth_OpenID_getEncryptedNegotiator()
+{
+ $x = new Auth_OpenID_SessionNegotiator(
+ Auth_OpenID_getOnlyEncryptedOrder());
+ return $x;
+}
+
+/**
+ * A session negotiator controls the allowed and preferred association
+ * types and association session types. Both the {@link
+ * Auth_OpenID_Consumer} and {@link Auth_OpenID_Server} use
+ * negotiators when creating associations.
+ *
+ * You can create and use negotiators if you:
+
+ * - Do not want to do Diffie-Hellman key exchange because you use
+ * transport-layer encryption (e.g. SSL)
+ *
+ * - Want to use only SHA-256 associations
+ *
+ * - Do not want to support plain-text associations over a non-secure
+ * channel
+ *
+ * It is up to you to set a policy for what kinds of associations to
+ * accept. By default, the library will make any kind of association
+ * that is allowed in the OpenID 2.0 specification.
+ *
+ * Use of negotiators in the library
+ * =================================
+ *
+ * When a consumer makes an association request, it calls {@link
+ * getAllowedType} to get the preferred association type and
+ * association session type.
+ *
+ * The server gets a request for a particular association/session type
+ * and calls {@link isAllowed} to determine if it should create an
+ * association. If it is supported, negotiation is complete. If it is
+ * not, the server calls {@link getAllowedType} to get an allowed
+ * association type to return to the consumer.
+ *
+ * If the consumer gets an error response indicating that the
+ * requested association/session type is not supported by the server
+ * that contains an assocation/session type to try, it calls {@link
+ * isAllowed} to determine if it should try again with the given
+ * combination of association/session type.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_SessionNegotiator {
+ function Auth_OpenID_SessionNegotiator($allowed_types)
+ {
+ $this->allowed_types = array();
+ $this->setAllowedTypes($allowed_types);
+ }
+
+ /**
+ * Set the allowed association types, checking to make sure each
+ * combination is valid.
+ *
+ * @access private
+ */
+ function setAllowedTypes($allowed_types)
+ {
+ foreach ($allowed_types as $pair) {
+ list($assoc_type, $session_type) = $pair;
+ if (!Auth_OpenID_checkSessionType($assoc_type, $session_type)) {
+ return false;
+ }
+ }
+
+ $this->allowed_types = $allowed_types;
+ return true;
+ }
+
+ /**
+ * Add an association type and session type to the allowed types
+ * list. The assocation/session pairs are tried in the order that
+ * they are added.
+ *
+ * @access private
+ */
+ function addAllowedType($assoc_type, $session_type = null)
+ {
+ if ($this->allowed_types === null) {
+ $this->allowed_types = array();
+ }
+
+ if ($session_type === null) {
+ $available = Auth_OpenID_getSessionTypes($assoc_type);
+
+ if (!$available) {
+ return false;
+ }
+
+ foreach ($available as $session_type) {
+ $this->addAllowedType($assoc_type, $session_type);
+ }
+ } else {
+ if (Auth_OpenID_checkSessionType($assoc_type, $session_type)) {
+ $this->allowed_types[] = array($assoc_type, $session_type);
+ } else {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ // Is this combination of association type and session type allowed?
+ function isAllowed($assoc_type, $session_type)
+ {
+ $assoc_good = in_array(array($assoc_type, $session_type),
+ $this->allowed_types);
+
+ $matches = in_array($session_type,
+ Auth_OpenID_getSessionTypes($assoc_type));
+
+ return ($assoc_good && $matches);
+ }
+
+ /**
+ * Get a pair of assocation type and session type that are
+ * supported.
+ */
+ function getAllowedType()
+ {
+ if (!$this->allowed_types) {
+ return array(null, null);
+ }
+
+ return $this->allowed_types[0];
+ }
+}
+
+?> \ No newline at end of file
diff --git a/extlib/Auth/OpenID/BigMath.php b/extlib/Auth/OpenID/BigMath.php
new file mode 100644
index 000000000..45104947d
--- /dev/null
+++ b/extlib/Auth/OpenID/BigMath.php
@@ -0,0 +1,471 @@
+<?php
+
+/**
+ * BigMath: A math library wrapper that abstracts out the underlying
+ * long integer library.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @access private
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005-2008 Janrain, Inc.
+ * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
+ */
+
+/**
+ * Needed for random number generation
+ */
+require_once 'Auth/OpenID/CryptUtil.php';
+
+/**
+ * Need Auth_OpenID::bytes().
+ */
+require_once 'Auth/OpenID.php';
+
+/**
+ * The superclass of all big-integer math implementations
+ * @access private
+ * @package OpenID
+ */
+class Auth_OpenID_MathLibrary {
+ /**
+ * Given a long integer, returns the number converted to a binary
+ * string. This function accepts long integer values of arbitrary
+ * magnitude and uses the local large-number math library when
+ * available.
+ *
+ * @param integer $long The long number (can be a normal PHP
+ * integer or a number created by one of the available long number
+ * libraries)
+ * @return string $binary The binary version of $long
+ */
+ function longToBinary($long)
+ {
+ $cmp = $this->cmp($long, 0);
+ if ($cmp < 0) {
+ $msg = __FUNCTION__ . " takes only positive integers.";
+ trigger_error($msg, E_USER_ERROR);
+ return null;
+ }
+
+ if ($cmp == 0) {
+ return "\x00";
+ }
+
+ $bytes = array();
+
+ while ($this->cmp($long, 0) > 0) {
+ array_unshift($bytes, $this->mod($long, 256));
+ $long = $this->div($long, pow(2, 8));
+ }
+
+ if ($bytes && ($bytes[0] > 127)) {
+ array_unshift($bytes, 0);
+ }
+
+ $string = '';
+ foreach ($bytes as $byte) {
+ $string .= pack('C', $byte);
+ }
+
+ return $string;
+ }
+
+ /**
+ * Given a binary string, returns the binary string converted to a
+ * long number.
+ *
+ * @param string $binary The binary version of a long number,
+ * probably as a result of calling longToBinary
+ * @return integer $long The long number equivalent of the binary
+ * string $str
+ */
+ function binaryToLong($str)
+ {
+ if ($str === null) {
+ return null;
+ }
+
+ // Use array_merge to return a zero-indexed array instead of a
+ // one-indexed array.
+ $bytes = array_merge(unpack('C*', $str));
+
+ $n = $this->init(0);
+
+ if ($bytes && ($bytes[0] > 127)) {
+ trigger_error("bytesToNum works only for positive integers.",
+ E_USER_WARNING);
+ return null;
+ }
+
+ foreach ($bytes as $byte) {
+ $n = $this->mul($n, pow(2, 8));
+ $n = $this->add($n, $byte);
+ }
+
+ return $n;
+ }
+
+ function base64ToLong($str)
+ {
+ $b64 = base64_decode($str);
+
+ if ($b64 === false) {
+ return false;
+ }
+
+ return $this->binaryToLong($b64);
+ }
+
+ function longToBase64($str)
+ {
+ return base64_encode($this->longToBinary($str));
+ }
+
+ /**
+ * Returns a random number in the specified range. This function
+ * accepts $start, $stop, and $step values of arbitrary magnitude
+ * and will utilize the local large-number math library when
+ * available.
+ *
+ * @param integer $start The start of the range, or the minimum
+ * random number to return
+ * @param integer $stop The end of the range, or the maximum
+ * random number to return
+ * @param integer $step The step size, such that $result - ($step
+ * * N) = $start for some N
+ * @return integer $result The resulting randomly-generated number
+ */
+ function rand($stop)
+ {
+ static $duplicate_cache = array();
+
+ // Used as the key for the duplicate cache
+ $rbytes = $this->longToBinary($stop);
+
+ if (array_key_exists($rbytes, $duplicate_cache)) {
+ list($duplicate, $nbytes) = $duplicate_cache[$rbytes];
+ } else {
+ if ($rbytes[0] == "\x00") {
+ $nbytes = Auth_OpenID::bytes($rbytes) - 1;
+ } else {
+ $nbytes = Auth_OpenID::bytes($rbytes);
+ }
+
+ $mxrand = $this->pow(256, $nbytes);
+
+ // If we get a number less than this, then it is in the
+ // duplicated range.
+ $duplicate = $this->mod($mxrand, $stop);
+
+ if (count($duplicate_cache) > 10) {
+ $duplicate_cache = array();
+ }
+
+ $duplicate_cache[$rbytes] = array($duplicate, $nbytes);
+ }
+
+ do {
+ $bytes = "\x00" . Auth_OpenID_CryptUtil::getBytes($nbytes);
+ $n = $this->binaryToLong($bytes);
+ // Keep looping if this value is in the low duplicated range
+ } while ($this->cmp($n, $duplicate) < 0);
+
+ return $this->mod($n, $stop);
+ }
+}
+
+/**
+ * Exposes BCmath math library functionality.
+ *
+ * {@link Auth_OpenID_BcMathWrapper} wraps the functionality provided
+ * by the BCMath extension.
+ *
+ * @access private
+ * @package OpenID
+ */
+class Auth_OpenID_BcMathWrapper extends Auth_OpenID_MathLibrary{
+ var $type = 'bcmath';
+
+ function add($x, $y)
+ {
+ return bcadd($x, $y);
+ }
+
+ function sub($x, $y)
+ {
+ return bcsub($x, $y);
+ }
+
+ function pow($base, $exponent)
+ {
+ return bcpow($base, $exponent);
+ }
+
+ function cmp($x, $y)
+ {
+ return bccomp($x, $y);
+ }
+
+ function init($number, $base = 10)
+ {
+ return $number;
+ }
+
+ function mod($base, $modulus)
+ {
+ return bcmod($base, $modulus);
+ }
+
+ function mul($x, $y)
+ {
+ return bcmul($x, $y);
+ }
+
+ function div($x, $y)
+ {
+ return bcdiv($x, $y);
+ }
+
+ /**
+ * Same as bcpowmod when bcpowmod is missing
+ *
+ * @access private
+ */
+ function _powmod($base, $exponent, $modulus)
+ {
+ $square = $this->mod($base, $modulus);
+ $result = 1;
+ while($this->cmp($exponent, 0) > 0) {
+ if ($this->mod($exponent, 2)) {
+ $result = $this->mod($this->mul($result, $square), $modulus);
+ }
+ $square = $this->mod($this->mul($square, $square), $modulus);
+ $exponent = $this->div($exponent, 2);
+ }
+ return $result;
+ }
+
+ function powmod($base, $exponent, $modulus)
+ {
+ if (function_exists('bcpowmod')) {
+ return bcpowmod($base, $exponent, $modulus);
+ } else {
+ return $this->_powmod($base, $exponent, $modulus);
+ }
+ }
+
+ function toString($num)
+ {
+ return $num;
+ }
+}
+
+/**
+ * Exposes GMP math library functionality.
+ *
+ * {@link Auth_OpenID_GmpMathWrapper} wraps the functionality provided
+ * by the GMP extension.
+ *
+ * @access private
+ * @package OpenID
+ */
+class Auth_OpenID_GmpMathWrapper extends Auth_OpenID_MathLibrary{
+ var $type = 'gmp';
+
+ function add($x, $y)
+ {
+ return gmp_add($x, $y);
+ }
+
+ function sub($x, $y)
+ {
+ return gmp_sub($x, $y);
+ }
+
+ function pow($base, $exponent)
+ {
+ return gmp_pow($base, $exponent);
+ }
+
+ function cmp($x, $y)
+ {
+ return gmp_cmp($x, $y);
+ }
+
+ function init($number, $base = 10)
+ {
+ return gmp_init($number, $base);
+ }
+
+ function mod($base, $modulus)
+ {
+ return gmp_mod($base, $modulus);
+ }
+
+ function mul($x, $y)
+ {
+ return gmp_mul($x, $y);
+ }
+
+ function div($x, $y)
+ {
+ return gmp_div_q($x, $y);
+ }
+
+ function powmod($base, $exponent, $modulus)
+ {
+ return gmp_powm($base, $exponent, $modulus);
+ }
+
+ function toString($num)
+ {
+ return gmp_strval($num);
+ }
+}
+
+/**
+ * Define the supported extensions. An extension array has keys
+ * 'modules', 'extension', and 'class'. 'modules' is an array of PHP
+ * module names which the loading code will attempt to load. These
+ * values will be suffixed with a library file extension (e.g. ".so").
+ * 'extension' is the name of a PHP extension which will be tested
+ * before 'modules' are loaded. 'class' is the string name of a
+ * {@link Auth_OpenID_MathWrapper} subclass which should be
+ * instantiated if a given extension is present.
+ *
+ * You can define new math library implementations and add them to
+ * this array.
+ */
+function Auth_OpenID_math_extensions()
+{
+ $result = array();
+
+ if (!defined('Auth_OpenID_BUGGY_GMP')) {
+ $result[] =
+ array('modules' => array('gmp', 'php_gmp'),
+ 'extension' => 'gmp',
+ 'class' => 'Auth_OpenID_GmpMathWrapper');
+ }
+
+ $result[] = array(
+ 'modules' => array('bcmath', 'php_bcmath'),
+ 'extension' => 'bcmath',
+ 'class' => 'Auth_OpenID_BcMathWrapper');
+
+ return $result;
+}
+
+/**
+ * Detect which (if any) math library is available
+ */
+function Auth_OpenID_detectMathLibrary($exts)
+{
+ $loaded = false;
+
+ foreach ($exts as $extension) {
+ // See if the extension specified is already loaded.
+ if ($extension['extension'] &&
+ extension_loaded($extension['extension'])) {
+ $loaded = true;
+ }
+
+ // Try to load dynamic modules.
+ if (!$loaded) {
+ foreach ($extension['modules'] as $module) {
+ if (@dl($module . "." . PHP_SHLIB_SUFFIX)) {
+ $loaded = true;
+ break;
+ }
+ }
+ }
+
+ // If the load succeeded, supply an instance of
+ // Auth_OpenID_MathWrapper which wraps the specified
+ // module's functionality.
+ if ($loaded) {
+ return $extension;
+ }
+ }
+
+ return false;
+}
+
+/**
+ * {@link Auth_OpenID_getMathLib} checks for the presence of long
+ * number extension modules and returns an instance of
+ * {@link Auth_OpenID_MathWrapper} which exposes the module's
+ * functionality.
+ *
+ * Checks for the existence of an extension module described by the
+ * result of {@link Auth_OpenID_math_extensions()} and returns an
+ * instance of a wrapper for that extension module. If no extension
+ * module is found, an instance of {@link Auth_OpenID_MathWrapper} is
+ * returned, which wraps the native PHP integer implementation. The
+ * proper calling convention for this method is $lib =&
+ * Auth_OpenID_getMathLib().
+ *
+ * This function checks for the existence of specific long number
+ * implementations in the following order: GMP followed by BCmath.
+ *
+ * @return Auth_OpenID_MathWrapper $instance An instance of
+ * {@link Auth_OpenID_MathWrapper} or one of its subclasses
+ *
+ * @package OpenID
+ */
+function &Auth_OpenID_getMathLib()
+{
+ // The instance of Auth_OpenID_MathWrapper that we choose to
+ // supply will be stored here, so that subseqent calls to this
+ // method will return a reference to the same object.
+ static $lib = null;
+
+ if (isset($lib)) {
+ return $lib;
+ }
+
+ if (Auth_OpenID_noMathSupport()) {
+ $null = null;
+ return $null;
+ }
+
+ // If this method has not been called before, look at
+ // Auth_OpenID_math_extensions and try to find an extension that
+ // works.
+ $ext = Auth_OpenID_detectMathLibrary(Auth_OpenID_math_extensions());
+ if ($ext === false) {
+ $tried = array();
+ foreach (Auth_OpenID_math_extensions() as $extinfo) {
+ $tried[] = $extinfo['extension'];
+ }
+ $triedstr = implode(", ", $tried);
+
+ Auth_OpenID_setNoMathSupport();
+
+ $result = null;
+ return $result;
+ }
+
+ // Instantiate a new wrapper
+ $class = $ext['class'];
+ $lib = new $class();
+
+ return $lib;
+}
+
+function Auth_OpenID_setNoMathSupport()
+{
+ if (!defined('Auth_OpenID_NO_MATH_SUPPORT')) {
+ define('Auth_OpenID_NO_MATH_SUPPORT', true);
+ }
+}
+
+function Auth_OpenID_noMathSupport()
+{
+ return defined('Auth_OpenID_NO_MATH_SUPPORT');
+}
+
+?>
diff --git a/extlib/Auth/OpenID/Consumer.php b/extlib/Auth/OpenID/Consumer.php
new file mode 100644
index 000000000..a72684c6b
--- /dev/null
+++ b/extlib/Auth/OpenID/Consumer.php
@@ -0,0 +1,2229 @@
+<?php
+
+/**
+ * This module documents the main interface with the OpenID consumer
+ * library. The only part of the library which has to be used and
+ * isn't documented in full here is the store required to create an
+ * Auth_OpenID_Consumer instance. More on the abstract store type and
+ * concrete implementations of it that are provided in the
+ * documentation for the Auth_OpenID_Consumer constructor.
+ *
+ * OVERVIEW
+ *
+ * The OpenID identity verification process most commonly uses the
+ * following steps, as visible to the user of this library:
+ *
+ * 1. The user enters their OpenID into a field on the consumer's
+ * site, and hits a login button.
+ * 2. The consumer site discovers the user's OpenID server using the
+ * YADIS protocol.
+ * 3. The consumer site sends the browser a redirect to the identity
+ * server. This is the authentication request as described in
+ * the OpenID specification.
+ * 4. The identity server's site sends the browser a redirect back
+ * to the consumer site. This redirect contains the server's
+ * response to the authentication request.
+ *
+ * The most important part of the flow to note is the consumer's site
+ * must handle two separate HTTP requests in order to perform the full
+ * identity check.
+ *
+ * LIBRARY DESIGN
+ *
+ * This consumer library is designed with that flow in mind. The goal
+ * is to make it as easy as possible to perform the above steps
+ * securely.
+ *
+ * At a high level, there are two important parts in the consumer
+ * library. The first important part is this module, which contains
+ * the interface to actually use this library. The second is the
+ * Auth_OpenID_Interface class, which describes the interface to use
+ * if you need to create a custom method for storing the state this
+ * library needs to maintain between requests.
+ *
+ * In general, the second part is less important for users of the
+ * library to know about, as several implementations are provided
+ * which cover a wide variety of situations in which consumers may use
+ * the library.
+ *
+ * This module contains a class, Auth_OpenID_Consumer, with methods
+ * corresponding to the actions necessary in each of steps 2, 3, and 4
+ * described in the overview. Use of this library should be as easy
+ * as creating an Auth_OpenID_Consumer instance and calling the
+ * methods appropriate for the action the site wants to take.
+ *
+ * STORES AND DUMB MODE
+ *
+ * OpenID is a protocol that works best when the consumer site is able
+ * to store some state. This is the normal mode of operation for the
+ * protocol, and is sometimes referred to as smart mode. There is
+ * also a fallback mode, known as dumb mode, which is available when
+ * the consumer site is not able to store state. This mode should be
+ * avoided when possible, as it leaves the implementation more
+ * vulnerable to replay attacks.
+ *
+ * The mode the library works in for normal operation is determined by
+ * the store that it is given. The store is an abstraction that
+ * handles the data that the consumer needs to manage between http
+ * requests in order to operate efficiently and securely.
+ *
+ * Several store implementation are provided, and the interface is
+ * fully documented so that custom stores can be used as well. See
+ * the documentation for the Auth_OpenID_Consumer class for more
+ * information on the interface for stores. The implementations that
+ * are provided allow the consumer site to store the necessary data in
+ * several different ways, including several SQL databases and normal
+ * files on disk.
+ *
+ * There is an additional concrete store provided that puts the system
+ * in dumb mode. This is not recommended, as it removes the library's
+ * ability to stop replay attacks reliably. It still uses time-based
+ * checking to make replay attacks only possible within a small
+ * window, but they remain possible within that window. This store
+ * should only be used if the consumer site has no way to retain data
+ * between requests at all.
+ *
+ * IMMEDIATE MODE
+ *
+ * In the flow described above, the user may need to confirm to the
+ * lidentity server that it's ok to authorize his or her identity.
+ * The server may draw pages asking for information from the user
+ * before it redirects the browser back to the consumer's site. This
+ * is generally transparent to the consumer site, so it is typically
+ * ignored as an implementation detail.
+ *
+ * There can be times, however, where the consumer site wants to get a
+ * response immediately. When this is the case, the consumer can put
+ * the library in immediate mode. In immediate mode, there is an
+ * extra response possible from the server, which is essentially the
+ * server reporting that it doesn't have enough information to answer
+ * the question yet.
+ *
+ * USING THIS LIBRARY
+ *
+ * Integrating this library into an application is usually a
+ * relatively straightforward process. The process should basically
+ * follow this plan:
+ *
+ * Add an OpenID login field somewhere on your site. When an OpenID
+ * is entered in that field and the form is submitted, it should make
+ * a request to the your site which includes that OpenID URL.
+ *
+ * First, the application should instantiate the Auth_OpenID_Consumer
+ * class using the store of choice (Auth_OpenID_FileStore or one of
+ * the SQL-based stores). If the application has a custom
+ * session-management implementation, an object implementing the
+ * {@link Auth_Yadis_PHPSession} interface should be passed as the
+ * second parameter. Otherwise, the default uses $_SESSION.
+ *
+ * Next, the application should call the Auth_OpenID_Consumer object's
+ * 'begin' method. This method takes the OpenID URL. The 'begin'
+ * method returns an Auth_OpenID_AuthRequest object.
+ *
+ * Next, the application should call the 'redirectURL' method of the
+ * Auth_OpenID_AuthRequest object. The 'return_to' URL parameter is
+ * the URL that the OpenID server will send the user back to after
+ * attempting to verify his or her identity. The 'trust_root' is the
+ * URL (or URL pattern) that identifies your web site to the user when
+ * he or she is authorizing it. Send a redirect to the resulting URL
+ * to the user's browser.
+ *
+ * That's the first half of the authentication process. The second
+ * half of the process is done after the user's ID server sends the
+ * user's browser a redirect back to your site to complete their
+ * login.
+ *
+ * When that happens, the user will contact your site at the URL given
+ * as the 'return_to' URL to the Auth_OpenID_AuthRequest::redirectURL
+ * call made above. The request will have several query parameters
+ * added to the URL by the identity server as the information
+ * necessary to finish the request.
+ *
+ * Lastly, instantiate an Auth_OpenID_Consumer instance as above and
+ * call its 'complete' method, passing in all the received query
+ * arguments.
+ *
+ * There are multiple possible return types possible from that
+ * method. These indicate the whether or not the login was successful,
+ * and include any additional information appropriate for their type.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005-2008 Janrain, Inc.
+ * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
+ */
+
+/**
+ * Require utility classes and functions for the consumer.
+ */
+require_once "Auth/OpenID.php";
+require_once "Auth/OpenID/Message.php";
+require_once "Auth/OpenID/HMAC.php";
+require_once "Auth/OpenID/Association.php";
+require_once "Auth/OpenID/CryptUtil.php";
+require_once "Auth/OpenID/DiffieHellman.php";
+require_once "Auth/OpenID/KVForm.php";
+require_once "Auth/OpenID/Nonce.php";
+require_once "Auth/OpenID/Discover.php";
+require_once "Auth/OpenID/URINorm.php";
+require_once "Auth/Yadis/Manager.php";
+require_once "Auth/Yadis/XRI.php";
+
+/**
+ * This is the status code returned when the complete method returns
+ * successfully.
+ */
+define('Auth_OpenID_SUCCESS', 'success');
+
+/**
+ * Status to indicate cancellation of OpenID authentication.
+ */
+define('Auth_OpenID_CANCEL', 'cancel');
+
+/**
+ * This is the status code completeAuth returns when the value it
+ * received indicated an invalid login.
+ */
+define('Auth_OpenID_FAILURE', 'failure');
+
+/**
+ * This is the status code completeAuth returns when the
+ * {@link Auth_OpenID_Consumer} instance is in immediate mode, and the
+ * identity server sends back a URL to send the user to to complete his
+ * or her login.
+ */
+define('Auth_OpenID_SETUP_NEEDED', 'setup needed');
+
+/**
+ * This is the status code beginAuth returns when the page fetched
+ * from the entered OpenID URL doesn't contain the necessary link tags
+ * to function as an identity page.
+ */
+define('Auth_OpenID_PARSE_ERROR', 'parse error');
+
+/**
+ * An OpenID consumer implementation that performs discovery and does
+ * session management. See the Consumer.php file documentation for
+ * more information.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_Consumer {
+
+ /**
+ * @access private
+ */
+ var $discoverMethod = 'Auth_OpenID_discover';
+
+ /**
+ * @access private
+ */
+ var $session_key_prefix = "_openid_consumer_";
+
+ /**
+ * @access private
+ */
+ var $_token_suffix = "last_token";
+
+ /**
+ * Initialize a Consumer instance.
+ *
+ * You should create a new instance of the Consumer object with
+ * every HTTP request that handles OpenID transactions.
+ *
+ * @param Auth_OpenID_OpenIDStore $store This must be an object
+ * that implements the interface in {@link
+ * Auth_OpenID_OpenIDStore}. Several concrete implementations are
+ * provided, to cover most common use cases. For stores backed by
+ * MySQL, PostgreSQL, or SQLite, see the {@link
+ * Auth_OpenID_SQLStore} class and its sublcasses. For a
+ * filesystem-backed store, see the {@link Auth_OpenID_FileStore}
+ * module. As a last resort, if it isn't possible for the server
+ * to store state at all, an instance of {@link
+ * Auth_OpenID_DumbStore} can be used.
+ *
+ * @param mixed $session An object which implements the interface
+ * of the {@link Auth_Yadis_PHPSession} class. Particularly, this
+ * object is expected to have these methods: get($key), set($key),
+ * $value), and del($key). This defaults to a session object
+ * which wraps PHP's native session machinery. You should only
+ * need to pass something here if you have your own sessioning
+ * implementation.
+ *
+ * @param str $consumer_cls The name of the class to instantiate
+ * when creating the internal consumer object. This is used for
+ * testing.
+ */
+ function Auth_OpenID_Consumer(&$store, $session = null,
+ $consumer_cls = null)
+ {
+ if ($session === null) {
+ $session = new Auth_Yadis_PHPSession();
+ }
+
+ $this->session =& $session;
+
+ if ($consumer_cls !== null) {
+ $this->consumer =& new $consumer_cls($store);
+ } else {
+ $this->consumer =& new Auth_OpenID_GenericConsumer($store);
+ }
+
+ $this->_token_key = $this->session_key_prefix . $this->_token_suffix;
+ }
+
+ /**
+ * Used in testing to define the discovery mechanism.
+ *
+ * @access private
+ */
+ function getDiscoveryObject(&$session, $openid_url,
+ $session_key_prefix)
+ {
+ return new Auth_Yadis_Discovery($session, $openid_url,
+ $session_key_prefix);
+ }
+
+ /**
+ * Start the OpenID authentication process. See steps 1-2 in the
+ * overview at the top of this file.
+ *
+ * @param string $user_url Identity URL given by the user. This
+ * method performs a textual transformation of the URL to try and
+ * make sure it is normalized. For example, a user_url of
+ * example.com will be normalized to http://example.com/
+ * normalizing and resolving any redirects the server might issue.
+ *
+ * @param bool $anonymous True if the OpenID request is to be sent
+ * to the server without any identifier information. Use this
+ * when you want to transport data but don't want to do OpenID
+ * authentication with identifiers.
+ *
+ * @return Auth_OpenID_AuthRequest $auth_request An object
+ * containing the discovered information will be returned, with a
+ * method for building a redirect URL to the server, as described
+ * in step 3 of the overview. This object may also be used to add
+ * extension arguments to the request, using its 'addExtensionArg'
+ * method.
+ */
+ function begin($user_url, $anonymous=false)
+ {
+ $openid_url = $user_url;
+
+ $disco = $this->getDiscoveryObject($this->session,
+ $openid_url,
+ $this->session_key_prefix);
+
+ // Set the 'stale' attribute of the manager. If discovery
+ // fails in a fatal way, the stale flag will cause the manager
+ // to be cleaned up next time discovery is attempted.
+
+ $m = $disco->getManager();
+ $loader = new Auth_Yadis_ManagerLoader();
+
+ if ($m) {
+ if ($m->stale) {
+ $disco->destroyManager();
+ } else {
+ $m->stale = true;
+ $disco->session->set($disco->session_key,
+ serialize($loader->toSession($m)));
+ }
+ }
+
+ $endpoint = $disco->getNextService($this->discoverMethod,
+ $this->consumer->fetcher);
+
+ // Reset the 'stale' attribute of the manager.
+ $m =& $disco->getManager();
+ if ($m) {
+ $m->stale = false;
+ $disco->session->set($disco->session_key,
+ serialize($loader->toSession($m)));
+ }
+
+ if ($endpoint === null) {
+ return null;
+ } else {
+ return $this->beginWithoutDiscovery($endpoint,
+ $anonymous);
+ }
+ }
+
+ /**
+ * Start OpenID verification without doing OpenID server
+ * discovery. This method is used internally by Consumer.begin
+ * after discovery is performed, and exists to provide an
+ * interface for library users needing to perform their own
+ * discovery.
+ *
+ * @param Auth_OpenID_ServiceEndpoint $endpoint an OpenID service
+ * endpoint descriptor.
+ *
+ * @param bool anonymous Set to true if you want to perform OpenID
+ * without identifiers.
+ *
+ * @return Auth_OpenID_AuthRequest $auth_request An OpenID
+ * authentication request object.
+ */
+ function &beginWithoutDiscovery($endpoint, $anonymous=false)
+ {
+ $loader = new Auth_OpenID_ServiceEndpointLoader();
+ $auth_req = $this->consumer->begin($endpoint);
+ $this->session->set($this->_token_key,
+ $loader->toSession($auth_req->endpoint));
+ if (!$auth_req->setAnonymous($anonymous)) {
+ return new Auth_OpenID_FailureResponse(null,
+ "OpenID 1 requests MUST include the identifier " .
+ "in the request.");
+ }
+ return $auth_req;
+ }
+
+ /**
+ * Called to interpret the server's response to an OpenID
+ * request. It is called in step 4 of the flow described in the
+ * consumer overview.
+ *
+ * @param string $current_url The URL used to invoke the application.
+ * Extract the URL from your application's web
+ * request framework and specify it here to have it checked
+ * against the openid.current_url value in the response. If
+ * the current_url URL check fails, the status of the
+ * completion will be FAILURE.
+ *
+ * @param array $query An array of the query parameters (key =>
+ * value pairs) for this HTTP request. Defaults to null. If
+ * null, the GET or POST data are automatically gotten from the
+ * PHP environment. It is only useful to override $query for
+ * testing.
+ *
+ * @return Auth_OpenID_ConsumerResponse $response A instance of an
+ * Auth_OpenID_ConsumerResponse subclass. The type of response is
+ * indicated by the status attribute, which will be one of
+ * SUCCESS, CANCEL, FAILURE, or SETUP_NEEDED.
+ */
+ function complete($current_url, $query=null)
+ {
+ if ($current_url && !is_string($current_url)) {
+ // This is ugly, but we need to complain loudly when
+ // someone uses the API incorrectly.
+ trigger_error("current_url must be a string; see NEWS file " .
+ "for upgrading notes.",
+ E_USER_ERROR);
+ }
+
+ if ($query === null) {
+ $query = Auth_OpenID::getQuery();
+ }
+
+ $loader = new Auth_OpenID_ServiceEndpointLoader();
+ $endpoint_data = $this->session->get($this->_token_key);
+ $endpoint =
+ $loader->fromSession($endpoint_data);
+
+ $message = Auth_OpenID_Message::fromPostArgs($query);
+ $response = $this->consumer->complete($message, $endpoint,
+ $current_url);
+ $this->session->del($this->_token_key);
+
+ if (in_array($response->status, array(Auth_OpenID_SUCCESS,
+ Auth_OpenID_CANCEL))) {
+ if ($response->identity_url !== null) {
+ $disco = $this->getDiscoveryObject($this->session,
+ $response->identity_url,
+ $this->session_key_prefix);
+ $disco->cleanup(true);
+ }
+ }
+
+ return $response;
+ }
+}
+
+/**
+ * A class implementing HMAC/DH-SHA1 consumer sessions.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_DiffieHellmanSHA1ConsumerSession {
+ var $session_type = 'DH-SHA1';
+ var $hash_func = 'Auth_OpenID_SHA1';
+ var $secret_size = 20;
+ var $allowed_assoc_types = array('HMAC-SHA1');
+
+ function Auth_OpenID_DiffieHellmanSHA1ConsumerSession($dh = null)
+ {
+ if ($dh === null) {
+ $dh = new Auth_OpenID_DiffieHellman();
+ }
+
+ $this->dh = $dh;
+ }
+
+ function getRequest()
+ {
+ $math =& Auth_OpenID_getMathLib();
+
+ $cpub = $math->longToBase64($this->dh->public);
+
+ $args = array('dh_consumer_public' => $cpub);
+
+ if (!$this->dh->usingDefaultValues()) {
+ $args = array_merge($args, array(
+ 'dh_modulus' =>
+ $math->longToBase64($this->dh->mod),
+ 'dh_gen' =>
+ $math->longToBase64($this->dh->gen)));
+ }
+
+ return $args;
+ }
+
+ function extractSecret($response)
+ {
+ if (!$response->hasKey(Auth_OpenID_OPENID_NS,
+ 'dh_server_public')) {
+ return null;
+ }
+
+ if (!$response->hasKey(Auth_OpenID_OPENID_NS,
+ 'enc_mac_key')) {
+ return null;
+ }
+
+ $math =& Auth_OpenID_getMathLib();
+
+ $spub = $math->base64ToLong($response->getArg(Auth_OpenID_OPENID_NS,
+ 'dh_server_public'));
+ $enc_mac_key = base64_decode($response->getArg(Auth_OpenID_OPENID_NS,
+ 'enc_mac_key'));
+
+ return $this->dh->xorSecret($spub, $enc_mac_key, $this->hash_func);
+ }
+}
+
+/**
+ * A class implementing HMAC/DH-SHA256 consumer sessions.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_DiffieHellmanSHA256ConsumerSession extends
+ Auth_OpenID_DiffieHellmanSHA1ConsumerSession {
+ var $session_type = 'DH-SHA256';
+ var $hash_func = 'Auth_OpenID_SHA256';
+ var $secret_size = 32;
+ var $allowed_assoc_types = array('HMAC-SHA256');
+}
+
+/**
+ * A class implementing plaintext consumer sessions.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_PlainTextConsumerSession {
+ var $session_type = 'no-encryption';
+ var $allowed_assoc_types = array('HMAC-SHA1', 'HMAC-SHA256');
+
+ function getRequest()
+ {
+ return array();
+ }
+
+ function extractSecret($response)
+ {
+ if (!$response->hasKey(Auth_OpenID_OPENID_NS, 'mac_key')) {
+ return null;
+ }
+
+ return base64_decode($response->getArg(Auth_OpenID_OPENID_NS,
+ 'mac_key'));
+ }
+}
+
+/**
+ * Returns available session types.
+ */
+function Auth_OpenID_getAvailableSessionTypes()
+{
+ $types = array(
+ 'no-encryption' => 'Auth_OpenID_PlainTextConsumerSession',
+ 'DH-SHA1' => 'Auth_OpenID_DiffieHellmanSHA1ConsumerSession',
+ 'DH-SHA256' => 'Auth_OpenID_DiffieHellmanSHA256ConsumerSession');
+
+ return $types;
+}
+
+/**
+ * This class is the interface to the OpenID consumer logic.
+ * Instances of it maintain no per-request state, so they can be
+ * reused (or even used by multiple threads concurrently) as needed.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_GenericConsumer {
+ /**
+ * @access private
+ */
+ var $discoverMethod = 'Auth_OpenID_discover';
+
+ /**
+ * This consumer's store object.
+ */
+ var $store;
+
+ /**
+ * @access private
+ */
+ var $_use_assocs;
+
+ /**
+ * @access private
+ */
+ var $openid1_nonce_query_arg_name = 'janrain_nonce';
+
+ /**
+ * Another query parameter that gets added to the return_to for
+ * OpenID 1; if the user's session state is lost, use this claimed
+ * identifier to do discovery when verifying the response.
+ */
+ var $openid1_return_to_identifier_name = 'openid1_claimed_id';
+
+ /**
+ * This method initializes a new {@link Auth_OpenID_Consumer}
+ * instance to access the library.
+ *
+ * @param Auth_OpenID_OpenIDStore $store This must be an object
+ * that implements the interface in {@link Auth_OpenID_OpenIDStore}.
+ * Several concrete implementations are provided, to cover most common use
+ * cases. For stores backed by MySQL, PostgreSQL, or SQLite, see
+ * the {@link Auth_OpenID_SQLStore} class and its sublcasses. For a
+ * filesystem-backed store, see the {@link Auth_OpenID_FileStore} module.
+ * As a last resort, if it isn't possible for the server to store
+ * state at all, an instance of {@link Auth_OpenID_DumbStore} can be used.
+ *
+ * @param bool $immediate This is an optional boolean value. It
+ * controls whether the library uses immediate mode, as explained
+ * in the module description. The default value is False, which
+ * disables immediate mode.
+ */
+ function Auth_OpenID_GenericConsumer(&$store)
+ {
+ $this->store =& $store;
+ $this->negotiator =& Auth_OpenID_getDefaultNegotiator();
+ $this->_use_assocs = ($this->store ? true : false);
+
+ $this->fetcher = Auth_Yadis_Yadis::getHTTPFetcher();
+
+ $this->session_types = Auth_OpenID_getAvailableSessionTypes();
+ }
+
+ /**
+ * Called to begin OpenID authentication using the specified
+ * {@link Auth_OpenID_ServiceEndpoint}.
+ *
+ * @access private
+ */
+ function begin($service_endpoint)
+ {
+ $assoc = $this->_getAssociation($service_endpoint);
+ $r = new Auth_OpenID_AuthRequest($service_endpoint, $assoc);
+ $r->return_to_args[$this->openid1_nonce_query_arg_name] =
+ Auth_OpenID_mkNonce();
+
+ if ($r->message->isOpenID1()) {
+ $r->return_to_args[$this->openid1_return_to_identifier_name] =
+ $r->endpoint->claimed_id;
+ }
+
+ return $r;
+ }
+
+ /**
+ * Given an {@link Auth_OpenID_Message}, {@link
+ * Auth_OpenID_ServiceEndpoint} and optional return_to URL,
+ * complete OpenID authentication.
+ *
+ * @access private
+ */
+ function complete($message, $endpoint, $return_to)
+ {
+ $mode = $message->getArg(Auth_OpenID_OPENID_NS, 'mode',
+ '<no mode set>');
+
+ $mode_methods = array(
+ 'cancel' => '_complete_cancel',
+ 'error' => '_complete_error',
+ 'setup_needed' => '_complete_setup_needed',
+ 'id_res' => '_complete_id_res',
+ );
+
+ $method = Auth_OpenID::arrayGet($mode_methods, $mode,
+ '_completeInvalid');
+
+ return call_user_func_array(array(&$this, $method),
+ array($message, $endpoint, $return_to));
+ }
+
+ /**
+ * @access private
+ */
+ function _completeInvalid($message, &$endpoint, $unused)
+ {
+ $mode = $message->getArg(Auth_OpenID_OPENID_NS, 'mode',
+ '<No mode set>');
+
+ return new Auth_OpenID_FailureResponse($endpoint,
+ sprintf("Invalid openid.mode '%s'", $mode));
+ }
+
+ /**
+ * @access private
+ */
+ function _complete_cancel($message, &$endpoint, $unused)
+ {
+ return new Auth_OpenID_CancelResponse($endpoint);
+ }
+
+ /**
+ * @access private
+ */
+ function _complete_error($message, &$endpoint, $unused)
+ {
+ $error = $message->getArg(Auth_OpenID_OPENID_NS, 'error');
+ $contact = $message->getArg(Auth_OpenID_OPENID_NS, 'contact');
+ $reference = $message->getArg(Auth_OpenID_OPENID_NS, 'reference');
+
+ return new Auth_OpenID_FailureResponse($endpoint, $error,
+ $contact, $reference);
+ }
+
+ /**
+ * @access private
+ */
+ function _complete_setup_needed($message, &$endpoint, $unused)
+ {
+ if (!$message->isOpenID2()) {
+ return $this->_completeInvalid($message, $endpoint);
+ }
+
+ $user_setup_url = $message->getArg(Auth_OpenID_OPENID2_NS,
+ 'user_setup_url');
+ return new Auth_OpenID_SetupNeededResponse($endpoint, $user_setup_url);
+ }
+
+ /**
+ * @access private
+ */
+ function _complete_id_res($message, &$endpoint, $return_to)
+ {
+ $user_setup_url = $message->getArg(Auth_OpenID_OPENID1_NS,
+ 'user_setup_url');
+
+ if ($this->_checkSetupNeeded($message)) {
+ return new Auth_OpenID_SetupNeededResponse(
+ $endpoint, $user_setup_url);
+ } else {
+ return $this->_doIdRes($message, $endpoint, $return_to);
+ }
+ }
+
+ /**
+ * @access private
+ */
+ function _checkSetupNeeded($message)
+ {
+ // In OpenID 1, we check to see if this is a cancel from
+ // immediate mode by the presence of the user_setup_url
+ // parameter.
+ if ($message->isOpenID1()) {
+ $user_setup_url = $message->getArg(Auth_OpenID_OPENID1_NS,
+ 'user_setup_url');
+ if ($user_setup_url !== null) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @access private
+ */
+ function _doIdRes($message, $endpoint, $return_to)
+ {
+ // Checks for presence of appropriate fields (and checks
+ // signed list fields)
+ $result = $this->_idResCheckForFields($message);
+
+ if (Auth_OpenID::isFailure($result)) {
+ return $result;
+ }
+
+ if (!$this->_checkReturnTo($message, $return_to)) {
+ return new Auth_OpenID_FailureResponse(null,
+ sprintf("return_to does not match return URL. Expected %s, got %s",
+ $return_to,
+ $message->getArg(Auth_OpenID_OPENID_NS, 'return_to')));
+ }
+
+ // Verify discovery information:
+ $result = $this->_verifyDiscoveryResults($message, $endpoint);
+
+ if (Auth_OpenID::isFailure($result)) {
+ return $result;
+ }
+
+ $endpoint = $result;
+
+ $result = $this->_idResCheckSignature($message,
+ $endpoint->server_url);
+
+ if (Auth_OpenID::isFailure($result)) {
+ return $result;
+ }
+
+ $result = $this->_idResCheckNonce($message, $endpoint);
+
+ if (Auth_OpenID::isFailure($result)) {
+ return $result;
+ }
+
+ $signed_list_str = $message->getArg(Auth_OpenID_OPENID_NS, 'signed',
+ Auth_OpenID_NO_DEFAULT);
+ if (Auth_OpenID::isFailure($signed_list_str)) {
+ return $signed_list_str;
+ }
+ $signed_list = explode(',', $signed_list_str);
+
+ $signed_fields = Auth_OpenID::addPrefix($signed_list, "openid.");
+
+ return new Auth_OpenID_SuccessResponse($endpoint, $message,
+ $signed_fields);
+
+ }
+
+ /**
+ * @access private
+ */
+ function _checkReturnTo($message, $return_to)
+ {
+ // Check an OpenID message and its openid.return_to value
+ // against a return_to URL from an application. Return True
+ // on success, False on failure.
+
+ // Check the openid.return_to args against args in the
+ // original message.
+ $result = Auth_OpenID_GenericConsumer::_verifyReturnToArgs(
+ $message->toPostArgs());
+ if (Auth_OpenID::isFailure($result)) {
+ return false;
+ }
+
+ // Check the return_to base URL against the one in the
+ // message.
+ $msg_return_to = $message->getArg(Auth_OpenID_OPENID_NS,
+ 'return_to');
+ if (Auth_OpenID::isFailure($return_to)) {
+ // XXX log me
+ return false;
+ }
+
+ $return_to_parts = parse_url(Auth_OpenID_urinorm($return_to));
+ $msg_return_to_parts = parse_url(Auth_OpenID_urinorm($msg_return_to));
+
+ // If port is absent from both, add it so it's equal in the
+ // check below.
+ if ((!array_key_exists('port', $return_to_parts)) &&
+ (!array_key_exists('port', $msg_return_to_parts))) {
+ $return_to_parts['port'] = null;
+ $msg_return_to_parts['port'] = null;
+ }
+
+ // If path is absent from both, add it so it's equal in the
+ // check below.
+ if ((!array_key_exists('path', $return_to_parts)) &&
+ (!array_key_exists('path', $msg_return_to_parts))) {
+ $return_to_parts['path'] = null;
+ $msg_return_to_parts['path'] = null;
+ }
+
+ // The URL scheme, authority, and path MUST be the same
+ // between the two URLs.
+ foreach (array('scheme', 'host', 'port', 'path') as $component) {
+ // If the url component is absent in either URL, fail.
+ // There should always be a scheme, host, port, and path.
+ if (!array_key_exists($component, $return_to_parts)) {
+ return false;
+ }
+
+ if (!array_key_exists($component, $msg_return_to_parts)) {
+ return false;
+ }
+
+ if (Auth_OpenID::arrayGet($return_to_parts, $component) !==
+ Auth_OpenID::arrayGet($msg_return_to_parts, $component)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * @access private
+ */
+ function _verifyReturnToArgs($query)
+ {
+ // Verify that the arguments in the return_to URL are present in this
+ // response.
+
+ $message = Auth_OpenID_Message::fromPostArgs($query);
+ $return_to = $message->getArg(Auth_OpenID_OPENID_NS, 'return_to');
+
+ if (Auth_OpenID::isFailure($return_to)) {
+ return $return_to;
+ }
+ // XXX: this should be checked by _idResCheckForFields
+ if (!$return_to) {
+ return new Auth_OpenID_FailureResponse(null,
+ "Response has no return_to");
+ }
+
+ $parsed_url = parse_url($return_to);
+
+ $q = array();
+ if (array_key_exists('query', $parsed_url)) {
+ $rt_query = $parsed_url['query'];
+ $q = Auth_OpenID::parse_str($rt_query);
+ }
+
+ foreach ($q as $rt_key => $rt_value) {
+ if (!array_key_exists($rt_key, $query)) {
+ return new Auth_OpenID_FailureResponse(null,
+ sprintf("return_to parameter %s absent from query", $rt_key));
+ } else {
+ $value = $query[$rt_key];
+ if ($rt_value != $value) {
+ return new Auth_OpenID_FailureResponse(null,
+ sprintf("parameter %s value %s does not match " .
+ "return_to value %s", $rt_key,
+ $value, $rt_value));
+ }
+ }
+ }
+
+ // Make sure all non-OpenID arguments in the response are also
+ // in the signed return_to.
+ $bare_args = $message->getArgs(Auth_OpenID_BARE_NS);
+ foreach ($bare_args as $key => $value) {
+ if (Auth_OpenID::arrayGet($q, $key) != $value) {
+ return new Auth_OpenID_FailureResponse(null,
+ sprintf("Parameter %s = %s not in return_to URL",
+ $key, $value));
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * @access private
+ */
+ function _idResCheckSignature($message, $server_url)
+ {
+ $assoc_handle = $message->getArg(Auth_OpenID_OPENID_NS,
+ 'assoc_handle');
+ if (Auth_OpenID::isFailure($assoc_handle)) {
+ return $assoc_handle;
+ }
+
+ $assoc = $this->store->getAssociation($server_url, $assoc_handle);
+
+ if ($assoc) {
+ if ($assoc->getExpiresIn() <= 0) {
+ // XXX: It might be a good idea sometimes to re-start
+ // the authentication with a new association. Doing it
+ // automatically opens the possibility for
+ // denial-of-service by a server that just returns
+ // expired associations (or really short-lived
+ // associations)
+ return new Auth_OpenID_FailureResponse(null,
+ 'Association with ' . $server_url . ' expired');
+ }
+
+ if (!$assoc->checkMessageSignature($message)) {
+ return new Auth_OpenID_FailureResponse(null,
+ "Bad signature");
+ }
+ } else {
+ // It's not an association we know about. Stateless mode
+ // is our only possible path for recovery. XXX - async
+ // framework will not want to block on this call to
+ // _checkAuth.
+ if (!$this->_checkAuth($message, $server_url)) {
+ return new Auth_OpenID_FailureResponse(null,
+ "Server denied check_authentication");
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * @access private
+ */
+ function _verifyDiscoveryResults($message, $endpoint=null)
+ {
+ if ($message->getOpenIDNamespace() == Auth_OpenID_OPENID2_NS) {
+ return $this->_verifyDiscoveryResultsOpenID2($message,
+ $endpoint);
+ } else {
+ return $this->_verifyDiscoveryResultsOpenID1($message,
+ $endpoint);
+ }
+ }
+
+ /**
+ * @access private
+ */
+ function _verifyDiscoveryResultsOpenID1($message, $endpoint)
+ {
+ $claimed_id = $message->getArg(Auth_OpenID_BARE_NS,
+ $this->openid1_return_to_identifier_name);
+
+ if (($endpoint === null) && ($claimed_id === null)) {
+ return new Auth_OpenID_FailureResponse($endpoint,
+ 'When using OpenID 1, the claimed ID must be supplied, ' .
+ 'either by passing it through as a return_to parameter ' .
+ 'or by using a session, and supplied to the GenericConsumer ' .
+ 'as the argument to complete()');
+ } else if (($endpoint !== null) && ($claimed_id === null)) {
+ $claimed_id = $endpoint->claimed_id;
+ }
+
+ $to_match = new Auth_OpenID_ServiceEndpoint();
+ $to_match->type_uris = array(Auth_OpenID_TYPE_1_1);
+ $to_match->local_id = $message->getArg(Auth_OpenID_OPENID1_NS,
+ 'identity');
+
+ // Restore delegate information from the initiation phase
+ $to_match->claimed_id = $claimed_id;
+
+ if ($to_match->local_id === null) {
+ return new Auth_OpenID_FailureResponse($endpoint,
+ "Missing required field openid.identity");
+ }
+
+ $to_match_1_0 = $to_match->copy();
+ $to_match_1_0->type_uris = array(Auth_OpenID_TYPE_1_0);
+
+ if ($endpoint !== null) {
+ $result = $this->_verifyDiscoverySingle($endpoint, $to_match);
+
+ if (is_a($result, 'Auth_OpenID_TypeURIMismatch')) {
+ $result = $this->_verifyDiscoverySingle($endpoint,
+ $to_match_1_0);
+ }
+
+ if (Auth_OpenID::isFailure($result)) {
+ // oidutil.log("Error attempting to use stored
+ // discovery information: " + str(e))
+ // oidutil.log("Attempting discovery to
+ // verify endpoint")
+ } else {
+ return $endpoint;
+ }
+ }
+
+ // Endpoint is either bad (failed verification) or None
+ return $this->_discoverAndVerify($to_match->claimed_id,
+ array($to_match, $to_match_1_0));
+ }
+
+ /**
+ * @access private
+ */
+ function _verifyDiscoverySingle($endpoint, $to_match)
+ {
+ // Every type URI that's in the to_match endpoint has to be
+ // present in the discovered endpoint.
+ foreach ($to_match->type_uris as $type_uri) {
+ if (!$endpoint->usesExtension($type_uri)) {
+ return new Auth_OpenID_TypeURIMismatch($endpoint,
+ "Required type ".$type_uri." not present");
+ }
+ }
+
+ // Fragments do not influence discovery, so we can't compare a
+ // claimed identifier with a fragment to discovered
+ // information.
+ list($defragged_claimed_id, $_) =
+ Auth_OpenID::urldefrag($to_match->claimed_id);
+
+ if ($defragged_claimed_id != $endpoint->claimed_id) {
+ return new Auth_OpenID_FailureResponse($endpoint,
+ sprintf('Claimed ID does not match (different subjects!), ' .
+ 'Expected %s, got %s', $defragged_claimed_id,
+ $endpoint->claimed_id));
+ }
+
+ if ($to_match->getLocalID() != $endpoint->getLocalID()) {
+ return new Auth_OpenID_FailureResponse($endpoint,
+ sprintf('local_id mismatch. Expected %s, got %s',
+ $to_match->getLocalID(), $endpoint->getLocalID()));
+ }
+
+ // If the server URL is None, this must be an OpenID 1
+ // response, because op_endpoint is a required parameter in
+ // OpenID 2. In that case, we don't actually care what the
+ // discovered server_url is, because signature checking or
+ // check_auth should take care of that check for us.
+ if ($to_match->server_url === null) {
+ if ($to_match->preferredNamespace() != Auth_OpenID_OPENID1_NS) {
+ return new Auth_OpenID_FailureResponse($endpoint,
+ "Preferred namespace mismatch (bug)");
+ }
+ } else if ($to_match->server_url != $endpoint->server_url) {
+ return new Auth_OpenID_FailureResponse($endpoint,
+ sprintf('OP Endpoint mismatch. Expected %s, got %s',
+ $to_match->server_url, $endpoint->server_url));
+ }
+
+ return null;
+ }
+
+ /**
+ * @access private
+ */
+ function _verifyDiscoveryResultsOpenID2($message, $endpoint)
+ {
+ $to_match = new Auth_OpenID_ServiceEndpoint();
+ $to_match->type_uris = array(Auth_OpenID_TYPE_2_0);
+ $to_match->claimed_id = $message->getArg(Auth_OpenID_OPENID2_NS,
+ 'claimed_id');
+
+ $to_match->local_id = $message->getArg(Auth_OpenID_OPENID2_NS,
+ 'identity');
+
+ $to_match->server_url = $message->getArg(Auth_OpenID_OPENID2_NS,
+ 'op_endpoint');
+
+ if ($to_match->server_url === null) {
+ return new Auth_OpenID_FailureResponse($endpoint,
+ "OP Endpoint URL missing");
+ }
+
+ // claimed_id and identifier must both be present or both be
+ // absent
+ if (($to_match->claimed_id === null) &&
+ ($to_match->local_id !== null)) {
+ return new Auth_OpenID_FailureResponse($endpoint,
+ 'openid.identity is present without openid.claimed_id');
+ }
+
+ if (($to_match->claimed_id !== null) &&
+ ($to_match->local_id === null)) {
+ return new Auth_OpenID_FailureResponse($endpoint,
+ 'openid.claimed_id is present without openid.identity');
+ }
+
+ if ($to_match->claimed_id === null) {
+ // This is a response without identifiers, so there's
+ // really no checking that we can do, so return an
+ // endpoint that's for the specified `openid.op_endpoint'
+ return Auth_OpenID_ServiceEndpoint::fromOPEndpointURL(
+ $to_match->server_url);
+ }
+
+ if (!$endpoint) {
+ // The claimed ID doesn't match, so we have to do
+ // discovery again. This covers not using sessions, OP
+ // identifier endpoints and responses that didn't match
+ // the original request.
+ // oidutil.log('No pre-discovered information supplied.')
+ return $this->_discoverAndVerify($to_match->claimed_id,
+ array($to_match));
+ } else {
+
+ // The claimed ID matches, so we use the endpoint that we
+ // discovered in initiation. This should be the most
+ // common case.
+ $result = $this->_verifyDiscoverySingle($endpoint, $to_match);
+
+ if (Auth_OpenID::isFailure($result)) {
+ $endpoint = $this->_discoverAndVerify($to_match->claimed_id,
+ array($to_match));
+ if (Auth_OpenID::isFailure($endpoint)) {
+ return $endpoint;
+ }
+ }
+ }
+
+ // The endpoint we return should have the claimed ID from the
+ // message we just verified, fragment and all.
+ if ($endpoint->claimed_id != $to_match->claimed_id) {
+ $endpoint->claimed_id = $to_match->claimed_id;
+ }
+
+ return $endpoint;
+ }
+
+ /**
+ * @access private
+ */
+ function _discoverAndVerify($claimed_id, $to_match_endpoints)
+ {
+ // oidutil.log('Performing discovery on %s' % (claimed_id,))
+ list($unused, $services) = call_user_func($this->discoverMethod,
+ $claimed_id,
+ $this->fetcher);
+
+ if (!$services) {
+ return new Auth_OpenID_FailureResponse(null,
+ sprintf("No OpenID information found at %s",
+ $claimed_id));
+ }
+
+ return $this->_verifyDiscoveryServices($claimed_id, $services,
+ $to_match_endpoints);
+ }
+
+ /**
+ * @access private
+ */
+ function _verifyDiscoveryServices($claimed_id,
+ &$services, &$to_match_endpoints)
+ {
+ // Search the services resulting from discovery to find one
+ // that matches the information from the assertion
+
+ foreach ($services as $endpoint) {
+ foreach ($to_match_endpoints as $to_match_endpoint) {
+ $result = $this->_verifyDiscoverySingle($endpoint,
+ $to_match_endpoint);
+
+ if (!Auth_OpenID::isFailure($result)) {
+ // It matches, so discover verification has
+ // succeeded. Return this endpoint.
+ return $endpoint;
+ }
+ }
+ }
+
+ return new Auth_OpenID_FailureResponse(null,
+ sprintf('No matching endpoint found after discovering %s',
+ $claimed_id));
+ }
+
+ /**
+ * Extract the nonce from an OpenID 1 response. Return the nonce
+ * from the BARE_NS since we independently check the return_to
+ * arguments are the same as those in the response message.
+ *
+ * See the openid1_nonce_query_arg_name class variable
+ *
+ * @returns $nonce The nonce as a string or null
+ *
+ * @access private
+ */
+ function _idResGetNonceOpenID1($message, $endpoint)
+ {
+ return $message->getArg(Auth_OpenID_BARE_NS,
+ $this->openid1_nonce_query_arg_name);
+ }
+
+ /**
+ * @access private
+ */
+ function _idResCheckNonce($message, $endpoint)
+ {
+ if ($message->isOpenID1()) {
+ // This indicates that the nonce was generated by the consumer
+ $nonce = $this->_idResGetNonceOpenID1($message, $endpoint);
+ $server_url = '';
+ } else {
+ $nonce = $message->getArg(Auth_OpenID_OPENID2_NS,
+ 'response_nonce');
+
+ $server_url = $endpoint->server_url;
+ }
+
+ if ($nonce === null) {
+ return new Auth_OpenID_FailureResponse($endpoint,
+ "Nonce missing from response");
+ }
+
+ $parts = Auth_OpenID_splitNonce($nonce);
+
+ if ($parts === null) {
+ return new Auth_OpenID_FailureResponse($endpoint,
+ "Malformed nonce in response");
+ }
+
+ list($timestamp, $salt) = $parts;
+
+ if (!$this->store->useNonce($server_url, $timestamp, $salt)) {
+ return new Auth_OpenID_FailureResponse($endpoint,
+ "Nonce already used or out of range");
+ }
+
+ return null;
+ }
+
+ /**
+ * @access private
+ */
+ function _idResCheckForFields($message)
+ {
+ $basic_fields = array('return_to', 'assoc_handle', 'sig', 'signed');
+ $basic_sig_fields = array('return_to', 'identity');
+
+ $require_fields = array(
+ Auth_OpenID_OPENID2_NS => array_merge($basic_fields,
+ array('op_endpoint')),
+
+ Auth_OpenID_OPENID1_NS => array_merge($basic_fields,
+ array('identity'))
+ );
+
+ $require_sigs = array(
+ Auth_OpenID_OPENID2_NS => array_merge($basic_sig_fields,
+ array('response_nonce',
+ 'claimed_id',
+ 'assoc_handle')),
+ Auth_OpenID_OPENID1_NS => array_merge($basic_sig_fields,
+ array('nonce'))
+ );
+
+ foreach ($require_fields[$message->getOpenIDNamespace()] as $field) {
+ if (!$message->hasKey(Auth_OpenID_OPENID_NS, $field)) {
+ return new Auth_OpenID_FailureResponse(null,
+ "Missing required field '".$field."'");
+ }
+ }
+
+ $signed_list_str = $message->getArg(Auth_OpenID_OPENID_NS,
+ 'signed',
+ Auth_OpenID_NO_DEFAULT);
+ if (Auth_OpenID::isFailure($signed_list_str)) {
+ return $signed_list_str;
+ }
+ $signed_list = explode(',', $signed_list_str);
+
+ foreach ($require_sigs[$message->getOpenIDNamespace()] as $field) {
+ // Field is present and not in signed list
+ if ($message->hasKey(Auth_OpenID_OPENID_NS, $field) &&
+ (!in_array($field, $signed_list))) {
+ return new Auth_OpenID_FailureResponse(null,
+ "'".$field."' not signed");
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * @access private
+ */
+ function _checkAuth($message, $server_url)
+ {
+ $request = $this->_createCheckAuthRequest($message);
+ if ($request === null) {
+ return false;
+ }
+
+ $resp_message = $this->_makeKVPost($request, $server_url);
+ if (($resp_message === null) ||
+ (is_a($resp_message, 'Auth_OpenID_ServerErrorContainer'))) {
+ return false;
+ }
+
+ return $this->_processCheckAuthResponse($resp_message, $server_url);
+ }
+
+ /**
+ * @access private
+ */
+ function _createCheckAuthRequest($message)
+ {
+ $signed = $message->getArg(Auth_OpenID_OPENID_NS, 'signed');
+ if ($signed) {
+ foreach (explode(',', $signed) as $k) {
+ $value = $message->getAliasedArg($k);
+ if ($value === null) {
+ return null;
+ }
+ }
+ }
+ $ca_message = $message->copy();
+ $ca_message->setArg(Auth_OpenID_OPENID_NS, 'mode',
+ 'check_authentication');
+ return $ca_message;
+ }
+
+ /**
+ * @access private
+ */
+ function _processCheckAuthResponse($response, $server_url)
+ {
+ $is_valid = $response->getArg(Auth_OpenID_OPENID_NS, 'is_valid',
+ 'false');
+
+ $invalidate_handle = $response->getArg(Auth_OpenID_OPENID_NS,
+ 'invalidate_handle');
+
+ if ($invalidate_handle !== null) {
+ $this->store->removeAssociation($server_url,
+ $invalidate_handle);
+ }
+
+ if ($is_valid == 'true') {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Adapt a POST response to a Message.
+ *
+ * @param $response Result of a POST to an OpenID endpoint.
+ *
+ * @access private
+ */
+ function _httpResponseToMessage($response, $server_url)
+ {
+ // Should this function be named Message.fromHTTPResponse instead?
+ $response_message = Auth_OpenID_Message::fromKVForm($response->body);
+
+ if ($response->status == 400) {
+ return Auth_OpenID_ServerErrorContainer::fromMessage(
+ $response_message);
+ } else if ($response->status != 200 and $response->status != 206) {
+ return null;
+ }
+
+ return $response_message;
+ }
+
+ /**
+ * @access private
+ */
+ function _makeKVPost($message, $server_url)
+ {
+ $body = $message->toURLEncoded();
+ $resp = $this->fetcher->post($server_url, $body);
+
+ if ($resp === null) {
+ return null;
+ }
+
+ return $this->_httpResponseToMessage($resp, $server_url);
+ }
+
+ /**
+ * @access private
+ */
+ function _getAssociation($endpoint)
+ {
+ if (!$this->_use_assocs) {
+ return null;
+ }
+
+ $assoc = $this->store->getAssociation($endpoint->server_url);
+
+ if (($assoc === null) ||
+ ($assoc->getExpiresIn() <= 0)) {
+
+ $assoc = $this->_negotiateAssociation($endpoint);
+
+ if ($assoc !== null) {
+ $this->store->storeAssociation($endpoint->server_url,
+ $assoc);
+ }
+ }
+
+ return $assoc;
+ }
+
+ /**
+ * Handle ServerErrors resulting from association requests.
+ *
+ * @return $result If server replied with an C{unsupported-type}
+ * error, return a tuple of supported C{association_type},
+ * C{session_type}. Otherwise logs the error and returns null.
+ *
+ * @access private
+ */
+ function _extractSupportedAssociationType(&$server_error, &$endpoint,
+ $assoc_type)
+ {
+ // Any error message whose code is not 'unsupported-type'
+ // should be considered a total failure.
+ if (($server_error->error_code != 'unsupported-type') ||
+ ($server_error->message->isOpenID1())) {
+ return null;
+ }
+
+ // The server didn't like the association/session type that we
+ // sent, and it sent us back a message that might tell us how
+ // to handle it.
+
+ // Extract the session_type and assoc_type from the error
+ // message
+ $assoc_type = $server_error->message->getArg(Auth_OpenID_OPENID_NS,
+ 'assoc_type');
+
+ $session_type = $server_error->message->getArg(Auth_OpenID_OPENID_NS,
+ 'session_type');
+
+ if (($assoc_type === null) || ($session_type === null)) {
+ return null;
+ } else if (!$this->negotiator->isAllowed($assoc_type,
+ $session_type)) {
+ return null;
+ } else {
+ return array($assoc_type, $session_type);
+ }
+ }
+
+ /**
+ * @access private
+ */
+ function _negotiateAssociation($endpoint)
+ {
+ // Get our preferred session/association type from the negotiatior.
+ list($assoc_type, $session_type) = $this->negotiator->getAllowedType();
+
+ $assoc = $this->_requestAssociation(
+ $endpoint, $assoc_type, $session_type);
+
+ if (Auth_OpenID::isFailure($assoc)) {
+ return null;
+ }
+
+ if (is_a($assoc, 'Auth_OpenID_ServerErrorContainer')) {
+ $why = $assoc;
+
+ $supportedTypes = $this->_extractSupportedAssociationType(
+ $why, $endpoint, $assoc_type);
+
+ if ($supportedTypes !== null) {
+ list($assoc_type, $session_type) = $supportedTypes;
+
+ // Attempt to create an association from the assoc_type
+ // and session_type that the server told us it
+ // supported.
+ $assoc = $this->_requestAssociation(
+ $endpoint, $assoc_type, $session_type);
+
+ if (is_a($assoc, 'Auth_OpenID_ServerErrorContainer')) {
+ // Do not keep trying, since it rejected the
+ // association type that it told us to use.
+ // oidutil.log('Server %s refused its suggested association
+ // 'type: session_type=%s, assoc_type=%s'
+ // % (endpoint.server_url, session_type,
+ // assoc_type))
+ return null;
+ } else {
+ return $assoc;
+ }
+ } else {
+ return null;
+ }
+ } else {
+ return $assoc;
+ }
+ }
+
+ /**
+ * @access private
+ */
+ function _requestAssociation($endpoint, $assoc_type, $session_type)
+ {
+ list($assoc_session, $args) = $this->_createAssociateRequest(
+ $endpoint, $assoc_type, $session_type);
+
+ $response_message = $this->_makeKVPost($args, $endpoint->server_url);
+
+ if ($response_message === null) {
+ // oidutil.log('openid.associate request failed: %s' % (why[0],))
+ return null;
+ } else if (is_a($response_message,
+ 'Auth_OpenID_ServerErrorContainer')) {
+ return $response_message;
+ }
+
+ return $this->_extractAssociation($response_message, $assoc_session);
+ }
+
+ /**
+ * @access private
+ */
+ function _extractAssociation(&$assoc_response, &$assoc_session)
+ {
+ // Extract the common fields from the response, raising an
+ // exception if they are not found
+ $assoc_type = $assoc_response->getArg(
+ Auth_OpenID_OPENID_NS, 'assoc_type',
+ Auth_OpenID_NO_DEFAULT);
+
+ if (Auth_OpenID::isFailure($assoc_type)) {
+ return $assoc_type;
+ }
+
+ $assoc_handle = $assoc_response->getArg(
+ Auth_OpenID_OPENID_NS, 'assoc_handle',
+ Auth_OpenID_NO_DEFAULT);
+
+ if (Auth_OpenID::isFailure($assoc_handle)) {
+ return $assoc_handle;
+ }
+
+ // expires_in is a base-10 string. The Python parsing will
+ // accept literals that have whitespace around them and will
+ // accept negative values. Neither of these are really in-spec,
+ // but we think it's OK to accept them.
+ $expires_in_str = $assoc_response->getArg(
+ Auth_OpenID_OPENID_NS, 'expires_in',
+ Auth_OpenID_NO_DEFAULT);
+
+ if (Auth_OpenID::isFailure($expires_in_str)) {
+ return $expires_in_str;
+ }
+
+ $expires_in = Auth_OpenID::intval($expires_in_str);
+ if ($expires_in === false) {
+
+ $err = sprintf("Could not parse expires_in from association ".
+ "response %s", print_r($assoc_response, true));
+ return new Auth_OpenID_FailureResponse(null, $err);
+ }
+
+ // OpenID 1 has funny association session behaviour.
+ if ($assoc_response->isOpenID1()) {
+ $session_type = $this->_getOpenID1SessionType($assoc_response);
+ } else {
+ $session_type = $assoc_response->getArg(
+ Auth_OpenID_OPENID2_NS, 'session_type',
+ Auth_OpenID_NO_DEFAULT);
+
+ if (Auth_OpenID::isFailure($session_type)) {
+ return $session_type;
+ }
+ }
+
+ // Session type mismatch
+ if ($assoc_session->session_type != $session_type) {
+ if ($assoc_response->isOpenID1() &&
+ ($session_type == 'no-encryption')) {
+ // In OpenID 1, any association request can result in
+ // a 'no-encryption' association response. Setting
+ // assoc_session to a new no-encryption session should
+ // make the rest of this function work properly for
+ // that case.
+ $assoc_session = new Auth_OpenID_PlainTextConsumerSession();
+ } else {
+ // Any other mismatch, regardless of protocol version
+ // results in the failure of the association session
+ // altogether.
+ return null;
+ }
+ }
+
+ // Make sure assoc_type is valid for session_type
+ if (!in_array($assoc_type, $assoc_session->allowed_assoc_types)) {
+ return null;
+ }
+
+ // Delegate to the association session to extract the secret
+ // from the response, however is appropriate for that session
+ // type.
+ $secret = $assoc_session->extractSecret($assoc_response);
+
+ if ($secret === null) {
+ return null;
+ }
+
+ return Auth_OpenID_Association::fromExpiresIn(
+ $expires_in, $assoc_handle, $secret, $assoc_type);
+ }
+
+ /**
+ * @access private
+ */
+ function _createAssociateRequest($endpoint, $assoc_type, $session_type)
+ {
+ if (array_key_exists($session_type, $this->session_types)) {
+ $session_type_class = $this->session_types[$session_type];
+
+ if (is_callable($session_type_class)) {
+ $assoc_session = $session_type_class();
+ } else {
+ $assoc_session = new $session_type_class();
+ }
+ } else {
+ return null;
+ }
+
+ $args = array(
+ 'mode' => 'associate',
+ 'assoc_type' => $assoc_type);
+
+ if (!$endpoint->compatibilityMode()) {
+ $args['ns'] = Auth_OpenID_OPENID2_NS;
+ }
+
+ // Leave out the session type if we're in compatibility mode
+ // *and* it's no-encryption.
+ if ((!$endpoint->compatibilityMode()) ||
+ ($assoc_session->session_type != 'no-encryption')) {
+ $args['session_type'] = $assoc_session->session_type;
+ }
+
+ $args = array_merge($args, $assoc_session->getRequest());
+ $message = Auth_OpenID_Message::fromOpenIDArgs($args);
+ return array($assoc_session, $message);
+ }
+
+ /**
+ * Given an association response message, extract the OpenID 1.X
+ * session type.
+ *
+ * This function mostly takes care of the 'no-encryption' default
+ * behavior in OpenID 1.
+ *
+ * If the association type is plain-text, this function will
+ * return 'no-encryption'
+ *
+ * @access private
+ * @return $typ The association type for this message
+ */
+ function _getOpenID1SessionType($assoc_response)
+ {
+ // If it's an OpenID 1 message, allow session_type to default
+ // to None (which signifies "no-encryption")
+ $session_type = $assoc_response->getArg(Auth_OpenID_OPENID1_NS,
+ 'session_type');
+
+ // Handle the differences between no-encryption association
+ // respones in OpenID 1 and 2:
+
+ // no-encryption is not really a valid session type for OpenID
+ // 1, but we'll accept it anyway, while issuing a warning.
+ if ($session_type == 'no-encryption') {
+ // oidutil.log('WARNING: OpenID server sent "no-encryption"'
+ // 'for OpenID 1.X')
+ } else if (($session_type == '') || ($session_type === null)) {
+ // Missing or empty session type is the way to flag a
+ // 'no-encryption' response. Change the session type to
+ // 'no-encryption' so that it can be handled in the same
+ // way as OpenID 2 'no-encryption' respones.
+ $session_type = 'no-encryption';
+ }
+
+ return $session_type;
+ }
+}
+
+/**
+ * This class represents an authentication request from a consumer to
+ * an OpenID server.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_AuthRequest {
+
+ /**
+ * Initialize an authentication request with the specified token,
+ * association, and endpoint.
+ *
+ * Users of this library should not create instances of this
+ * class. Instances of this class are created by the library when
+ * needed.
+ */
+ function Auth_OpenID_AuthRequest(&$endpoint, $assoc)
+ {
+ $this->assoc = $assoc;
+ $this->endpoint =& $endpoint;
+ $this->return_to_args = array();
+ $this->message = new Auth_OpenID_Message(
+ $endpoint->preferredNamespace());
+ $this->_anonymous = false;
+ }
+
+ /**
+ * Add an extension to this checkid request.
+ *
+ * $extension_request: An object that implements the extension
+ * request interface for adding arguments to an OpenID message.
+ */
+ function addExtension(&$extension_request)
+ {
+ $extension_request->toMessage($this->message);
+ }
+
+ /**
+ * Add an extension argument to this OpenID authentication
+ * request.
+ *
+ * Use caution when adding arguments, because they will be
+ * URL-escaped and appended to the redirect URL, which can easily
+ * get quite long.
+ *
+ * @param string $namespace The namespace for the extension. For
+ * example, the simple registration extension uses the namespace
+ * 'sreg'.
+ *
+ * @param string $key The key within the extension namespace. For
+ * example, the nickname field in the simple registration
+ * extension's key is 'nickname'.
+ *
+ * @param string $value The value to provide to the server for
+ * this argument.
+ */
+ function addExtensionArg($namespace, $key, $value)
+ {
+ return $this->message->setArg($namespace, $key, $value);
+ }
+
+ /**
+ * Set whether this request should be made anonymously. If a
+ * request is anonymous, the identifier will not be sent in the
+ * request. This is only useful if you are making another kind of
+ * request with an extension in this request.
+ *
+ * Anonymous requests are not allowed when the request is made
+ * with OpenID 1.
+ */
+ function setAnonymous($is_anonymous)
+ {
+ if ($is_anonymous && $this->message->isOpenID1()) {
+ return false;
+ } else {
+ $this->_anonymous = $is_anonymous;
+ return true;
+ }
+ }
+
+ /**
+ * Produce a {@link Auth_OpenID_Message} representing this
+ * request.
+ *
+ * @param string $realm The URL (or URL pattern) that identifies
+ * your web site to the user when she is authorizing it.
+ *
+ * @param string $return_to The URL that the OpenID provider will
+ * send the user back to after attempting to verify her identity.
+ *
+ * Not specifying a return_to URL means that the user will not be
+ * returned to the site issuing the request upon its completion.
+ *
+ * @param bool $immediate If true, the OpenID provider is to send
+ * back a response immediately, useful for behind-the-scenes
+ * authentication attempts. Otherwise the OpenID provider may
+ * engage the user before providing a response. This is the
+ * default case, as the user may need to provide credentials or
+ * approve the request before a positive response can be sent.
+ */
+ function getMessage($realm, $return_to=null, $immediate=false)
+ {
+ if ($return_to) {
+ $return_to = Auth_OpenID::appendArgs($return_to,
+ $this->return_to_args);
+ } else if ($immediate) {
+ // raise ValueError(
+ // '"return_to" is mandatory when
+ //using "checkid_immediate"')
+ return new Auth_OpenID_FailureResponse(null,
+ "'return_to' is mandatory when using checkid_immediate");
+ } else if ($this->message->isOpenID1()) {
+ // raise ValueError('"return_to" is
+ // mandatory for OpenID 1 requests')
+ return new Auth_OpenID_FailureResponse(null,
+ "'return_to' is mandatory for OpenID 1 requests");
+ } else if ($this->return_to_args) {
+ // raise ValueError('extra "return_to" arguments
+ // were specified, but no return_to was specified')
+ return new Auth_OpenID_FailureResponse(null,
+ "extra 'return_to' arguments where specified, " .
+ "but no return_to was specified");
+ }
+
+ if ($immediate) {
+ $mode = 'checkid_immediate';
+ } else {
+ $mode = 'checkid_setup';
+ }
+
+ $message = $this->message->copy();
+ if ($message->isOpenID1()) {
+ $realm_key = 'trust_root';
+ } else {
+ $realm_key = 'realm';
+ }
+
+ $message->updateArgs(Auth_OpenID_OPENID_NS,
+ array(
+ $realm_key => $realm,
+ 'mode' => $mode,
+ 'return_to' => $return_to));
+
+ if (!$this->_anonymous) {
+ if ($this->endpoint->isOPIdentifier()) {
+ // This will never happen when we're in compatibility
+ // mode, as long as isOPIdentifier() returns False
+ // whenever preferredNamespace() returns OPENID1_NS.
+ $claimed_id = $request_identity =
+ Auth_OpenID_IDENTIFIER_SELECT;
+ } else {
+ $request_identity = $this->endpoint->getLocalID();
+ $claimed_id = $this->endpoint->claimed_id;
+ }
+
+ // This is true for both OpenID 1 and 2
+ $message->setArg(Auth_OpenID_OPENID_NS, 'identity',
+ $request_identity);
+
+ if ($message->isOpenID2()) {
+ $message->setArg(Auth_OpenID_OPENID2_NS, 'claimed_id',
+ $claimed_id);
+ }
+ }
+
+ if ($this->assoc) {
+ $message->setArg(Auth_OpenID_OPENID_NS, 'assoc_handle',
+ $this->assoc->handle);
+ }
+
+ return $message;
+ }
+
+ function redirectURL($realm, $return_to = null,
+ $immediate = false)
+ {
+ $message = $this->getMessage($realm, $return_to, $immediate);
+
+ if (Auth_OpenID::isFailure($message)) {
+ return $message;
+ }
+
+ return $message->toURL($this->endpoint->server_url);
+ }
+
+ /**
+ * Get html for a form to submit this request to the IDP.
+ *
+ * form_tag_attrs: An array of attributes to be added to the form
+ * tag. 'accept-charset' and 'enctype' have defaults that can be
+ * overridden. If a value is supplied for 'action' or 'method', it
+ * will be replaced.
+ */
+ function formMarkup($realm, $return_to=null, $immediate=false,
+ $form_tag_attrs=null)
+ {
+ $message = $this->getMessage($realm, $return_to, $immediate);
+
+ if (Auth_OpenID::isFailure($message)) {
+ return $message;
+ }
+
+ return $message->toFormMarkup($this->endpoint->server_url,
+ $form_tag_attrs);
+ }
+
+ /**
+ * Get a complete html document that will autosubmit the request
+ * to the IDP.
+ *
+ * Wraps formMarkup. See the documentation for that function.
+ */
+ function htmlMarkup($realm, $return_to=null, $immediate=false,
+ $form_tag_attrs=null)
+ {
+ $form = $this->formMarkup($realm, $return_to, $immediate,
+ $form_tag_attrs);
+
+ if (Auth_OpenID::isFailure($form)) {
+ return $form;
+ }
+ return Auth_OpenID::autoSubmitHTML($form);
+ }
+
+ function shouldSendRedirect()
+ {
+ return $this->endpoint->compatibilityMode();
+ }
+}
+
+/**
+ * The base class for responses from the Auth_OpenID_Consumer.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_ConsumerResponse {
+ var $status = null;
+
+ function setEndpoint($endpoint)
+ {
+ $this->endpoint = $endpoint;
+ if ($endpoint === null) {
+ $this->identity_url = null;
+ } else {
+ $this->identity_url = $endpoint->claimed_id;
+ }
+ }
+
+ /**
+ * Return the display identifier for this response.
+ *
+ * The display identifier is related to the Claimed Identifier, but the
+ * two are not always identical. The display identifier is something the
+ * user should recognize as what they entered, whereas the response's
+ * claimed identifier (in the identity_url attribute) may have extra
+ * information for better persistence.
+ *
+ * URLs will be stripped of their fragments for display. XRIs will
+ * display the human-readable identifier (i-name) instead of the
+ * persistent identifier (i-number).
+ *
+ * Use the display identifier in your user interface. Use
+ * identity_url for querying your database or authorization server.
+ *
+ */
+ function getDisplayIdentifier()
+ {
+ if ($this->endpoint !== null) {
+ return $this->endpoint->getDisplayIdentifier();
+ }
+ return null;
+ }
+}
+
+/**
+ * A response with a status of Auth_OpenID_SUCCESS. Indicates that
+ * this request is a successful acknowledgement from the OpenID server
+ * that the supplied URL is, indeed controlled by the requesting
+ * agent. This has three relevant attributes:
+ *
+ * claimed_id - The identity URL that has been authenticated
+ *
+ * signed_args - The arguments in the server's response that were
+ * signed and verified.
+ *
+ * status - Auth_OpenID_SUCCESS.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_SuccessResponse extends Auth_OpenID_ConsumerResponse {
+ var $status = Auth_OpenID_SUCCESS;
+
+ /**
+ * @access private
+ */
+ function Auth_OpenID_SuccessResponse($endpoint, $message, $signed_args=null)
+ {
+ $this->endpoint = $endpoint;
+ $this->identity_url = $endpoint->claimed_id;
+ $this->signed_args = $signed_args;
+ $this->message = $message;
+
+ if ($this->signed_args === null) {
+ $this->signed_args = array();
+ }
+ }
+
+ /**
+ * Extract signed extension data from the server's response.
+ *
+ * @param string $prefix The extension namespace from which to
+ * extract the extension data.
+ */
+ function extensionResponse($namespace_uri, $require_signed)
+ {
+ if ($require_signed) {
+ return $this->getSignedNS($namespace_uri);
+ } else {
+ return $this->message->getArgs($namespace_uri);
+ }
+ }
+
+ function isOpenID1()
+ {
+ return $this->message->isOpenID1();
+ }
+
+ function isSigned($ns_uri, $ns_key)
+ {
+ // Return whether a particular key is signed, regardless of
+ // its namespace alias
+ return in_array($this->message->getKey($ns_uri, $ns_key),
+ $this->signed_args);
+ }
+
+ function getSigned($ns_uri, $ns_key, $default = null)
+ {
+ // Return the specified signed field if available, otherwise
+ // return default
+ if ($this->isSigned($ns_uri, $ns_key)) {
+ return $this->message->getArg($ns_uri, $ns_key, $default);
+ } else {
+ return $default;
+ }
+ }
+
+ function getSignedNS($ns_uri)
+ {
+ $args = array();
+
+ $msg_args = $this->message->getArgs($ns_uri);
+ if (Auth_OpenID::isFailure($msg_args)) {
+ return null;
+ }
+
+ foreach ($msg_args as $key => $value) {
+ if (!$this->isSigned($ns_uri, $key)) {
+ return null;
+ }
+ }
+
+ return $msg_args;
+ }
+
+ /**
+ * Get the openid.return_to argument from this response.
+ *
+ * This is useful for verifying that this request was initiated by
+ * this consumer.
+ *
+ * @return string $return_to The return_to URL supplied to the
+ * server on the initial request, or null if the response did not
+ * contain an 'openid.return_to' argument.
+ */
+ function getReturnTo()
+ {
+ return $this->getSigned(Auth_OpenID_OPENID_NS, 'return_to');
+ }
+}
+
+/**
+ * A response with a status of Auth_OpenID_FAILURE. Indicates that the
+ * OpenID protocol has failed. This could be locally or remotely
+ * triggered. This has three relevant attributes:
+ *
+ * claimed_id - The identity URL for which authentication was
+ * attempted, if it can be determined. Otherwise, null.
+ *
+ * message - A message indicating why the request failed, if one is
+ * supplied. Otherwise, null.
+ *
+ * status - Auth_OpenID_FAILURE.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_FailureResponse extends Auth_OpenID_ConsumerResponse {
+ var $status = Auth_OpenID_FAILURE;
+
+ function Auth_OpenID_FailureResponse($endpoint, $message = null,
+ $contact = null, $reference = null)
+ {
+ $this->setEndpoint($endpoint);
+ $this->message = $message;
+ $this->contact = $contact;
+ $this->reference = $reference;
+ }
+}
+
+/**
+ * A specific, internal failure used to detect type URI mismatch.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_TypeURIMismatch extends Auth_OpenID_FailureResponse {
+}
+
+/**
+ * Exception that is raised when the server returns a 400 response
+ * code to a direct request.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_ServerErrorContainer {
+ function Auth_OpenID_ServerErrorContainer($error_text,
+ $error_code,
+ $message)
+ {
+ $this->error_text = $error_text;
+ $this->error_code = $error_code;
+ $this->message = $message;
+ }
+
+ /**
+ * @access private
+ */
+ function fromMessage($message)
+ {
+ $error_text = $message->getArg(
+ Auth_OpenID_OPENID_NS, 'error', '<no error message supplied>');
+ $error_code = $message->getArg(Auth_OpenID_OPENID_NS, 'error_code');
+ return new Auth_OpenID_ServerErrorContainer($error_text,
+ $error_code,
+ $message);
+ }
+}
+
+/**
+ * A response with a status of Auth_OpenID_CANCEL. Indicates that the
+ * user cancelled the OpenID authentication request. This has two
+ * relevant attributes:
+ *
+ * claimed_id - The identity URL for which authentication was
+ * attempted, if it can be determined. Otherwise, null.
+ *
+ * status - Auth_OpenID_SUCCESS.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_CancelResponse extends Auth_OpenID_ConsumerResponse {
+ var $status = Auth_OpenID_CANCEL;
+
+ function Auth_OpenID_CancelResponse($endpoint)
+ {
+ $this->setEndpoint($endpoint);
+ }
+}
+
+/**
+ * A response with a status of Auth_OpenID_SETUP_NEEDED. Indicates
+ * that the request was in immediate mode, and the server is unable to
+ * authenticate the user without further interaction.
+ *
+ * claimed_id - The identity URL for which authentication was
+ * attempted.
+ *
+ * setup_url - A URL that can be used to send the user to the server
+ * to set up for authentication. The user should be redirected in to
+ * the setup_url, either in the current window or in a new browser
+ * window. Null in OpenID 2.
+ *
+ * status - Auth_OpenID_SETUP_NEEDED.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_SetupNeededResponse extends Auth_OpenID_ConsumerResponse {
+ var $status = Auth_OpenID_SETUP_NEEDED;
+
+ function Auth_OpenID_SetupNeededResponse($endpoint,
+ $setup_url = null)
+ {
+ $this->setEndpoint($endpoint);
+ $this->setup_url = $setup_url;
+ }
+}
+
+?>
diff --git a/extlib/Auth/OpenID/CryptUtil.php b/extlib/Auth/OpenID/CryptUtil.php
new file mode 100644
index 000000000..aacc3cd39
--- /dev/null
+++ b/extlib/Auth/OpenID/CryptUtil.php
@@ -0,0 +1,109 @@
+<?php
+
+/**
+ * CryptUtil: A suite of wrapper utility functions for the OpenID
+ * library.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @access private
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005-2008 Janrain, Inc.
+ * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
+ */
+
+if (!defined('Auth_OpenID_RAND_SOURCE')) {
+ /**
+ * The filename for a source of random bytes. Define this yourself
+ * if you have a different source of randomness.
+ */
+ define('Auth_OpenID_RAND_SOURCE', '/dev/urandom');
+}
+
+class Auth_OpenID_CryptUtil {
+ /**
+ * Get the specified number of random bytes.
+ *
+ * Attempts to use a cryptographically secure (not predictable)
+ * source of randomness if available. If there is no high-entropy
+ * randomness source available, it will fail. As a last resort,
+ * for non-critical systems, define
+ * <code>Auth_OpenID_RAND_SOURCE</code> as <code>null</code>, and
+ * the code will fall back on a pseudo-random number generator.
+ *
+ * @param int $num_bytes The length of the return value
+ * @return string $bytes random bytes
+ */
+ function getBytes($num_bytes)
+ {
+ static $f = null;
+ $bytes = '';
+ if ($f === null) {
+ if (Auth_OpenID_RAND_SOURCE === null) {
+ $f = false;
+ } else {
+ $f = @fopen(Auth_OpenID_RAND_SOURCE, "r");
+ if ($f === false) {
+ $msg = 'Define Auth_OpenID_RAND_SOURCE as null to ' .
+ ' continue with an insecure random number generator.';
+ trigger_error($msg, E_USER_ERROR);
+ }
+ }
+ }
+ if ($f === false) {
+ // pseudorandom used
+ $bytes = '';
+ for ($i = 0; $i < $num_bytes; $i += 4) {
+ $bytes .= pack('L', mt_rand());
+ }
+ $bytes = substr($bytes, 0, $num_bytes);
+ } else {
+ $bytes = fread($f, $num_bytes);
+ }
+ return $bytes;
+ }
+
+ /**
+ * Produce a string of length random bytes, chosen from chrs. If
+ * $chrs is null, the resulting string may contain any characters.
+ *
+ * @param integer $length The length of the resulting
+ * randomly-generated string
+ * @param string $chrs A string of characters from which to choose
+ * to build the new string
+ * @return string $result A string of randomly-chosen characters
+ * from $chrs
+ */
+ function randomString($length, $population = null)
+ {
+ if ($population === null) {
+ return Auth_OpenID_CryptUtil::getBytes($length);
+ }
+
+ $popsize = strlen($population);
+
+ if ($popsize > 256) {
+ $msg = 'More than 256 characters supplied to ' . __FUNCTION__;
+ trigger_error($msg, E_USER_ERROR);
+ }
+
+ $duplicate = 256 % $popsize;
+
+ $str = "";
+ for ($i = 0; $i < $length; $i++) {
+ do {
+ $n = ord(Auth_OpenID_CryptUtil::getBytes(1));
+ } while ($n < $duplicate);
+
+ $n %= $popsize;
+ $str .= $population[$n];
+ }
+
+ return $str;
+ }
+}
+
+?> \ No newline at end of file
diff --git a/extlib/Auth/OpenID/DatabaseConnection.php b/extlib/Auth/OpenID/DatabaseConnection.php
new file mode 100644
index 000000000..9db6e0eb3
--- /dev/null
+++ b/extlib/Auth/OpenID/DatabaseConnection.php
@@ -0,0 +1,131 @@
+<?php
+
+/**
+ * The Auth_OpenID_DatabaseConnection class, which is used to emulate
+ * a PEAR database connection.
+ *
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005-2008 Janrain, Inc.
+ * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
+ */
+
+/**
+ * An empty base class intended to emulate PEAR connection
+ * functionality in applications that supply their own database
+ * abstraction mechanisms. See {@link Auth_OpenID_SQLStore} for more
+ * information. You should subclass this class if you need to create
+ * an SQL store that needs to access its database using an
+ * application's database abstraction layer instead of a PEAR database
+ * connection. Any subclass of Auth_OpenID_DatabaseConnection MUST
+ * adhere to the interface specified here.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_DatabaseConnection {
+ /**
+ * Sets auto-commit mode on this database connection.
+ *
+ * @param bool $mode True if auto-commit is to be used; false if
+ * not.
+ */
+ function autoCommit($mode)
+ {
+ }
+
+ /**
+ * Run an SQL query with the specified parameters, if any.
+ *
+ * @param string $sql An SQL string with placeholders. The
+ * placeholders are assumed to be specific to the database engine
+ * for this connection.
+ *
+ * @param array $params An array of parameters to insert into the
+ * SQL string using this connection's escaping mechanism.
+ *
+ * @return mixed $result The result of calling this connection's
+ * internal query function. The type of result depends on the
+ * underlying database engine. This method is usually used when
+ * the result of a query is not important, like a DDL query.
+ */
+ function query($sql, $params = array())
+ {
+ }
+
+ /**
+ * Starts a transaction on this connection, if supported.
+ */
+ function begin()
+ {
+ }
+
+ /**
+ * Commits a transaction on this connection, if supported.
+ */
+ function commit()
+ {
+ }
+
+ /**
+ * Performs a rollback on this connection, if supported.
+ */
+ function rollback()
+ {
+ }
+
+ /**
+ * Run an SQL query and return the first column of the first row
+ * of the result set, if any.
+ *
+ * @param string $sql An SQL string with placeholders. The
+ * placeholders are assumed to be specific to the database engine
+ * for this connection.
+ *
+ * @param array $params An array of parameters to insert into the
+ * SQL string using this connection's escaping mechanism.
+ *
+ * @return mixed $result The value of the first column of the
+ * first row of the result set. False if no such result was
+ * found.
+ */
+ function getOne($sql, $params = array())
+ {
+ }
+
+ /**
+ * Run an SQL query and return the first row of the result set, if
+ * any.
+ *
+ * @param string $sql An SQL string with placeholders. The
+ * placeholders are assumed to be specific to the database engine
+ * for this connection.
+ *
+ * @param array $params An array of parameters to insert into the
+ * SQL string using this connection's escaping mechanism.
+ *
+ * @return array $result The first row of the result set, if any,
+ * keyed on column name. False if no such result was found.
+ */
+ function getRow($sql, $params = array())
+ {
+ }
+
+ /**
+ * Run an SQL query with the specified parameters, if any.
+ *
+ * @param string $sql An SQL string with placeholders. The
+ * placeholders are assumed to be specific to the database engine
+ * for this connection.
+ *
+ * @param array $params An array of parameters to insert into the
+ * SQL string using this connection's escaping mechanism.
+ *
+ * @return array $result An array of arrays representing the
+ * result of the query; each array is keyed on column name.
+ */
+ function getAll($sql, $params = array())
+ {
+ }
+}
+
+?> \ No newline at end of file
diff --git a/extlib/Auth/OpenID/DiffieHellman.php b/extlib/Auth/OpenID/DiffieHellman.php
new file mode 100644
index 000000000..f4ded7eba
--- /dev/null
+++ b/extlib/Auth/OpenID/DiffieHellman.php
@@ -0,0 +1,113 @@
+<?php
+
+/**
+ * The OpenID library's Diffie-Hellman implementation.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @access private
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005-2008 Janrain, Inc.
+ * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
+ */
+
+require_once 'Auth/OpenID.php';
+require_once 'Auth/OpenID/BigMath.php';
+
+function Auth_OpenID_getDefaultMod()
+{
+ return '155172898181473697471232257763715539915724801'.
+ '966915404479707795314057629378541917580651227423'.
+ '698188993727816152646631438561595825688188889951'.
+ '272158842675419950341258706556549803580104870537'.
+ '681476726513255747040765857479291291572334510643'.
+ '245094715007229621094194349783925984760375594985'.
+ '848253359305585439638443';
+}
+
+function Auth_OpenID_getDefaultGen()
+{
+ return '2';
+}
+
+/**
+ * The Diffie-Hellman key exchange class. This class relies on
+ * {@link Auth_OpenID_MathLibrary} to perform large number operations.
+ *
+ * @access private
+ * @package OpenID
+ */
+class Auth_OpenID_DiffieHellman {
+
+ var $mod;
+ var $gen;
+ var $private;
+ var $lib = null;
+
+ function Auth_OpenID_DiffieHellman($mod = null, $gen = null,
+ $private = null, $lib = null)
+ {
+ if ($lib === null) {
+ $this->lib =& Auth_OpenID_getMathLib();
+ } else {
+ $this->lib =& $lib;
+ }
+
+ if ($mod === null) {
+ $this->mod = $this->lib->init(Auth_OpenID_getDefaultMod());
+ } else {
+ $this->mod = $mod;
+ }
+
+ if ($gen === null) {
+ $this->gen = $this->lib->init(Auth_OpenID_getDefaultGen());
+ } else {
+ $this->gen = $gen;
+ }
+
+ if ($private === null) {
+ $r = $this->lib->rand($this->mod);
+ $this->private = $this->lib->add($r, 1);
+ } else {
+ $this->private = $private;
+ }
+
+ $this->public = $this->lib->powmod($this->gen, $this->private,
+ $this->mod);
+ }
+
+ function getSharedSecret($composite)
+ {
+ return $this->lib->powmod($composite, $this->private, $this->mod);
+ }
+
+ function getPublicKey()
+ {
+ return $this->public;
+ }
+
+ function usingDefaultValues()
+ {
+ return ($this->mod == Auth_OpenID_getDefaultMod() &&
+ $this->gen == Auth_OpenID_getDefaultGen());
+ }
+
+ function xorSecret($composite, $secret, $hash_func)
+ {
+ $dh_shared = $this->getSharedSecret($composite);
+ $dh_shared_str = $this->lib->longToBinary($dh_shared);
+ $hash_dh_shared = $hash_func($dh_shared_str);
+
+ $xsecret = "";
+ for ($i = 0; $i < Auth_OpenID::bytes($secret); $i++) {
+ $xsecret .= chr(ord($secret[$i]) ^ ord($hash_dh_shared[$i]));
+ }
+
+ return $xsecret;
+ }
+}
+
+?>
diff --git a/extlib/Auth/OpenID/Discover.php b/extlib/Auth/OpenID/Discover.php
new file mode 100644
index 000000000..62aeb1d2b
--- /dev/null
+++ b/extlib/Auth/OpenID/Discover.php
@@ -0,0 +1,548 @@
+<?php
+
+/**
+ * The OpenID and Yadis discovery implementation for OpenID 1.2.
+ */
+
+require_once "Auth/OpenID.php";
+require_once "Auth/OpenID/Parse.php";
+require_once "Auth/OpenID/Message.php";
+require_once "Auth/Yadis/XRIRes.php";
+require_once "Auth/Yadis/Yadis.php";
+
+// XML namespace value
+define('Auth_OpenID_XMLNS_1_0', 'http://openid.net/xmlns/1.0');
+
+// Yadis service types
+define('Auth_OpenID_TYPE_1_2', 'http://openid.net/signon/1.2');
+define('Auth_OpenID_TYPE_1_1', 'http://openid.net/signon/1.1');
+define('Auth_OpenID_TYPE_1_0', 'http://openid.net/signon/1.0');
+define('Auth_OpenID_TYPE_2_0_IDP', 'http://specs.openid.net/auth/2.0/server');
+define('Auth_OpenID_TYPE_2_0', 'http://specs.openid.net/auth/2.0/signon');
+define('Auth_OpenID_RP_RETURN_TO_URL_TYPE',
+ 'http://specs.openid.net/auth/2.0/return_to');
+
+function Auth_OpenID_getOpenIDTypeURIs()
+{
+ return array(Auth_OpenID_TYPE_2_0_IDP,
+ Auth_OpenID_TYPE_2_0,
+ Auth_OpenID_TYPE_1_2,
+ Auth_OpenID_TYPE_1_1,
+ Auth_OpenID_TYPE_1_0,
+ Auth_OpenID_RP_RETURN_TO_URL_TYPE);
+}
+
+/**
+ * Object representing an OpenID service endpoint.
+ */
+class Auth_OpenID_ServiceEndpoint {
+ function Auth_OpenID_ServiceEndpoint()
+ {
+ $this->claimed_id = null;
+ $this->server_url = null;
+ $this->type_uris = array();
+ $this->local_id = null;
+ $this->canonicalID = null;
+ $this->used_yadis = false; // whether this came from an XRDS
+ $this->display_identifier = null;
+ }
+
+ function getDisplayIdentifier()
+ {
+ if ($this->display_identifier) {
+ return $this->display_identifier;
+ }
+ if (! $this->claimed_id) {
+ return $this->claimed_id;
+ }
+ $parsed = parse_url($this->claimed_id);
+ $scheme = $parsed['scheme'];
+ $host = $parsed['host'];
+ $path = $parsed['path'];
+ if (array_key_exists('query', $parsed)) {
+ $query = $parsed['query'];
+ $no_frag = "$scheme://$host$path?$query";
+ } else {
+ $no_frag = "$scheme://$host$path";
+ }
+ return $no_frag;
+ }
+
+ function usesExtension($extension_uri)
+ {
+ return in_array($extension_uri, $this->type_uris);
+ }
+
+ function preferredNamespace()
+ {
+ if (in_array(Auth_OpenID_TYPE_2_0_IDP, $this->type_uris) ||
+ in_array(Auth_OpenID_TYPE_2_0, $this->type_uris)) {
+ return Auth_OpenID_OPENID2_NS;
+ } else {
+ return Auth_OpenID_OPENID1_NS;
+ }
+ }
+
+ /*
+ * Query this endpoint to see if it has any of the given type
+ * URIs. This is useful for implementing other endpoint classes
+ * that e.g. need to check for the presence of multiple versions
+ * of a single protocol.
+ *
+ * @param $type_uris The URIs that you wish to check
+ *
+ * @return all types that are in both in type_uris and
+ * $this->type_uris
+ */
+ function matchTypes($type_uris)
+ {
+ $result = array();
+ foreach ($type_uris as $test_uri) {
+ if ($this->supportsType($test_uri)) {
+ $result[] = $test_uri;
+ }
+ }
+
+ return $result;
+ }
+
+ function supportsType($type_uri)
+ {
+ // Does this endpoint support this type?
+ return ((in_array($type_uri, $this->type_uris)) ||
+ (($type_uri == Auth_OpenID_TYPE_2_0) &&
+ $this->isOPIdentifier()));
+ }
+
+ function compatibilityMode()
+ {
+ return $this->preferredNamespace() != Auth_OpenID_OPENID2_NS;
+ }
+
+ function isOPIdentifier()
+ {
+ return in_array(Auth_OpenID_TYPE_2_0_IDP, $this->type_uris);
+ }
+
+ function fromOPEndpointURL($op_endpoint_url)
+ {
+ // Construct an OP-Identifier OpenIDServiceEndpoint object for
+ // a given OP Endpoint URL
+ $obj = new Auth_OpenID_ServiceEndpoint();
+ $obj->server_url = $op_endpoint_url;
+ $obj->type_uris = array(Auth_OpenID_TYPE_2_0_IDP);
+ return $obj;
+ }
+
+ function parseService($yadis_url, $uri, $type_uris, $service_element)
+ {
+ // Set the state of this object based on the contents of the
+ // service element. Return true if successful, false if not
+ // (if findOPLocalIdentifier returns false).
+ $this->type_uris = $type_uris;
+ $this->server_url = $uri;
+ $this->used_yadis = true;
+
+ if (!$this->isOPIdentifier()) {
+ $this->claimed_id = $yadis_url;
+ $this->local_id = Auth_OpenID_findOPLocalIdentifier(
+ $service_element,
+ $this->type_uris);
+ if ($this->local_id === false) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ function getLocalID()
+ {
+ // Return the identifier that should be sent as the
+ // openid.identity_url parameter to the server.
+ if ($this->local_id === null && $this->canonicalID === null) {
+ return $this->claimed_id;
+ } else {
+ if ($this->local_id) {
+ return $this->local_id;
+ } else {
+ return $this->canonicalID;
+ }
+ }
+ }
+
+ /*
+ * Parse the given document as XRDS looking for OpenID services.
+ *
+ * @return array of Auth_OpenID_ServiceEndpoint or null if the
+ * document cannot be parsed.
+ */
+ function fromXRDS($uri, $xrds_text)
+ {
+ $xrds =& Auth_Yadis_XRDS::parseXRDS($xrds_text);
+
+ if ($xrds) {
+ $yadis_services =
+ $xrds->services(array('filter_MatchesAnyOpenIDType'));
+ return Auth_OpenID_makeOpenIDEndpoints($uri, $yadis_services);
+ }
+
+ return null;
+ }
+
+ /*
+ * Create endpoints from a DiscoveryResult.
+ *
+ * @param discoveryResult Auth_Yadis_DiscoveryResult
+ * @return array of Auth_OpenID_ServiceEndpoint or null if
+ * endpoints cannot be created.
+ */
+ function fromDiscoveryResult($discoveryResult)
+ {
+ if ($discoveryResult->isXRDS()) {
+ return Auth_OpenID_ServiceEndpoint::fromXRDS(
+ $discoveryResult->normalized_uri,
+ $discoveryResult->response_text);
+ } else {
+ return Auth_OpenID_ServiceEndpoint::fromHTML(
+ $discoveryResult->normalized_uri,
+ $discoveryResult->response_text);
+ }
+ }
+
+ function fromHTML($uri, $html)
+ {
+ $discovery_types = array(
+ array(Auth_OpenID_TYPE_2_0,
+ 'openid2.provider', 'openid2.local_id'),
+ array(Auth_OpenID_TYPE_1_1,
+ 'openid.server', 'openid.delegate')
+ );
+
+ $services = array();
+
+ foreach ($discovery_types as $triple) {
+ list($type_uri, $server_rel, $delegate_rel) = $triple;
+
+ $urls = Auth_OpenID_legacy_discover($html, $server_rel,
+ $delegate_rel);
+
+ if ($urls === false) {
+ continue;
+ }
+
+ list($delegate_url, $server_url) = $urls;
+
+ $service = new Auth_OpenID_ServiceEndpoint();
+ $service->claimed_id = $uri;
+ $service->local_id = $delegate_url;
+ $service->server_url = $server_url;
+ $service->type_uris = array($type_uri);
+
+ $services[] = $service;
+ }
+
+ return $services;
+ }
+
+ function copy()
+ {
+ $x = new Auth_OpenID_ServiceEndpoint();
+
+ $x->claimed_id = $this->claimed_id;
+ $x->server_url = $this->server_url;
+ $x->type_uris = $this->type_uris;
+ $x->local_id = $this->local_id;
+ $x->canonicalID = $this->canonicalID;
+ $x->used_yadis = $this->used_yadis;
+
+ return $x;
+ }
+}
+
+function Auth_OpenID_findOPLocalIdentifier($service, $type_uris)
+{
+ // Extract a openid:Delegate value from a Yadis Service element.
+ // If no delegate is found, returns null. Returns false on
+ // discovery failure (when multiple delegate/localID tags have
+ // different values).
+
+ $service->parser->registerNamespace('openid',
+ Auth_OpenID_XMLNS_1_0);
+
+ $service->parser->registerNamespace('xrd',
+ Auth_Yadis_XMLNS_XRD_2_0);
+
+ $parser =& $service->parser;
+
+ $permitted_tags = array();
+
+ if (in_array(Auth_OpenID_TYPE_1_1, $type_uris) ||
+ in_array(Auth_OpenID_TYPE_1_0, $type_uris)) {
+ $permitted_tags[] = 'openid:Delegate';
+ }
+
+ if (in_array(Auth_OpenID_TYPE_2_0, $type_uris)) {
+ $permitted_tags[] = 'xrd:LocalID';
+ }
+
+ $local_id = null;
+
+ foreach ($permitted_tags as $tag_name) {
+ $tags = $service->getElements($tag_name);
+
+ foreach ($tags as $tag) {
+ $content = $parser->content($tag);
+
+ if ($local_id === null) {
+ $local_id = $content;
+ } else if ($local_id != $content) {
+ return false;
+ }
+ }
+ }
+
+ return $local_id;
+}
+
+function filter_MatchesAnyOpenIDType(&$service)
+{
+ $uris = $service->getTypes();
+
+ foreach ($uris as $uri) {
+ if (in_array($uri, Auth_OpenID_getOpenIDTypeURIs())) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+function Auth_OpenID_bestMatchingService($service, $preferred_types)
+{
+ // Return the index of the first matching type, or something
+ // higher if no type matches.
+ //
+ // This provides an ordering in which service elements that
+ // contain a type that comes earlier in the preferred types list
+ // come before service elements that come later. If a service
+ // element has more than one type, the most preferred one wins.
+
+ foreach ($preferred_types as $index => $typ) {
+ if (in_array($typ, $service->type_uris)) {
+ return $index;
+ }
+ }
+
+ return count($preferred_types);
+}
+
+function Auth_OpenID_arrangeByType($service_list, $preferred_types)
+{
+ // Rearrange service_list in a new list so services are ordered by
+ // types listed in preferred_types. Return the new list.
+
+ // Build a list with the service elements in tuples whose
+ // comparison will prefer the one with the best matching service
+ $prio_services = array();
+ foreach ($service_list as $index => $service) {
+ $prio_services[] = array(Auth_OpenID_bestMatchingService($service,
+ $preferred_types),
+ $index, $service);
+ }
+
+ sort($prio_services);
+
+ // Now that the services are sorted by priority, remove the sort
+ // keys from the list.
+ foreach ($prio_services as $index => $s) {
+ $prio_services[$index] = $prio_services[$index][2];
+ }
+
+ return $prio_services;
+}
+
+// Extract OP Identifier services. If none found, return the rest,
+// sorted with most preferred first according to
+// OpenIDServiceEndpoint.openid_type_uris.
+//
+// openid_services is a list of OpenIDServiceEndpoint objects.
+//
+// Returns a list of OpenIDServiceEndpoint objects."""
+function Auth_OpenID_getOPOrUserServices($openid_services)
+{
+ $op_services = Auth_OpenID_arrangeByType($openid_services,
+ array(Auth_OpenID_TYPE_2_0_IDP));
+
+ $openid_services = Auth_OpenID_arrangeByType($openid_services,
+ Auth_OpenID_getOpenIDTypeURIs());
+
+ if ($op_services) {
+ return $op_services;
+ } else {
+ return $openid_services;
+ }
+}
+
+function Auth_OpenID_makeOpenIDEndpoints($uri, $yadis_services)
+{
+ $s = array();
+
+ if (!$yadis_services) {
+ return $s;
+ }
+
+ foreach ($yadis_services as $service) {
+ $type_uris = $service->getTypes();
+ $uris = $service->getURIs();
+
+ // If any Type URIs match and there is an endpoint URI
+ // specified, then this is an OpenID endpoint
+ if ($type_uris &&
+ $uris) {
+ foreach ($uris as $service_uri) {
+ $openid_endpoint = new Auth_OpenID_ServiceEndpoint();
+ if ($openid_endpoint->parseService($uri,
+ $service_uri,
+ $type_uris,
+ $service)) {
+ $s[] = $openid_endpoint;
+ }
+ }
+ }
+ }
+
+ return $s;
+}
+
+function Auth_OpenID_discoverWithYadis($uri, &$fetcher,
+ $endpoint_filter='Auth_OpenID_getOPOrUserServices',
+ $discover_function=null)
+{
+ // Discover OpenID services for a URI. Tries Yadis and falls back
+ // on old-style <link rel='...'> discovery if Yadis fails.
+
+ // Might raise a yadis.discover.DiscoveryFailure if no document
+ // came back for that URI at all. I don't think falling back to
+ // OpenID 1.0 discovery on the same URL will help, so don't bother
+ // to catch it.
+ if ($discover_function === null) {
+ $discover_function = array('Auth_Yadis_Yadis', 'discover');
+ }
+
+ $openid_services = array();
+
+ $response = call_user_func_array($discover_function,
+ array($uri, &$fetcher));
+
+ $yadis_url = $response->normalized_uri;
+ $yadis_services = array();
+
+ if ($response->isFailure()) {
+ return array($uri, array());
+ }
+
+ $openid_services = Auth_OpenID_ServiceEndpoint::fromXRDS(
+ $yadis_url,
+ $response->response_text);
+
+ if (!$openid_services) {
+ if ($response->isXRDS()) {
+ return Auth_OpenID_discoverWithoutYadis($uri,
+ $fetcher);
+ }
+
+ // Try to parse the response as HTML to get OpenID 1.0/1.1
+ // <link rel="...">
+ $openid_services = Auth_OpenID_ServiceEndpoint::fromHTML(
+ $yadis_url,
+ $response->response_text);
+ }
+
+ $openid_services = call_user_func_array($endpoint_filter,
+ array(&$openid_services));
+
+ return array($yadis_url, $openid_services);
+}
+
+function Auth_OpenID_discoverURI($uri, &$fetcher)
+{
+ $uri = Auth_OpenID::normalizeUrl($uri);
+ return Auth_OpenID_discoverWithYadis($uri, $fetcher);
+}
+
+function Auth_OpenID_discoverWithoutYadis($uri, &$fetcher)
+{
+ $http_resp = @$fetcher->get($uri);
+
+ if ($http_resp->status != 200 and $http_resp->status != 206) {
+ return array($uri, array());
+ }
+
+ $identity_url = $http_resp->final_url;
+
+ // Try to parse the response as HTML to get OpenID 1.0/1.1 <link
+ // rel="...">
+ $openid_services = Auth_OpenID_ServiceEndpoint::fromHTML(
+ $identity_url,
+ $http_resp->body);
+
+ return array($identity_url, $openid_services);
+}
+
+function Auth_OpenID_discoverXRI($iname, &$fetcher)
+{
+ $resolver = new Auth_Yadis_ProxyResolver($fetcher);
+ list($canonicalID, $yadis_services) =
+ $resolver->query($iname,
+ Auth_OpenID_getOpenIDTypeURIs(),
+ array('filter_MatchesAnyOpenIDType'));
+
+ $openid_services = Auth_OpenID_makeOpenIDEndpoints($iname,
+ $yadis_services);
+
+ $openid_services = Auth_OpenID_getOPOrUserServices($openid_services);
+
+ for ($i = 0; $i < count($openid_services); $i++) {
+ $openid_services[$i]->canonicalID = $canonicalID;
+ $openid_services[$i]->claimed_id = $canonicalID;
+ $openid_services[$i]->display_identifier = $iname;
+ }
+
+ // FIXME: returned xri should probably be in some normal form
+ return array($iname, $openid_services);
+}
+
+function Auth_OpenID_discover($uri, &$fetcher)
+{
+ // If the fetcher (i.e., PHP) doesn't support SSL, we can't do
+ // discovery on an HTTPS URL.
+ if ($fetcher->isHTTPS($uri) && !$fetcher->supportsSSL()) {
+ return array($uri, array());
+ }
+
+ if (Auth_Yadis_identifierScheme($uri) == 'XRI') {
+ $result = Auth_OpenID_discoverXRI($uri, $fetcher);
+ } else {
+ $result = Auth_OpenID_discoverURI($uri, $fetcher);
+ }
+
+ // If the fetcher doesn't support SSL, we can't interact with
+ // HTTPS server URLs; remove those endpoints from the list.
+ if (!$fetcher->supportsSSL()) {
+ $http_endpoints = array();
+ list($new_uri, $endpoints) = $result;
+
+ foreach ($endpoints as $e) {
+ if (!$fetcher->isHTTPS($e->server_url)) {
+ $http_endpoints[] = $e;
+ }
+ }
+
+ $result = array($new_uri, $http_endpoints);
+ }
+
+ return $result;
+}
+
+?>
diff --git a/extlib/Auth/OpenID/DumbStore.php b/extlib/Auth/OpenID/DumbStore.php
new file mode 100644
index 000000000..22fd2d366
--- /dev/null
+++ b/extlib/Auth/OpenID/DumbStore.php
@@ -0,0 +1,100 @@
+<?php
+
+/**
+ * This file supplies a dumb store backend for OpenID servers and
+ * consumers.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005-2008 Janrain, Inc.
+ * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
+ */
+
+/**
+ * Import the interface for creating a new store class.
+ */
+require_once 'Auth/OpenID/Interface.php';
+require_once 'Auth/OpenID/HMAC.php';
+
+/**
+ * This is a store for use in the worst case, when you have no way of
+ * saving state on the consumer site. Using this store makes the
+ * consumer vulnerable to replay attacks, as it's unable to use
+ * nonces. Avoid using this store if it is at all possible.
+ *
+ * Most of the methods of this class are implementation details.
+ * Users of this class need to worry only about the constructor.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_DumbStore extends Auth_OpenID_OpenIDStore {
+
+ /**
+ * Creates a new {@link Auth_OpenID_DumbStore} instance. For the security
+ * of the tokens generated by the library, this class attempts to
+ * at least have a secure implementation of getAuthKey.
+ *
+ * When you create an instance of this class, pass in a secret
+ * phrase. The phrase is hashed with sha1 to make it the correct
+ * length and form for an auth key. That allows you to use a long
+ * string as the secret phrase, which means you can make it very
+ * difficult to guess.
+ *
+ * Each {@link Auth_OpenID_DumbStore} instance that is created for use by
+ * your consumer site needs to use the same $secret_phrase.
+ *
+ * @param string secret_phrase The phrase used to create the auth
+ * key returned by getAuthKey
+ */
+ function Auth_OpenID_DumbStore($secret_phrase)
+ {
+ $this->auth_key = Auth_OpenID_SHA1($secret_phrase);
+ }
+
+ /**
+ * This implementation does nothing.
+ */
+ function storeAssociation($server_url, $association)
+ {
+ }
+
+ /**
+ * This implementation always returns null.
+ */
+ function getAssociation($server_url, $handle = null)
+ {
+ return null;
+ }
+
+ /**
+ * This implementation always returns false.
+ */
+ function removeAssociation($server_url, $handle)
+ {
+ return false;
+ }
+
+ /**
+ * In a system truly limited to dumb mode, nonces must all be
+ * accepted. This therefore always returns true, which makes
+ * replay attacks feasible.
+ */
+ function useNonce($server_url, $timestamp, $salt)
+ {
+ return true;
+ }
+
+ /**
+ * This method returns the auth key generated by the constructor.
+ */
+ function getAuthKey()
+ {
+ return $this->auth_key;
+ }
+}
+
+?> \ No newline at end of file
diff --git a/extlib/Auth/OpenID/Extension.php b/extlib/Auth/OpenID/Extension.php
new file mode 100644
index 000000000..f362a4b38
--- /dev/null
+++ b/extlib/Auth/OpenID/Extension.php
@@ -0,0 +1,62 @@
+<?php
+
+/**
+ * An interface for OpenID extensions.
+ *
+ * @package OpenID
+ */
+
+/**
+ * Require the Message implementation.
+ */
+require_once 'Auth/OpenID/Message.php';
+
+/**
+ * A base class for accessing extension request and response data for
+ * the OpenID 2 protocol.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_Extension {
+ /**
+ * ns_uri: The namespace to which to add the arguments for this
+ * extension
+ */
+ var $ns_uri = null;
+ var $ns_alias = null;
+
+ /**
+ * Get the string arguments that should be added to an OpenID
+ * message for this extension.
+ */
+ function getExtensionArgs()
+ {
+ return null;
+ }
+
+ /**
+ * Add the arguments from this extension to the provided message.
+ *
+ * Returns the message with the extension arguments added.
+ */
+ function toMessage(&$message)
+ {
+ $implicit = $message->isOpenID1();
+ $added = $message->namespaces->addAlias($this->ns_uri,
+ $this->ns_alias,
+ $implicit);
+
+ if ($added === null) {
+ if ($message->namespaces->getAlias($this->ns_uri) !=
+ $this->ns_alias) {
+ return null;
+ }
+ }
+
+ $message->updateArgs($this->ns_uri,
+ $this->getExtensionArgs());
+ return $message;
+ }
+}
+
+?> \ No newline at end of file
diff --git a/extlib/Auth/OpenID/FileStore.php b/extlib/Auth/OpenID/FileStore.php
new file mode 100644
index 000000000..29d8d20e7
--- /dev/null
+++ b/extlib/Auth/OpenID/FileStore.php
@@ -0,0 +1,618 @@
+<?php
+
+/**
+ * This file supplies a Memcached store backend for OpenID servers and
+ * consumers.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005-2008 Janrain, Inc.
+ * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
+ */
+
+/**
+ * Require base class for creating a new interface.
+ */
+require_once 'Auth/OpenID.php';
+require_once 'Auth/OpenID/Interface.php';
+require_once 'Auth/OpenID/HMAC.php';
+require_once 'Auth/OpenID/Nonce.php';
+
+/**
+ * This is a filesystem-based store for OpenID associations and
+ * nonces. This store should be safe for use in concurrent systems on
+ * both windows and unix (excluding NFS filesystems). There are a
+ * couple race conditions in the system, but those failure cases have
+ * been set up in such a way that the worst-case behavior is someone
+ * having to try to log in a second time.
+ *
+ * Most of the methods of this class are implementation details.
+ * People wishing to just use this store need only pay attention to
+ * the constructor.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_FileStore extends Auth_OpenID_OpenIDStore {
+
+ /**
+ * Initializes a new {@link Auth_OpenID_FileStore}. This
+ * initializes the nonce and association directories, which are
+ * subdirectories of the directory passed in.
+ *
+ * @param string $directory This is the directory to put the store
+ * directories in.
+ */
+ function Auth_OpenID_FileStore($directory)
+ {
+ if (!Auth_OpenID::ensureDir($directory)) {
+ trigger_error('Not a directory and failed to create: '
+ . $directory, E_USER_ERROR);
+ }
+ $directory = realpath($directory);
+
+ $this->directory = $directory;
+ $this->active = true;
+
+ $this->nonce_dir = $directory . DIRECTORY_SEPARATOR . 'nonces';
+
+ $this->association_dir = $directory . DIRECTORY_SEPARATOR .
+ 'associations';
+
+ // Temp dir must be on the same filesystem as the assciations
+ // $directory.
+ $this->temp_dir = $directory . DIRECTORY_SEPARATOR . 'temp';
+
+ $this->max_nonce_age = 6 * 60 * 60; // Six hours, in seconds
+
+ if (!$this->_setup()) {
+ trigger_error('Failed to initialize OpenID file store in ' .
+ $directory, E_USER_ERROR);
+ }
+ }
+
+ function destroy()
+ {
+ Auth_OpenID_FileStore::_rmtree($this->directory);
+ $this->active = false;
+ }
+
+ /**
+ * Make sure that the directories in which we store our data
+ * exist.
+ *
+ * @access private
+ */
+ function _setup()
+ {
+ return (Auth_OpenID::ensureDir($this->nonce_dir) &&
+ Auth_OpenID::ensureDir($this->association_dir) &&
+ Auth_OpenID::ensureDir($this->temp_dir));
+ }
+
+ /**
+ * Create a temporary file on the same filesystem as
+ * $this->association_dir.
+ *
+ * The temporary directory should not be cleaned if there are any
+ * processes using the store. If there is no active process using
+ * the store, it is safe to remove all of the files in the
+ * temporary directory.
+ *
+ * @return array ($fd, $filename)
+ * @access private
+ */
+ function _mktemp()
+ {
+ $name = Auth_OpenID_FileStore::_mkstemp($dir = $this->temp_dir);
+ $file_obj = @fopen($name, 'wb');
+ if ($file_obj !== false) {
+ return array($file_obj, $name);
+ } else {
+ Auth_OpenID_FileStore::_removeIfPresent($name);
+ }
+ }
+
+ function cleanupNonces()
+ {
+ global $Auth_OpenID_SKEW;
+
+ $nonces = Auth_OpenID_FileStore::_listdir($this->nonce_dir);
+ $now = time();
+
+ $removed = 0;
+ // Check all nonces for expiry
+ foreach ($nonces as $nonce_fname) {
+ $base = basename($nonce_fname);
+ $parts = explode('-', $base, 2);
+ $timestamp = $parts[0];
+ $timestamp = intval($timestamp, 16);
+ if (abs($timestamp - $now) > $Auth_OpenID_SKEW) {
+ Auth_OpenID_FileStore::_removeIfPresent($nonce_fname);
+ $removed += 1;
+ }
+ }
+ return $removed;
+ }
+
+ /**
+ * Create a unique filename for a given server url and
+ * handle. This implementation does not assume anything about the
+ * format of the handle. The filename that is returned will
+ * contain the domain name from the server URL for ease of human
+ * inspection of the data directory.
+ *
+ * @return string $filename
+ */
+ function getAssociationFilename($server_url, $handle)
+ {
+ if (!$this->active) {
+ trigger_error("FileStore no longer active", E_USER_ERROR);
+ return null;
+ }
+
+ if (strpos($server_url, '://') === false) {
+ trigger_error(sprintf("Bad server URL: %s", $server_url),
+ E_USER_WARNING);
+ return null;
+ }
+
+ list($proto, $rest) = explode('://', $server_url, 2);
+ $parts = explode('/', $rest);
+ $domain = Auth_OpenID_FileStore::_filenameEscape($parts[0]);
+ $url_hash = Auth_OpenID_FileStore::_safe64($server_url);
+ if ($handle) {
+ $handle_hash = Auth_OpenID_FileStore::_safe64($handle);
+ } else {
+ $handle_hash = '';
+ }
+
+ $filename = sprintf('%s-%s-%s-%s', $proto, $domain, $url_hash,
+ $handle_hash);
+
+ return $this->association_dir. DIRECTORY_SEPARATOR . $filename;
+ }
+
+ /**
+ * Store an association in the association directory.
+ */
+ function storeAssociation($server_url, $association)
+ {
+ if (!$this->active) {
+ trigger_error("FileStore no longer active", E_USER_ERROR);
+ return false;
+ }
+
+ $association_s = $association->serialize();
+ $filename = $this->getAssociationFilename($server_url,
+ $association->handle);
+ list($tmp_file, $tmp) = $this->_mktemp();
+
+ if (!$tmp_file) {
+ trigger_error("_mktemp didn't return a valid file descriptor",
+ E_USER_WARNING);
+ return false;
+ }
+
+ fwrite($tmp_file, $association_s);
+
+ fflush($tmp_file);
+
+ fclose($tmp_file);
+
+ if (@rename($tmp, $filename)) {
+ return true;
+ } else {
+ // In case we are running on Windows, try unlinking the
+ // file in case it exists.
+ @unlink($filename);
+
+ // Now the target should not exist. Try renaming again,
+ // giving up if it fails.
+ if (@rename($tmp, $filename)) {
+ return true;
+ }
+ }
+
+ // If there was an error, don't leave the temporary file
+ // around.
+ Auth_OpenID_FileStore::_removeIfPresent($tmp);
+ return false;
+ }
+
+ /**
+ * Retrieve an association. If no handle is specified, return the
+ * association with the most recent issue time.
+ *
+ * @return mixed $association
+ */
+ function getAssociation($server_url, $handle = null)
+ {
+ if (!$this->active) {
+ trigger_error("FileStore no longer active", E_USER_ERROR);
+ return null;
+ }
+
+ if ($handle === null) {
+ $handle = '';
+ }
+
+ // The filename with the empty handle is a prefix of all other
+ // associations for the given server URL.
+ $filename = $this->getAssociationFilename($server_url, $handle);
+
+ if ($handle) {
+ return $this->_getAssociation($filename);
+ } else {
+ $association_files =
+ Auth_OpenID_FileStore::_listdir($this->association_dir);
+ $matching_files = array();
+
+ // strip off the path to do the comparison
+ $name = basename($filename);
+ foreach ($association_files as $association_file) {
+ $base = basename($association_file);
+ if (strpos($base, $name) === 0) {
+ $matching_files[] = $association_file;
+ }
+ }
+
+ $matching_associations = array();
+ // read the matching files and sort by time issued
+ foreach ($matching_files as $full_name) {
+ $association = $this->_getAssociation($full_name);
+ if ($association !== null) {
+ $matching_associations[] = array($association->issued,
+ $association);
+ }
+ }
+
+ $issued = array();
+ $assocs = array();
+ foreach ($matching_associations as $key => $assoc) {
+ $issued[$key] = $assoc[0];
+ $assocs[$key] = $assoc[1];
+ }
+
+ array_multisort($issued, SORT_DESC, $assocs, SORT_DESC,
+ $matching_associations);
+
+ // return the most recently issued one.
+ if ($matching_associations) {
+ list($issued, $assoc) = $matching_associations[0];
+ return $assoc;
+ } else {
+ return null;
+ }
+ }
+ }
+
+ /**
+ * @access private
+ */
+ function _getAssociation($filename)
+ {
+ if (!$this->active) {
+ trigger_error("FileStore no longer active", E_USER_ERROR);
+ return null;
+ }
+
+ $assoc_file = @fopen($filename, 'rb');
+
+ if ($assoc_file === false) {
+ return null;
+ }
+
+ $assoc_s = fread($assoc_file, filesize($filename));
+ fclose($assoc_file);
+
+ if (!$assoc_s) {
+ return null;
+ }
+
+ $association =
+ Auth_OpenID_Association::deserialize('Auth_OpenID_Association',
+ $assoc_s);
+
+ if (!$association) {
+ Auth_OpenID_FileStore::_removeIfPresent($filename);
+ return null;
+ }
+
+ if ($association->getExpiresIn() == 0) {
+ Auth_OpenID_FileStore::_removeIfPresent($filename);
+ return null;
+ } else {
+ return $association;
+ }
+ }
+
+ /**
+ * Remove an association if it exists. Do nothing if it does not.
+ *
+ * @return bool $success
+ */
+ function removeAssociation($server_url, $handle)
+ {
+ if (!$this->active) {
+ trigger_error("FileStore no longer active", E_USER_ERROR);
+ return null;
+ }
+
+ $assoc = $this->getAssociation($server_url, $handle);
+ if ($assoc === null) {
+ return false;
+ } else {
+ $filename = $this->getAssociationFilename($server_url, $handle);
+ return Auth_OpenID_FileStore::_removeIfPresent($filename);
+ }
+ }
+
+ /**
+ * Return whether this nonce is present. As a side effect, mark it
+ * as no longer present.
+ *
+ * @return bool $present
+ */
+ function useNonce($server_url, $timestamp, $salt)
+ {
+ global $Auth_OpenID_SKEW;
+
+ if (!$this->active) {
+ trigger_error("FileStore no longer active", E_USER_ERROR);
+ return null;
+ }
+
+ if ( abs($timestamp - time()) > $Auth_OpenID_SKEW ) {
+ return False;
+ }
+
+ if ($server_url) {
+ list($proto, $rest) = explode('://', $server_url, 2);
+ } else {
+ $proto = '';
+ $rest = '';
+ }
+
+ $parts = explode('/', $rest, 2);
+ $domain = $this->_filenameEscape($parts[0]);
+ $url_hash = $this->_safe64($server_url);
+ $salt_hash = $this->_safe64($salt);
+
+ $filename = sprintf('%08x-%s-%s-%s-%s', $timestamp, $proto,
+ $domain, $url_hash, $salt_hash);
+ $filename = $this->nonce_dir . DIRECTORY_SEPARATOR . $filename;
+
+ $result = @fopen($filename, 'x');
+
+ if ($result === false) {
+ return false;
+ } else {
+ fclose($result);
+ return true;
+ }
+ }
+
+ /**
+ * Remove expired entries from the database. This is potentially
+ * expensive, so only run when it is acceptable to take time.
+ *
+ * @access private
+ */
+ function _allAssocs()
+ {
+ $all_associations = array();
+
+ $association_filenames =
+ Auth_OpenID_FileStore::_listdir($this->association_dir);
+
+ foreach ($association_filenames as $association_filename) {
+ $association_file = fopen($association_filename, 'rb');
+
+ if ($association_file !== false) {
+ $assoc_s = fread($association_file,
+ filesize($association_filename));
+ fclose($association_file);
+
+ // Remove expired or corrupted associations
+ $association =
+ Auth_OpenID_Association::deserialize(
+ 'Auth_OpenID_Association', $assoc_s);
+
+ if ($association === null) {
+ Auth_OpenID_FileStore::_removeIfPresent(
+ $association_filename);
+ } else {
+ if ($association->getExpiresIn() == 0) {
+ $all_associations[] = array($association_filename,
+ $association);
+ }
+ }
+ }
+ }
+
+ return $all_associations;
+ }
+
+ function clean()
+ {
+ if (!$this->active) {
+ trigger_error("FileStore no longer active", E_USER_ERROR);
+ return null;
+ }
+
+ $nonces = Auth_OpenID_FileStore::_listdir($this->nonce_dir);
+ $now = time();
+
+ // Check all nonces for expiry
+ foreach ($nonces as $nonce) {
+ if (!Auth_OpenID_checkTimestamp($nonce, $now)) {
+ $filename = $this->nonce_dir . DIRECTORY_SEPARATOR . $nonce;
+ Auth_OpenID_FileStore::_removeIfPresent($filename);
+ }
+ }
+
+ foreach ($this->_allAssocs() as $pair) {
+ list($assoc_filename, $assoc) = $pair;
+ if ($assoc->getExpiresIn() == 0) {
+ Auth_OpenID_FileStore::_removeIfPresent($assoc_filename);
+ }
+ }
+ }
+
+ /**
+ * @access private
+ */
+ function _rmtree($dir)
+ {
+ if ($dir[strlen($dir) - 1] != DIRECTORY_SEPARATOR) {
+ $dir .= DIRECTORY_SEPARATOR;
+ }
+
+ if ($handle = opendir($dir)) {
+ while ($item = readdir($handle)) {
+ if (!in_array($item, array('.', '..'))) {
+ if (is_dir($dir . $item)) {
+
+ if (!Auth_OpenID_FileStore::_rmtree($dir . $item)) {
+ return false;
+ }
+ } else if (is_file($dir . $item)) {
+ if (!unlink($dir . $item)) {
+ return false;
+ }
+ }
+ }
+ }
+
+ closedir($handle);
+
+ if (!@rmdir($dir)) {
+ return false;
+ }
+
+ return true;
+ } else {
+ // Couldn't open directory.
+ return false;
+ }
+ }
+
+ /**
+ * @access private
+ */
+ function _mkstemp($dir)
+ {
+ foreach (range(0, 4) as $i) {
+ $name = tempnam($dir, "php_openid_filestore_");
+
+ if ($name !== false) {
+ return $name;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @access private
+ */
+ function _mkdtemp($dir)
+ {
+ foreach (range(0, 4) as $i) {
+ $name = $dir . strval(DIRECTORY_SEPARATOR) . strval(getmypid()) .
+ "-" . strval(rand(1, time()));
+ if (!mkdir($name, 0700)) {
+ return false;
+ } else {
+ return $name;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @access private
+ */
+ function _listdir($dir)
+ {
+ $handle = opendir($dir);
+ $files = array();
+ while (false !== ($filename = readdir($handle))) {
+ if (!in_array($filename, array('.', '..'))) {
+ $files[] = $dir . DIRECTORY_SEPARATOR . $filename;
+ }
+ }
+ return $files;
+ }
+
+ /**
+ * @access private
+ */
+ function _isFilenameSafe($char)
+ {
+ $_Auth_OpenID_filename_allowed = Auth_OpenID_letters .
+ Auth_OpenID_digits . ".";
+ return (strpos($_Auth_OpenID_filename_allowed, $char) !== false);
+ }
+
+ /**
+ * @access private
+ */
+ function _safe64($str)
+ {
+ $h64 = base64_encode(Auth_OpenID_SHA1($str));
+ $h64 = str_replace('+', '_', $h64);
+ $h64 = str_replace('/', '.', $h64);
+ $h64 = str_replace('=', '', $h64);
+ return $h64;
+ }
+
+ /**
+ * @access private
+ */
+ function _filenameEscape($str)
+ {
+ $filename = "";
+ $b = Auth_OpenID::toBytes($str);
+
+ for ($i = 0; $i < count($b); $i++) {
+ $c = $b[$i];
+ if (Auth_OpenID_FileStore::_isFilenameSafe($c)) {
+ $filename .= $c;
+ } else {
+ $filename .= sprintf("_%02X", ord($c));
+ }
+ }
+ return $filename;
+ }
+
+ /**
+ * Attempt to remove a file, returning whether the file existed at
+ * the time of the call.
+ *
+ * @access private
+ * @return bool $result True if the file was present, false if not.
+ */
+ function _removeIfPresent($filename)
+ {
+ return @unlink($filename);
+ }
+
+ function cleanupAssociations()
+ {
+ $removed = 0;
+ foreach ($this->_allAssocs() as $pair) {
+ list($assoc_filename, $assoc) = $pair;
+ if ($assoc->getExpiresIn() == 0) {
+ $this->_removeIfPresent($assoc_filename);
+ $removed += 1;
+ }
+ }
+ return $removed;
+ }
+}
+
+?>
diff --git a/extlib/Auth/OpenID/HMAC.php b/extlib/Auth/OpenID/HMAC.php
new file mode 100644
index 000000000..ec42db8df
--- /dev/null
+++ b/extlib/Auth/OpenID/HMAC.php
@@ -0,0 +1,99 @@
+<?php
+
+/**
+ * This is the HMACSHA1 implementation for the OpenID library.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @access private
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005-2008 Janrain, Inc.
+ * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
+ */
+
+require_once 'Auth/OpenID.php';
+
+/**
+ * SHA1_BLOCKSIZE is this module's SHA1 blocksize used by the fallback
+ * implementation.
+ */
+define('Auth_OpenID_SHA1_BLOCKSIZE', 64);
+
+function Auth_OpenID_SHA1($text)
+{
+ if (function_exists('hash') &&
+ function_exists('hash_algos') &&
+ (in_array('sha1', hash_algos()))) {
+ // PHP 5 case (sometimes): 'hash' available and 'sha1' algo
+ // supported.
+ return hash('sha1', $text, true);
+ } else if (function_exists('sha1')) {
+ // PHP 4 case: 'sha1' available.
+ $hex = sha1($text);
+ $raw = '';
+ for ($i = 0; $i < 40; $i += 2) {
+ $hexcode = substr($hex, $i, 2);
+ $charcode = (int)base_convert($hexcode, 16, 10);
+ $raw .= chr($charcode);
+ }
+ return $raw;
+ } else {
+ // Explode.
+ trigger_error('No SHA1 function found', E_USER_ERROR);
+ }
+}
+
+/**
+ * Compute an HMAC/SHA1 hash.
+ *
+ * @access private
+ * @param string $key The HMAC key
+ * @param string $text The message text to hash
+ * @return string $mac The MAC
+ */
+function Auth_OpenID_HMACSHA1($key, $text)
+{
+ if (Auth_OpenID::bytes($key) > Auth_OpenID_SHA1_BLOCKSIZE) {
+ $key = Auth_OpenID_SHA1($key, true);
+ }
+
+ $key = str_pad($key, Auth_OpenID_SHA1_BLOCKSIZE, chr(0x00));
+ $ipad = str_repeat(chr(0x36), Auth_OpenID_SHA1_BLOCKSIZE);
+ $opad = str_repeat(chr(0x5c), Auth_OpenID_SHA1_BLOCKSIZE);
+ $hash1 = Auth_OpenID_SHA1(($key ^ $ipad) . $text, true);
+ $hmac = Auth_OpenID_SHA1(($key ^ $opad) . $hash1, true);
+ return $hmac;
+}
+
+if (function_exists('hash') &&
+ function_exists('hash_algos') &&
+ (in_array('sha256', hash_algos()))) {
+ function Auth_OpenID_SHA256($text)
+ {
+ // PHP 5 case: 'hash' available and 'sha256' algo supported.
+ return hash('sha256', $text, true);
+ }
+ define('Auth_OpenID_SHA256_SUPPORTED', true);
+} else {
+ define('Auth_OpenID_SHA256_SUPPORTED', false);
+}
+
+if (function_exists('hash_hmac') &&
+ function_exists('hash_algos') &&
+ (in_array('sha256', hash_algos()))) {
+
+ function Auth_OpenID_HMACSHA256($key, $text)
+ {
+ // Return raw MAC (not hex string).
+ return hash_hmac('sha256', $text, $key, true);
+ }
+
+ define('Auth_OpenID_HMACSHA256_SUPPORTED', true);
+} else {
+ define('Auth_OpenID_HMACSHA256_SUPPORTED', false);
+}
+
+?> \ No newline at end of file
diff --git a/extlib/Auth/OpenID/Interface.php b/extlib/Auth/OpenID/Interface.php
new file mode 100644
index 000000000..f4c6062f8
--- /dev/null
+++ b/extlib/Auth/OpenID/Interface.php
@@ -0,0 +1,197 @@
+<?php
+
+/**
+ * This file specifies the interface for PHP OpenID store implementations.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005-2008 Janrain, Inc.
+ * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
+ */
+
+/**
+ * This is the interface for the store objects the OpenID library
+ * uses. It is a single class that provides all of the persistence
+ * mechanisms that the OpenID library needs, for both servers and
+ * consumers. If you want to create an SQL-driven store, please see
+ * then {@link Auth_OpenID_SQLStore} class.
+ *
+ * Change: Version 2.0 removed the storeNonce, getAuthKey, and isDumb
+ * methods, and changed the behavior of the useNonce method to support
+ * one-way nonces.
+ *
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ */
+class Auth_OpenID_OpenIDStore {
+ /**
+ * This method puts an Association object into storage,
+ * retrievable by server URL and handle.
+ *
+ * @param string $server_url The URL of the identity server that
+ * this association is with. Because of the way the server portion
+ * of the library uses this interface, don't assume there are any
+ * limitations on the character set of the input string. In
+ * particular, expect to see unescaped non-url-safe characters in
+ * the server_url field.
+ *
+ * @param Association $association The Association to store.
+ */
+ function storeAssociation($server_url, $association)
+ {
+ trigger_error("Auth_OpenID_OpenIDStore::storeAssociation ".
+ "not implemented", E_USER_ERROR);
+ }
+
+ /*
+ * Remove expired nonces from the store.
+ *
+ * Discards any nonce from storage that is old enough that its
+ * timestamp would not pass useNonce().
+ *
+ * This method is not called in the normal operation of the
+ * library. It provides a way for store admins to keep their
+ * storage from filling up with expired data.
+ *
+ * @return the number of nonces expired
+ */
+ function cleanupNonces()
+ {
+ trigger_error("Auth_OpenID_OpenIDStore::cleanupNonces ".
+ "not implemented", E_USER_ERROR);
+ }
+
+ /*
+ * Remove expired associations from the store.
+ *
+ * This method is not called in the normal operation of the
+ * library. It provides a way for store admins to keep their
+ * storage from filling up with expired data.
+ *
+ * @return the number of associations expired.
+ */
+ function cleanupAssociations()
+ {
+ trigger_error("Auth_OpenID_OpenIDStore::cleanupAssociations ".
+ "not implemented", E_USER_ERROR);
+ }
+
+ /*
+ * Shortcut for cleanupNonces(), cleanupAssociations().
+ *
+ * This method is not called in the normal operation of the
+ * library. It provides a way for store admins to keep their
+ * storage from filling up with expired data.
+ */
+ function cleanup()
+ {
+ return array($this->cleanupNonces(),
+ $this->cleanupAssociations());
+ }
+
+ /**
+ * Report whether this storage supports cleanup
+ */
+ function supportsCleanup()
+ {
+ return true;
+ }
+
+ /**
+ * This method returns an Association object from storage that
+ * matches the server URL and, if specified, handle. It returns
+ * null if no such association is found or if the matching
+ * association is expired.
+ *
+ * If no handle is specified, the store may return any association
+ * which matches the server URL. If multiple associations are
+ * valid, the recommended return value for this method is the one
+ * most recently issued.
+ *
+ * This method is allowed (and encouraged) to garbage collect
+ * expired associations when found. This method must not return
+ * expired associations.
+ *
+ * @param string $server_url The URL of the identity server to get
+ * the association for. Because of the way the server portion of
+ * the library uses this interface, don't assume there are any
+ * limitations on the character set of the input string. In
+ * particular, expect to see unescaped non-url-safe characters in
+ * the server_url field.
+ *
+ * @param mixed $handle This optional parameter is the handle of
+ * the specific association to get. If no specific handle is
+ * provided, any valid association matching the server URL is
+ * returned.
+ *
+ * @return Association The Association for the given identity
+ * server.
+ */
+ function getAssociation($server_url, $handle = null)
+ {
+ trigger_error("Auth_OpenID_OpenIDStore::getAssociation ".
+ "not implemented", E_USER_ERROR);
+ }
+
+ /**
+ * This method removes the matching association if it's found, and
+ * returns whether the association was removed or not.
+ *
+ * @param string $server_url The URL of the identity server the
+ * association to remove belongs to. Because of the way the server
+ * portion of the library uses this interface, don't assume there
+ * are any limitations on the character set of the input
+ * string. In particular, expect to see unescaped non-url-safe
+ * characters in the server_url field.
+ *
+ * @param string $handle This is the handle of the association to
+ * remove. If there isn't an association found that matches both
+ * the given URL and handle, then there was no matching handle
+ * found.
+ *
+ * @return mixed Returns whether or not the given association existed.
+ */
+ function removeAssociation($server_url, $handle)
+ {
+ trigger_error("Auth_OpenID_OpenIDStore::removeAssociation ".
+ "not implemented", E_USER_ERROR);
+ }
+
+ /**
+ * Called when using a nonce.
+ *
+ * This method should return C{True} if the nonce has not been
+ * used before, and store it for a while to make sure nobody
+ * tries to use the same value again. If the nonce has already
+ * been used, return C{False}.
+ *
+ * Change: In earlier versions, round-trip nonces were used and a
+ * nonce was only valid if it had been previously stored with
+ * storeNonce. Version 2.0 uses one-way nonces, requiring a
+ * different implementation here that does not depend on a
+ * storeNonce call. (storeNonce is no longer part of the
+ * interface.
+ *
+ * @param string $nonce The nonce to use.
+ *
+ * @return bool Whether or not the nonce was valid.
+ */
+ function useNonce($server_url, $timestamp, $salt)
+ {
+ trigger_error("Auth_OpenID_OpenIDStore::useNonce ".
+ "not implemented", E_USER_ERROR);
+ }
+
+ /**
+ * Removes all entries from the store; implementation is optional.
+ */
+ function reset()
+ {
+ }
+
+}
+?> \ No newline at end of file
diff --git a/extlib/Auth/OpenID/KVForm.php b/extlib/Auth/OpenID/KVForm.php
new file mode 100644
index 000000000..fb342a001
--- /dev/null
+++ b/extlib/Auth/OpenID/KVForm.php
@@ -0,0 +1,112 @@
+<?php
+
+/**
+ * OpenID protocol key-value/comma-newline format parsing and
+ * serialization
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @access private
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005-2008 Janrain, Inc.
+ * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
+ */
+
+/**
+ * Container for key-value/comma-newline OpenID format and parsing
+ */
+class Auth_OpenID_KVForm {
+ /**
+ * Convert an OpenID colon/newline separated string into an
+ * associative array
+ *
+ * @static
+ * @access private
+ */
+ function toArray($kvs, $strict=false)
+ {
+ $lines = explode("\n", $kvs);
+
+ $last = array_pop($lines);
+ if ($last !== '') {
+ array_push($lines, $last);
+ if ($strict) {
+ return false;
+ }
+ }
+
+ $values = array();
+
+ for ($lineno = 0; $lineno < count($lines); $lineno++) {
+ $line = $lines[$lineno];
+ $kv = explode(':', $line, 2);
+ if (count($kv) != 2) {
+ if ($strict) {
+ return false;
+ }
+ continue;
+ }
+
+ $key = $kv[0];
+ $tkey = trim($key);
+ if ($tkey != $key) {
+ if ($strict) {
+ return false;
+ }
+ }
+
+ $value = $kv[1];
+ $tval = trim($value);
+ if ($tval != $value) {
+ if ($strict) {
+ return false;
+ }
+ }
+
+ $values[$tkey] = $tval;
+ }
+
+ return $values;
+ }
+
+ /**
+ * Convert an array into an OpenID colon/newline separated string
+ *
+ * @static
+ * @access private
+ */
+ function fromArray($values)
+ {
+ if ($values === null) {
+ return null;
+ }
+
+ ksort($values);
+
+ $serialized = '';
+ foreach ($values as $key => $value) {
+ if (is_array($value)) {
+ list($key, $value) = array($value[0], $value[1]);
+ }
+
+ if (strpos($key, ':') !== false) {
+ return null;
+ }
+
+ if (strpos($key, "\n") !== false) {
+ return null;
+ }
+
+ if (strpos($value, "\n") !== false) {
+ return null;
+ }
+ $serialized .= "$key:$value\n";
+ }
+ return $serialized;
+ }
+}
+
+?> \ No newline at end of file
diff --git a/extlib/Auth/OpenID/MemcachedStore.php b/extlib/Auth/OpenID/MemcachedStore.php
new file mode 100644
index 000000000..d357c6b11
--- /dev/null
+++ b/extlib/Auth/OpenID/MemcachedStore.php
@@ -0,0 +1,208 @@
+<?php
+
+/**
+ * This file supplies a memcached store backend for OpenID servers and
+ * consumers.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @package OpenID
+ * @author Artemy Tregubenko <me@arty.name>
+ * @copyright 2008 JanRain, Inc.
+ * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
+ * Contributed by Open Web Technologies <http://openwebtech.ru/>
+ */
+
+/**
+ * Import the interface for creating a new store class.
+ */
+require_once 'Auth/OpenID/Interface.php';
+
+/**
+ * This is a memcached-based store for OpenID associations and
+ * nonces.
+ *
+ * As memcache has limit of 250 chars for key length,
+ * server_url, handle and salt are hashed with sha1().
+ *
+ * Most of the methods of this class are implementation details.
+ * People wishing to just use this store need only pay attention to
+ * the constructor.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_MemcachedStore extends Auth_OpenID_OpenIDStore {
+
+ /**
+ * Initializes a new {@link Auth_OpenID_MemcachedStore} instance.
+ * Just saves memcached object as property.
+ *
+ * @param resource connection Memcache connection resourse
+ */
+ function Auth_OpenID_MemcachedStore($connection, $compress = false)
+ {
+ $this->connection = $connection;
+ $this->compress = $compress ? MEMCACHE_COMPRESSED : 0;
+ }
+
+ /**
+ * Store association until its expiration time in memcached.
+ * Overwrites any existing association with same server_url and
+ * handle. Handles list of associations for every server.
+ */
+ function storeAssociation($server_url, $association)
+ {
+ // create memcached keys for association itself
+ // and list of associations for this server
+ $associationKey = $this->associationKey($server_url,
+ $association->handle);
+ $serverKey = $this->associationServerKey($server_url);
+
+ // get list of associations
+ $serverAssociations = $this->connection->get($serverKey);
+
+ // if no such list, initialize it with empty array
+ if (!$serverAssociations) {
+ $serverAssociations = array();
+ }
+ // and store given association key in it
+ $serverAssociations[$association->issued] = $associationKey;
+
+ // save associations' keys list
+ $this->connection->set(
+ $serverKey,
+ $serverAssociations,
+ $this->compress
+ );
+ // save association itself
+ $this->connection->set(
+ $associationKey,
+ $association,
+ $this->compress,
+ $association->issued + $association->lifetime);
+ }
+
+ /**
+ * Read association from memcached. If no handle given
+ * and multiple associations found, returns latest issued
+ */
+ function getAssociation($server_url, $handle = null)
+ {
+ // simple case: handle given
+ if ($handle !== null) {
+ // get association, return null if failed
+ $association = $this->connection->get(
+ $this->associationKey($server_url, $handle));
+ return $association ? $association : null;
+ }
+
+ // no handle given, working with list
+ // create key for list of associations
+ $serverKey = $this->associationServerKey($server_url);
+
+ // get list of associations
+ $serverAssociations = $this->connection->get($serverKey);
+ // return null if failed or got empty list
+ if (!$serverAssociations) {
+ return null;
+ }
+
+ // get key of most recently issued association
+ $keys = array_keys($serverAssociations);
+ sort($keys);
+ $lastKey = $serverAssociations[array_pop($keys)];
+
+ // get association, return null if failed
+ $association = $this->connection->get($lastKey);
+ return $association ? $association : null;
+ }
+
+ /**
+ * Immediately delete association from memcache.
+ */
+ function removeAssociation($server_url, $handle)
+ {
+ // create memcached keys for association itself
+ // and list of associations for this server
+ $serverKey = $this->associationServerKey($server_url);
+ $associationKey = $this->associationKey($server_url,
+ $handle);
+
+ // get list of associations
+ $serverAssociations = $this->connection->get($serverKey);
+ // return null if failed or got empty list
+ if (!$serverAssociations) {
+ return false;
+ }
+
+ // ensure that given association key exists in list
+ $serverAssociations = array_flip($serverAssociations);
+ if (!array_key_exists($associationKey, $serverAssociations)) {
+ return false;
+ }
+
+ // remove given association key from list
+ unset($serverAssociations[$associationKey]);
+ $serverAssociations = array_flip($serverAssociations);
+
+ // save updated list
+ $this->connection->set(
+ $serverKey,
+ $serverAssociations,
+ $this->compress
+ );
+
+ // delete association
+ return $this->connection->delete($associationKey);
+ }
+
+ /**
+ * Create nonce for server and salt, expiring after
+ * $Auth_OpenID_SKEW seconds.
+ */
+ function useNonce($server_url, $timestamp, $salt)
+ {
+ global $Auth_OpenID_SKEW;
+
+ // save one request to memcache when nonce obviously expired
+ if (abs($timestamp - time()) > $Auth_OpenID_SKEW) {
+ return false;
+ }
+
+ // returns false when nonce already exists
+ // otherwise adds nonce
+ return $this->connection->add(
+ 'openid_nonce_' . sha1($server_url) . '_' . sha1($salt),
+ 1, // any value here
+ $this->compress,
+ $Auth_OpenID_SKEW);
+ }
+
+ /**
+ * Memcache key is prefixed with 'openid_association_' string.
+ */
+ function associationKey($server_url, $handle = null)
+ {
+ return 'openid_association_' . sha1($server_url) . '_' . sha1($handle);
+ }
+
+ /**
+ * Memcache key is prefixed with 'openid_association_' string.
+ */
+ function associationServerKey($server_url)
+ {
+ return 'openid_association_server_' . sha1($server_url);
+ }
+
+ /**
+ * Report that this storage doesn't support cleanup
+ */
+ function supportsCleanup()
+ {
+ return false;
+ }
+}
+
+?> \ No newline at end of file
diff --git a/extlib/Auth/OpenID/Message.php b/extlib/Auth/OpenID/Message.php
new file mode 100644
index 000000000..fd23e67a3
--- /dev/null
+++ b/extlib/Auth/OpenID/Message.php
@@ -0,0 +1,915 @@
+<?php
+
+/**
+ * Extension argument processing code
+ *
+ * @package OpenID
+ */
+
+/**
+ * Import tools needed to deal with messages.
+ */
+require_once 'Auth/OpenID.php';
+require_once 'Auth/OpenID/KVForm.php';
+require_once 'Auth/Yadis/XML.php';
+require_once 'Auth/OpenID/Consumer.php'; // For Auth_OpenID_FailureResponse
+
+// This doesn't REALLY belong here, but where is better?
+define('Auth_OpenID_IDENTIFIER_SELECT',
+ "http://specs.openid.net/auth/2.0/identifier_select");
+
+// URI for Simple Registration extension, the only commonly deployed
+// OpenID 1.x extension, and so a special case
+define('Auth_OpenID_SREG_URI', 'http://openid.net/sreg/1.0');
+
+// The OpenID 1.X namespace URI
+define('Auth_OpenID_OPENID1_NS', 'http://openid.net/signon/1.0');
+define('Auth_OpenID_THE_OTHER_OPENID1_NS', 'http://openid.net/signon/1.1');
+
+function Auth_OpenID_isOpenID1($ns)
+{
+ return ($ns == Auth_OpenID_THE_OTHER_OPENID1_NS) ||
+ ($ns == Auth_OpenID_OPENID1_NS);
+}
+
+// The OpenID 2.0 namespace URI
+define('Auth_OpenID_OPENID2_NS', 'http://specs.openid.net/auth/2.0');
+
+// The namespace consisting of pairs with keys that are prefixed with
+// "openid." but not in another namespace.
+define('Auth_OpenID_NULL_NAMESPACE', 'Null namespace');
+
+// The null namespace, when it is an allowed OpenID namespace
+define('Auth_OpenID_OPENID_NS', 'OpenID namespace');
+
+// The top-level namespace, excluding all pairs with keys that start
+// with "openid."
+define('Auth_OpenID_BARE_NS', 'Bare namespace');
+
+// Sentinel for Message implementation to indicate that getArg should
+// return null instead of returning a default.
+define('Auth_OpenID_NO_DEFAULT', 'NO DEFAULT ALLOWED');
+
+// Limit, in bytes, of identity provider and return_to URLs, including
+// response payload. See OpenID 1.1 specification, Appendix D.
+define('Auth_OpenID_OPENID1_URL_LIMIT', 2047);
+
+// All OpenID protocol fields. Used to check namespace aliases.
+global $Auth_OpenID_OPENID_PROTOCOL_FIELDS;
+$Auth_OpenID_OPENID_PROTOCOL_FIELDS = array(
+ 'ns', 'mode', 'error', 'return_to', 'contact', 'reference',
+ 'signed', 'assoc_type', 'session_type', 'dh_modulus', 'dh_gen',
+ 'dh_consumer_public', 'claimed_id', 'identity', 'realm',
+ 'invalidate_handle', 'op_endpoint', 'response_nonce', 'sig',
+ 'assoc_handle', 'trust_root', 'openid');
+
+// Global namespace / alias registration map. See
+// Auth_OpenID_registerNamespaceAlias.
+global $Auth_OpenID_registered_aliases;
+$Auth_OpenID_registered_aliases = array();
+
+/**
+ * Registers a (namespace URI, alias) mapping in a global namespace
+ * alias map. Raises NamespaceAliasRegistrationError if either the
+ * namespace URI or alias has already been registered with a different
+ * value. This function is required if you want to use a namespace
+ * with an OpenID 1 message.
+ */
+function Auth_OpenID_registerNamespaceAlias($namespace_uri, $alias)
+{
+ global $Auth_OpenID_registered_aliases;
+
+ if (Auth_OpenID::arrayGet($Auth_OpenID_registered_aliases,
+ $alias) == $namespace_uri) {
+ return true;
+ }
+
+ if (in_array($namespace_uri,
+ array_values($Auth_OpenID_registered_aliases))) {
+ return false;
+ }
+
+ if (in_array($alias, array_keys($Auth_OpenID_registered_aliases))) {
+ return false;
+ }
+
+ $Auth_OpenID_registered_aliases[$alias] = $namespace_uri;
+ return true;
+}
+
+/**
+ * Removes a (namespace_uri, alias) registration from the global
+ * namespace alias map. Returns true if the removal succeeded; false
+ * if not (if the mapping did not exist).
+ */
+function Auth_OpenID_removeNamespaceAlias($namespace_uri, $alias)
+{
+ global $Auth_OpenID_registered_aliases;
+
+ if (Auth_OpenID::arrayGet($Auth_OpenID_registered_aliases,
+ $alias) === $namespace_uri) {
+ unset($Auth_OpenID_registered_aliases[$alias]);
+ return true;
+ }
+
+ return false;
+}
+
+/**
+ * An Auth_OpenID_Mapping maintains a mapping from arbitrary keys to
+ * arbitrary values. (This is unlike an ordinary PHP array, whose
+ * keys may be only simple scalars.)
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_Mapping {
+ /**
+ * Initialize a mapping. If $classic_array is specified, its keys
+ * and values are used to populate the mapping.
+ */
+ function Auth_OpenID_Mapping($classic_array = null)
+ {
+ $this->keys = array();
+ $this->values = array();
+
+ if (is_array($classic_array)) {
+ foreach ($classic_array as $key => $value) {
+ $this->set($key, $value);
+ }
+ }
+ }
+
+ /**
+ * Returns true if $thing is an Auth_OpenID_Mapping object; false
+ * if not.
+ */
+ function isA($thing)
+ {
+ return (is_object($thing) &&
+ strtolower(get_class($thing)) == 'auth_openid_mapping');
+ }
+
+ /**
+ * Returns an array of the keys in the mapping.
+ */
+ function keys()
+ {
+ return $this->keys;
+ }
+
+ /**
+ * Returns an array of values in the mapping.
+ */
+ function values()
+ {
+ return $this->values;
+ }
+
+ /**
+ * Returns an array of (key, value) pairs in the mapping.
+ */
+ function items()
+ {
+ $temp = array();
+
+ for ($i = 0; $i < count($this->keys); $i++) {
+ $temp[] = array($this->keys[$i],
+ $this->values[$i]);
+ }
+ return $temp;
+ }
+
+ /**
+ * Returns the "length" of the mapping, or the number of keys.
+ */
+ function len()
+ {
+ return count($this->keys);
+ }
+
+ /**
+ * Sets a key-value pair in the mapping. If the key already
+ * exists, its value is replaced with the new value.
+ */
+ function set($key, $value)
+ {
+ $index = array_search($key, $this->keys);
+
+ if ($index !== false) {
+ $this->values[$index] = $value;
+ } else {
+ $this->keys[] = $key;
+ $this->values[] = $value;
+ }
+ }
+
+ /**
+ * Gets a specified value from the mapping, associated with the
+ * specified key. If the key does not exist in the mapping,
+ * $default is returned instead.
+ */
+ function get($key, $default = null)
+ {
+ $index = array_search($key, $this->keys);
+
+ if ($index !== false) {
+ return $this->values[$index];
+ } else {
+ return $default;
+ }
+ }
+
+ /**
+ * @access private
+ */
+ function _reflow()
+ {
+ // PHP is broken yet again. Sort the arrays to remove the
+ // hole in the numeric indexes that make up the array.
+ $old_keys = $this->keys;
+ $old_values = $this->values;
+
+ $this->keys = array();
+ $this->values = array();
+
+ foreach ($old_keys as $k) {
+ $this->keys[] = $k;
+ }
+
+ foreach ($old_values as $v) {
+ $this->values[] = $v;
+ }
+ }
+
+ /**
+ * Deletes a key-value pair from the mapping with the specified
+ * key.
+ */
+ function del($key)
+ {
+ $index = array_search($key, $this->keys);
+
+ if ($index !== false) {
+ unset($this->keys[$index]);
+ unset($this->values[$index]);
+ $this->_reflow();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the specified value has a key in the mapping;
+ * false if not.
+ */
+ function contains($value)
+ {
+ return (array_search($value, $this->keys) !== false);
+ }
+}
+
+/**
+ * Maintains a bijective map between namespace uris and aliases.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_NamespaceMap {
+ function Auth_OpenID_NamespaceMap()
+ {
+ $this->alias_to_namespace = new Auth_OpenID_Mapping();
+ $this->namespace_to_alias = new Auth_OpenID_Mapping();
+ $this->implicit_namespaces = array();
+ }
+
+ function getAlias($namespace_uri)
+ {
+ return $this->namespace_to_alias->get($namespace_uri);
+ }
+
+ function getNamespaceURI($alias)
+ {
+ return $this->alias_to_namespace->get($alias);
+ }
+
+ function iterNamespaceURIs()
+ {
+ // Return an iterator over the namespace URIs
+ return $this->namespace_to_alias->keys();
+ }
+
+ function iterAliases()
+ {
+ // Return an iterator over the aliases"""
+ return $this->alias_to_namespace->keys();
+ }
+
+ function iteritems()
+ {
+ return $this->namespace_to_alias->items();
+ }
+
+ function isImplicit($namespace_uri)
+ {
+ return in_array($namespace_uri, $this->implicit_namespaces);
+ }
+
+ function addAlias($namespace_uri, $desired_alias, $implicit=false)
+ {
+ // Add an alias from this namespace URI to the desired alias
+ global $Auth_OpenID_OPENID_PROTOCOL_FIELDS;
+
+ // Check that desired_alias is not an openid protocol field as
+ // per the spec.
+ if (in_array($desired_alias, $Auth_OpenID_OPENID_PROTOCOL_FIELDS)) {
+ Auth_OpenID::log("\"%s\" is not an allowed namespace alias",
+ $desired_alias);
+ return null;
+ }
+
+ // Check that desired_alias does not contain a period as per
+ // the spec.
+ if (strpos($desired_alias, '.') !== false) {
+ Auth_OpenID::log('"%s" must not contain a dot', $desired_alias);
+ return null;
+ }
+
+ // Check that there is not a namespace already defined for the
+ // desired alias
+ $current_namespace_uri =
+ $this->alias_to_namespace->get($desired_alias);
+
+ if (($current_namespace_uri !== null) &&
+ ($current_namespace_uri != $namespace_uri)) {
+ Auth_OpenID::log('Cannot map "%s" because previous mapping exists',
+ $namespace_uri);
+ return null;
+ }
+
+ // Check that there is not already a (different) alias for
+ // this namespace URI
+ $alias = $this->namespace_to_alias->get($namespace_uri);
+
+ if (($alias !== null) && ($alias != $desired_alias)) {
+ Auth_OpenID::log('Cannot map %s to alias %s. ' .
+ 'It is already mapped to alias %s',
+ $namespace_uri, $desired_alias, $alias);
+ return null;
+ }
+
+ assert((Auth_OpenID_NULL_NAMESPACE === $desired_alias) ||
+ is_string($desired_alias));
+
+ $this->alias_to_namespace->set($desired_alias, $namespace_uri);
+ $this->namespace_to_alias->set($namespace_uri, $desired_alias);
+ if ($implicit) {
+ array_push($this->implicit_namespaces, $namespace_uri);
+ }
+
+ return $desired_alias;
+ }
+
+ function add($namespace_uri)
+ {
+ // Add this namespace URI to the mapping, without caring what
+ // alias it ends up with
+
+ // See if this namespace is already mapped to an alias
+ $alias = $this->namespace_to_alias->get($namespace_uri);
+
+ if ($alias !== null) {
+ return $alias;
+ }
+
+ // Fall back to generating a numerical alias
+ $i = 0;
+ while (1) {
+ $alias = 'ext' . strval($i);
+ if ($this->addAlias($namespace_uri, $alias) === null) {
+ $i += 1;
+ } else {
+ return $alias;
+ }
+ }
+
+ // Should NEVER be reached!
+ return null;
+ }
+
+ function contains($namespace_uri)
+ {
+ return $this->isDefined($namespace_uri);
+ }
+
+ function isDefined($namespace_uri)
+ {
+ return $this->namespace_to_alias->contains($namespace_uri);
+ }
+}
+
+/**
+ * In the implementation of this object, null represents the global
+ * namespace as well as a namespace with no key.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_Message {
+
+ function Auth_OpenID_Message($openid_namespace = null)
+ {
+ // Create an empty Message
+ $this->allowed_openid_namespaces = array(
+ Auth_OpenID_OPENID1_NS,
+ Auth_OpenID_THE_OTHER_OPENID1_NS,
+ Auth_OpenID_OPENID2_NS);
+
+ $this->args = new Auth_OpenID_Mapping();
+ $this->namespaces = new Auth_OpenID_NamespaceMap();
+ if ($openid_namespace === null) {
+ $this->_openid_ns_uri = null;
+ } else {
+ $implicit = Auth_OpenID_isOpenID1($openid_namespace);
+ $this->setOpenIDNamespace($openid_namespace, $implicit);
+ }
+ }
+
+ function isOpenID1()
+ {
+ return Auth_OpenID_isOpenID1($this->getOpenIDNamespace());
+ }
+
+ function isOpenID2()
+ {
+ return $this->getOpenIDNamespace() == Auth_OpenID_OPENID2_NS;
+ }
+
+ function fromPostArgs($args)
+ {
+ // Construct a Message containing a set of POST arguments
+ $obj = new Auth_OpenID_Message();
+
+ // Partition into "openid." args and bare args
+ $openid_args = array();
+ foreach ($args as $key => $value) {
+
+ if (is_array($value)) {
+ return null;
+ }
+
+ $parts = explode('.', $key, 2);
+
+ if (count($parts) == 2) {
+ list($prefix, $rest) = $parts;
+ } else {
+ $prefix = null;
+ }
+
+ if ($prefix != 'openid') {
+ $obj->args->set(array(Auth_OpenID_BARE_NS, $key), $value);
+ } else {
+ $openid_args[$rest] = $value;
+ }
+ }
+
+ if ($obj->_fromOpenIDArgs($openid_args)) {
+ return $obj;
+ } else {
+ return null;
+ }
+ }
+
+ function fromOpenIDArgs($openid_args)
+ {
+ // Takes an array.
+
+ // Construct a Message from a parsed KVForm message
+ $obj = new Auth_OpenID_Message();
+ if ($obj->_fromOpenIDArgs($openid_args)) {
+ return $obj;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * @access private
+ */
+ function _fromOpenIDArgs($openid_args)
+ {
+ global $Auth_OpenID_registered_aliases;
+
+ // Takes an Auth_OpenID_Mapping instance OR an array.
+
+ if (!Auth_OpenID_Mapping::isA($openid_args)) {
+ $openid_args = new Auth_OpenID_Mapping($openid_args);
+ }
+
+ $ns_args = array();
+
+ // Resolve namespaces
+ foreach ($openid_args->items() as $pair) {
+ list($rest, $value) = $pair;
+
+ $parts = explode('.', $rest, 2);
+
+ if (count($parts) == 2) {
+ list($ns_alias, $ns_key) = $parts;
+ } else {
+ $ns_alias = Auth_OpenID_NULL_NAMESPACE;
+ $ns_key = $rest;
+ }
+
+ if ($ns_alias == 'ns') {
+ if ($this->namespaces->addAlias($value, $ns_key) === null) {
+ return false;
+ }
+ } else if (($ns_alias == Auth_OpenID_NULL_NAMESPACE) &&
+ ($ns_key == 'ns')) {
+ // null namespace
+ if ($this->setOpenIDNamespace($value, false) === false) {
+ return false;
+ }
+ } else {
+ $ns_args[] = array($ns_alias, $ns_key, $value);
+ }
+ }
+
+ if (!$this->getOpenIDNamespace()) {
+ if ($this->setOpenIDNamespace(Auth_OpenID_OPENID1_NS, true) ===
+ false) {
+ return false;
+ }
+ }
+
+ // Actually put the pairs into the appropriate namespaces
+ foreach ($ns_args as $triple) {
+ list($ns_alias, $ns_key, $value) = $triple;
+ $ns_uri = $this->namespaces->getNamespaceURI($ns_alias);
+ if ($ns_uri === null) {
+ $ns_uri = $this->_getDefaultNamespace($ns_alias);
+ if ($ns_uri === null) {
+
+ $ns_uri = Auth_OpenID_OPENID_NS;
+ $ns_key = sprintf('%s.%s', $ns_alias, $ns_key);
+ } else {
+ $this->namespaces->addAlias($ns_uri, $ns_alias, true);
+ }
+ }
+
+ $this->setArg($ns_uri, $ns_key, $value);
+ }
+
+ return true;
+ }
+
+ function _getDefaultNamespace($mystery_alias)
+ {
+ global $Auth_OpenID_registered_aliases;
+ if ($this->isOpenID1()) {
+ return @$Auth_OpenID_registered_aliases[$mystery_alias];
+ }
+ return null;
+ }
+
+ function setOpenIDNamespace($openid_ns_uri, $implicit)
+ {
+ if (!in_array($openid_ns_uri, $this->allowed_openid_namespaces)) {
+ Auth_OpenID::log('Invalid null namespace: "%s"', $openid_ns_uri);
+ return false;
+ }
+
+ $succeeded = $this->namespaces->addAlias($openid_ns_uri,
+ Auth_OpenID_NULL_NAMESPACE,
+ $implicit);
+ if ($succeeded === false) {
+ return false;
+ }
+
+ $this->_openid_ns_uri = $openid_ns_uri;
+
+ return true;
+ }
+
+ function getOpenIDNamespace()
+ {
+ return $this->_openid_ns_uri;
+ }
+
+ function fromKVForm($kvform_string)
+ {
+ // Create a Message from a KVForm string
+ return Auth_OpenID_Message::fromOpenIDArgs(
+ Auth_OpenID_KVForm::toArray($kvform_string));
+ }
+
+ function copy()
+ {
+ return $this;
+ }
+
+ function toPostArgs()
+ {
+ // Return all arguments with openid. in front of namespaced
+ // arguments.
+
+ $args = array();
+
+ // Add namespace definitions to the output
+ foreach ($this->namespaces->iteritems() as $pair) {
+ list($ns_uri, $alias) = $pair;
+ if ($this->namespaces->isImplicit($ns_uri)) {
+ continue;
+ }
+ if ($alias == Auth_OpenID_NULL_NAMESPACE) {
+ $ns_key = 'openid.ns';
+ } else {
+ $ns_key = 'openid.ns.' . $alias;
+ }
+ $args[$ns_key] = $ns_uri;
+ }
+
+ foreach ($this->args->items() as $pair) {
+ list($ns_parts, $value) = $pair;
+ list($ns_uri, $ns_key) = $ns_parts;
+ $key = $this->getKey($ns_uri, $ns_key);
+ $args[$key] = $value;
+ }
+
+ return $args;
+ }
+
+ function toArgs()
+ {
+ // Return all namespaced arguments, failing if any
+ // non-namespaced arguments exist.
+ $post_args = $this->toPostArgs();
+ $kvargs = array();
+ foreach ($post_args as $k => $v) {
+ if (strpos($k, 'openid.') !== 0) {
+ // raise ValueError(
+ // 'This message can only be encoded as a POST, because it '
+ // 'contains arguments that are not prefixed with "openid."')
+ return null;
+ } else {
+ $kvargs[substr($k, 7)] = $v;
+ }
+ }
+
+ return $kvargs;
+ }
+
+ function toFormMarkup($action_url, $form_tag_attrs = null,
+ $submit_text = "Continue")
+ {
+ $form = "<form accept-charset=\"UTF-8\" ".
+ "enctype=\"application/x-www-form-urlencoded\"";
+
+ if (!$form_tag_attrs) {
+ $form_tag_attrs = array();
+ }
+
+ $form_tag_attrs['action'] = $action_url;
+ $form_tag_attrs['method'] = 'post';
+
+ unset($form_tag_attrs['enctype']);
+ unset($form_tag_attrs['accept-charset']);
+
+ if ($form_tag_attrs) {
+ foreach ($form_tag_attrs as $name => $attr) {
+ $form .= sprintf(" %s=\"%s\"", $name, $attr);
+ }
+ }
+
+ $form .= ">\n";
+
+ foreach ($this->toPostArgs() as $name => $value) {
+ $form .= sprintf(
+ "<input type=\"hidden\" name=\"%s\" value=\"%s\" />\n",
+ $name, $value);
+ }
+
+ $form .= sprintf("<input type=\"submit\" value=\"%s\" />\n",
+ $submit_text);
+
+ $form .= "</form>\n";
+
+ return $form;
+ }
+
+ function toURL($base_url)
+ {
+ // Generate a GET URL with the parameters in this message
+ // attached as query parameters.
+ return Auth_OpenID::appendArgs($base_url, $this->toPostArgs());
+ }
+
+ function toKVForm()
+ {
+ // Generate a KVForm string that contains the parameters in
+ // this message. This will fail if the message contains
+ // arguments outside of the 'openid.' prefix.
+ return Auth_OpenID_KVForm::fromArray($this->toArgs());
+ }
+
+ function toURLEncoded()
+ {
+ // Generate an x-www-urlencoded string
+ $args = array();
+
+ foreach ($this->toPostArgs() as $k => $v) {
+ $args[] = array($k, $v);
+ }
+
+ sort($args);
+ return Auth_OpenID::httpBuildQuery($args);
+ }
+
+ /**
+ * @access private
+ */
+ function _fixNS($namespace)
+ {
+ // Convert an input value into the internally used values of
+ // this object
+
+ if ($namespace == Auth_OpenID_OPENID_NS) {
+ if ($this->_openid_ns_uri === null) {
+ return new Auth_OpenID_FailureResponse(null,
+ 'OpenID namespace not set');
+ } else {
+ $namespace = $this->_openid_ns_uri;
+ }
+ }
+
+ if (($namespace != Auth_OpenID_BARE_NS) &&
+ (!is_string($namespace))) {
+ //TypeError
+ $err_msg = sprintf("Namespace must be Auth_OpenID_BARE_NS, ".
+ "Auth_OpenID_OPENID_NS or a string. got %s",
+ print_r($namespace, true));
+ return new Auth_OpenID_FailureResponse(null, $err_msg);
+ }
+
+ if (($namespace != Auth_OpenID_BARE_NS) &&
+ (strpos($namespace, ':') === false)) {
+ // fmt = 'OpenID 2.0 namespace identifiers SHOULD be URIs. Got %r'
+ // warnings.warn(fmt % (namespace,), DeprecationWarning)
+
+ if ($namespace == 'sreg') {
+ // fmt = 'Using %r instead of "sreg" as namespace'
+ // warnings.warn(fmt % (SREG_URI,), DeprecationWarning,)
+ return Auth_OpenID_SREG_URI;
+ }
+ }
+
+ return $namespace;
+ }
+
+ function hasKey($namespace, $ns_key)
+ {
+ $namespace = $this->_fixNS($namespace);
+ if (Auth_OpenID::isFailure($namespace)) {
+ // XXX log me
+ return false;
+ } else {
+ return $this->args->contains(array($namespace, $ns_key));
+ }
+ }
+
+ function getKey($namespace, $ns_key)
+ {
+ // Get the key for a particular namespaced argument
+ $namespace = $this->_fixNS($namespace);
+ if (Auth_OpenID::isFailure($namespace)) {
+ return $namespace;
+ }
+ if ($namespace == Auth_OpenID_BARE_NS) {
+ return $ns_key;
+ }
+
+ $ns_alias = $this->namespaces->getAlias($namespace);
+
+ // No alias is defined, so no key can exist
+ if ($ns_alias === null) {
+ return null;
+ }
+
+ if ($ns_alias == Auth_OpenID_NULL_NAMESPACE) {
+ $tail = $ns_key;
+ } else {
+ $tail = sprintf('%s.%s', $ns_alias, $ns_key);
+ }
+
+ return 'openid.' . $tail;
+ }
+
+ function getArg($namespace, $key, $default = null)
+ {
+ // Get a value for a namespaced key.
+ $namespace = $this->_fixNS($namespace);
+
+ if (Auth_OpenID::isFailure($namespace)) {
+ return $namespace;
+ } else {
+ if ((!$this->args->contains(array($namespace, $key))) &&
+ ($default == Auth_OpenID_NO_DEFAULT)) {
+ $err_msg = sprintf("Namespace %s missing required field %s",
+ $namespace, $key);
+ return new Auth_OpenID_FailureResponse(null, $err_msg);
+ } else {
+ return $this->args->get(array($namespace, $key), $default);
+ }
+ }
+ }
+
+ function getArgs($namespace)
+ {
+ // Get the arguments that are defined for this namespace URI
+
+ $namespace = $this->_fixNS($namespace);
+ if (Auth_OpenID::isFailure($namespace)) {
+ return $namespace;
+ } else {
+ $stuff = array();
+ foreach ($this->args->items() as $pair) {
+ list($key, $value) = $pair;
+ list($pair_ns, $ns_key) = $key;
+ if ($pair_ns == $namespace) {
+ $stuff[$ns_key] = $value;
+ }
+ }
+
+ return $stuff;
+ }
+ }
+
+ function updateArgs($namespace, $updates)
+ {
+ // Set multiple key/value pairs in one call
+
+ $namespace = $this->_fixNS($namespace);
+
+ if (Auth_OpenID::isFailure($namespace)) {
+ return $namespace;
+ } else {
+ foreach ($updates as $k => $v) {
+ $this->setArg($namespace, $k, $v);
+ }
+ return true;
+ }
+ }
+
+ function setArg($namespace, $key, $value)
+ {
+ // Set a single argument in this namespace
+ $namespace = $this->_fixNS($namespace);
+
+ if (Auth_OpenID::isFailure($namespace)) {
+ return $namespace;
+ } else {
+ $this->args->set(array($namespace, $key), $value);
+ if ($namespace !== Auth_OpenID_BARE_NS) {
+ $this->namespaces->add($namespace);
+ }
+ return true;
+ }
+ }
+
+ function delArg($namespace, $key)
+ {
+ $namespace = $this->_fixNS($namespace);
+
+ if (Auth_OpenID::isFailure($namespace)) {
+ return $namespace;
+ } else {
+ return $this->args->del(array($namespace, $key));
+ }
+ }
+
+ function getAliasedArg($aliased_key, $default = null)
+ {
+ $parts = explode('.', $aliased_key, 2);
+
+ if (count($parts) != 2) {
+ $ns = null;
+ } else {
+ list($alias, $key) = $parts;
+
+ if ($alias == 'ns') {
+ // Return the namespace URI for a namespace alias
+ // parameter.
+ return $this->namespaces->getNamespaceURI($key);
+ } else {
+ $ns = $this->namespaces->getNamespaceURI($alias);
+ }
+ }
+
+ if ($ns === null) {
+ $key = $aliased_key;
+ $ns = $this->getOpenIDNamespace();
+ }
+
+ return $this->getArg($ns, $key, $default);
+ }
+}
+
+?>
diff --git a/extlib/Auth/OpenID/MySQLStore.php b/extlib/Auth/OpenID/MySQLStore.php
new file mode 100644
index 000000000..eb08af016
--- /dev/null
+++ b/extlib/Auth/OpenID/MySQLStore.php
@@ -0,0 +1,78 @@
+<?php
+
+/**
+ * A MySQL store.
+ *
+ * @package OpenID
+ */
+
+/**
+ * Require the base class file.
+ */
+require_once "Auth/OpenID/SQLStore.php";
+
+/**
+ * An SQL store that uses MySQL as its backend.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_MySQLStore extends Auth_OpenID_SQLStore {
+ /**
+ * @access private
+ */
+ function setSQL()
+ {
+ $this->sql['nonce_table'] =
+ "CREATE TABLE %s (\n".
+ " server_url VARCHAR(2047) NOT NULL,\n".
+ " timestamp INTEGER NOT NULL,\n".
+ " salt CHAR(40) NOT NULL,\n".
+ " UNIQUE (server_url(255), timestamp, salt)\n".
+ ") ENGINE=InnoDB";
+
+ $this->sql['assoc_table'] =
+ "CREATE TABLE %s (\n".
+ " server_url BLOB NOT NULL,\n".
+ " handle VARCHAR(255) NOT NULL,\n".
+ " secret BLOB NOT NULL,\n".
+ " issued INTEGER NOT NULL,\n".
+ " lifetime INTEGER NOT NULL,\n".
+ " assoc_type VARCHAR(64) NOT NULL,\n".
+ " PRIMARY KEY (server_url(255), handle)\n".
+ ") ENGINE=InnoDB";
+
+ $this->sql['set_assoc'] =
+ "REPLACE INTO %s (server_url, handle, secret, issued,\n".
+ " lifetime, assoc_type) VALUES (?, ?, !, ?, ?, ?)";
+
+ $this->sql['get_assocs'] =
+ "SELECT handle, secret, issued, lifetime, assoc_type FROM %s ".
+ "WHERE server_url = ?";
+
+ $this->sql['get_assoc'] =
+ "SELECT handle, secret, issued, lifetime, assoc_type FROM %s ".
+ "WHERE server_url = ? AND handle = ?";
+
+ $this->sql['remove_assoc'] =
+ "DELETE FROM %s WHERE server_url = ? AND handle = ?";
+
+ $this->sql['add_nonce'] =
+ "INSERT INTO %s (server_url, timestamp, salt) VALUES (?, ?, ?)";
+
+ $this->sql['clean_nonce'] =
+ "DELETE FROM %s WHERE timestamp < ?";
+
+ $this->sql['clean_assoc'] =
+ "DELETE FROM %s WHERE issued + lifetime < ?";
+ }
+
+ /**
+ * @access private
+ */
+ function blobEncode($blob)
+ {
+ return "0x" . bin2hex($blob);
+ }
+}
+
+?> \ No newline at end of file
diff --git a/extlib/Auth/OpenID/Nonce.php b/extlib/Auth/OpenID/Nonce.php
new file mode 100644
index 000000000..effecac38
--- /dev/null
+++ b/extlib/Auth/OpenID/Nonce.php
@@ -0,0 +1,109 @@
+<?php
+
+/**
+ * Nonce-related functionality.
+ *
+ * @package OpenID
+ */
+
+/**
+ * Need CryptUtil to generate random strings.
+ */
+require_once 'Auth/OpenID/CryptUtil.php';
+
+/**
+ * This is the characters that the nonces are made from.
+ */
+define('Auth_OpenID_Nonce_CHRS',"abcdefghijklmnopqrstuvwxyz" .
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789");
+
+// Keep nonces for five hours (allow five hours for the combination of
+// request time and clock skew). This is probably way more than is
+// necessary, but there is not much overhead in storing nonces.
+global $Auth_OpenID_SKEW;
+$Auth_OpenID_SKEW = 60 * 60 * 5;
+
+define('Auth_OpenID_Nonce_REGEX',
+ '/(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)Z(.*)/');
+
+define('Auth_OpenID_Nonce_TIME_FMT',
+ '%Y-%m-%dT%H:%M:%SZ');
+
+function Auth_OpenID_splitNonce($nonce_string)
+{
+ // Extract a timestamp from the given nonce string
+ $result = preg_match(Auth_OpenID_Nonce_REGEX, $nonce_string, $matches);
+ if ($result != 1 || count($matches) != 8) {
+ return null;
+ }
+
+ list($unused,
+ $tm_year,
+ $tm_mon,
+ $tm_mday,
+ $tm_hour,
+ $tm_min,
+ $tm_sec,
+ $uniquifier) = $matches;
+
+ $timestamp =
+ @gmmktime($tm_hour, $tm_min, $tm_sec, $tm_mon, $tm_mday, $tm_year);
+
+ if ($timestamp === false || $timestamp < 0) {
+ return null;
+ }
+
+ return array($timestamp, $uniquifier);
+}
+
+function Auth_OpenID_checkTimestamp($nonce_string,
+ $allowed_skew = null,
+ $now = null)
+{
+ // Is the timestamp that is part of the specified nonce string
+ // within the allowed clock-skew of the current time?
+ global $Auth_OpenID_SKEW;
+
+ if ($allowed_skew === null) {
+ $allowed_skew = $Auth_OpenID_SKEW;
+ }
+
+ $parts = Auth_OpenID_splitNonce($nonce_string);
+ if ($parts == null) {
+ return false;
+ }
+
+ if ($now === null) {
+ $now = time();
+ }
+
+ $stamp = $parts[0];
+
+ // Time after which we should not use the nonce
+ $past = $now - $allowed_skew;
+
+ // Time that is too far in the future for us to allow
+ $future = $now + $allowed_skew;
+
+ // the stamp is not too far in the future and is not too far
+ // in the past
+ return (($past <= $stamp) && ($stamp <= $future));
+}
+
+function Auth_OpenID_mkNonce($when = null)
+{
+ // Generate a nonce with the current timestamp
+ $salt = Auth_OpenID_CryptUtil::randomString(
+ 6, Auth_OpenID_Nonce_CHRS);
+ if ($when === null) {
+ // It's safe to call time() with no arguments; it returns a
+ // GMT unix timestamp on PHP 4 and PHP 5. gmmktime() with no
+ // args returns a local unix timestamp on PHP 4, so don't use
+ // that.
+ $when = time();
+ }
+ $time_str = gmstrftime(Auth_OpenID_Nonce_TIME_FMT, $when);
+ return $time_str . $salt;
+}
+
+?> \ No newline at end of file
diff --git a/extlib/Auth/OpenID/PAPE.php b/extlib/Auth/OpenID/PAPE.php
new file mode 100644
index 000000000..62cba8a91
--- /dev/null
+++ b/extlib/Auth/OpenID/PAPE.php
@@ -0,0 +1,301 @@
+<?php
+
+/**
+ * An implementation of the OpenID Provider Authentication Policy
+ * Extension 1.0
+ *
+ * See:
+ * http://openid.net/developers/specs/
+ */
+
+require_once "Auth/OpenID/Extension.php";
+
+define('Auth_OpenID_PAPE_NS_URI',
+ "http://specs.openid.net/extensions/pape/1.0");
+
+define('PAPE_AUTH_MULTI_FACTOR_PHYSICAL',
+ 'http://schemas.openid.net/pape/policies/2007/06/multi-factor-physical');
+define('PAPE_AUTH_MULTI_FACTOR',
+ 'http://schemas.openid.net/pape/policies/2007/06/multi-factor');
+define('PAPE_AUTH_PHISHING_RESISTANT',
+ 'http://schemas.openid.net/pape/policies/2007/06/phishing-resistant');
+
+define('PAPE_TIME_VALIDATOR',
+ '^[0-9]{4,4}-[0-9][0-9]-[0-9][0-9]T[0-9][0-9]:[0-9][0-9]:[0-9][0-9]Z$');
+/**
+ * A Provider Authentication Policy request, sent from a relying party
+ * to a provider
+ *
+ * preferred_auth_policies: The authentication policies that
+ * the relying party prefers
+ *
+ * max_auth_age: The maximum time, in seconds, that the relying party
+ * wants to allow to have elapsed before the user must re-authenticate
+ */
+class Auth_OpenID_PAPE_Request extends Auth_OpenID_Extension {
+
+ var $ns_alias = 'pape';
+ var $ns_uri = Auth_OpenID_PAPE_NS_URI;
+
+ function Auth_OpenID_PAPE_Request($preferred_auth_policies=null,
+ $max_auth_age=null)
+ {
+ if ($preferred_auth_policies === null) {
+ $preferred_auth_policies = array();
+ }
+
+ $this->preferred_auth_policies = $preferred_auth_policies;
+ $this->max_auth_age = $max_auth_age;
+ }
+
+ /**
+ * Add an acceptable authentication policy URI to this request
+ *
+ * This method is intended to be used by the relying party to add
+ * acceptable authentication types to the request.
+ *
+ * policy_uri: The identifier for the preferred type of
+ * authentication.
+ */
+ function addPolicyURI($policy_uri)
+ {
+ if (!in_array($policy_uri, $this->preferred_auth_policies)) {
+ $this->preferred_auth_policies[] = $policy_uri;
+ }
+ }
+
+ function getExtensionArgs()
+ {
+ $ns_args = array(
+ 'preferred_auth_policies' =>
+ implode(' ', $this->preferred_auth_policies)
+ );
+
+ if ($this->max_auth_age !== null) {
+ $ns_args['max_auth_age'] = strval($this->max_auth_age);
+ }
+
+ return $ns_args;
+ }
+
+ /**
+ * Instantiate a Request object from the arguments in a checkid_*
+ * OpenID message
+ */
+ function fromOpenIDRequest($request)
+ {
+ $obj = new Auth_OpenID_PAPE_Request();
+ $args = $request->message->getArgs(Auth_OpenID_PAPE_NS_URI);
+
+ if ($args === null || $args === array()) {
+ return null;
+ }
+
+ $obj->parseExtensionArgs($args);
+ return $obj;
+ }
+
+ /**
+ * Set the state of this request to be that expressed in these
+ * PAPE arguments
+ *
+ * @param args: The PAPE arguments without a namespace
+ */
+ function parseExtensionArgs($args)
+ {
+ // preferred_auth_policies is a space-separated list of policy
+ // URIs
+ $this->preferred_auth_policies = array();
+
+ $policies_str = Auth_OpenID::arrayGet($args, 'preferred_auth_policies');
+ if ($policies_str) {
+ foreach (explode(' ', $policies_str) as $uri) {
+ if (!in_array($uri, $this->preferred_auth_policies)) {
+ $this->preferred_auth_policies[] = $uri;
+ }
+ }
+ }
+
+ // max_auth_age is base-10 integer number of seconds
+ $max_auth_age_str = Auth_OpenID::arrayGet($args, 'max_auth_age');
+ if ($max_auth_age_str) {
+ $this->max_auth_age = Auth_OpenID::intval($max_auth_age_str);
+ } else {
+ $this->max_auth_age = null;
+ }
+ }
+
+ /**
+ * Given a list of authentication policy URIs that a provider
+ * supports, this method returns the subsequence of those types
+ * that are preferred by the relying party.
+ *
+ * @param supported_types: A sequence of authentication policy
+ * type URIs that are supported by a provider
+ *
+ * @return array The sub-sequence of the supported types that are
+ * preferred by the relying party. This list will be ordered in
+ * the order that the types appear in the supported_types
+ * sequence, and may be empty if the provider does not prefer any
+ * of the supported authentication types.
+ */
+ function preferredTypes($supported_types)
+ {
+ $result = array();
+
+ foreach ($supported_types as $st) {
+ if (in_array($st, $this->preferred_auth_policies)) {
+ $result[] = $st;
+ }
+ }
+ return $result;
+ }
+}
+
+/**
+ * A Provider Authentication Policy response, sent from a provider to
+ * a relying party
+ */
+class Auth_OpenID_PAPE_Response extends Auth_OpenID_Extension {
+
+ var $ns_alias = 'pape';
+ var $ns_uri = Auth_OpenID_PAPE_NS_URI;
+
+ function Auth_OpenID_PAPE_Response($auth_policies=null, $auth_time=null,
+ $nist_auth_level=null)
+ {
+ if ($auth_policies) {
+ $this->auth_policies = $auth_policies;
+ } else {
+ $this->auth_policies = array();
+ }
+
+ $this->auth_time = $auth_time;
+ $this->nist_auth_level = $nist_auth_level;
+ }
+
+ /**
+ * Add a authentication policy to this response
+ *
+ * This method is intended to be used by the provider to add a
+ * policy that the provider conformed to when authenticating the
+ * user.
+ *
+ * @param policy_uri: The identifier for the preferred type of
+ * authentication.
+ */
+ function addPolicyURI($policy_uri)
+ {
+ if (!in_array($policy_uri, $this->auth_policies)) {
+ $this->auth_policies[] = $policy_uri;
+ }
+ }
+
+ /**
+ * Create an Auth_OpenID_PAPE_Response object from a successful
+ * OpenID library response.
+ *
+ * @param success_response $success_response A SuccessResponse
+ * from Auth_OpenID_Consumer::complete()
+ *
+ * @returns: A provider authentication policy response from the
+ * data that was supplied with the id_res response.
+ */
+ function fromSuccessResponse($success_response)
+ {
+ $obj = new Auth_OpenID_PAPE_Response();
+
+ // PAPE requires that the args be signed.
+ $args = $success_response->getSignedNS(Auth_OpenID_PAPE_NS_URI);
+
+ if ($args === null || $args === array()) {
+ return null;
+ }
+
+ $result = $obj->parseExtensionArgs($args);
+
+ if ($result === false) {
+ return null;
+ } else {
+ return $obj;
+ }
+ }
+
+ /**
+ * Parse the provider authentication policy arguments into the
+ * internal state of this object
+ *
+ * @param args: unqualified provider authentication policy
+ * arguments
+ *
+ * @param strict: Whether to return false when bad data is
+ * encountered
+ *
+ * @return null The data is parsed into the internal fields of
+ * this object.
+ */
+ function parseExtensionArgs($args, $strict=false)
+ {
+ $policies_str = Auth_OpenID::arrayGet($args, 'auth_policies');
+ if ($policies_str && $policies_str != "none") {
+ $this->auth_policies = explode(" ", $policies_str);
+ }
+
+ $nist_level_str = Auth_OpenID::arrayGet($args, 'nist_auth_level');
+ if ($nist_level_str !== null) {
+ $nist_level = Auth_OpenID::intval($nist_level_str);
+
+ if ($nist_level === false) {
+ if ($strict) {
+ return false;
+ } else {
+ $nist_level = null;
+ }
+ }
+
+ if (0 <= $nist_level && $nist_level < 5) {
+ $this->nist_auth_level = $nist_level;
+ } else if ($strict) {
+ return false;
+ }
+ }
+
+ $auth_time = Auth_OpenID::arrayGet($args, 'auth_time');
+ if ($auth_time !== null) {
+ if (ereg(PAPE_TIME_VALIDATOR, $auth_time)) {
+ $this->auth_time = $auth_time;
+ } else if ($strict) {
+ return false;
+ }
+ }
+ }
+
+ function getExtensionArgs()
+ {
+ $ns_args = array();
+ if (count($this->auth_policies) > 0) {
+ $ns_args['auth_policies'] = implode(' ', $this->auth_policies);
+ } else {
+ $ns_args['auth_policies'] = 'none';
+ }
+
+ if ($this->nist_auth_level !== null) {
+ if (!in_array($this->nist_auth_level, range(0, 4), true)) {
+ return false;
+ }
+ $ns_args['nist_auth_level'] = strval($this->nist_auth_level);
+ }
+
+ if ($this->auth_time !== null) {
+ if (!ereg(PAPE_TIME_VALIDATOR, $this->auth_time)) {
+ return false;
+ }
+
+ $ns_args['auth_time'] = $this->auth_time;
+ }
+
+ return $ns_args;
+ }
+}
+
+?> \ No newline at end of file
diff --git a/extlib/Auth/OpenID/Parse.php b/extlib/Auth/OpenID/Parse.php
new file mode 100644
index 000000000..546f34f6b
--- /dev/null
+++ b/extlib/Auth/OpenID/Parse.php
@@ -0,0 +1,352 @@
+<?php
+
+/**
+ * This module implements a VERY limited parser that finds <link> tags
+ * in the head of HTML or XHTML documents and parses out their
+ * attributes according to the OpenID spec. It is a liberal parser,
+ * but it requires these things from the data in order to work:
+ *
+ * - There must be an open <html> tag
+ *
+ * - There must be an open <head> tag inside of the <html> tag
+ *
+ * - Only <link>s that are found inside of the <head> tag are parsed
+ * (this is by design)
+ *
+ * - The parser follows the OpenID specification in resolving the
+ * attributes of the link tags. This means that the attributes DO
+ * NOT get resolved as they would by an XML or HTML parser. In
+ * particular, only certain entities get replaced, and href
+ * attributes do not get resolved relative to a base URL.
+ *
+ * From http://openid.net/specs.bml:
+ *
+ * - The openid.server URL MUST be an absolute URL. OpenID consumers
+ * MUST NOT attempt to resolve relative URLs.
+ *
+ * - The openid.server URL MUST NOT include entities other than &amp;,
+ * &lt;, &gt;, and &quot;.
+ *
+ * The parser ignores SGML comments and <![CDATA[blocks]]>. Both kinds
+ * of quoting are allowed for attributes.
+ *
+ * The parser deals with invalid markup in these ways:
+ *
+ * - Tag names are not case-sensitive
+ *
+ * - The <html> tag is accepted even when it is not at the top level
+ *
+ * - The <head> tag is accepted even when it is not a direct child of
+ * the <html> tag, but a <html> tag must be an ancestor of the
+ * <head> tag
+ *
+ * - <link> tags are accepted even when they are not direct children
+ * of the <head> tag, but a <head> tag must be an ancestor of the
+ * <link> tag
+ *
+ * - If there is no closing tag for an open <html> or <head> tag, the
+ * remainder of the document is viewed as being inside of the
+ * tag. If there is no closing tag for a <link> tag, the link tag is
+ * treated as a short tag. Exceptions to this rule are that <html>
+ * closes <html> and <body> or <head> closes <head>
+ *
+ * - Attributes of the <link> tag are not required to be quoted.
+ *
+ * - In the case of duplicated attribute names, the attribute coming
+ * last in the tag will be the value returned.
+ *
+ * - Any text that does not parse as an attribute within a link tag
+ * will be ignored. (e.g. <link pumpkin rel='openid.server' /> will
+ * ignore pumpkin)
+ *
+ * - If there are more than one <html> or <head> tag, the parser only
+ * looks inside of the first one.
+ *
+ * - The contents of <script> tags are ignored entirely, except
+ * unclosed <script> tags. Unclosed <script> tags are ignored.
+ *
+ * - Any other invalid markup is ignored, including unclosed SGML
+ * comments and unclosed <![CDATA[blocks.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @access private
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005-2008 Janrain, Inc.
+ * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
+ */
+
+/**
+ * Require Auth_OpenID::arrayGet().
+ */
+require_once "Auth/OpenID.php";
+
+class Auth_OpenID_Parse {
+
+ /**
+ * Specify some flags for use with regex matching.
+ */
+ var $_re_flags = "si";
+
+ /**
+ * Stuff to remove before we start looking for tags
+ */
+ var $_removed_re =
+ "<!--.*?-->|<!\[CDATA\[.*?\]\]>|<script\b(?!:)[^>]*>.*?<\/script>";
+
+ /**
+ * Starts with the tag name at a word boundary, where the tag name
+ * is not a namespace
+ */
+ var $_tag_expr = "<%s\b(?!:)([^>]*?)(?:\/>|>(.*?)(?:<\/?%s\s*>|\Z))";
+
+ var $_attr_find = '\b(\w+)=("[^"]*"|\'[^\']*\'|[^\'"\s\/<>]+)';
+
+ var $_open_tag_expr = "<%s\b";
+ var $_close_tag_expr = "<((\/%s\b)|(%s[^>\/]*\/))>";
+
+ function Auth_OpenID_Parse()
+ {
+ $this->_link_find = sprintf("/<link\b(?!:)([^>]*)(?!<)>/%s",
+ $this->_re_flags);
+
+ $this->_entity_replacements = array(
+ 'amp' => '&',
+ 'lt' => '<',
+ 'gt' => '>',
+ 'quot' => '"'
+ );
+
+ $this->_attr_find = sprintf("/%s/%s",
+ $this->_attr_find,
+ $this->_re_flags);
+
+ $this->_removed_re = sprintf("/%s/%s",
+ $this->_removed_re,
+ $this->_re_flags);
+
+ $this->_ent_replace =
+ sprintf("&(%s);", implode("|",
+ $this->_entity_replacements));
+ }
+
+ /**
+ * Returns a regular expression that will match a given tag in an
+ * SGML string.
+ */
+ function tagMatcher($tag_name, $close_tags = null)
+ {
+ $expr = $this->_tag_expr;
+
+ if ($close_tags) {
+ $options = implode("|", array_merge(array($tag_name), $close_tags));
+ $closer = sprintf("(?:%s)", $options);
+ } else {
+ $closer = $tag_name;
+ }
+
+ $expr = sprintf($expr, $tag_name, $closer);
+ return sprintf("/%s/%s", $expr, $this->_re_flags);
+ }
+
+ function openTag($tag_name)
+ {
+ $expr = sprintf($this->_open_tag_expr, $tag_name);
+ return sprintf("/%s/%s", $expr, $this->_re_flags);
+ }
+
+ function closeTag($tag_name)
+ {
+ $expr = sprintf($this->_close_tag_expr, $tag_name, $tag_name);
+ return sprintf("/%s/%s", $expr, $this->_re_flags);
+ }
+
+ function htmlBegin($s)
+ {
+ $matches = array();
+ $result = preg_match($this->openTag('html'), $s,
+ $matches, PREG_OFFSET_CAPTURE);
+ if ($result === false || !$matches) {
+ return false;
+ }
+ // Return the offset of the first match.
+ return $matches[0][1];
+ }
+
+ function htmlEnd($s)
+ {
+ $matches = array();
+ $result = preg_match($this->closeTag('html'), $s,
+ $matches, PREG_OFFSET_CAPTURE);
+ if ($result === false || !$matches) {
+ return false;
+ }
+ // Return the offset of the first match.
+ return $matches[count($matches) - 1][1];
+ }
+
+ function headFind()
+ {
+ return $this->tagMatcher('head', array('body', 'html'));
+ }
+
+ function replaceEntities($str)
+ {
+ foreach ($this->_entity_replacements as $old => $new) {
+ $str = preg_replace(sprintf("/&%s;/", $old), $new, $str);
+ }
+ return $str;
+ }
+
+ function removeQuotes($str)
+ {
+ $matches = array();
+ $double = '/^"(.*)"$/';
+ $single = "/^\'(.*)\'$/";
+
+ if (preg_match($double, $str, $matches)) {
+ return $matches[1];
+ } else if (preg_match($single, $str, $matches)) {
+ return $matches[1];
+ } else {
+ return $str;
+ }
+ }
+
+ /**
+ * Find all link tags in a string representing a HTML document and
+ * return a list of their attributes.
+ *
+ * @param string $html The text to parse
+ * @return array $list An array of arrays of attributes, one for each
+ * link tag
+ */
+ function parseLinkAttrs($html)
+ {
+ $stripped = preg_replace($this->_removed_re,
+ "",
+ $html);
+
+ $html_begin = $this->htmlBegin($stripped);
+ $html_end = $this->htmlEnd($stripped);
+
+ if ($html_begin === false) {
+ return array();
+ }
+
+ if ($html_end === false) {
+ $html_end = strlen($stripped);
+ }
+
+ $stripped = substr($stripped, $html_begin,
+ $html_end - $html_begin);
+
+ // Try to find the <HEAD> tag.
+ $head_re = $this->headFind();
+ $head_matches = array();
+ if (!preg_match($head_re, $stripped, $head_matches)) {
+ return array();
+ }
+
+ $link_data = array();
+ $link_matches = array();
+
+ if (!preg_match_all($this->_link_find, $head_matches[0],
+ $link_matches)) {
+ return array();
+ }
+
+ foreach ($link_matches[0] as $link) {
+ $attr_matches = array();
+ preg_match_all($this->_attr_find, $link, $attr_matches);
+ $link_attrs = array();
+ foreach ($attr_matches[0] as $index => $full_match) {
+ $name = $attr_matches[1][$index];
+ $value = $this->replaceEntities(
+ $this->removeQuotes($attr_matches[2][$index]));
+
+ $link_attrs[strtolower($name)] = $value;
+ }
+ $link_data[] = $link_attrs;
+ }
+
+ return $link_data;
+ }
+
+ function relMatches($rel_attr, $target_rel)
+ {
+ // Does this target_rel appear in the rel_str?
+ // XXX: TESTME
+ $rels = preg_split("/\s+/", trim($rel_attr));
+ foreach ($rels as $rel) {
+ $rel = strtolower($rel);
+ if ($rel == $target_rel) {
+ return 1;
+ }
+ }
+
+ return 0;
+ }
+
+ function linkHasRel($link_attrs, $target_rel)
+ {
+ // Does this link have target_rel as a relationship?
+ // XXX: TESTME
+ $rel_attr = Auth_OpeniD::arrayGet($link_attrs, 'rel', null);
+ return ($rel_attr && $this->relMatches($rel_attr,
+ $target_rel));
+ }
+
+ function findLinksRel($link_attrs_list, $target_rel)
+ {
+ // Filter the list of link attributes on whether it has
+ // target_rel as a relationship.
+ // XXX: TESTME
+ $result = array();
+ foreach ($link_attrs_list as $attr) {
+ if ($this->linkHasRel($attr, $target_rel)) {
+ $result[] = $attr;
+ }
+ }
+
+ return $result;
+ }
+
+ function findFirstHref($link_attrs_list, $target_rel)
+ {
+ // Return the value of the href attribute for the first link
+ // tag in the list that has target_rel as a relationship.
+ // XXX: TESTME
+ $matches = $this->findLinksRel($link_attrs_list,
+ $target_rel);
+ if (!$matches) {
+ return null;
+ }
+ $first = $matches[0];
+ return Auth_OpenID::arrayGet($first, 'href', null);
+ }
+}
+
+function Auth_OpenID_legacy_discover($html_text, $server_rel,
+ $delegate_rel)
+{
+ $p = new Auth_OpenID_Parse();
+
+ $link_attrs = $p->parseLinkAttrs($html_text);
+
+ $server_url = $p->findFirstHref($link_attrs,
+ $server_rel);
+
+ if ($server_url === null) {
+ return false;
+ } else {
+ $delegate_url = $p->findFirstHref($link_attrs,
+ $delegate_rel);
+ return array($delegate_url, $server_url);
+ }
+}
+
+?> \ No newline at end of file
diff --git a/extlib/Auth/OpenID/PostgreSQLStore.php b/extlib/Auth/OpenID/PostgreSQLStore.php
new file mode 100644
index 000000000..69d95e7b8
--- /dev/null
+++ b/extlib/Auth/OpenID/PostgreSQLStore.php
@@ -0,0 +1,113 @@
+<?php
+
+/**
+ * A PostgreSQL store.
+ *
+ * @package OpenID
+ */
+
+/**
+ * Require the base class file.
+ */
+require_once "Auth/OpenID/SQLStore.php";
+
+/**
+ * An SQL store that uses PostgreSQL as its backend.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_PostgreSQLStore extends Auth_OpenID_SQLStore {
+ /**
+ * @access private
+ */
+ function setSQL()
+ {
+ $this->sql['nonce_table'] =
+ "CREATE TABLE %s (server_url VARCHAR(2047) NOT NULL, ".
+ "timestamp INTEGER NOT NULL, ".
+ "salt CHAR(40) NOT NULL, ".
+ "UNIQUE (server_url, timestamp, salt))";
+
+ $this->sql['assoc_table'] =
+ "CREATE TABLE %s (server_url VARCHAR(2047) NOT NULL, ".
+ "handle VARCHAR(255) NOT NULL, ".
+ "secret BYTEA NOT NULL, ".
+ "issued INTEGER NOT NULL, ".
+ "lifetime INTEGER NOT NULL, ".
+ "assoc_type VARCHAR(64) NOT NULL, ".
+ "PRIMARY KEY (server_url, handle), ".
+ "CONSTRAINT secret_length_constraint CHECK ".
+ "(LENGTH(secret) <= 128))";
+
+ $this->sql['set_assoc'] =
+ array(
+ 'insert_assoc' => "INSERT INTO %s (server_url, handle, ".
+ "secret, issued, lifetime, assoc_type) VALUES ".
+ "(?, ?, '!', ?, ?, ?)",
+ 'update_assoc' => "UPDATE %s SET secret = '!', issued = ?, ".
+ "lifetime = ?, assoc_type = ? WHERE server_url = ? AND ".
+ "handle = ?"
+ );
+
+ $this->sql['get_assocs'] =
+ "SELECT handle, secret, issued, lifetime, assoc_type FROM %s ".
+ "WHERE server_url = ?";
+
+ $this->sql['get_assoc'] =
+ "SELECT handle, secret, issued, lifetime, assoc_type FROM %s ".
+ "WHERE server_url = ? AND handle = ?";
+
+ $this->sql['remove_assoc'] =
+ "DELETE FROM %s WHERE server_url = ? AND handle = ?";
+
+ $this->sql['add_nonce'] =
+ "INSERT INTO %s (server_url, timestamp, salt) VALUES ".
+ "(?, ?, ?)"
+ ;
+
+ $this->sql['clean_nonce'] =
+ "DELETE FROM %s WHERE timestamp < ?";
+
+ $this->sql['clean_assoc'] =
+ "DELETE FROM %s WHERE issued + lifetime < ?";
+ }
+
+ /**
+ * @access private
+ */
+ function _set_assoc($server_url, $handle, $secret, $issued, $lifetime,
+ $assoc_type)
+ {
+ $result = $this->_get_assoc($server_url, $handle);
+ if ($result) {
+ // Update the table since this associations already exists.
+ $this->connection->query($this->sql['set_assoc']['update_assoc'],
+ array($secret, $issued, $lifetime,
+ $assoc_type, $server_url, $handle));
+ } else {
+ // Insert a new record because this association wasn't
+ // found.
+ $this->connection->query($this->sql['set_assoc']['insert_assoc'],
+ array($server_url, $handle, $secret,
+ $issued, $lifetime, $assoc_type));
+ }
+ }
+
+ /**
+ * @access private
+ */
+ function blobEncode($blob)
+ {
+ return $this->_octify($blob);
+ }
+
+ /**
+ * @access private
+ */
+ function blobDecode($blob)
+ {
+ return $this->_unoctify($blob);
+ }
+}
+
+?> \ No newline at end of file
diff --git a/extlib/Auth/OpenID/SQLStore.php b/extlib/Auth/OpenID/SQLStore.php
new file mode 100644
index 000000000..da93c6aa2
--- /dev/null
+++ b/extlib/Auth/OpenID/SQLStore.php
@@ -0,0 +1,569 @@
+<?php
+
+/**
+ * SQL-backed OpenID stores.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005-2008 Janrain, Inc.
+ * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
+ */
+
+/**
+ * Require the PEAR DB module because we'll need it for the SQL-based
+ * stores implemented here. We silence any errors from the inclusion
+ * because it might not be present, and a user of the SQL stores may
+ * supply an Auth_OpenID_DatabaseConnection instance that implements
+ * its own storage.
+ */
+global $__Auth_OpenID_PEAR_AVAILABLE;
+$__Auth_OpenID_PEAR_AVAILABLE = @include_once 'DB.php';
+
+/**
+ * @access private
+ */
+require_once 'Auth/OpenID/Interface.php';
+require_once 'Auth/OpenID/Nonce.php';
+
+/**
+ * @access private
+ */
+require_once 'Auth/OpenID.php';
+
+/**
+ * @access private
+ */
+require_once 'Auth/OpenID/Nonce.php';
+
+/**
+ * This is the parent class for the SQL stores, which contains the
+ * logic common to all of the SQL stores.
+ *
+ * The table names used are determined by the class variables
+ * associations_table_name and nonces_table_name. To change the name
+ * of the tables used, pass new table names into the constructor.
+ *
+ * To create the tables with the proper schema, see the createTables
+ * method.
+ *
+ * This class shouldn't be used directly. Use one of its subclasses
+ * instead, as those contain the code necessary to use a specific
+ * database. If you're an OpenID integrator and you'd like to create
+ * an SQL-driven store that wraps an application's database
+ * abstraction, be sure to create a subclass of
+ * {@link Auth_OpenID_DatabaseConnection} that calls the application's
+ * database abstraction calls. Then, pass an instance of your new
+ * database connection class to your SQLStore subclass constructor.
+ *
+ * All methods other than the constructor and createTables should be
+ * considered implementation details.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_SQLStore extends Auth_OpenID_OpenIDStore {
+
+ /**
+ * This creates a new SQLStore instance. It requires an
+ * established database connection be given to it, and it allows
+ * overriding the default table names.
+ *
+ * @param connection $connection This must be an established
+ * connection to a database of the correct type for the SQLStore
+ * subclass you're using. This must either be an PEAR DB
+ * connection handle or an instance of a subclass of
+ * Auth_OpenID_DatabaseConnection.
+ *
+ * @param associations_table: This is an optional parameter to
+ * specify the name of the table used for storing associations.
+ * The default value is 'oid_associations'.
+ *
+ * @param nonces_table: This is an optional parameter to specify
+ * the name of the table used for storing nonces. The default
+ * value is 'oid_nonces'.
+ */
+ function Auth_OpenID_SQLStore($connection,
+ $associations_table = null,
+ $nonces_table = null)
+ {
+ global $__Auth_OpenID_PEAR_AVAILABLE;
+
+ $this->associations_table_name = "oid_associations";
+ $this->nonces_table_name = "oid_nonces";
+
+ // Check the connection object type to be sure it's a PEAR
+ // database connection.
+ if (!(is_object($connection) &&
+ (is_subclass_of($connection, 'db_common') ||
+ is_subclass_of($connection,
+ 'auth_openid_databaseconnection')))) {
+ trigger_error("Auth_OpenID_SQLStore expected PEAR connection " .
+ "object (got ".get_class($connection).")",
+ E_USER_ERROR);
+ return;
+ }
+
+ $this->connection = $connection;
+
+ // Be sure to set the fetch mode so the results are keyed on
+ // column name instead of column index. This is a PEAR
+ // constant, so only try to use it if PEAR is present. Note
+ // that Auth_Openid_Databaseconnection instances need not
+ // implement ::setFetchMode for this reason.
+ if ($__Auth_OpenID_PEAR_AVAILABLE) {
+ $this->connection->setFetchMode(DB_FETCHMODE_ASSOC);
+ }
+
+ if ($associations_table) {
+ $this->associations_table_name = $associations_table;
+ }
+
+ if ($nonces_table) {
+ $this->nonces_table_name = $nonces_table;
+ }
+
+ $this->max_nonce_age = 6 * 60 * 60;
+
+ // Be sure to run the database queries with auto-commit mode
+ // turned OFF, because we want every function to run in a
+ // transaction, implicitly. As a rule, methods named with a
+ // leading underscore will NOT control transaction behavior.
+ // Callers of these methods will worry about transactions.
+ $this->connection->autoCommit(false);
+
+ // Create an empty SQL strings array.
+ $this->sql = array();
+
+ // Call this method (which should be overridden by subclasses)
+ // to populate the $this->sql array with SQL strings.
+ $this->setSQL();
+
+ // Verify that all required SQL statements have been set, and
+ // raise an error if any expected SQL strings were either
+ // absent or empty.
+ list($missing, $empty) = $this->_verifySQL();
+
+ if ($missing) {
+ trigger_error("Expected keys in SQL query list: " .
+ implode(", ", $missing),
+ E_USER_ERROR);
+ return;
+ }
+
+ if ($empty) {
+ trigger_error("SQL list keys have no SQL strings: " .
+ implode(", ", $empty),
+ E_USER_ERROR);
+ return;
+ }
+
+ // Add table names to queries.
+ $this->_fixSQL();
+ }
+
+ function tableExists($table_name)
+ {
+ return !$this->isError(
+ $this->connection->query(
+ sprintf("SELECT * FROM %s LIMIT 0",
+ $table_name)));
+ }
+
+ /**
+ * Returns true if $value constitutes a database error; returns
+ * false otherwise.
+ */
+ function isError($value)
+ {
+ return PEAR::isError($value);
+ }
+
+ /**
+ * Converts a query result to a boolean. If the result is a
+ * database error according to $this->isError(), this returns
+ * false; otherwise, this returns true.
+ */
+ function resultToBool($obj)
+ {
+ if ($this->isError($obj)) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ /**
+ * This method should be overridden by subclasses. This method is
+ * called by the constructor to set values in $this->sql, which is
+ * an array keyed on sql name.
+ */
+ function setSQL()
+ {
+ }
+
+ /**
+ * Resets the store by removing all records from the store's
+ * tables.
+ */
+ function reset()
+ {
+ $this->connection->query(sprintf("DELETE FROM %s",
+ $this->associations_table_name));
+
+ $this->connection->query(sprintf("DELETE FROM %s",
+ $this->nonces_table_name));
+ }
+
+ /**
+ * @access private
+ */
+ function _verifySQL()
+ {
+ $missing = array();
+ $empty = array();
+
+ $required_sql_keys = array(
+ 'nonce_table',
+ 'assoc_table',
+ 'set_assoc',
+ 'get_assoc',
+ 'get_assocs',
+ 'remove_assoc'
+ );
+
+ foreach ($required_sql_keys as $key) {
+ if (!array_key_exists($key, $this->sql)) {
+ $missing[] = $key;
+ } else if (!$this->sql[$key]) {
+ $empty[] = $key;
+ }
+ }
+
+ return array($missing, $empty);
+ }
+
+ /**
+ * @access private
+ */
+ function _fixSQL()
+ {
+ $replacements = array(
+ array(
+ 'value' => $this->nonces_table_name,
+ 'keys' => array('nonce_table',
+ 'add_nonce',
+ 'clean_nonce')
+ ),
+ array(
+ 'value' => $this->associations_table_name,
+ 'keys' => array('assoc_table',
+ 'set_assoc',
+ 'get_assoc',
+ 'get_assocs',
+ 'remove_assoc',
+ 'clean_assoc')
+ )
+ );
+
+ foreach ($replacements as $item) {
+ $value = $item['value'];
+ $keys = $item['keys'];
+
+ foreach ($keys as $k) {
+ if (is_array($this->sql[$k])) {
+ foreach ($this->sql[$k] as $part_key => $part_value) {
+ $this->sql[$k][$part_key] = sprintf($part_value,
+ $value);
+ }
+ } else {
+ $this->sql[$k] = sprintf($this->sql[$k], $value);
+ }
+ }
+ }
+ }
+
+ function blobDecode($blob)
+ {
+ return $blob;
+ }
+
+ function blobEncode($str)
+ {
+ return $str;
+ }
+
+ function createTables()
+ {
+ $this->connection->autoCommit(true);
+ $n = $this->create_nonce_table();
+ $a = $this->create_assoc_table();
+ $this->connection->autoCommit(false);
+
+ if ($n && $a) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ function create_nonce_table()
+ {
+ if (!$this->tableExists($this->nonces_table_name)) {
+ $r = $this->connection->query($this->sql['nonce_table']);
+ return $this->resultToBool($r);
+ }
+ return true;
+ }
+
+ function create_assoc_table()
+ {
+ if (!$this->tableExists($this->associations_table_name)) {
+ $r = $this->connection->query($this->sql['assoc_table']);
+ return $this->resultToBool($r);
+ }
+ return true;
+ }
+
+ /**
+ * @access private
+ */
+ function _set_assoc($server_url, $handle, $secret, $issued,
+ $lifetime, $assoc_type)
+ {
+ return $this->connection->query($this->sql['set_assoc'],
+ array(
+ $server_url,
+ $handle,
+ $secret,
+ $issued,
+ $lifetime,
+ $assoc_type));
+ }
+
+ function storeAssociation($server_url, $association)
+ {
+ if ($this->resultToBool($this->_set_assoc(
+ $server_url,
+ $association->handle,
+ $this->blobEncode(
+ $association->secret),
+ $association->issued,
+ $association->lifetime,
+ $association->assoc_type
+ ))) {
+ $this->connection->commit();
+ } else {
+ $this->connection->rollback();
+ }
+ }
+
+ /**
+ * @access private
+ */
+ function _get_assoc($server_url, $handle)
+ {
+ $result = $this->connection->getRow($this->sql['get_assoc'],
+ array($server_url, $handle));
+ if ($this->isError($result)) {
+ return null;
+ } else {
+ return $result;
+ }
+ }
+
+ /**
+ * @access private
+ */
+ function _get_assocs($server_url)
+ {
+ $result = $this->connection->getAll($this->sql['get_assocs'],
+ array($server_url));
+
+ if ($this->isError($result)) {
+ return array();
+ } else {
+ return $result;
+ }
+ }
+
+ function removeAssociation($server_url, $handle)
+ {
+ if ($this->_get_assoc($server_url, $handle) == null) {
+ return false;
+ }
+
+ if ($this->resultToBool($this->connection->query(
+ $this->sql['remove_assoc'],
+ array($server_url, $handle)))) {
+ $this->connection->commit();
+ } else {
+ $this->connection->rollback();
+ }
+
+ return true;
+ }
+
+ function getAssociation($server_url, $handle = null)
+ {
+ if ($handle !== null) {
+ $assoc = $this->_get_assoc($server_url, $handle);
+
+ $assocs = array();
+ if ($assoc) {
+ $assocs[] = $assoc;
+ }
+ } else {
+ $assocs = $this->_get_assocs($server_url);
+ }
+
+ if (!$assocs || (count($assocs) == 0)) {
+ return null;
+ } else {
+ $associations = array();
+
+ foreach ($assocs as $assoc_row) {
+ $assoc = new Auth_OpenID_Association($assoc_row['handle'],
+ $assoc_row['secret'],
+ $assoc_row['issued'],
+ $assoc_row['lifetime'],
+ $assoc_row['assoc_type']);
+
+ $assoc->secret = $this->blobDecode($assoc->secret);
+
+ if ($assoc->getExpiresIn() == 0) {
+ $this->removeAssociation($server_url, $assoc->handle);
+ } else {
+ $associations[] = array($assoc->issued, $assoc);
+ }
+ }
+
+ if ($associations) {
+ $issued = array();
+ $assocs = array();
+ foreach ($associations as $key => $assoc) {
+ $issued[$key] = $assoc[0];
+ $assocs[$key] = $assoc[1];
+ }
+
+ array_multisort($issued, SORT_DESC, $assocs, SORT_DESC,
+ $associations);
+
+ // return the most recently issued one.
+ list($issued, $assoc) = $associations[0];
+ return $assoc;
+ } else {
+ return null;
+ }
+ }
+ }
+
+ /**
+ * @access private
+ */
+ function _add_nonce($server_url, $timestamp, $salt)
+ {
+ $sql = $this->sql['add_nonce'];
+ $result = $this->connection->query($sql, array($server_url,
+ $timestamp,
+ $salt));
+ if ($this->isError($result)) {
+ $this->connection->rollback();
+ } else {
+ $this->connection->commit();
+ }
+ return $this->resultToBool($result);
+ }
+
+ function useNonce($server_url, $timestamp, $salt)
+ {
+ global $Auth_OpenID_SKEW;
+
+ if ( abs($timestamp - time()) > $Auth_OpenID_SKEW ) {
+ return False;
+ }
+
+ return $this->_add_nonce($server_url, $timestamp, $salt);
+ }
+
+ /**
+ * "Octifies" a binary string by returning a string with escaped
+ * octal bytes. This is used for preparing binary data for
+ * PostgreSQL BYTEA fields.
+ *
+ * @access private
+ */
+ function _octify($str)
+ {
+ $result = "";
+ for ($i = 0; $i < Auth_OpenID::bytes($str); $i++) {
+ $ch = substr($str, $i, 1);
+ if ($ch == "\\") {
+ $result .= "\\\\\\\\";
+ } else if (ord($ch) == 0) {
+ $result .= "\\\\000";
+ } else {
+ $result .= "\\" . strval(decoct(ord($ch)));
+ }
+ }
+ return $result;
+ }
+
+ /**
+ * "Unoctifies" octal-escaped data from PostgreSQL and returns the
+ * resulting ASCII (possibly binary) string.
+ *
+ * @access private
+ */
+ function _unoctify($str)
+ {
+ $result = "";
+ $i = 0;
+ while ($i < strlen($str)) {
+ $char = $str[$i];
+ if ($char == "\\") {
+ // Look to see if the next char is a backslash and
+ // append it.
+ if ($str[$i + 1] != "\\") {
+ $octal_digits = substr($str, $i + 1, 3);
+ $dec = octdec($octal_digits);
+ $char = chr($dec);
+ $i += 4;
+ } else {
+ $char = "\\";
+ $i += 2;
+ }
+ } else {
+ $i += 1;
+ }
+
+ $result .= $char;
+ }
+
+ return $result;
+ }
+
+ function cleanupNonces()
+ {
+ global $Auth_OpenID_SKEW;
+ $v = time() - $Auth_OpenID_SKEW;
+
+ $this->connection->query($this->sql['clean_nonce'], array($v));
+ $num = $this->connection->affectedRows();
+ $this->connection->commit();
+ return $num;
+ }
+
+ function cleanupAssociations()
+ {
+ $this->connection->query($this->sql['clean_assoc'],
+ array(time()));
+ $num = $this->connection->affectedRows();
+ $this->connection->commit();
+ return $num;
+ }
+}
+
+?>
diff --git a/extlib/Auth/OpenID/SQLiteStore.php b/extlib/Auth/OpenID/SQLiteStore.php
new file mode 100644
index 000000000..ec2bf58e4
--- /dev/null
+++ b/extlib/Auth/OpenID/SQLiteStore.php
@@ -0,0 +1,71 @@
+<?php
+
+/**
+ * An SQLite store.
+ *
+ * @package OpenID
+ */
+
+/**
+ * Require the base class file.
+ */
+require_once "Auth/OpenID/SQLStore.php";
+
+/**
+ * An SQL store that uses SQLite as its backend.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_SQLiteStore extends Auth_OpenID_SQLStore {
+ function setSQL()
+ {
+ $this->sql['nonce_table'] =
+ "CREATE TABLE %s (server_url VARCHAR(2047), timestamp INTEGER, ".
+ "salt CHAR(40), UNIQUE (server_url, timestamp, salt))";
+
+ $this->sql['assoc_table'] =
+ "CREATE TABLE %s (server_url VARCHAR(2047), handle VARCHAR(255), ".
+ "secret BLOB(128), issued INTEGER, lifetime INTEGER, ".
+ "assoc_type VARCHAR(64), PRIMARY KEY (server_url, handle))";
+
+ $this->sql['set_assoc'] =
+ "INSERT OR REPLACE INTO %s VALUES (?, ?, ?, ?, ?, ?)";
+
+ $this->sql['get_assocs'] =
+ "SELECT handle, secret, issued, lifetime, assoc_type FROM %s ".
+ "WHERE server_url = ?";
+
+ $this->sql['get_assoc'] =
+ "SELECT handle, secret, issued, lifetime, assoc_type FROM %s ".
+ "WHERE server_url = ? AND handle = ?";
+
+ $this->sql['remove_assoc'] =
+ "DELETE FROM %s WHERE server_url = ? AND handle = ?";
+
+ $this->sql['add_nonce'] =
+ "INSERT INTO %s (server_url, timestamp, salt) VALUES (?, ?, ?)";
+
+ $this->sql['clean_nonce'] =
+ "DELETE FROM %s WHERE timestamp < ?";
+
+ $this->sql['clean_assoc'] =
+ "DELETE FROM %s WHERE issued + lifetime < ?";
+ }
+
+ /**
+ * @access private
+ */
+ function _add_nonce($server_url, $timestamp, $salt)
+ {
+ // PECL SQLite extensions 1.0.3 and older (1.0.3 is the
+ // current release at the time of this writing) have a broken
+ // sqlite_escape_string function that breaks when passed the
+ // empty string. Prefixing all strings with one character
+ // keeps them unique and avoids this bug. The nonce table is
+ // write-only, so we don't have to worry about updating other
+ // functions with this same bad hack.
+ return parent::_add_nonce('x' . $server_url, $timestamp, $salt);
+ }
+}
+
+?> \ No newline at end of file
diff --git a/extlib/Auth/OpenID/SReg.php b/extlib/Auth/OpenID/SReg.php
new file mode 100644
index 000000000..63280769f
--- /dev/null
+++ b/extlib/Auth/OpenID/SReg.php
@@ -0,0 +1,521 @@
+<?php
+
+/**
+ * Simple registration request and response parsing and object
+ * representation.
+ *
+ * This module contains objects representing simple registration
+ * requests and responses that can be used with both OpenID relying
+ * parties and OpenID providers.
+ *
+ * 1. The relying party creates a request object and adds it to the
+ * {@link Auth_OpenID_AuthRequest} object before making the
+ * checkid request to the OpenID provider:
+ *
+ * $sreg_req = Auth_OpenID_SRegRequest::build(array('email'));
+ * $auth_request->addExtension($sreg_req);
+ *
+ * 2. The OpenID provider extracts the simple registration request
+ * from the OpenID request using {@link
+ * Auth_OpenID_SRegRequest::fromOpenIDRequest}, gets the user's
+ * approval and data, creates an {@link Auth_OpenID_SRegResponse}
+ * object and adds it to the id_res response:
+ *
+ * $sreg_req = Auth_OpenID_SRegRequest::fromOpenIDRequest(
+ * $checkid_request);
+ * // [ get the user's approval and data, informing the user that
+ * // the fields in sreg_response were requested ]
+ * $sreg_resp = Auth_OpenID_SRegResponse::extractResponse(
+ * $sreg_req, $user_data);
+ * $sreg_resp->toMessage($openid_response->fields);
+ *
+ * 3. The relying party uses {@link
+ * Auth_OpenID_SRegResponse::fromSuccessResponse} to extract the data
+ * from the OpenID response:
+ *
+ * $sreg_resp = Auth_OpenID_SRegResponse::fromSuccessResponse(
+ * $success_response);
+ *
+ * @package OpenID
+ */
+
+/**
+ * Import message and extension internals.
+ */
+require_once 'Auth/OpenID/Message.php';
+require_once 'Auth/OpenID/Extension.php';
+
+// The data fields that are listed in the sreg spec
+global $Auth_OpenID_sreg_data_fields;
+$Auth_OpenID_sreg_data_fields = array(
+ 'fullname' => 'Full Name',
+ 'nickname' => 'Nickname',
+ 'dob' => 'Date of Birth',
+ 'email' => 'E-mail Address',
+ 'gender' => 'Gender',
+ 'postcode' => 'Postal Code',
+ 'country' => 'Country',
+ 'language' => 'Language',
+ 'timezone' => 'Time Zone');
+
+/**
+ * Check to see that the given value is a valid simple registration
+ * data field name. Return true if so, false if not.
+ */
+function Auth_OpenID_checkFieldName($field_name)
+{
+ global $Auth_OpenID_sreg_data_fields;
+
+ if (!in_array($field_name, array_keys($Auth_OpenID_sreg_data_fields))) {
+ return false;
+ }
+ return true;
+}
+
+// URI used in the wild for Yadis documents advertising simple
+// registration support
+define('Auth_OpenID_SREG_NS_URI_1_0', 'http://openid.net/sreg/1.0');
+
+// URI in the draft specification for simple registration 1.1
+// <http://openid.net/specs/openid-simple-registration-extension-1_1-01.html>
+define('Auth_OpenID_SREG_NS_URI_1_1', 'http://openid.net/extensions/sreg/1.1');
+
+// This attribute will always hold the preferred URI to use when
+// adding sreg support to an XRDS file or in an OpenID namespace
+// declaration.
+define('Auth_OpenID_SREG_NS_URI', Auth_OpenID_SREG_NS_URI_1_1);
+
+Auth_OpenID_registerNamespaceAlias(Auth_OpenID_SREG_NS_URI_1_1, 'sreg');
+
+/**
+ * Does the given endpoint advertise support for simple
+ * registration?
+ *
+ * $endpoint: The endpoint object as returned by OpenID discovery.
+ * returns whether an sreg type was advertised by the endpoint
+ */
+function Auth_OpenID_supportsSReg(&$endpoint)
+{
+ return ($endpoint->usesExtension(Auth_OpenID_SREG_NS_URI_1_1) ||
+ $endpoint->usesExtension(Auth_OpenID_SREG_NS_URI_1_0));
+}
+
+/**
+ * A base class for classes dealing with Simple Registration protocol
+ * messages.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_SRegBase extends Auth_OpenID_Extension {
+ /**
+ * Extract the simple registration namespace URI from the given
+ * OpenID message. Handles OpenID 1 and 2, as well as both sreg
+ * namespace URIs found in the wild, as well as missing namespace
+ * definitions (for OpenID 1)
+ *
+ * $message: The OpenID message from which to parse simple
+ * registration fields. This may be a request or response message.
+ *
+ * Returns the sreg namespace URI for the supplied message. The
+ * message may be modified to define a simple registration
+ * namespace.
+ *
+ * @access private
+ */
+ function _getSRegNS(&$message)
+ {
+ $alias = null;
+ $found_ns_uri = null;
+
+ // See if there exists an alias for one of the two defined
+ // simple registration types.
+ foreach (array(Auth_OpenID_SREG_NS_URI_1_1,
+ Auth_OpenID_SREG_NS_URI_1_0) as $sreg_ns_uri) {
+ $alias = $message->namespaces->getAlias($sreg_ns_uri);
+ if ($alias !== null) {
+ $found_ns_uri = $sreg_ns_uri;
+ break;
+ }
+ }
+
+ if ($alias === null) {
+ // There is no alias for either of the types, so try to
+ // add one. We default to using the modern value (1.1)
+ $found_ns_uri = Auth_OpenID_SREG_NS_URI_1_1;
+ if ($message->namespaces->addAlias(Auth_OpenID_SREG_NS_URI_1_1,
+ 'sreg') === null) {
+ // An alias for the string 'sreg' already exists, but
+ // it's defined for something other than simple
+ // registration
+ return null;
+ }
+ }
+
+ return $found_ns_uri;
+ }
+}
+
+/**
+ * An object to hold the state of a simple registration request.
+ *
+ * required: A list of the required fields in this simple registration
+ * request
+ *
+ * optional: A list of the optional fields in this simple registration
+ * request
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_SRegRequest extends Auth_OpenID_SRegBase {
+
+ var $ns_alias = 'sreg';
+
+ /**
+ * Initialize an empty simple registration request.
+ */
+ function build($required=null, $optional=null,
+ $policy_url=null,
+ $sreg_ns_uri=Auth_OpenID_SREG_NS_URI,
+ $cls='Auth_OpenID_SRegRequest')
+ {
+ $obj = new $cls();
+
+ $obj->required = array();
+ $obj->optional = array();
+ $obj->policy_url = $policy_url;
+ $obj->ns_uri = $sreg_ns_uri;
+
+ if ($required) {
+ if (!$obj->requestFields($required, true, true)) {
+ return null;
+ }
+ }
+
+ if ($optional) {
+ if (!$obj->requestFields($optional, false, true)) {
+ return null;
+ }
+ }
+
+ return $obj;
+ }
+
+ /**
+ * Create a simple registration request that contains the fields
+ * that were requested in the OpenID request with the given
+ * arguments
+ *
+ * $request: The OpenID authentication request from which to
+ * extract an sreg request.
+ *
+ * $cls: name of class to use when creating sreg request object.
+ * Used for testing.
+ *
+ * Returns the newly created simple registration request
+ */
+ function fromOpenIDRequest($request, $cls='Auth_OpenID_SRegRequest')
+ {
+
+ $obj = call_user_func_array(array($cls, 'build'),
+ array(null, null, null, Auth_OpenID_SREG_NS_URI, $cls));
+
+ // Since we're going to mess with namespace URI mapping, don't
+ // mutate the object that was passed in.
+ $m = $request->message;
+
+ $obj->ns_uri = $obj->_getSRegNS($m);
+ $args = $m->getArgs($obj->ns_uri);
+
+ if ($args === null || Auth_OpenID::isFailure($args)) {
+ return null;
+ }
+
+ $obj->parseExtensionArgs($args);
+
+ return $obj;
+ }
+
+ /**
+ * Parse the unqualified simple registration request parameters
+ * and add them to this object.
+ *
+ * This method is essentially the inverse of
+ * getExtensionArgs. This method restores the serialized simple
+ * registration request fields.
+ *
+ * If you are extracting arguments from a standard OpenID
+ * checkid_* request, you probably want to use fromOpenIDRequest,
+ * which will extract the sreg namespace and arguments from the
+ * OpenID request. This method is intended for cases where the
+ * OpenID server needs more control over how the arguments are
+ * parsed than that method provides.
+ *
+ * $args == $message->getArgs($ns_uri);
+ * $request->parseExtensionArgs($args);
+ *
+ * $args: The unqualified simple registration arguments
+ *
+ * strict: Whether requests with fields that are not defined in
+ * the simple registration specification should be tolerated (and
+ * ignored)
+ */
+ function parseExtensionArgs($args, $strict=false)
+ {
+ foreach (array('required', 'optional') as $list_name) {
+ $required = ($list_name == 'required');
+ $items = Auth_OpenID::arrayGet($args, $list_name);
+ if ($items) {
+ foreach (explode(',', $items) as $field_name) {
+ if (!$this->requestField($field_name, $required, $strict)) {
+ if ($strict) {
+ return false;
+ }
+ }
+ }
+ }
+ }
+
+ $this->policy_url = Auth_OpenID::arrayGet($args, 'policy_url');
+
+ return true;
+ }
+
+ /**
+ * A list of all of the simple registration fields that were
+ * requested, whether they were required or optional.
+ */
+ function allRequestedFields()
+ {
+ return array_merge($this->required, $this->optional);
+ }
+
+ /**
+ * Have any simple registration fields been requested?
+ */
+ function wereFieldsRequested()
+ {
+ return count($this->allRequestedFields());
+ }
+
+ /**
+ * Was this field in the request?
+ */
+ function contains($field_name)
+ {
+ return (in_array($field_name, $this->required) ||
+ in_array($field_name, $this->optional));
+ }
+
+ /**
+ * Request the specified field from the OpenID user
+ *
+ * $field_name: the unqualified simple registration field name
+ *
+ * required: whether the given field should be presented to the
+ * user as being a required to successfully complete the request
+ *
+ * strict: whether to raise an exception when a field is added to
+ * a request more than once
+ */
+ function requestField($field_name,
+ $required=false, $strict=false)
+ {
+ if (!Auth_OpenID_checkFieldName($field_name)) {
+ return false;
+ }
+
+ if ($strict) {
+ if ($this->contains($field_name)) {
+ return false;
+ }
+ } else {
+ if (in_array($field_name, $this->required)) {
+ return true;
+ }
+
+ if (in_array($field_name, $this->optional)) {
+ if ($required) {
+ unset($this->optional[array_search($field_name,
+ $this->optional)]);
+ } else {
+ return true;
+ }
+ }
+ }
+
+ if ($required) {
+ $this->required[] = $field_name;
+ } else {
+ $this->optional[] = $field_name;
+ }
+
+ return true;
+ }
+
+ /**
+ * Add the given list of fields to the request
+ *
+ * field_names: The simple registration data fields to request
+ *
+ * required: Whether these values should be presented to the user
+ * as required
+ *
+ * strict: whether to raise an exception when a field is added to
+ * a request more than once
+ */
+ function requestFields($field_names, $required=false, $strict=false)
+ {
+ if (!is_array($field_names)) {
+ return false;
+ }
+
+ foreach ($field_names as $field_name) {
+ if (!$this->requestField($field_name, $required, $strict=$strict)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Get a dictionary of unqualified simple registration arguments
+ * representing this request.
+ *
+ * This method is essentially the inverse of
+ * C{L{parseExtensionArgs}}. This method serializes the simple
+ * registration request fields.
+ */
+ function getExtensionArgs()
+ {
+ $args = array();
+
+ if ($this->required) {
+ $args['required'] = implode(',', $this->required);
+ }
+
+ if ($this->optional) {
+ $args['optional'] = implode(',', $this->optional);
+ }
+
+ if ($this->policy_url) {
+ $args['policy_url'] = $this->policy_url;
+ }
+
+ return $args;
+ }
+}
+
+/**
+ * Represents the data returned in a simple registration response
+ * inside of an OpenID C{id_res} response. This object will be created
+ * by the OpenID server, added to the C{id_res} response object, and
+ * then extracted from the C{id_res} message by the Consumer.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_SRegResponse extends Auth_OpenID_SRegBase {
+
+ var $ns_alias = 'sreg';
+
+ function Auth_OpenID_SRegResponse($data=null,
+ $sreg_ns_uri=Auth_OpenID_SREG_NS_URI)
+ {
+ if ($data === null) {
+ $this->data = array();
+ } else {
+ $this->data = $data;
+ }
+
+ $this->ns_uri = $sreg_ns_uri;
+ }
+
+ /**
+ * Take a C{L{SRegRequest}} and a dictionary of simple
+ * registration values and create a C{L{SRegResponse}} object
+ * containing that data.
+ *
+ * request: The simple registration request object
+ *
+ * data: The simple registration data for this response, as a
+ * dictionary from unqualified simple registration field name to
+ * string (unicode) value. For instance, the nickname should be
+ * stored under the key 'nickname'.
+ */
+ function extractResponse($request, $data)
+ {
+ $obj = new Auth_OpenID_SRegResponse();
+ $obj->ns_uri = $request->ns_uri;
+
+ foreach ($request->allRequestedFields() as $field) {
+ $value = Auth_OpenID::arrayGet($data, $field);
+ if ($value !== null) {
+ $obj->data[$field] = $value;
+ }
+ }
+
+ return $obj;
+ }
+
+ /**
+ * Create a C{L{SRegResponse}} object from a successful OpenID
+ * library response
+ * (C{L{openid.consumer.consumer.SuccessResponse}}) response
+ * message
+ *
+ * success_response: A SuccessResponse from consumer.complete()
+ *
+ * signed_only: Whether to process only data that was
+ * signed in the id_res message from the server.
+ *
+ * Returns a simple registration response containing the data that
+ * was supplied with the C{id_res} response.
+ */
+ function fromSuccessResponse(&$success_response, $signed_only=true)
+ {
+ global $Auth_OpenID_sreg_data_fields;
+
+ $obj = new Auth_OpenID_SRegResponse();
+ $obj->ns_uri = $obj->_getSRegNS($success_response->message);
+
+ if ($signed_only) {
+ $args = $success_response->getSignedNS($obj->ns_uri);
+ } else {
+ $args = $success_response->message->getArgs($obj->ns_uri);
+ }
+
+ if ($args === null || Auth_OpenID::isFailure($args)) {
+ return null;
+ }
+
+ foreach ($Auth_OpenID_sreg_data_fields as $field_name => $desc) {
+ if (in_array($field_name, array_keys($args))) {
+ $obj->data[$field_name] = $args[$field_name];
+ }
+ }
+
+ return $obj;
+ }
+
+ function getExtensionArgs()
+ {
+ return $this->data;
+ }
+
+ // Read-only dictionary interface
+ function get($field_name, $default=null)
+ {
+ if (!Auth_OpenID_checkFieldName($field_name)) {
+ return null;
+ }
+
+ return Auth_OpenID::arrayGet($this->data, $field_name, $default);
+ }
+
+ function contents()
+ {
+ return $this->data;
+ }
+}
+
+?>
diff --git a/extlib/Auth/OpenID/Server.php b/extlib/Auth/OpenID/Server.php
new file mode 100644
index 000000000..f1db4d872
--- /dev/null
+++ b/extlib/Auth/OpenID/Server.php
@@ -0,0 +1,1760 @@
+<?php
+
+/**
+ * OpenID server protocol and logic.
+ *
+ * Overview
+ *
+ * An OpenID server must perform three tasks:
+ *
+ * 1. Examine the incoming request to determine its nature and validity.
+ * 2. Make a decision about how to respond to this request.
+ * 3. Format the response according to the protocol.
+ *
+ * The first and last of these tasks may performed by the {@link
+ * Auth_OpenID_Server::decodeRequest()} and {@link
+ * Auth_OpenID_Server::encodeResponse} methods. Who gets to do the
+ * intermediate task -- deciding how to respond to the request -- will
+ * depend on what type of request it is.
+ *
+ * If it's a request to authenticate a user (a 'checkid_setup' or
+ * 'checkid_immediate' request), you need to decide if you will assert
+ * that this user may claim the identity in question. Exactly how you
+ * do that is a matter of application policy, but it generally
+ * involves making sure the user has an account with your system and
+ * is logged in, checking to see if that identity is hers to claim,
+ * and verifying with the user that she does consent to releasing that
+ * information to the party making the request.
+ *
+ * Examine the properties of the {@link Auth_OpenID_CheckIDRequest}
+ * object, and if and when you've come to a decision, form a response
+ * by calling {@link Auth_OpenID_CheckIDRequest::answer()}.
+ *
+ * Other types of requests relate to establishing associations between
+ * client and server and verifing the authenticity of previous
+ * communications. {@link Auth_OpenID_Server} contains all the logic
+ * and data necessary to respond to such requests; just pass it to
+ * {@link Auth_OpenID_Server::handleRequest()}.
+ *
+ * OpenID Extensions
+ *
+ * Do you want to provide other information for your users in addition
+ * to authentication? Version 1.2 of the OpenID protocol allows
+ * consumers to add extensions to their requests. For example, with
+ * sites using the Simple Registration
+ * Extension
+ * (http://www.openidenabled.com/openid/simple-registration-extension/),
+ * a user can agree to have their nickname and e-mail address sent to
+ * a site when they sign up.
+ *
+ * Since extensions do not change the way OpenID authentication works,
+ * code to handle extension requests may be completely separate from
+ * the {@link Auth_OpenID_Request} class here. But you'll likely want
+ * data sent back by your extension to be signed. {@link
+ * Auth_OpenID_ServerResponse} provides methods with which you can add
+ * data to it which can be signed with the other data in the OpenID
+ * signature.
+ *
+ * For example:
+ *
+ * <pre> // when request is a checkid_* request
+ * $response = $request->answer(true);
+ * // this will a signed 'openid.sreg.timezone' parameter to the response
+ * response.addField('sreg', 'timezone', 'America/Los_Angeles')</pre>
+ *
+ * Stores
+ *
+ * The OpenID server needs to maintain state between requests in order
+ * to function. Its mechanism for doing this is called a store. The
+ * store interface is defined in Interface.php. Additionally, several
+ * concrete store implementations are provided, so that most sites
+ * won't need to implement a custom store. For a store backed by flat
+ * files on disk, see {@link Auth_OpenID_FileStore}. For stores based
+ * on MySQL, SQLite, or PostgreSQL, see the {@link
+ * Auth_OpenID_SQLStore} subclasses.
+ *
+ * Upgrading
+ *
+ * The keys by which a server looks up associations in its store have
+ * changed in version 1.2 of this library. If your store has entries
+ * created from version 1.0 code, you should empty it.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005-2008 Janrain, Inc.
+ * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
+ */
+
+/**
+ * Required imports
+ */
+require_once "Auth/OpenID.php";
+require_once "Auth/OpenID/Association.php";
+require_once "Auth/OpenID/CryptUtil.php";
+require_once "Auth/OpenID/BigMath.php";
+require_once "Auth/OpenID/DiffieHellman.php";
+require_once "Auth/OpenID/KVForm.php";
+require_once "Auth/OpenID/TrustRoot.php";
+require_once "Auth/OpenID/ServerRequest.php";
+require_once "Auth/OpenID/Message.php";
+require_once "Auth/OpenID/Nonce.php";
+
+define('AUTH_OPENID_HTTP_OK', 200);
+define('AUTH_OPENID_HTTP_REDIRECT', 302);
+define('AUTH_OPENID_HTTP_ERROR', 400);
+
+/**
+ * @access private
+ */
+global $_Auth_OpenID_Request_Modes;
+$_Auth_OpenID_Request_Modes = array('checkid_setup',
+ 'checkid_immediate');
+
+/**
+ * @access private
+ */
+define('Auth_OpenID_ENCODE_KVFORM', 'kfvorm');
+
+/**
+ * @access private
+ */
+define('Auth_OpenID_ENCODE_URL', 'URL/redirect');
+
+/**
+ * @access private
+ */
+define('Auth_OpenID_ENCODE_HTML_FORM', 'HTML form');
+
+/**
+ * @access private
+ */
+function Auth_OpenID_isError($obj, $cls = 'Auth_OpenID_ServerError')
+{
+ return is_a($obj, $cls);
+}
+
+/**
+ * An error class which gets instantiated and returned whenever an
+ * OpenID protocol error occurs. Be prepared to use this in place of
+ * an ordinary server response.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_ServerError {
+ /**
+ * @access private
+ */
+ function Auth_OpenID_ServerError($message = null, $text = null,
+ $reference = null, $contact = null)
+ {
+ $this->message = $message;
+ $this->text = $text;
+ $this->contact = $contact;
+ $this->reference = $reference;
+ }
+
+ function getReturnTo()
+ {
+ if ($this->message &&
+ $this->message->hasKey(Auth_OpenID_OPENID_NS, 'return_to')) {
+ return $this->message->getArg(Auth_OpenID_OPENID_NS,
+ 'return_to');
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the return_to URL for the request which caused this
+ * error.
+ */
+ function hasReturnTo()
+ {
+ return $this->getReturnTo() !== null;
+ }
+
+ /**
+ * Encodes this error's response as a URL suitable for
+ * redirection. If the response has no return_to, another
+ * Auth_OpenID_ServerError is returned.
+ */
+ function encodeToURL()
+ {
+ if (!$this->message) {
+ return null;
+ }
+
+ $msg = $this->toMessage();
+ return $msg->toURL($this->getReturnTo());
+ }
+
+ /**
+ * Encodes the response to key-value form. This is a
+ * machine-readable format used to respond to messages which came
+ * directly from the consumer and not through the user-agent. See
+ * the OpenID specification.
+ */
+ function encodeToKVForm()
+ {
+ return Auth_OpenID_KVForm::fromArray(
+ array('mode' => 'error',
+ 'error' => $this->toString()));
+ }
+
+ function toFormMarkup($form_tag_attrs=null)
+ {
+ $msg = $this->toMessage();
+ return $msg->toFormMarkup($this->getReturnTo(), $form_tag_attrs);
+ }
+
+ function toHTML($form_tag_attrs=null)
+ {
+ return Auth_OpenID::autoSubmitHTML(
+ $this->toFormMarkup($form_tag_attrs));
+ }
+
+ function toMessage()
+ {
+ // Generate a Message object for sending to the relying party,
+ // after encoding.
+ $namespace = $this->message->getOpenIDNamespace();
+ $reply = new Auth_OpenID_Message($namespace);
+ $reply->setArg(Auth_OpenID_OPENID_NS, 'mode', 'error');
+ $reply->setArg(Auth_OpenID_OPENID_NS, 'error', $this->toString());
+
+ if ($this->contact !== null) {
+ $reply->setArg(Auth_OpenID_OPENID_NS, 'contact', $this->contact);
+ }
+
+ if ($this->reference !== null) {
+ $reply->setArg(Auth_OpenID_OPENID_NS, 'reference',
+ $this->reference);
+ }
+
+ return $reply;
+ }
+
+ /**
+ * Returns one of Auth_OpenID_ENCODE_URL,
+ * Auth_OpenID_ENCODE_KVFORM, or null, depending on the type of
+ * encoding expected for this error's payload.
+ */
+ function whichEncoding()
+ {
+ global $_Auth_OpenID_Request_Modes;
+
+ if ($this->hasReturnTo()) {
+ if ($this->message->isOpenID2() &&
+ (strlen($this->encodeToURL()) >
+ Auth_OpenID_OPENID1_URL_LIMIT)) {
+ return Auth_OpenID_ENCODE_HTML_FORM;
+ } else {
+ return Auth_OpenID_ENCODE_URL;
+ }
+ }
+
+ if (!$this->message) {
+ return null;
+ }
+
+ $mode = $this->message->getArg(Auth_OpenID_OPENID_NS,
+ 'mode');
+
+ if ($mode) {
+ if (!in_array($mode, $_Auth_OpenID_Request_Modes)) {
+ return Auth_OpenID_ENCODE_KVFORM;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns this error message.
+ */
+ function toString()
+ {
+ if ($this->text) {
+ return $this->text;
+ } else {
+ return get_class($this) . " error";
+ }
+ }
+}
+
+/**
+ * Error returned by the server code when a return_to is absent from a
+ * request.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_NoReturnToError extends Auth_OpenID_ServerError {
+ function Auth_OpenID_NoReturnToError($message = null,
+ $text = "No return_to URL available")
+ {
+ parent::Auth_OpenID_ServerError($message, $text);
+ }
+
+ function toString()
+ {
+ return "No return_to available";
+ }
+}
+
+/**
+ * An error indicating that the return_to URL is malformed.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_MalformedReturnURL extends Auth_OpenID_ServerError {
+ function Auth_OpenID_MalformedReturnURL($message, $return_to)
+ {
+ $this->return_to = $return_to;
+ parent::Auth_OpenID_ServerError($message, "malformed return_to URL");
+ }
+}
+
+/**
+ * This error is returned when the trust_root value is malformed.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_MalformedTrustRoot extends Auth_OpenID_ServerError {
+ function Auth_OpenID_MalformedTrustRoot($message = null,
+ $text = "Malformed trust root")
+ {
+ parent::Auth_OpenID_ServerError($message, $text);
+ }
+
+ function toString()
+ {
+ return "Malformed trust root";
+ }
+}
+
+/**
+ * The base class for all server request classes.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_Request {
+ var $mode = null;
+}
+
+/**
+ * A request to verify the validity of a previous response.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_CheckAuthRequest extends Auth_OpenID_Request {
+ var $mode = "check_authentication";
+ var $invalidate_handle = null;
+
+ function Auth_OpenID_CheckAuthRequest($assoc_handle, $signed,
+ $invalidate_handle = null)
+ {
+ $this->assoc_handle = $assoc_handle;
+ $this->signed = $signed;
+ if ($invalidate_handle !== null) {
+ $this->invalidate_handle = $invalidate_handle;
+ }
+ $this->namespace = Auth_OpenID_OPENID2_NS;
+ $this->message = null;
+ }
+
+ function fromMessage($message, $server=null)
+ {
+ $required_keys = array('assoc_handle', 'sig', 'signed');
+
+ foreach ($required_keys as $k) {
+ if (!$message->getArg(Auth_OpenID_OPENID_NS, $k)) {
+ return new Auth_OpenID_ServerError($message,
+ sprintf("%s request missing required parameter %s from \
+ query", "check_authentication", $k));
+ }
+ }
+
+ $assoc_handle = $message->getArg(Auth_OpenID_OPENID_NS, 'assoc_handle');
+ $sig = $message->getArg(Auth_OpenID_OPENID_NS, 'sig');
+
+ $signed_list = $message->getArg(Auth_OpenID_OPENID_NS, 'signed');
+ $signed_list = explode(",", $signed_list);
+
+ $signed = $message;
+ if ($signed->hasKey(Auth_OpenID_OPENID_NS, 'mode')) {
+ $signed->setArg(Auth_OpenID_OPENID_NS, 'mode', 'id_res');
+ }
+
+ $result = new Auth_OpenID_CheckAuthRequest($assoc_handle, $signed);
+ $result->message = $message;
+ $result->sig = $sig;
+ $result->invalidate_handle = $message->getArg(Auth_OpenID_OPENID_NS,
+ 'invalidate_handle');
+ return $result;
+ }
+
+ function answer(&$signatory)
+ {
+ $is_valid = $signatory->verify($this->assoc_handle, $this->signed);
+
+ // Now invalidate that assoc_handle so it this checkAuth
+ // message cannot be replayed.
+ $signatory->invalidate($this->assoc_handle, true);
+ $response = new Auth_OpenID_ServerResponse($this);
+
+ $response->fields->setArg(Auth_OpenID_OPENID_NS,
+ 'is_valid',
+ ($is_valid ? "true" : "false"));
+
+ if ($this->invalidate_handle) {
+ $assoc = $signatory->getAssociation($this->invalidate_handle,
+ false);
+ if (!$assoc) {
+ $response->fields->setArg(Auth_OpenID_OPENID_NS,
+ 'invalidate_handle',
+ $this->invalidate_handle);
+ }
+ }
+ return $response;
+ }
+}
+
+/**
+ * A class implementing plaintext server sessions.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_PlainTextServerSession {
+ /**
+ * An object that knows how to handle association requests with no
+ * session type.
+ */
+ var $session_type = 'no-encryption';
+ var $needs_math = false;
+ var $allowed_assoc_types = array('HMAC-SHA1', 'HMAC-SHA256');
+
+ function fromMessage($unused_request)
+ {
+ return new Auth_OpenID_PlainTextServerSession();
+ }
+
+ function answer($secret)
+ {
+ return array('mac_key' => base64_encode($secret));
+ }
+}
+
+/**
+ * A class implementing DH-SHA1 server sessions.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_DiffieHellmanSHA1ServerSession {
+ /**
+ * An object that knows how to handle association requests with
+ * the Diffie-Hellman session type.
+ */
+
+ var $session_type = 'DH-SHA1';
+ var $needs_math = true;
+ var $allowed_assoc_types = array('HMAC-SHA1');
+ var $hash_func = 'Auth_OpenID_SHA1';
+
+ function Auth_OpenID_DiffieHellmanSHA1ServerSession($dh, $consumer_pubkey)
+ {
+ $this->dh = $dh;
+ $this->consumer_pubkey = $consumer_pubkey;
+ }
+
+ function getDH($message)
+ {
+ $dh_modulus = $message->getArg(Auth_OpenID_OPENID_NS, 'dh_modulus');
+ $dh_gen = $message->getArg(Auth_OpenID_OPENID_NS, 'dh_gen');
+
+ if ((($dh_modulus === null) && ($dh_gen !== null)) ||
+ (($dh_gen === null) && ($dh_modulus !== null))) {
+
+ if ($dh_modulus === null) {
+ $missing = 'modulus';
+ } else {
+ $missing = 'generator';
+ }
+
+ return new Auth_OpenID_ServerError($message,
+ 'If non-default modulus or generator is '.
+ 'supplied, both must be supplied. Missing '.
+ $missing);
+ }
+
+ $lib =& Auth_OpenID_getMathLib();
+
+ if ($dh_modulus || $dh_gen) {
+ $dh_modulus = $lib->base64ToLong($dh_modulus);
+ $dh_gen = $lib->base64ToLong($dh_gen);
+ if ($lib->cmp($dh_modulus, 0) == 0 ||
+ $lib->cmp($dh_gen, 0) == 0) {
+ return new Auth_OpenID_ServerError(
+ $message, "Failed to parse dh_mod or dh_gen");
+ }
+ $dh = new Auth_OpenID_DiffieHellman($dh_modulus, $dh_gen);
+ } else {
+ $dh = new Auth_OpenID_DiffieHellman();
+ }
+
+ $consumer_pubkey = $message->getArg(Auth_OpenID_OPENID_NS,
+ 'dh_consumer_public');
+ if ($consumer_pubkey === null) {
+ return new Auth_OpenID_ServerError($message,
+ 'Public key for DH-SHA1 session '.
+ 'not found in query');
+ }
+
+ $consumer_pubkey =
+ $lib->base64ToLong($consumer_pubkey);
+
+ if ($consumer_pubkey === false) {
+ return new Auth_OpenID_ServerError($message,
+ "dh_consumer_public is not base64");
+ }
+
+ return array($dh, $consumer_pubkey);
+ }
+
+ function fromMessage($message)
+ {
+ $result = Auth_OpenID_DiffieHellmanSHA1ServerSession::getDH($message);
+
+ if (is_a($result, 'Auth_OpenID_ServerError')) {
+ return $result;
+ } else {
+ list($dh, $consumer_pubkey) = $result;
+ return new Auth_OpenID_DiffieHellmanSHA1ServerSession($dh,
+ $consumer_pubkey);
+ }
+ }
+
+ function answer($secret)
+ {
+ $lib =& Auth_OpenID_getMathLib();
+ $mac_key = $this->dh->xorSecret($this->consumer_pubkey, $secret,
+ $this->hash_func);
+ return array(
+ 'dh_server_public' =>
+ $lib->longToBase64($this->dh->public),
+ 'enc_mac_key' => base64_encode($mac_key));
+ }
+}
+
+/**
+ * A class implementing DH-SHA256 server sessions.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_DiffieHellmanSHA256ServerSession
+ extends Auth_OpenID_DiffieHellmanSHA1ServerSession {
+
+ var $session_type = 'DH-SHA256';
+ var $hash_func = 'Auth_OpenID_SHA256';
+ var $allowed_assoc_types = array('HMAC-SHA256');
+
+ function fromMessage($message)
+ {
+ $result = Auth_OpenID_DiffieHellmanSHA1ServerSession::getDH($message);
+
+ if (is_a($result, 'Auth_OpenID_ServerError')) {
+ return $result;
+ } else {
+ list($dh, $consumer_pubkey) = $result;
+ return new Auth_OpenID_DiffieHellmanSHA256ServerSession($dh,
+ $consumer_pubkey);
+ }
+ }
+}
+
+/**
+ * A request to associate with the server.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_AssociateRequest extends Auth_OpenID_Request {
+ var $mode = "associate";
+
+ function getSessionClasses()
+ {
+ return array(
+ 'no-encryption' => 'Auth_OpenID_PlainTextServerSession',
+ 'DH-SHA1' => 'Auth_OpenID_DiffieHellmanSHA1ServerSession',
+ 'DH-SHA256' => 'Auth_OpenID_DiffieHellmanSHA256ServerSession');
+ }
+
+ function Auth_OpenID_AssociateRequest(&$session, $assoc_type)
+ {
+ $this->session =& $session;
+ $this->namespace = Auth_OpenID_OPENID2_NS;
+ $this->assoc_type = $assoc_type;
+ }
+
+ function fromMessage($message, $server=null)
+ {
+ if ($message->isOpenID1()) {
+ $session_type = $message->getArg(Auth_OpenID_OPENID_NS,
+ 'session_type');
+
+ if ($session_type == 'no-encryption') {
+ // oidutil.log('Received OpenID 1 request with a no-encryption '
+ // 'assocaition session type. Continuing anyway.')
+ } else if (!$session_type) {
+ $session_type = 'no-encryption';
+ }
+ } else {
+ $session_type = $message->getArg(Auth_OpenID_OPENID_NS,
+ 'session_type');
+ if ($session_type === null) {
+ return new Auth_OpenID_ServerError($message,
+ "session_type missing from request");
+ }
+ }
+
+ $session_class = Auth_OpenID::arrayGet(
+ Auth_OpenID_AssociateRequest::getSessionClasses(),
+ $session_type);
+
+ if ($session_class === null) {
+ return new Auth_OpenID_ServerError($message,
+ "Unknown session type " .
+ $session_type);
+ }
+
+ $session = call_user_func(array($session_class, 'fromMessage'),
+ $message);
+ if (is_a($session, 'Auth_OpenID_ServerError')) {
+ return $session;
+ }
+
+ $assoc_type = $message->getArg(Auth_OpenID_OPENID_NS,
+ 'assoc_type', 'HMAC-SHA1');
+
+ if (!in_array($assoc_type, $session->allowed_assoc_types)) {
+ $fmt = "Session type %s does not support association type %s";
+ return new Auth_OpenID_ServerError($message,
+ sprintf($fmt, $session_type, $assoc_type));
+ }
+
+ $obj = new Auth_OpenID_AssociateRequest($session, $assoc_type);
+ $obj->message = $message;
+ $obj->namespace = $message->getOpenIDNamespace();
+ return $obj;
+ }
+
+ function answer($assoc)
+ {
+ $response = new Auth_OpenID_ServerResponse($this);
+ $response->fields->updateArgs(Auth_OpenID_OPENID_NS,
+ array(
+ 'expires_in' => sprintf('%d', $assoc->getExpiresIn()),
+ 'assoc_type' => $this->assoc_type,
+ 'assoc_handle' => $assoc->handle));
+
+ $response->fields->updateArgs(Auth_OpenID_OPENID_NS,
+ $this->session->answer($assoc->secret));
+
+ if (! ($this->session->session_type == 'no-encryption'
+ && $this->message->isOpenID1())) {
+ $response->fields->setArg(Auth_OpenID_OPENID_NS,
+ 'session_type',
+ $this->session->session_type);
+ }
+
+ return $response;
+ }
+
+ function answerUnsupported($text_message,
+ $preferred_association_type=null,
+ $preferred_session_type=null)
+ {
+ if ($this->message->isOpenID1()) {
+ return new Auth_OpenID_ServerError($this->message);
+ }
+
+ $response = new Auth_OpenID_ServerResponse($this);
+ $response->fields->setArg(Auth_OpenID_OPENID_NS,
+ 'error_code', 'unsupported-type');
+ $response->fields->setArg(Auth_OpenID_OPENID_NS,
+ 'error', $text_message);
+
+ if ($preferred_association_type) {
+ $response->fields->setArg(Auth_OpenID_OPENID_NS,
+ 'assoc_type',
+ $preferred_association_type);
+ }
+
+ if ($preferred_session_type) {
+ $response->fields->setArg(Auth_OpenID_OPENID_NS,
+ 'session_type',
+ $preferred_session_type);
+ }
+
+ return $response;
+ }
+}
+
+/**
+ * A request to confirm the identity of a user.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_CheckIDRequest extends Auth_OpenID_Request {
+ /**
+ * Return-to verification callback. Default is
+ * Auth_OpenID_verifyReturnTo from TrustRoot.php.
+ */
+ var $verifyReturnTo = 'Auth_OpenID_verifyReturnTo';
+
+ /**
+ * The mode of this request.
+ */
+ var $mode = "checkid_setup"; // or "checkid_immediate"
+
+ /**
+ * Whether this request is for immediate mode.
+ */
+ var $immediate = false;
+
+ /**
+ * The trust_root value for this request.
+ */
+ var $trust_root = null;
+
+ /**
+ * The OpenID namespace for this request.
+ * deprecated since version 2.0.2
+ */
+ var $namespace;
+
+ function make(&$message, $identity, $return_to, $trust_root = null,
+ $immediate = false, $assoc_handle = null, $server = null)
+ {
+ if ($server === null) {
+ return new Auth_OpenID_ServerError($message,
+ "server must not be null");
+ }
+
+ if ($return_to &&
+ !Auth_OpenID_TrustRoot::_parse($return_to)) {
+ return new Auth_OpenID_MalformedReturnURL($message, $return_to);
+ }
+
+ $r = new Auth_OpenID_CheckIDRequest($identity, $return_to,
+ $trust_root, $immediate,
+ $assoc_handle, $server);
+
+ $r->namespace = $message->getOpenIDNamespace();
+ $r->message =& $message;
+
+ if (!$r->trustRootValid()) {
+ return new Auth_OpenID_UntrustedReturnURL($message,
+ $return_to,
+ $trust_root);
+ } else {
+ return $r;
+ }
+ }
+
+ function Auth_OpenID_CheckIDRequest($identity, $return_to,
+ $trust_root = null, $immediate = false,
+ $assoc_handle = null, $server = null,
+ $claimed_id = null)
+ {
+ $this->namespace = Auth_OpenID_OPENID2_NS;
+ $this->assoc_handle = $assoc_handle;
+ $this->identity = $identity;
+ if ($claimed_id === null) {
+ $this->claimed_id = $identity;
+ } else {
+ $this->claimed_id = $claimed_id;
+ }
+ $this->return_to = $return_to;
+ $this->trust_root = $trust_root;
+ $this->server =& $server;
+
+ if ($immediate) {
+ $this->immediate = true;
+ $this->mode = "checkid_immediate";
+ } else {
+ $this->immediate = false;
+ $this->mode = "checkid_setup";
+ }
+ }
+
+ function equals($other)
+ {
+ return (
+ (is_a($other, 'Auth_OpenID_CheckIDRequest')) &&
+ ($this->namespace == $other->namespace) &&
+ ($this->assoc_handle == $other->assoc_handle) &&
+ ($this->identity == $other->identity) &&
+ ($this->claimed_id == $other->claimed_id) &&
+ ($this->return_to == $other->return_to) &&
+ ($this->trust_root == $other->trust_root));
+ }
+
+ /*
+ * Does the relying party publish the return_to URL for this
+ * response under the realm? It is up to the provider to set a
+ * policy for what kinds of realms should be allowed. This
+ * return_to URL verification reduces vulnerability to data-theft
+ * attacks based on open proxies, corss-site-scripting, or open
+ * redirectors.
+ *
+ * This check should only be performed after making sure that the
+ * return_to URL matches the realm.
+ *
+ * @return true if the realm publishes a document with the
+ * return_to URL listed, false if not or if discovery fails
+ */
+ function returnToVerified()
+ {
+ return call_user_func_array($this->verifyReturnTo,
+ array($this->trust_root, $this->return_to));
+ }
+
+ function fromMessage(&$message, $server)
+ {
+ $mode = $message->getArg(Auth_OpenID_OPENID_NS, 'mode');
+ $immediate = null;
+
+ if ($mode == "checkid_immediate") {
+ $immediate = true;
+ $mode = "checkid_immediate";
+ } else {
+ $immediate = false;
+ $mode = "checkid_setup";
+ }
+
+ $return_to = $message->getArg(Auth_OpenID_OPENID_NS,
+ 'return_to');
+
+ if (($message->isOpenID1()) &&
+ (!$return_to)) {
+ $fmt = "Missing required field 'return_to' from checkid request";
+ return new Auth_OpenID_ServerError($message, $fmt);
+ }
+
+ $identity = $message->getArg(Auth_OpenID_OPENID_NS,
+ 'identity');
+ $claimed_id = $message->getArg(Auth_OpenID_OPENID_NS, 'claimed_id');
+ if ($message->isOpenID1()) {
+ if ($identity === null) {
+ $s = "OpenID 1 message did not contain openid.identity";
+ return new Auth_OpenID_ServerError($message, $s);
+ }
+ } else {
+ if ($identity && !$claimed_id) {
+ $s = "OpenID 2.0 message contained openid.identity but not " .
+ "claimed_id";
+ return new Auth_OpenID_ServerError($message, $s);
+ } else if ($claimed_id && !$identity) {
+ $s = "OpenID 2.0 message contained openid.claimed_id " .
+ "but not identity";
+ return new Auth_OpenID_ServerError($message, $s);
+ }
+ }
+
+ // There's a case for making self.trust_root be a TrustRoot
+ // here. But if TrustRoot isn't currently part of the
+ // "public" API, I'm not sure it's worth doing.
+ if ($message->isOpenID1()) {
+ $trust_root_param = 'trust_root';
+ } else {
+ $trust_root_param = 'realm';
+ }
+ $trust_root = $message->getArg(Auth_OpenID_OPENID_NS,
+ $trust_root_param);
+ if (! $trust_root) {
+ $trust_root = $return_to;
+ }
+
+ if (! $message->isOpenID1() &&
+ ($return_to === null) &&
+ ($trust_root === null)) {
+ return new Auth_OpenID_ServerError($message,
+ "openid.realm required when openid.return_to absent");
+ }
+
+ $assoc_handle = $message->getArg(Auth_OpenID_OPENID_NS,
+ 'assoc_handle');
+
+ $obj = Auth_OpenID_CheckIDRequest::make($message,
+ $identity,
+ $return_to,
+ $trust_root,
+ $immediate,
+ $assoc_handle,
+ $server);
+
+ if (is_a($obj, 'Auth_OpenID_ServerError')) {
+ return $obj;
+ }
+
+ $obj->claimed_id = $claimed_id;
+
+ return $obj;
+ }
+
+ function idSelect()
+ {
+ // Is the identifier to be selected by the IDP?
+ // So IDPs don't have to import the constant
+ return $this->identity == Auth_OpenID_IDENTIFIER_SELECT;
+ }
+
+ function trustRootValid()
+ {
+ if (!$this->trust_root) {
+ return true;
+ }
+
+ $tr = Auth_OpenID_TrustRoot::_parse($this->trust_root);
+ if ($tr === false) {
+ return new Auth_OpenID_MalformedTrustRoot($this->message,
+ $this->trust_root);
+ }
+
+ if ($this->return_to !== null) {
+ return Auth_OpenID_TrustRoot::match($this->trust_root,
+ $this->return_to);
+ } else {
+ return true;
+ }
+ }
+
+ /**
+ * Respond to this request. Return either an
+ * {@link Auth_OpenID_ServerResponse} or
+ * {@link Auth_OpenID_ServerError}.
+ *
+ * @param bool $allow Allow this user to claim this identity, and
+ * allow the consumer to have this information?
+ *
+ * @param string $server_url DEPRECATED. Passing $op_endpoint to
+ * the {@link Auth_OpenID_Server} constructor makes this optional.
+ *
+ * When an OpenID 1.x immediate mode request does not succeed, it
+ * gets back a URL where the request may be carried out in a
+ * not-so-immediate fashion. Pass my URL in here (the fully
+ * qualified address of this server's endpoint, i.e.
+ * http://example.com/server), and I will use it as a base for the
+ * URL for a new request.
+ *
+ * Optional for requests where {@link $immediate} is false or
+ * $allow is true.
+ *
+ * @param string $identity The OP-local identifier to answer with.
+ * Only for use when the relying party requested identifier
+ * selection.
+ *
+ * @param string $claimed_id The claimed identifier to answer
+ * with, for use with identifier selection in the case where the
+ * claimed identifier and the OP-local identifier differ,
+ * i.e. when the claimed_id uses delegation.
+ *
+ * If $identity is provided but this is not, $claimed_id will
+ * default to the value of $identity. When answering requests
+ * that did not ask for identifier selection, the response
+ * $claimed_id will default to that of the request.
+ *
+ * This parameter is new in OpenID 2.0.
+ *
+ * @return mixed
+ */
+ function answer($allow, $server_url = null, $identity = null,
+ $claimed_id = null)
+ {
+ if (!$this->return_to) {
+ return new Auth_OpenID_NoReturnToError();
+ }
+
+ if (!$server_url) {
+ if ((!$this->message->isOpenID1()) &&
+ (!$this->server->op_endpoint)) {
+ return new Auth_OpenID_ServerError(null,
+ "server should be constructed with op_endpoint to " .
+ "respond to OpenID 2.0 messages.");
+ }
+
+ $server_url = $this->server->op_endpoint;
+ }
+
+ if ($allow) {
+ $mode = 'id_res';
+ } else if ($this->message->isOpenID1()) {
+ if ($this->immediate) {
+ $mode = 'id_res';
+ } else {
+ $mode = 'cancel';
+ }
+ } else {
+ if ($this->immediate) {
+ $mode = 'setup_needed';
+ } else {
+ $mode = 'cancel';
+ }
+ }
+
+ if (!$this->trustRootValid()) {
+ return new Auth_OpenID_UntrustedReturnURL(null,
+ $this->return_to,
+ $this->trust_root);
+ }
+
+ $response = new Auth_OpenID_ServerResponse($this);
+
+ if ($claimed_id &&
+ ($this->message->isOpenID1())) {
+ return new Auth_OpenID_ServerError(null,
+ "claimed_id is new in OpenID 2.0 and not " .
+ "available for ".$this->namespace);
+ }
+
+ if ($identity && !$claimed_id) {
+ $claimed_id = $identity;
+ }
+
+ if ($allow) {
+
+ if ($this->identity == Auth_OpenID_IDENTIFIER_SELECT) {
+ if (!$identity) {
+ return new Auth_OpenID_ServerError(null,
+ "This request uses IdP-driven identifier selection. " .
+ "You must supply an identifier in the response.");
+ }
+
+ $response_identity = $identity;
+ $response_claimed_id = $claimed_id;
+
+ } else if ($this->identity) {
+ if ($identity &&
+ ($this->identity != $identity)) {
+ $fmt = "Request was for %s, cannot reply with identity %s";
+ return new Auth_OpenID_ServerError(null,
+ sprintf($fmt, $this->identity, $identity));
+ }
+
+ $response_identity = $this->identity;
+ $response_claimed_id = $this->claimed_id;
+ } else {
+ if ($identity) {
+ return new Auth_OpenID_ServerError(null,
+ "This request specified no identity and " .
+ "you supplied ".$identity);
+ }
+
+ $response_identity = null;
+ }
+
+ if (($this->message->isOpenID1()) &&
+ ($response_identity === null)) {
+ return new Auth_OpenID_ServerError(null,
+ "Request was an OpenID 1 request, so response must " .
+ "include an identifier.");
+ }
+
+ $response->fields->updateArgs(Auth_OpenID_OPENID_NS,
+ array('mode' => $mode,
+ 'return_to' => $this->return_to,
+ 'response_nonce' => Auth_OpenID_mkNonce()));
+
+ if (!$this->message->isOpenID1()) {
+ $response->fields->setArg(Auth_OpenID_OPENID_NS,
+ 'op_endpoint', $server_url);
+ }
+
+ if ($response_identity !== null) {
+ $response->fields->setArg(
+ Auth_OpenID_OPENID_NS,
+ 'identity',
+ $response_identity);
+ if ($this->message->isOpenID2()) {
+ $response->fields->setArg(
+ Auth_OpenID_OPENID_NS,
+ 'claimed_id',
+ $response_claimed_id);
+ }
+ }
+
+ } else {
+ $response->fields->setArg(Auth_OpenID_OPENID_NS,
+ 'mode', $mode);
+
+ if ($this->immediate) {
+ if (($this->message->isOpenID1()) &&
+ (!$server_url)) {
+ return new Auth_OpenID_ServerError(null,
+ 'setup_url is required for $allow=false \
+ in OpenID 1.x immediate mode.');
+ }
+
+ $setup_request =& new Auth_OpenID_CheckIDRequest(
+ $this->identity,
+ $this->return_to,
+ $this->trust_root,
+ false,
+ $this->assoc_handle,
+ $this->server,
+ $this->claimed_id);
+ $setup_request->message = $this->message;
+
+ $setup_url = $setup_request->encodeToURL($server_url);
+
+ if ($setup_url === null) {
+ return new Auth_OpenID_NoReturnToError();
+ }
+
+ $response->fields->setArg(Auth_OpenID_OPENID_NS,
+ 'user_setup_url',
+ $setup_url);
+ }
+ }
+
+ return $response;
+ }
+
+ function encodeToURL($server_url)
+ {
+ if (!$this->return_to) {
+ return new Auth_OpenID_NoReturnToError();
+ }
+
+ // Imported from the alternate reality where these classes are
+ // used in both the client and server code, so Requests are
+ // Encodable too. That's right, code imported from alternate
+ // realities all for the love of you, id_res/user_setup_url.
+
+ $q = array('mode' => $this->mode,
+ 'identity' => $this->identity,
+ 'claimed_id' => $this->claimed_id,
+ 'return_to' => $this->return_to);
+
+ if ($this->trust_root) {
+ if ($this->message->isOpenID1()) {
+ $q['trust_root'] = $this->trust_root;
+ } else {
+ $q['realm'] = $this->trust_root;
+ }
+ }
+
+ if ($this->assoc_handle) {
+ $q['assoc_handle'] = $this->assoc_handle;
+ }
+
+ $response = new Auth_OpenID_Message(
+ $this->message->getOpenIDNamespace());
+ $response->updateArgs(Auth_OpenID_OPENID_NS, $q);
+ return $response->toURL($server_url);
+ }
+
+ function getCancelURL()
+ {
+ if (!$this->return_to) {
+ return new Auth_OpenID_NoReturnToError();
+ }
+
+ if ($this->immediate) {
+ return new Auth_OpenID_ServerError(null,
+ "Cancel is not an appropriate \
+ response to immediate mode \
+ requests.");
+ }
+
+ $response = new Auth_OpenID_Message(
+ $this->message->getOpenIDNamespace());
+ $response->setArg(Auth_OpenID_OPENID_NS, 'mode', 'cancel');
+ return $response->toURL($this->return_to);
+ }
+}
+
+/**
+ * This class encapsulates the response to an OpenID server request.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_ServerResponse {
+
+ function Auth_OpenID_ServerResponse(&$request)
+ {
+ $this->request =& $request;
+ $this->fields = new Auth_OpenID_Message($this->request->namespace);
+ }
+
+ function whichEncoding()
+ {
+ global $_Auth_OpenID_Request_Modes;
+
+ if (in_array($this->request->mode, $_Auth_OpenID_Request_Modes)) {
+ if ($this->fields->isOpenID2() &&
+ (strlen($this->encodeToURL()) >
+ Auth_OpenID_OPENID1_URL_LIMIT)) {
+ return Auth_OpenID_ENCODE_HTML_FORM;
+ } else {
+ return Auth_OpenID_ENCODE_URL;
+ }
+ } else {
+ return Auth_OpenID_ENCODE_KVFORM;
+ }
+ }
+
+ /*
+ * Returns the form markup for this response.
+ *
+ * @return str
+ */
+ function toFormMarkup($form_tag_attrs=null)
+ {
+ return $this->fields->toFormMarkup($this->request->return_to,
+ $form_tag_attrs);
+ }
+
+ /*
+ * Returns an HTML document containing the form markup for this
+ * response that autosubmits with javascript.
+ */
+ function toHTML()
+ {
+ return Auth_OpenID::autoSubmitHTML($this->toFormMarkup());
+ }
+
+ /*
+ * Returns True if this response's encoding is ENCODE_HTML_FORM.
+ * Convenience method for server authors.
+ *
+ * @return bool
+ */
+ function renderAsForm()
+ {
+ return $this->whichEncoding() == Auth_OpenID_ENCODE_HTML_FORM;
+ }
+
+
+ function encodeToURL()
+ {
+ return $this->fields->toURL($this->request->return_to);
+ }
+
+ function addExtension($extension_response)
+ {
+ $extension_response->toMessage($this->fields);
+ }
+
+ function needsSigning()
+ {
+ return $this->fields->getArg(Auth_OpenID_OPENID_NS,
+ 'mode') == 'id_res';
+ }
+
+ function encodeToKVForm()
+ {
+ return $this->fields->toKVForm();
+ }
+}
+
+/**
+ * A web-capable response object which you can use to generate a
+ * user-agent response.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_WebResponse {
+ var $code = AUTH_OPENID_HTTP_OK;
+ var $body = "";
+
+ function Auth_OpenID_WebResponse($code = null, $headers = null,
+ $body = null)
+ {
+ if ($code) {
+ $this->code = $code;
+ }
+
+ if ($headers !== null) {
+ $this->headers = $headers;
+ } else {
+ $this->headers = array();
+ }
+
+ if ($body !== null) {
+ $this->body = $body;
+ }
+ }
+}
+
+/**
+ * Responsible for the signature of query data and the verification of
+ * OpenID signature values.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_Signatory {
+
+ // = 14 * 24 * 60 * 60; # 14 days, in seconds
+ var $SECRET_LIFETIME = 1209600;
+
+ // keys have a bogus server URL in them because the filestore
+ // really does expect that key to be a URL. This seems a little
+ // silly for the server store, since I expect there to be only one
+ // server URL.
+ var $normal_key = 'http://localhost/|normal';
+ var $dumb_key = 'http://localhost/|dumb';
+
+ /**
+ * Create a new signatory using a given store.
+ */
+ function Auth_OpenID_Signatory(&$store)
+ {
+ // assert store is not None
+ $this->store =& $store;
+ }
+
+ /**
+ * Verify, using a given association handle, a signature with
+ * signed key-value pairs from an HTTP request.
+ */
+ function verify($assoc_handle, $message)
+ {
+ $assoc = $this->getAssociation($assoc_handle, true);
+ if (!$assoc) {
+ // oidutil.log("failed to get assoc with handle %r to verify sig %r"
+ // % (assoc_handle, sig))
+ return false;
+ }
+
+ return $assoc->checkMessageSignature($message);
+ }
+
+ /**
+ * Given a response, sign the fields in the response's 'signed'
+ * list, and insert the signature into the response.
+ */
+ function sign($response)
+ {
+ $signed_response = $response;
+ $assoc_handle = $response->request->assoc_handle;
+
+ if ($assoc_handle) {
+ // normal mode
+ $assoc = $this->getAssociation($assoc_handle, false, false);
+ if (!$assoc || ($assoc->getExpiresIn() <= 0)) {
+ // fall back to dumb mode
+ $signed_response->fields->setArg(Auth_OpenID_OPENID_NS,
+ 'invalidate_handle', $assoc_handle);
+ $assoc_type = ($assoc ? $assoc->assoc_type : 'HMAC-SHA1');
+
+ if ($assoc && ($assoc->getExpiresIn() <= 0)) {
+ $this->invalidate($assoc_handle, false);
+ }
+
+ $assoc = $this->createAssociation(true, $assoc_type);
+ }
+ } else {
+ // dumb mode.
+ $assoc = $this->createAssociation(true);
+ }
+
+ $signed_response->fields = $assoc->signMessage(
+ $signed_response->fields);
+ return $signed_response;
+ }
+
+ /**
+ * Make a new association.
+ */
+ function createAssociation($dumb = true, $assoc_type = 'HMAC-SHA1')
+ {
+ $secret = Auth_OpenID_CryptUtil::getBytes(
+ Auth_OpenID_getSecretSize($assoc_type));
+
+ $uniq = base64_encode(Auth_OpenID_CryptUtil::getBytes(4));
+ $handle = sprintf('{%s}{%x}{%s}', $assoc_type, intval(time()), $uniq);
+
+ $assoc = Auth_OpenID_Association::fromExpiresIn(
+ $this->SECRET_LIFETIME, $handle, $secret, $assoc_type);
+
+ if ($dumb) {
+ $key = $this->dumb_key;
+ } else {
+ $key = $this->normal_key;
+ }
+
+ $this->store->storeAssociation($key, $assoc);
+ return $assoc;
+ }
+
+ /**
+ * Given an association handle, get the association from the
+ * store, or return a ServerError or null if something goes wrong.
+ */
+ function getAssociation($assoc_handle, $dumb, $check_expiration=true)
+ {
+ if ($assoc_handle === null) {
+ return new Auth_OpenID_ServerError(null,
+ "assoc_handle must not be null");
+ }
+
+ if ($dumb) {
+ $key = $this->dumb_key;
+ } else {
+ $key = $this->normal_key;
+ }
+
+ $assoc = $this->store->getAssociation($key, $assoc_handle);
+
+ if (($assoc !== null) && ($assoc->getExpiresIn() <= 0)) {
+ if ($check_expiration) {
+ $this->store->removeAssociation($key, $assoc_handle);
+ $assoc = null;
+ }
+ }
+
+ return $assoc;
+ }
+
+ /**
+ * Invalidate a given association handle.
+ */
+ function invalidate($assoc_handle, $dumb)
+ {
+ if ($dumb) {
+ $key = $this->dumb_key;
+ } else {
+ $key = $this->normal_key;
+ }
+ $this->store->removeAssociation($key, $assoc_handle);
+ }
+}
+
+/**
+ * Encode an {@link Auth_OpenID_ServerResponse} to an
+ * {@link Auth_OpenID_WebResponse}.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_Encoder {
+
+ var $responseFactory = 'Auth_OpenID_WebResponse';
+
+ /**
+ * Encode an {@link Auth_OpenID_ServerResponse} and return an
+ * {@link Auth_OpenID_WebResponse}.
+ */
+ function encode(&$response)
+ {
+ $cls = $this->responseFactory;
+
+ $encode_as = $response->whichEncoding();
+ if ($encode_as == Auth_OpenID_ENCODE_KVFORM) {
+ $wr = new $cls(null, null, $response->encodeToKVForm());
+ if (is_a($response, 'Auth_OpenID_ServerError')) {
+ $wr->code = AUTH_OPENID_HTTP_ERROR;
+ }
+ } else if ($encode_as == Auth_OpenID_ENCODE_URL) {
+ $location = $response->encodeToURL();
+ $wr = new $cls(AUTH_OPENID_HTTP_REDIRECT,
+ array('location' => $location));
+ } else if ($encode_as == Auth_OpenID_ENCODE_HTML_FORM) {
+ $wr = new $cls(AUTH_OPENID_HTTP_OK, array(),
+ $response->toFormMarkup());
+ } else {
+ return new Auth_OpenID_EncodingError($response);
+ }
+ return $wr;
+ }
+}
+
+/**
+ * An encoder which also takes care of signing fields when required.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_SigningEncoder extends Auth_OpenID_Encoder {
+
+ function Auth_OpenID_SigningEncoder(&$signatory)
+ {
+ $this->signatory =& $signatory;
+ }
+
+ /**
+ * Sign an {@link Auth_OpenID_ServerResponse} and return an
+ * {@link Auth_OpenID_WebResponse}.
+ */
+ function encode(&$response)
+ {
+ // the isinstance is a bit of a kludge... it means there isn't
+ // really an adapter to make the interfaces quite match.
+ if (!is_a($response, 'Auth_OpenID_ServerError') &&
+ $response->needsSigning()) {
+
+ if (!$this->signatory) {
+ return new Auth_OpenID_ServerError(null,
+ "Must have a store to sign request");
+ }
+
+ if ($response->fields->hasKey(Auth_OpenID_OPENID_NS, 'sig')) {
+ return new Auth_OpenID_AlreadySigned($response);
+ }
+ $response = $this->signatory->sign($response);
+ }
+
+ return parent::encode($response);
+ }
+}
+
+/**
+ * Decode an incoming query into an Auth_OpenID_Request.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_Decoder {
+
+ function Auth_OpenID_Decoder(&$server)
+ {
+ $this->server =& $server;
+
+ $this->handlers = array(
+ 'checkid_setup' => 'Auth_OpenID_CheckIDRequest',
+ 'checkid_immediate' => 'Auth_OpenID_CheckIDRequest',
+ 'check_authentication' => 'Auth_OpenID_CheckAuthRequest',
+ 'associate' => 'Auth_OpenID_AssociateRequest'
+ );
+ }
+
+ /**
+ * Given an HTTP query in an array (key-value pairs), decode it
+ * into an Auth_OpenID_Request object.
+ */
+ function decode($query)
+ {
+ if (!$query) {
+ return null;
+ }
+
+ $message = Auth_OpenID_Message::fromPostArgs($query);
+
+ if ($message === null) {
+ /*
+ * It's useful to have a Message attached to a
+ * ProtocolError, so we override the bad ns value to build
+ * a Message out of it. Kinda kludgy, since it's made of
+ * lies, but the parts that aren't lies are more useful
+ * than a 'None'.
+ */
+ $old_ns = $query['openid.ns'];
+
+ $query['openid.ns'] = Auth_OpenID_OPENID2_NS;
+ $message = Auth_OpenID_Message::fromPostArgs($query);
+ return new Auth_OpenID_ServerError(
+ $message,
+ sprintf("Invalid OpenID namespace URI: %s", $old_ns));
+ }
+
+ $mode = $message->getArg(Auth_OpenID_OPENID_NS, 'mode');
+ if (!$mode) {
+ return new Auth_OpenID_ServerError($message,
+ "No mode value in message");
+ }
+
+ if (Auth_OpenID::isFailure($mode)) {
+ return new Auth_OpenID_ServerError($message,
+ $mode->message);
+ }
+
+ $handlerCls = Auth_OpenID::arrayGet($this->handlers, $mode,
+ $this->defaultDecoder($message));
+
+ if (!is_a($handlerCls, 'Auth_OpenID_ServerError')) {
+ return call_user_func_array(array($handlerCls, 'fromMessage'),
+ array($message, $this->server));
+ } else {
+ return $handlerCls;
+ }
+ }
+
+ function defaultDecoder($message)
+ {
+ $mode = $message->getArg(Auth_OpenID_OPENID_NS, 'mode');
+
+ if (Auth_OpenID::isFailure($mode)) {
+ return new Auth_OpenID_ServerError($message,
+ $mode->message);
+ }
+
+ return new Auth_OpenID_ServerError($message,
+ sprintf("Unrecognized OpenID mode %s", $mode));
+ }
+}
+
+/**
+ * An error that indicates an encoding problem occurred.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_EncodingError {
+ function Auth_OpenID_EncodingError(&$response)
+ {
+ $this->response =& $response;
+ }
+}
+
+/**
+ * An error that indicates that a response was already signed.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_AlreadySigned extends Auth_OpenID_EncodingError {
+ // This response is already signed.
+}
+
+/**
+ * An error that indicates that the given return_to is not under the
+ * given trust_root.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_UntrustedReturnURL extends Auth_OpenID_ServerError {
+ function Auth_OpenID_UntrustedReturnURL($message, $return_to,
+ $trust_root)
+ {
+ parent::Auth_OpenID_ServerError($message, "Untrusted return_to URL");
+ $this->return_to = $return_to;
+ $this->trust_root = $trust_root;
+ }
+
+ function toString()
+ {
+ return sprintf("return_to %s not under trust_root %s",
+ $this->return_to, $this->trust_root);
+ }
+}
+
+/**
+ * I handle requests for an OpenID server.
+ *
+ * Some types of requests (those which are not checkid requests) may
+ * be handed to my {@link handleRequest} method, and I will take care
+ * of it and return a response.
+ *
+ * For your convenience, I also provide an interface to {@link
+ * Auth_OpenID_Decoder::decode()} and {@link
+ * Auth_OpenID_SigningEncoder::encode()} through my methods {@link
+ * decodeRequest} and {@link encodeResponse}.
+ *
+ * All my state is encapsulated in an {@link Auth_OpenID_OpenIDStore}.
+ *
+ * Example:
+ *
+ * <pre> $oserver = new Auth_OpenID_Server(Auth_OpenID_FileStore($data_path),
+ * "http://example.com/op");
+ * $request = $oserver->decodeRequest();
+ * if (in_array($request->mode, array('checkid_immediate',
+ * 'checkid_setup'))) {
+ * if ($app->isAuthorized($request->identity, $request->trust_root)) {
+ * $response = $request->answer(true);
+ * } else if ($request->immediate) {
+ * $response = $request->answer(false);
+ * } else {
+ * $app->showDecidePage($request);
+ * return;
+ * }
+ * } else {
+ * $response = $oserver->handleRequest($request);
+ * }
+ *
+ * $webresponse = $oserver->encode($response);</pre>
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_Server {
+ function Auth_OpenID_Server(&$store, $op_endpoint=null)
+ {
+ $this->store =& $store;
+ $this->signatory =& new Auth_OpenID_Signatory($this->store);
+ $this->encoder =& new Auth_OpenID_SigningEncoder($this->signatory);
+ $this->decoder =& new Auth_OpenID_Decoder($this);
+ $this->op_endpoint = $op_endpoint;
+ $this->negotiator =& Auth_OpenID_getDefaultNegotiator();
+ }
+
+ /**
+ * Handle a request. Given an {@link Auth_OpenID_Request} object,
+ * call the appropriate {@link Auth_OpenID_Server} method to
+ * process the request and generate a response.
+ *
+ * @param Auth_OpenID_Request $request An {@link Auth_OpenID_Request}
+ * returned by {@link Auth_OpenID_Server::decodeRequest()}.
+ *
+ * @return Auth_OpenID_ServerResponse $response A response object
+ * capable of generating a user-agent reply.
+ */
+ function handleRequest($request)
+ {
+ if (method_exists($this, "openid_" . $request->mode)) {
+ $handler = array($this, "openid_" . $request->mode);
+ return call_user_func($handler, $request);
+ }
+ return null;
+ }
+
+ /**
+ * The callback for 'check_authentication' messages.
+ */
+ function openid_check_authentication(&$request)
+ {
+ return $request->answer($this->signatory);
+ }
+
+ /**
+ * The callback for 'associate' messages.
+ */
+ function openid_associate(&$request)
+ {
+ $assoc_type = $request->assoc_type;
+ $session_type = $request->session->session_type;
+ if ($this->negotiator->isAllowed($assoc_type, $session_type)) {
+ $assoc = $this->signatory->createAssociation(false,
+ $assoc_type);
+ return $request->answer($assoc);
+ } else {
+ $message = sprintf('Association type %s is not supported with '.
+ 'session type %s', $assoc_type, $session_type);
+ list($preferred_assoc_type, $preferred_session_type) =
+ $this->negotiator->getAllowedType();
+ return $request->answerUnsupported($message,
+ $preferred_assoc_type,
+ $preferred_session_type);
+ }
+ }
+
+ /**
+ * Encodes as response in the appropriate format suitable for
+ * sending to the user agent.
+ */
+ function encodeResponse(&$response)
+ {
+ return $this->encoder->encode($response);
+ }
+
+ /**
+ * Decodes a query args array into the appropriate
+ * {@link Auth_OpenID_Request} object.
+ */
+ function decodeRequest($query=null)
+ {
+ if ($query === null) {
+ $query = Auth_OpenID::getQuery();
+ }
+
+ return $this->decoder->decode($query);
+ }
+}
+
+?>
diff --git a/extlib/Auth/OpenID/ServerRequest.php b/extlib/Auth/OpenID/ServerRequest.php
new file mode 100644
index 000000000..33a8556ce
--- /dev/null
+++ b/extlib/Auth/OpenID/ServerRequest.php
@@ -0,0 +1,37 @@
+<?php
+/**
+ * OpenID Server Request
+ *
+ * @see Auth_OpenID_Server
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005-2008 Janrain, Inc.
+ * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
+ */
+
+/**
+ * Imports
+ */
+require_once "Auth/OpenID.php";
+
+/**
+ * Object that holds the state of a request to the OpenID server
+ *
+ * With accessor functions to get at the internal request data.
+ *
+ * @see Auth_OpenID_Server
+ * @package OpenID
+ */
+class Auth_OpenID_ServerRequest {
+ function Auth_OpenID_ServerRequest()
+ {
+ $this->mode = null;
+ }
+}
+
+?> \ No newline at end of file
diff --git a/extlib/Auth/OpenID/TrustRoot.php b/extlib/Auth/OpenID/TrustRoot.php
new file mode 100644
index 000000000..4919a6065
--- /dev/null
+++ b/extlib/Auth/OpenID/TrustRoot.php
@@ -0,0 +1,462 @@
+<?php
+/**
+ * Functions for dealing with OpenID trust roots
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005-2008 Janrain, Inc.
+ * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
+ */
+
+require_once 'Auth/OpenID/Discover.php';
+
+/**
+ * A regular expression that matches a domain ending in a top-level domains.
+ * Used in checking trust roots for sanity.
+ *
+ * @access private
+ */
+define('Auth_OpenID___TLDs',
+ '/\.(ac|ad|ae|aero|af|ag|ai|al|am|an|ao|aq|ar|arpa|as|asia' .
+ '|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|biz|bj|bm|bn|bo|br' .
+ '|bs|bt|bv|bw|by|bz|ca|cat|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co' .
+ '|com|coop|cr|cu|cv|cx|cy|cz|de|dj|dk|dm|do|dz|ec|edu|ee|eg' .
+ '|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl' .
+ '|gm|gn|gov|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie' .
+ '|il|im|in|info|int|io|iq|ir|is|it|je|jm|jo|jobs|jp|ke|kg|kh' .
+ '|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly' .
+ '|ma|mc|md|me|mg|mh|mil|mk|ml|mm|mn|mo|mobi|mp|mq|mr|ms|mt' .
+ '|mu|museum|mv|mw|mx|my|mz|na|name|nc|ne|net|nf|ng|ni|nl|no' .
+ '|np|nr|nu|nz|om|org|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|pro|ps|pt' .
+ '|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl' .
+ '|sm|sn|so|sr|st|su|sv|sy|sz|tc|td|tel|tf|tg|th|tj|tk|tl|tm' .
+ '|tn|to|tp|tr|travel|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve' .
+ '|vg|vi|vn|vu|wf|ws|xn--0zwm56d|xn--11b5bs3a9aj6g' .
+ '|xn--80akhbyknj4f|xn--9t4b11yi5a|xn--deba0ad|xn--g6w251d' .
+ '|xn--hgbk6aj7f53bba|xn--hlcj6aya9esc7a|xn--jxalpdlp' .
+ '|xn--kgbechtv|xn--zckzah|ye|yt|yu|za|zm|zw)\.?$/');
+
+define('Auth_OpenID___HostSegmentRe',
+ "/^(?:[-a-zA-Z0-9!$&'\\(\\)\\*+,;=._~]|%[a-zA-Z0-9]{2})*$/");
+
+/**
+ * A wrapper for trust-root related functions
+ */
+class Auth_OpenID_TrustRoot {
+ /*
+ * Return a discovery URL for this realm.
+ *
+ * Return null if the realm could not be parsed or was not valid.
+ *
+ * @param return_to The relying party return URL of the OpenID
+ * authentication request
+ *
+ * @return The URL upon which relying party discovery should be
+ * run in order to verify the return_to URL
+ */
+ function buildDiscoveryURL($realm)
+ {
+ $parsed = Auth_OpenID_TrustRoot::_parse($realm);
+
+ if ($parsed === false) {
+ return false;
+ }
+
+ if ($parsed['wildcard']) {
+ // Use "www." in place of the star
+ if ($parsed['host'][0] != '.') {
+ return false;
+ }
+
+ $www_domain = 'www' . $parsed['host'];
+
+ return sprintf('%s://%s%s', $parsed['scheme'],
+ $www_domain, $parsed['path']);
+ } else {
+ return $parsed['unparsed'];
+ }
+ }
+
+ /**
+ * Parse a URL into its trust_root parts.
+ *
+ * @static
+ *
+ * @access private
+ *
+ * @param string $trust_root The url to parse
+ *
+ * @return mixed $parsed Either an associative array of trust root
+ * parts or false if parsing failed.
+ */
+ function _parse($trust_root)
+ {
+ $trust_root = Auth_OpenID_urinorm($trust_root);
+ if ($trust_root === null) {
+ return false;
+ }
+
+ if (preg_match("/:\/\/[^:]+(:\d+){2,}(\/|$)/", $trust_root)) {
+ return false;
+ }
+
+ $parts = @parse_url($trust_root);
+ if ($parts === false) {
+ return false;
+ }
+
+ $required_parts = array('scheme', 'host');
+ $forbidden_parts = array('user', 'pass', 'fragment');
+ $keys = array_keys($parts);
+ if (array_intersect($keys, $required_parts) != $required_parts) {
+ return false;
+ }
+
+ if (array_intersect($keys, $forbidden_parts) != array()) {
+ return false;
+ }
+
+ if (!preg_match(Auth_OpenID___HostSegmentRe, $parts['host'])) {
+ return false;
+ }
+
+ $scheme = strtolower($parts['scheme']);
+ $allowed_schemes = array('http', 'https');
+ if (!in_array($scheme, $allowed_schemes)) {
+ return false;
+ }
+ $parts['scheme'] = $scheme;
+
+ $host = strtolower($parts['host']);
+ $hostparts = explode('*', $host);
+ switch (count($hostparts)) {
+ case 1:
+ $parts['wildcard'] = false;
+ break;
+ case 2:
+ if ($hostparts[0] ||
+ ($hostparts[1] && substr($hostparts[1], 0, 1) != '.')) {
+ return false;
+ }
+ $host = $hostparts[1];
+ $parts['wildcard'] = true;
+ break;
+ default:
+ return false;
+ }
+ if (strpos($host, ':') !== false) {
+ return false;
+ }
+
+ $parts['host'] = $host;
+
+ if (isset($parts['path'])) {
+ $path = strtolower($parts['path']);
+ if (substr($path, 0, 1) != '/') {
+ return false;
+ }
+ } else {
+ $path = '/';
+ }
+
+ $parts['path'] = $path;
+ if (!isset($parts['port'])) {
+ $parts['port'] = false;
+ }
+
+
+ $parts['unparsed'] = $trust_root;
+
+ return $parts;
+ }
+
+ /**
+ * Is this trust root sane?
+ *
+ * A trust root is sane if it is syntactically valid and it has a
+ * reasonable domain name. Specifically, the domain name must be
+ * more than one level below a standard TLD or more than two
+ * levels below a two-letter tld.
+ *
+ * For example, '*.com' is not a sane trust root, but '*.foo.com'
+ * is. '*.co.uk' is not sane, but '*.bbc.co.uk' is.
+ *
+ * This check is not always correct, but it attempts to err on the
+ * side of marking sane trust roots insane instead of marking
+ * insane trust roots sane. For example, 'kink.fm' is marked as
+ * insane even though it "should" (for some meaning of should) be
+ * marked sane.
+ *
+ * This function should be used when creating OpenID servers to
+ * alert the users of the server when a consumer attempts to get
+ * the user to accept a suspicious trust root.
+ *
+ * @static
+ * @param string $trust_root The trust root to check
+ * @return bool $sanity Whether the trust root looks OK
+ */
+ function isSane($trust_root)
+ {
+ $parts = Auth_OpenID_TrustRoot::_parse($trust_root);
+ if ($parts === false) {
+ return false;
+ }
+
+ // Localhost is a special case
+ if ($parts['host'] == 'localhost') {
+ return true;
+ }
+
+ $host_parts = explode('.', $parts['host']);
+ if ($parts['wildcard']) {
+ // Remove the empty string from the beginning of the array
+ array_shift($host_parts);
+ }
+
+ if ($host_parts && !$host_parts[count($host_parts) - 1]) {
+ array_pop($host_parts);
+ }
+
+ if (!$host_parts) {
+ return false;
+ }
+
+ // Don't allow adjacent dots
+ if (in_array('', $host_parts, true)) {
+ return false;
+ }
+
+ // Get the top-level domain of the host. If it is not a valid TLD,
+ // it's not sane.
+ preg_match(Auth_OpenID___TLDs, $parts['host'], $matches);
+ if (!$matches) {
+ return false;
+ }
+ $tld = $matches[1];
+
+ if (count($host_parts) == 1) {
+ return false;
+ }
+
+ if ($parts['wildcard']) {
+ // It's a 2-letter tld with a short second to last segment
+ // so there needs to be more than two segments specified
+ // (e.g. *.co.uk is insane)
+ $second_level = $host_parts[count($host_parts) - 2];
+ if (strlen($tld) == 2 && strlen($second_level) <= 3) {
+ return count($host_parts) > 2;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Does this URL match the given trust root?
+ *
+ * Return whether the URL falls under the given trust root. This
+ * does not check whether the trust root is sane. If the URL or
+ * trust root do not parse, this function will return false.
+ *
+ * @param string $trust_root The trust root to match against
+ *
+ * @param string $url The URL to check
+ *
+ * @return bool $matches Whether the URL matches against the
+ * trust root
+ */
+ function match($trust_root, $url)
+ {
+ $trust_root_parsed = Auth_OpenID_TrustRoot::_parse($trust_root);
+ $url_parsed = Auth_OpenID_TrustRoot::_parse($url);
+ if (!$trust_root_parsed || !$url_parsed) {
+ return false;
+ }
+
+ // Check hosts matching
+ if ($url_parsed['wildcard']) {
+ return false;
+ }
+ if ($trust_root_parsed['wildcard']) {
+ $host_tail = $trust_root_parsed['host'];
+ $host = $url_parsed['host'];
+ if ($host_tail &&
+ substr($host, -(strlen($host_tail))) != $host_tail &&
+ substr($host_tail, 1) != $host) {
+ return false;
+ }
+ } else {
+ if ($trust_root_parsed['host'] != $url_parsed['host']) {
+ return false;
+ }
+ }
+
+ // Check path and query matching
+ $base_path = $trust_root_parsed['path'];
+ $path = $url_parsed['path'];
+ if (!isset($trust_root_parsed['query'])) {
+ if ($base_path != $path) {
+ if (substr($path, 0, strlen($base_path)) != $base_path) {
+ return false;
+ }
+ if (substr($base_path, strlen($base_path) - 1, 1) != '/' &&
+ substr($path, strlen($base_path), 1) != '/') {
+ return false;
+ }
+ }
+ } else {
+ $base_query = $trust_root_parsed['query'];
+ $query = @$url_parsed['query'];
+ $qplus = substr($query, 0, strlen($base_query) + 1);
+ $bqplus = $base_query . '&';
+ if ($base_path != $path ||
+ ($base_query != $query && $qplus != $bqplus)) {
+ return false;
+ }
+ }
+
+ // The port and scheme need to match exactly
+ return ($trust_root_parsed['scheme'] == $url_parsed['scheme'] &&
+ $url_parsed['port'] === $trust_root_parsed['port']);
+ }
+}
+
+/*
+ * If the endpoint is a relying party OpenID return_to endpoint,
+ * return the endpoint URL. Otherwise, return None.
+ *
+ * This function is intended to be used as a filter for the Yadis
+ * filtering interface.
+ *
+ * @see: C{L{openid.yadis.services}}
+ * @see: C{L{openid.yadis.filters}}
+ *
+ * @param endpoint: An XRDS BasicServiceEndpoint, as returned by
+ * performing Yadis dicovery.
+ *
+ * @returns: The endpoint URL or None if the endpoint is not a
+ * relying party endpoint.
+ */
+function filter_extractReturnURL(&$endpoint)
+{
+ if ($endpoint->matchTypes(array(Auth_OpenID_RP_RETURN_TO_URL_TYPE))) {
+ return $endpoint;
+ } else {
+ return null;
+ }
+}
+
+function &Auth_OpenID_extractReturnURL(&$endpoint_list)
+{
+ $result = array();
+
+ foreach ($endpoint_list as $endpoint) {
+ if (filter_extractReturnURL($endpoint)) {
+ $result[] = $endpoint;
+ }
+ }
+
+ return $result;
+}
+
+/*
+ * Is the return_to URL under one of the supplied allowed return_to
+ * URLs?
+ */
+function Auth_OpenID_returnToMatches($allowed_return_to_urls, $return_to)
+{
+ foreach ($allowed_return_to_urls as $allowed_return_to) {
+ // A return_to pattern works the same as a realm, except that
+ // it's not allowed to use a wildcard. We'll model this by
+ // parsing it as a realm, and not trying to match it if it has
+ // a wildcard.
+
+ $return_realm = Auth_OpenID_TrustRoot::_parse($allowed_return_to);
+ if (// Parses as a trust root
+ ($return_realm !== false) &&
+ // Does not have a wildcard
+ (!$return_realm['wildcard']) &&
+ // Matches the return_to that we passed in with it
+ (Auth_OpenID_TrustRoot::match($allowed_return_to, $return_to))) {
+ return true;
+ }
+ }
+
+ // No URL in the list matched
+ return false;
+}
+
+/*
+ * Given a relying party discovery URL return a list of return_to
+ * URLs.
+ */
+function Auth_OpenID_getAllowedReturnURLs($relying_party_url, &$fetcher,
+ $discover_function=null)
+{
+ if ($discover_function === null) {
+ $discover_function = array('Auth_Yadis_Yadis', 'discover');
+ }
+
+ $xrds_parse_cb = array('Auth_OpenID_ServiceEndpoint', 'fromXRDS');
+
+ list($rp_url_after_redirects, $endpoints) =
+ Auth_Yadis_getServiceEndpoints($relying_party_url, $xrds_parse_cb,
+ $discover_function, $fetcher);
+
+ if ($rp_url_after_redirects != $relying_party_url) {
+ // Verification caused a redirect
+ return false;
+ }
+
+ call_user_func_array($discover_function,
+ array($relying_party_url, $fetcher));
+
+ $return_to_urls = array();
+ $matching_endpoints = Auth_OpenID_extractReturnURL($endpoints);
+
+ foreach ($matching_endpoints as $e) {
+ $return_to_urls[] = $e->server_url;
+ }
+
+ return $return_to_urls;
+}
+
+/*
+ * Verify that a return_to URL is valid for the given realm.
+ *
+ * This function builds a discovery URL, performs Yadis discovery on
+ * it, makes sure that the URL does not redirect, parses out the
+ * return_to URLs, and finally checks to see if the current return_to
+ * URL matches the return_to.
+ *
+ * @return true if the return_to URL is valid for the realm
+ */
+function Auth_OpenID_verifyReturnTo($realm_str, $return_to, &$fetcher,
+ $_vrfy='Auth_OpenID_getAllowedReturnURLs')
+{
+ $disco_url = Auth_OpenID_TrustRoot::buildDiscoveryURL($realm_str);
+
+ if ($disco_url === false) {
+ return false;
+ }
+
+ $allowable_urls = call_user_func_array($_vrfy,
+ array($disco_url, &$fetcher));
+
+ // The realm_str could not be parsed.
+ if ($allowable_urls === false) {
+ return false;
+ }
+
+ if (Auth_OpenID_returnToMatches($allowable_urls, $return_to)) {
+ return true;
+ } else {
+ return false;
+ }
+}
+
+?> \ No newline at end of file
diff --git a/extlib/Auth/OpenID/URINorm.php b/extlib/Auth/OpenID/URINorm.php
new file mode 100644
index 000000000..f821d836a
--- /dev/null
+++ b/extlib/Auth/OpenID/URINorm.php
@@ -0,0 +1,249 @@
+<?php
+
+/**
+ * URI normalization routines.
+ *
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005-2008 Janrain, Inc.
+ * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
+ */
+
+require_once 'Auth/Yadis/Misc.php';
+
+// from appendix B of rfc 3986 (http://www.ietf.org/rfc/rfc3986.txt)
+function Auth_OpenID_getURIPattern()
+{
+ return '&^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?&';
+}
+
+function Auth_OpenID_getAuthorityPattern()
+{
+ return '/^([^@]*@)?([^:]*)(:.*)?/';
+}
+
+function Auth_OpenID_getEncodedPattern()
+{
+ return '/%([0-9A-Fa-f]{2})/';
+}
+
+# gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@"
+#
+# sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
+# / "*" / "+" / "," / ";" / "="
+#
+# unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
+function Auth_OpenID_getURLIllegalCharRE()
+{
+ return "/([^-A-Za-z0-9:\/\?#\[\]@\!\$&'\(\)\*\+,;=\._~\%])/";
+}
+
+function Auth_OpenID_getUnreserved()
+{
+ $_unreserved = array();
+ for ($i = 0; $i < 256; $i++) {
+ $_unreserved[$i] = false;
+ }
+
+ for ($i = ord('A'); $i <= ord('Z'); $i++) {
+ $_unreserved[$i] = true;
+ }
+
+ for ($i = ord('0'); $i <= ord('9'); $i++) {
+ $_unreserved[$i] = true;
+ }
+
+ for ($i = ord('a'); $i <= ord('z'); $i++) {
+ $_unreserved[$i] = true;
+ }
+
+ $_unreserved[ord('-')] = true;
+ $_unreserved[ord('.')] = true;
+ $_unreserved[ord('_')] = true;
+ $_unreserved[ord('~')] = true;
+
+ return $_unreserved;
+}
+
+function Auth_OpenID_getEscapeRE()
+{
+ $parts = array();
+ foreach (array_merge(Auth_Yadis_getUCSChars(),
+ Auth_Yadis_getIPrivateChars()) as $pair) {
+ list($m, $n) = $pair;
+ $parts[] = sprintf("%s-%s", chr($m), chr($n));
+ }
+
+ return sprintf('[%s]', implode('', $parts));
+}
+
+function Auth_OpenID_pct_encoded_replace_unreserved($mo)
+{
+ $_unreserved = Auth_OpenID_getUnreserved();
+
+ $i = intval($mo[1], 16);
+ if ($_unreserved[$i]) {
+ return chr($i);
+ } else {
+ return strtoupper($mo[0]);
+ }
+
+ return $mo[0];
+}
+
+function Auth_OpenID_pct_encoded_replace($mo)
+{
+ return chr(intval($mo[1], 16));
+}
+
+function Auth_OpenID_remove_dot_segments($path)
+{
+ $result_segments = array();
+
+ while ($path) {
+ if (Auth_Yadis_startswith($path, '../')) {
+ $path = substr($path, 3);
+ } else if (Auth_Yadis_startswith($path, './')) {
+ $path = substr($path, 2);
+ } else if (Auth_Yadis_startswith($path, '/./')) {
+ $path = substr($path, 2);
+ } else if ($path == '/.') {
+ $path = '/';
+ } else if (Auth_Yadis_startswith($path, '/../')) {
+ $path = substr($path, 3);
+ if ($result_segments) {
+ array_pop($result_segments);
+ }
+ } else if ($path == '/..') {
+ $path = '/';
+ if ($result_segments) {
+ array_pop($result_segments);
+ }
+ } else if (($path == '..') ||
+ ($path == '.')) {
+ $path = '';
+ } else {
+ $i = 0;
+ if ($path[0] == '/') {
+ $i = 1;
+ }
+ $i = strpos($path, '/', $i);
+ if ($i === false) {
+ $i = strlen($path);
+ }
+ $result_segments[] = substr($path, 0, $i);
+ $path = substr($path, $i);
+ }
+ }
+
+ return implode('', $result_segments);
+}
+
+function Auth_OpenID_urinorm($uri)
+{
+ $uri_matches = array();
+ preg_match(Auth_OpenID_getURIPattern(), $uri, $uri_matches);
+
+ if (count($uri_matches) < 9) {
+ for ($i = count($uri_matches); $i <= 9; $i++) {
+ $uri_matches[] = '';
+ }
+ }
+
+ $illegal_matches = array();
+ preg_match(Auth_OpenID_getURLIllegalCharRE(),
+ $uri, $illegal_matches);
+ if ($illegal_matches) {
+ return null;
+ }
+
+ $scheme = $uri_matches[2];
+ if ($scheme) {
+ $scheme = strtolower($scheme);
+ }
+
+ $scheme = $uri_matches[2];
+ if ($scheme === '') {
+ // No scheme specified
+ return null;
+ }
+
+ $scheme = strtolower($scheme);
+ if (!in_array($scheme, array('http', 'https'))) {
+ // Not an absolute HTTP or HTTPS URI
+ return null;
+ }
+
+ $authority = $uri_matches[4];
+ if ($authority === '') {
+ // Not an absolute URI
+ return null;
+ }
+
+ $authority_matches = array();
+ preg_match(Auth_OpenID_getAuthorityPattern(),
+ $authority, $authority_matches);
+ if (count($authority_matches) === 0) {
+ // URI does not have a valid authority
+ return null;
+ }
+
+ if (count($authority_matches) < 4) {
+ for ($i = count($authority_matches); $i <= 4; $i++) {
+ $authority_matches[] = '';
+ }
+ }
+
+ list($_whole, $userinfo, $host, $port) = $authority_matches;
+
+ if ($userinfo === null) {
+ $userinfo = '';
+ }
+
+ if (strpos($host, '%') !== -1) {
+ $host = strtolower($host);
+ $host = preg_replace_callback(
+ Auth_OpenID_getEncodedPattern(),
+ 'Auth_OpenID_pct_encoded_replace', $host);
+ // NO IDNA.
+ // $host = unicode($host, 'utf-8').encode('idna');
+ } else {
+ $host = strtolower($host);
+ }
+
+ if ($port) {
+ if (($port == ':') ||
+ ($scheme == 'http' && $port == ':80') ||
+ ($scheme == 'https' && $port == ':443')) {
+ $port = '';
+ }
+ } else {
+ $port = '';
+ }
+
+ $authority = $userinfo . $host . $port;
+
+ $path = $uri_matches[5];
+ $path = preg_replace_callback(
+ Auth_OpenID_getEncodedPattern(),
+ 'Auth_OpenID_pct_encoded_replace_unreserved', $path);
+
+ $path = Auth_OpenID_remove_dot_segments($path);
+ if (!$path) {
+ $path = '/';
+ }
+
+ $query = $uri_matches[6];
+ if ($query === null) {
+ $query = '';
+ }
+
+ $fragment = $uri_matches[8];
+ if ($fragment === null) {
+ $fragment = '';
+ }
+
+ return $scheme . '://' . $authority . $path . $query . $fragment;
+}
+
+?>
diff --git a/extlib/Auth/Yadis/HTTPFetcher.php b/extlib/Auth/Yadis/HTTPFetcher.php
new file mode 100644
index 000000000..a1825403d
--- /dev/null
+++ b/extlib/Auth/Yadis/HTTPFetcher.php
@@ -0,0 +1,147 @@
+<?php
+
+/**
+ * This module contains the HTTP fetcher interface
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005-2008 Janrain, Inc.
+ * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
+ */
+
+/**
+ * Require logging functionality
+ */
+require_once "Auth/OpenID.php";
+
+define('Auth_OpenID_FETCHER_MAX_RESPONSE_KB', 1024);
+define('Auth_OpenID_USER_AGENT',
+ 'php-openid/'.Auth_OpenID_VERSION.' (php/'.phpversion().')');
+
+class Auth_Yadis_HTTPResponse {
+ function Auth_Yadis_HTTPResponse($final_url = null, $status = null,
+ $headers = null, $body = null)
+ {
+ $this->final_url = $final_url;
+ $this->status = $status;
+ $this->headers = $headers;
+ $this->body = $body;
+ }
+}
+
+/**
+ * This class is the interface for HTTP fetchers the Yadis library
+ * uses. This interface is only important if you need to write a new
+ * fetcher for some reason.
+ *
+ * @access private
+ * @package OpenID
+ */
+class Auth_Yadis_HTTPFetcher {
+
+ var $timeout = 20; // timeout in seconds.
+
+ /**
+ * Return whether a URL can be fetched. Returns false if the URL
+ * scheme is not allowed or is not supported by this fetcher
+ * implementation; returns true otherwise.
+ *
+ * @return bool
+ */
+ function canFetchURL($url)
+ {
+ if ($this->isHTTPS($url) && !$this->supportsSSL()) {
+ Auth_OpenID::log("HTTPS URL unsupported fetching %s",
+ $url);
+ return false;
+ }
+
+ if (!$this->allowedURL($url)) {
+ Auth_OpenID::log("URL fetching not allowed for '%s'",
+ $url);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Return whether a URL should be allowed. Override this method to
+ * conform to your local policy.
+ *
+ * By default, will attempt to fetch any http or https URL.
+ */
+ function allowedURL($url)
+ {
+ return $this->URLHasAllowedScheme($url);
+ }
+
+ /**
+ * Does this fetcher implementation (and runtime) support fetching
+ * HTTPS URLs? May inspect the runtime environment.
+ *
+ * @return bool $support True if this fetcher supports HTTPS
+ * fetching; false if not.
+ */
+ function supportsSSL()
+ {
+ trigger_error("not implemented", E_USER_ERROR);
+ }
+
+ /**
+ * Is this an https URL?
+ *
+ * @access private
+ */
+ function isHTTPS($url)
+ {
+ return (bool)preg_match('/^https:\/\//i', $url);
+ }
+
+ /**
+ * Is this an http or https URL?
+ *
+ * @access private
+ */
+ function URLHasAllowedScheme($url)
+ {
+ return (bool)preg_match('/^https?:\/\//i', $url);
+ }
+
+ /**
+ * @access private
+ */
+ function _findRedirect($headers)
+ {
+ foreach ($headers as $line) {
+ if (strpos(strtolower($line), "location: ") === 0) {
+ $parts = explode(" ", $line, 2);
+ return $parts[1];
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Fetches the specified URL using optional extra headers and
+ * returns the server's response.
+ *
+ * @param string $url The URL to be fetched.
+ * @param array $extra_headers An array of header strings
+ * (e.g. "Accept: text/html").
+ * @return mixed $result An array of ($code, $url, $headers,
+ * $body) if the URL could be fetched; null if the URL does not
+ * pass the URLHasAllowedScheme check or if the server's response
+ * is malformed.
+ */
+ function get($url, $headers)
+ {
+ trigger_error("not implemented", E_USER_ERROR);
+ }
+}
+
+?> \ No newline at end of file
diff --git a/extlib/Auth/Yadis/Manager.php b/extlib/Auth/Yadis/Manager.php
new file mode 100644
index 000000000..d50cf7ad6
--- /dev/null
+++ b/extlib/Auth/Yadis/Manager.php
@@ -0,0 +1,529 @@
+<?php
+
+/**
+ * Yadis service manager to be used during yadis-driven authentication
+ * attempts.
+ *
+ * @package OpenID
+ */
+
+/**
+ * The base session class used by the Auth_Yadis_Manager. This
+ * class wraps the default PHP session machinery and should be
+ * subclassed if your application doesn't use PHP sessioning.
+ *
+ * @package OpenID
+ */
+class Auth_Yadis_PHPSession {
+ /**
+ * Set a session key/value pair.
+ *
+ * @param string $name The name of the session key to add.
+ * @param string $value The value to add to the session.
+ */
+ function set($name, $value)
+ {
+ $_SESSION[$name] = $value;
+ }
+
+ /**
+ * Get a key's value from the session.
+ *
+ * @param string $name The name of the key to retrieve.
+ * @param string $default The optional value to return if the key
+ * is not found in the session.
+ * @return string $result The key's value in the session or
+ * $default if it isn't found.
+ */
+ function get($name, $default=null)
+ {
+ if (array_key_exists($name, $_SESSION)) {
+ return $_SESSION[$name];
+ } else {
+ return $default;
+ }
+ }
+
+ /**
+ * Remove a key/value pair from the session.
+ *
+ * @param string $name The name of the key to remove.
+ */
+ function del($name)
+ {
+ unset($_SESSION[$name]);
+ }
+
+ /**
+ * Return the contents of the session in array form.
+ */
+ function contents()
+ {
+ return $_SESSION;
+ }
+}
+
+/**
+ * A session helper class designed to translate between arrays and
+ * objects. Note that the class used must have a constructor that
+ * takes no parameters. This is not a general solution, but it works
+ * for dumb objects that just need to have attributes set. The idea
+ * is that you'll subclass this and override $this->check($data) ->
+ * bool to implement your own session data validation.
+ *
+ * @package OpenID
+ */
+class Auth_Yadis_SessionLoader {
+ /**
+ * Override this.
+ *
+ * @access private
+ */
+ function check($data)
+ {
+ return true;
+ }
+
+ /**
+ * Given a session data value (an array), this creates an object
+ * (returned by $this->newObject()) whose attributes and values
+ * are those in $data. Returns null if $data lacks keys found in
+ * $this->requiredKeys(). Returns null if $this->check($data)
+ * evaluates to false. Returns null if $this->newObject()
+ * evaluates to false.
+ *
+ * @access private
+ */
+ function fromSession($data)
+ {
+ if (!$data) {
+ return null;
+ }
+
+ $required = $this->requiredKeys();
+
+ foreach ($required as $k) {
+ if (!array_key_exists($k, $data)) {
+ return null;
+ }
+ }
+
+ if (!$this->check($data)) {
+ return null;
+ }
+
+ $data = array_merge($data, $this->prepareForLoad($data));
+ $obj = $this->newObject($data);
+
+ if (!$obj) {
+ return null;
+ }
+
+ foreach ($required as $k) {
+ $obj->$k = $data[$k];
+ }
+
+ return $obj;
+ }
+
+ /**
+ * Prepares the data array by making any necessary changes.
+ * Returns an array whose keys and values will be used to update
+ * the original data array before calling $this->newObject($data).
+ *
+ * @access private
+ */
+ function prepareForLoad($data)
+ {
+ return array();
+ }
+
+ /**
+ * Returns a new instance of this loader's class, using the
+ * session data to construct it if necessary. The object need
+ * only be created; $this->fromSession() will take care of setting
+ * the object's attributes.
+ *
+ * @access private
+ */
+ function newObject($data)
+ {
+ return null;
+ }
+
+ /**
+ * Returns an array of keys and values built from the attributes
+ * of $obj. If $this->prepareForSave($obj) returns an array, its keys
+ * and values are used to update the $data array of attributes
+ * from $obj.
+ *
+ * @access private
+ */
+ function toSession($obj)
+ {
+ $data = array();
+ foreach ($obj as $k => $v) {
+ $data[$k] = $v;
+ }
+
+ $extra = $this->prepareForSave($obj);
+
+ if ($extra && is_array($extra)) {
+ foreach ($extra as $k => $v) {
+ $data[$k] = $v;
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * Override this.
+ *
+ * @access private
+ */
+ function prepareForSave($obj)
+ {
+ return array();
+ }
+}
+
+/**
+ * A concrete loader implementation for Auth_OpenID_ServiceEndpoints.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_ServiceEndpointLoader extends Auth_Yadis_SessionLoader {
+ function newObject($data)
+ {
+ return new Auth_OpenID_ServiceEndpoint();
+ }
+
+ function requiredKeys()
+ {
+ $obj = new Auth_OpenID_ServiceEndpoint();
+ $data = array();
+ foreach ($obj as $k => $v) {
+ $data[] = $k;
+ }
+ return $data;
+ }
+
+ function check($data)
+ {
+ return is_array($data['type_uris']);
+ }
+}
+
+/**
+ * A concrete loader implementation for Auth_Yadis_Managers.
+ *
+ * @package OpenID
+ */
+class Auth_Yadis_ManagerLoader extends Auth_Yadis_SessionLoader {
+ function requiredKeys()
+ {
+ return array('starting_url',
+ 'yadis_url',
+ 'services',
+ 'session_key',
+ '_current',
+ 'stale');
+ }
+
+ function newObject($data)
+ {
+ return new Auth_Yadis_Manager($data['starting_url'],
+ $data['yadis_url'],
+ $data['services'],
+ $data['session_key']);
+ }
+
+ function check($data)
+ {
+ return is_array($data['services']);
+ }
+
+ function prepareForLoad($data)
+ {
+ $loader = new Auth_OpenID_ServiceEndpointLoader();
+ $services = array();
+ foreach ($data['services'] as $s) {
+ $services[] = $loader->fromSession($s);
+ }
+ return array('services' => $services);
+ }
+
+ function prepareForSave($obj)
+ {
+ $loader = new Auth_OpenID_ServiceEndpointLoader();
+ $services = array();
+ foreach ($obj->services as $s) {
+ $services[] = $loader->toSession($s);
+ }
+ return array('services' => $services);
+ }
+}
+
+/**
+ * The Yadis service manager which stores state in a session and
+ * iterates over <Service> elements in a Yadis XRDS document and lets
+ * a caller attempt to use each one. This is used by the Yadis
+ * library internally.
+ *
+ * @package OpenID
+ */
+class Auth_Yadis_Manager {
+
+ /**
+ * Intialize a new yadis service manager.
+ *
+ * @access private
+ */
+ function Auth_Yadis_Manager($starting_url, $yadis_url,
+ $services, $session_key)
+ {
+ // The URL that was used to initiate the Yadis protocol
+ $this->starting_url = $starting_url;
+
+ // The URL after following redirects (the identifier)
+ $this->yadis_url = $yadis_url;
+
+ // List of service elements
+ $this->services = $services;
+
+ $this->session_key = $session_key;
+
+ // Reference to the current service object
+ $this->_current = null;
+
+ // Stale flag for cleanup if PHP lib has trouble.
+ $this->stale = false;
+ }
+
+ /**
+ * @access private
+ */
+ function length()
+ {
+ // How many untried services remain?
+ return count($this->services);
+ }
+
+ /**
+ * Return the next service
+ *
+ * $this->current() will continue to return that service until the
+ * next call to this method.
+ */
+ function nextService()
+ {
+
+ if ($this->services) {
+ $this->_current = array_shift($this->services);
+ } else {
+ $this->_current = null;
+ }
+
+ return $this->_current;
+ }
+
+ /**
+ * @access private
+ */
+ function current()
+ {
+ // Return the current service.
+ // Returns None if there are no services left.
+ return $this->_current;
+ }
+
+ /**
+ * @access private
+ */
+ function forURL($url)
+ {
+ return in_array($url, array($this->starting_url, $this->yadis_url));
+ }
+
+ /**
+ * @access private
+ */
+ function started()
+ {
+ // Has the first service been returned?
+ return $this->_current !== null;
+ }
+}
+
+/**
+ * State management for discovery.
+ *
+ * High-level usage pattern is to call .getNextService(discover) in
+ * order to find the next available service for this user for this
+ * session. Once a request completes, call .cleanup() to clean up the
+ * session state.
+ *
+ * @package OpenID
+ */
+class Auth_Yadis_Discovery {
+
+ /**
+ * @access private
+ */
+ var $DEFAULT_SUFFIX = 'auth';
+
+ /**
+ * @access private
+ */
+ var $PREFIX = '_yadis_services_';
+
+ /**
+ * Initialize a discovery object.
+ *
+ * @param Auth_Yadis_PHPSession $session An object which
+ * implements the Auth_Yadis_PHPSession API.
+ * @param string $url The URL on which to attempt discovery.
+ * @param string $session_key_suffix The optional session key
+ * suffix override.
+ */
+ function Auth_Yadis_Discovery(&$session, $url,
+ $session_key_suffix = null)
+ {
+ /// Initialize a discovery object
+ $this->session =& $session;
+ $this->url = $url;
+ if ($session_key_suffix === null) {
+ $session_key_suffix = $this->DEFAULT_SUFFIX;
+ }
+
+ $this->session_key_suffix = $session_key_suffix;
+ $this->session_key = $this->PREFIX . $this->session_key_suffix;
+ }
+
+ /**
+ * Return the next authentication service for the pair of
+ * user_input and session. This function handles fallback.
+ */
+ function getNextService($discover_cb, &$fetcher)
+ {
+ $manager = $this->getManager();
+ if (!$manager || (!$manager->services)) {
+ $this->destroyManager();
+
+ list($yadis_url, $services) = call_user_func($discover_cb,
+ $this->url,
+ $fetcher);
+
+ $manager = $this->createManager($services, $yadis_url);
+ }
+
+ if ($manager) {
+ $loader = new Auth_Yadis_ManagerLoader();
+ $service = $manager->nextService();
+ $this->session->set($this->session_key,
+ serialize($loader->toSession($manager)));
+ } else {
+ $service = null;
+ }
+
+ return $service;
+ }
+
+ /**
+ * Clean up Yadis-related services in the session and return the
+ * most-recently-attempted service from the manager, if one
+ * exists.
+ *
+ * @param $force True if the manager should be deleted regardless
+ * of whether it's a manager for $this->url.
+ */
+ function cleanup($force=false)
+ {
+ $manager = $this->getManager($force);
+ if ($manager) {
+ $service = $manager->current();
+ $this->destroyManager($force);
+ } else {
+ $service = null;
+ }
+
+ return $service;
+ }
+
+ /**
+ * @access private
+ */
+ function getSessionKey()
+ {
+ // Get the session key for this starting URL and suffix
+ return $this->PREFIX . $this->session_key_suffix;
+ }
+
+ /**
+ * @access private
+ *
+ * @param $force True if the manager should be returned regardless
+ * of whether it's a manager for $this->url.
+ */
+ function &getManager($force=false)
+ {
+ // Extract the YadisServiceManager for this object's URL and
+ // suffix from the session.
+
+ $manager_str = $this->session->get($this->getSessionKey());
+ $manager = null;
+
+ if ($manager_str !== null) {
+ $loader = new Auth_Yadis_ManagerLoader();
+ $manager = $loader->fromSession(unserialize($manager_str));
+ }
+
+ if ($manager && ($manager->forURL($this->url) || $force)) {
+ return $manager;
+ } else {
+ $unused = null;
+ return $unused;
+ }
+ }
+
+ /**
+ * @access private
+ */
+ function &createManager($services, $yadis_url = null)
+ {
+ $key = $this->getSessionKey();
+ if ($this->getManager()) {
+ return $this->getManager();
+ }
+
+ if ($services) {
+ $loader = new Auth_Yadis_ManagerLoader();
+ $manager = new Auth_Yadis_Manager($this->url, $yadis_url,
+ $services, $key);
+ $this->session->set($this->session_key,
+ serialize($loader->toSession($manager)));
+ return $manager;
+ } else {
+ // Oh, PHP.
+ $unused = null;
+ return $unused;
+ }
+ }
+
+ /**
+ * @access private
+ *
+ * @param $force True if the manager should be deleted regardless
+ * of whether it's a manager for $this->url.
+ */
+ function destroyManager($force=false)
+ {
+ if ($this->getManager($force) !== null) {
+ $key = $this->getSessionKey();
+ $this->session->del($key);
+ }
+ }
+}
+
+?> \ No newline at end of file
diff --git a/extlib/Auth/Yadis/Misc.php b/extlib/Auth/Yadis/Misc.php
new file mode 100644
index 000000000..1134a4ff4
--- /dev/null
+++ b/extlib/Auth/Yadis/Misc.php
@@ -0,0 +1,59 @@
+<?php
+
+/**
+ * Miscellaneous utility values and functions for OpenID and Yadis.
+ *
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005-2008 Janrain, Inc.
+ * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
+ */
+
+function Auth_Yadis_getUCSChars()
+{
+ return array(
+ array(0xA0, 0xD7FF),
+ array(0xF900, 0xFDCF),
+ array(0xFDF0, 0xFFEF),
+ array(0x10000, 0x1FFFD),
+ array(0x20000, 0x2FFFD),
+ array(0x30000, 0x3FFFD),
+ array(0x40000, 0x4FFFD),
+ array(0x50000, 0x5FFFD),
+ array(0x60000, 0x6FFFD),
+ array(0x70000, 0x7FFFD),
+ array(0x80000, 0x8FFFD),
+ array(0x90000, 0x9FFFD),
+ array(0xA0000, 0xAFFFD),
+ array(0xB0000, 0xBFFFD),
+ array(0xC0000, 0xCFFFD),
+ array(0xD0000, 0xDFFFD),
+ array(0xE1000, 0xEFFFD)
+ );
+}
+
+function Auth_Yadis_getIPrivateChars()
+{
+ return array(
+ array(0xE000, 0xF8FF),
+ array(0xF0000, 0xFFFFD),
+ array(0x100000, 0x10FFFD)
+ );
+}
+
+function Auth_Yadis_pct_escape_unicode($char_match)
+{
+ $c = $char_match[0];
+ $result = "";
+ for ($i = 0; $i < strlen($c); $i++) {
+ $result .= "%".sprintf("%X", ord($c[$i]));
+ }
+ return $result;
+}
+
+function Auth_Yadis_startswith($s, $stuff)
+{
+ return strpos($s, $stuff) === 0;
+}
+
+?> \ No newline at end of file
diff --git a/extlib/Auth/Yadis/ParanoidHTTPFetcher.php b/extlib/Auth/Yadis/ParanoidHTTPFetcher.php
new file mode 100644
index 000000000..8975d7f89
--- /dev/null
+++ b/extlib/Auth/Yadis/ParanoidHTTPFetcher.php
@@ -0,0 +1,228 @@
+<?php
+
+/**
+ * This module contains the CURL-based HTTP fetcher implementation.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005-2008 Janrain, Inc.
+ * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
+ */
+
+/**
+ * Interface import
+ */
+require_once "Auth/Yadis/HTTPFetcher.php";
+
+require_once "Auth/OpenID.php";
+
+/**
+ * A paranoid {@link Auth_Yadis_HTTPFetcher} class which uses CURL
+ * for fetching.
+ *
+ * @package OpenID
+ */
+class Auth_Yadis_ParanoidHTTPFetcher extends Auth_Yadis_HTTPFetcher {
+ function Auth_Yadis_ParanoidHTTPFetcher()
+ {
+ $this->reset();
+ }
+
+ function reset()
+ {
+ $this->headers = array();
+ $this->data = "";
+ }
+
+ /**
+ * @access private
+ */
+ function _writeHeader($ch, $header)
+ {
+ array_push($this->headers, rtrim($header));
+ return strlen($header);
+ }
+
+ /**
+ * @access private
+ */
+ function _writeData($ch, $data)
+ {
+ if (strlen($this->data) > 1024*Auth_OpenID_FETCHER_MAX_RESPONSE_KB) {
+ return 0;
+ } else {
+ $this->data .= $data;
+ return strlen($data);
+ }
+ }
+
+ /**
+ * Does this fetcher support SSL URLs?
+ */
+ function supportsSSL()
+ {
+ $v = curl_version();
+ if(is_array($v)) {
+ return in_array('https', $v['protocols']);
+ } elseif (is_string($v)) {
+ return preg_match('/OpenSSL/i', $v);
+ } else {
+ return 0;
+ }
+ }
+
+ function get($url, $extra_headers = null)
+ {
+ if (!$this->canFetchURL($url)) {
+ return null;
+ }
+
+ $stop = time() + $this->timeout;
+ $off = $this->timeout;
+
+ $redir = true;
+
+ while ($redir && ($off > 0)) {
+ $this->reset();
+
+ $c = curl_init();
+
+ if ($c === false) {
+ Auth_OpenID::log(
+ "curl_init returned false; could not " .
+ "initialize for URL '%s'", $url);
+ return null;
+ }
+
+ if (defined('CURLOPT_NOSIGNAL')) {
+ curl_setopt($c, CURLOPT_NOSIGNAL, true);
+ }
+
+ if (!$this->allowedURL($url)) {
+ Auth_OpenID::log("Fetching URL not allowed: %s",
+ $url);
+ return null;
+ }
+
+ curl_setopt($c, CURLOPT_WRITEFUNCTION,
+ array(&$this, "_writeData"));
+ curl_setopt($c, CURLOPT_HEADERFUNCTION,
+ array(&$this, "_writeHeader"));
+
+ if ($extra_headers) {
+ curl_setopt($c, CURLOPT_HTTPHEADER, $extra_headers);
+ }
+
+ $cv = curl_version();
+ if(is_array($cv)) {
+ $curl_user_agent = 'curl/'.$cv['version'];
+ } else {
+ $curl_user_agent = $cv;
+ }
+ curl_setopt($c, CURLOPT_USERAGENT,
+ Auth_OpenID_USER_AGENT.' '.$curl_user_agent);
+ curl_setopt($c, CURLOPT_TIMEOUT, $off);
+ curl_setopt($c, CURLOPT_URL, $url);
+ curl_setopt($c, CURLOPT_RANGE,
+ "0-".(1024 * Auth_OpenID_FETCHER_MAX_RESPONSE_KB));
+
+ curl_exec($c);
+
+ $code = curl_getinfo($c, CURLINFO_HTTP_CODE);
+ $body = $this->data;
+ $headers = $this->headers;
+
+ if (!$code) {
+ Auth_OpenID::log("Got no response code when fetching %s", $url);
+ Auth_OpenID::log("CURL error (%s): %s",
+ curl_errno($c), curl_error($c));
+ return null;
+ }
+
+ if (in_array($code, array(301, 302, 303, 307))) {
+ $url = $this->_findRedirect($headers);
+ $redir = true;
+ } else {
+ $redir = false;
+ curl_close($c);
+
+ $new_headers = array();
+
+ foreach ($headers as $header) {
+ if (strpos($header, ': ')) {
+ list($name, $value) = explode(': ', $header, 2);
+ $new_headers[$name] = $value;
+ }
+ }
+
+ Auth_OpenID::log(
+ "Successfully fetched '%s': GET response code %s",
+ $url, $code);
+
+ return new Auth_Yadis_HTTPResponse($url, $code,
+ $new_headers, $body);
+ }
+
+ $off = $stop - time();
+ }
+
+ return null;
+ }
+
+ function post($url, $body, $extra_headers = null)
+ {
+ if (!$this->canFetchURL($url)) {
+ return null;
+ }
+
+ $this->reset();
+
+ $c = curl_init();
+
+ if (defined('CURLOPT_NOSIGNAL')) {
+ curl_setopt($c, CURLOPT_NOSIGNAL, true);
+ }
+
+ curl_setopt($c, CURLOPT_POST, true);
+ curl_setopt($c, CURLOPT_POSTFIELDS, $body);
+ curl_setopt($c, CURLOPT_TIMEOUT, $this->timeout);
+ curl_setopt($c, CURLOPT_URL, $url);
+ curl_setopt($c, CURLOPT_WRITEFUNCTION,
+ array(&$this, "_writeData"));
+
+ curl_exec($c);
+
+ $code = curl_getinfo($c, CURLINFO_HTTP_CODE);
+
+ if (!$code) {
+ Auth_OpenID::log("Got no response code when fetching %s", $url);
+ return null;
+ }
+
+ $body = $this->data;
+
+ curl_close($c);
+
+ $new_headers = $extra_headers;
+
+ foreach ($this->headers as $header) {
+ if (strpos($header, ': ')) {
+ list($name, $value) = explode(': ', $header, 2);
+ $new_headers[$name] = $value;
+ }
+
+ }
+
+ Auth_OpenID::log("Successfully fetched '%s': POST response code %s",
+ $url, $code);
+
+ return new Auth_Yadis_HTTPResponse($url, $code,
+ $new_headers, $body);
+ }
+}
+
+?> \ No newline at end of file
diff --git a/extlib/Auth/Yadis/ParseHTML.php b/extlib/Auth/Yadis/ParseHTML.php
new file mode 100644
index 000000000..297ccbd2c
--- /dev/null
+++ b/extlib/Auth/Yadis/ParseHTML.php
@@ -0,0 +1,259 @@
+<?php
+
+/**
+ * This is the HTML pseudo-parser for the Yadis library.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005-2008 Janrain, Inc.
+ * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
+ */
+
+/**
+ * This class is responsible for scanning an HTML string to find META
+ * tags and their attributes. This is used by the Yadis discovery
+ * process. This class must be instantiated to be used.
+ *
+ * @package OpenID
+ */
+class Auth_Yadis_ParseHTML {
+
+ /**
+ * @access private
+ */
+ var $_re_flags = "si";
+
+ /**
+ * @access private
+ */
+ var $_removed_re =
+ "<!--.*?-->|<!\[CDATA\[.*?\]\]>|<script\b(?!:)[^>]*>.*?<\/script>";
+
+ /**
+ * @access private
+ */
+ var $_tag_expr = "<%s%s(?:\s.*?)?%s>";
+
+ /**
+ * @access private
+ */
+ var $_attr_find = '\b([-\w]+)=(".*?"|\'.*?\'|.+?)[\/\s>]';
+
+ function Auth_Yadis_ParseHTML()
+ {
+ $this->_attr_find = sprintf("/%s/%s",
+ $this->_attr_find,
+ $this->_re_flags);
+
+ $this->_removed_re = sprintf("/%s/%s",
+ $this->_removed_re,
+ $this->_re_flags);
+
+ $this->_entity_replacements = array(
+ 'amp' => '&',
+ 'lt' => '<',
+ 'gt' => '>',
+ 'quot' => '"'
+ );
+
+ $this->_ent_replace =
+ sprintf("&(%s);", implode("|",
+ $this->_entity_replacements));
+ }
+
+ /**
+ * Replace HTML entities (amp, lt, gt, and quot) as well as
+ * numeric entities (e.g. #x9f;) with their actual values and
+ * return the new string.
+ *
+ * @access private
+ * @param string $str The string in which to look for entities
+ * @return string $new_str The new string entities decoded
+ */
+ function replaceEntities($str)
+ {
+ foreach ($this->_entity_replacements as $old => $new) {
+ $str = preg_replace(sprintf("/&%s;/", $old), $new, $str);
+ }
+
+ // Replace numeric entities because html_entity_decode doesn't
+ // do it for us.
+ $str = preg_replace('~&#x([0-9a-f]+);~ei', 'chr(hexdec("\\1"))', $str);
+ $str = preg_replace('~&#([0-9]+);~e', 'chr(\\1)', $str);
+
+ return $str;
+ }
+
+ /**
+ * Strip single and double quotes off of a string, if they are
+ * present.
+ *
+ * @access private
+ * @param string $str The original string
+ * @return string $new_str The new string with leading and
+ * trailing quotes removed
+ */
+ function removeQuotes($str)
+ {
+ $matches = array();
+ $double = '/^"(.*)"$/';
+ $single = "/^\'(.*)\'$/";
+
+ if (preg_match($double, $str, $matches)) {
+ return $matches[1];
+ } else if (preg_match($single, $str, $matches)) {
+ return $matches[1];
+ } else {
+ return $str;
+ }
+ }
+
+ /**
+ * Create a regular expression that will match an opening
+ * or closing tag from a set of names.
+ *
+ * @access private
+ * @param mixed $tag_names Tag names to match
+ * @param mixed $close false/0 = no, true/1 = yes, other = maybe
+ * @param mixed $self_close false/0 = no, true/1 = yes, other = maybe
+ * @return string $regex A regular expression string to be used
+ * in, say, preg_match.
+ */
+ function tagPattern($tag_names, $close, $self_close)
+ {
+ if (is_array($tag_names)) {
+ $tag_names = '(?:'.implode('|',$tag_names).')';
+ }
+ if ($close) {
+ $close = '\/' . (($close == 1)? '' : '?');
+ } else {
+ $close = '';
+ }
+ if ($self_close) {
+ $self_close = '(?:\/\s*)' . (($self_close == 1)? '' : '?');
+ } else {
+ $self_close = '';
+ }
+ $expr = sprintf($this->_tag_expr, $close, $tag_names, $self_close);
+
+ return sprintf("/%s/%s", $expr, $this->_re_flags);
+ }
+
+ /**
+ * Given an HTML document string, this finds all the META tags in
+ * the document, provided they are found in the
+ * <HTML><HEAD>...</HEAD> section of the document. The <HTML> tag
+ * may be missing.
+ *
+ * @access private
+ * @param string $html_string An HTMl document string
+ * @return array $tag_list Array of tags; each tag is an array of
+ * attribute -> value.
+ */
+ function getMetaTags($html_string)
+ {
+ $html_string = preg_replace($this->_removed_re,
+ "",
+ $html_string);
+
+ $key_tags = array($this->tagPattern('html', false, false),
+ $this->tagPattern('head', false, false),
+ $this->tagPattern('head', true, false),
+ $this->tagPattern('html', true, false),
+ $this->tagPattern(array(
+ 'body', 'frameset', 'frame', 'p', 'div',
+ 'table','span','a'), 'maybe', 'maybe'));
+ $key_tags_pos = array();
+ foreach ($key_tags as $pat) {
+ $matches = array();
+ preg_match($pat, $html_string, $matches, PREG_OFFSET_CAPTURE);
+ if($matches) {
+ $key_tags_pos[] = $matches[0][1];
+ } else {
+ $key_tags_pos[] = null;
+ }
+ }
+ // no opening head tag
+ if (is_null($key_tags_pos[1])) {
+ return array();
+ }
+ // the effective </head> is the min of the following
+ if (is_null($key_tags_pos[2])) {
+ $key_tags_pos[2] = strlen($html_string);
+ }
+ foreach (array($key_tags_pos[3], $key_tags_pos[4]) as $pos) {
+ if (!is_null($pos) && $pos < $key_tags_pos[2]) {
+ $key_tags_pos[2] = $pos;
+ }
+ }
+ // closing head tag comes before opening head tag
+ if ($key_tags_pos[1] > $key_tags_pos[2]) {
+ return array();
+ }
+ // if there is an opening html tag, make sure the opening head tag
+ // comes after it
+ if (!is_null($key_tags_pos[0]) && $key_tags_pos[1] < $key_tags_pos[0]) {
+ return array();
+ }
+ $html_string = substr($html_string, $key_tags_pos[1],
+ ($key_tags_pos[2]-$key_tags_pos[1]));
+
+ $link_data = array();
+ $link_matches = array();
+
+ if (!preg_match_all($this->tagPattern('meta', false, 'maybe'),
+ $html_string, $link_matches)) {
+ return array();
+ }
+
+ foreach ($link_matches[0] as $link) {
+ $attr_matches = array();
+ preg_match_all($this->_attr_find, $link, $attr_matches);
+ $link_attrs = array();
+ foreach ($attr_matches[0] as $index => $full_match) {
+ $name = $attr_matches[1][$index];
+ $value = $this->replaceEntities(
+ $this->removeQuotes($attr_matches[2][$index]));
+
+ $link_attrs[strtolower($name)] = $value;
+ }
+ $link_data[] = $link_attrs;
+ }
+
+ return $link_data;
+ }
+
+ /**
+ * Looks for a META tag with an "http-equiv" attribute whose value
+ * is one of ("x-xrds-location", "x-yadis-location"), ignoring
+ * case. If such a META tag is found, its "content" attribute
+ * value is returned.
+ *
+ * @param string $html_string An HTML document in string format
+ * @return mixed $content The "content" attribute value of the
+ * META tag, if found, or null if no such tag was found.
+ */
+ function getHTTPEquiv($html_string)
+ {
+ $meta_tags = $this->getMetaTags($html_string);
+
+ if ($meta_tags) {
+ foreach ($meta_tags as $tag) {
+ if (array_key_exists('http-equiv', $tag) &&
+ (in_array(strtolower($tag['http-equiv']),
+ array('x-xrds-location', 'x-yadis-location'))) &&
+ array_key_exists('content', $tag)) {
+ return $tag['content'];
+ }
+ }
+ }
+
+ return null;
+ }
+}
+
+?> \ No newline at end of file
diff --git a/extlib/Auth/Yadis/PlainHTTPFetcher.php b/extlib/Auth/Yadis/PlainHTTPFetcher.php
new file mode 100644
index 000000000..8882e3cef
--- /dev/null
+++ b/extlib/Auth/Yadis/PlainHTTPFetcher.php
@@ -0,0 +1,251 @@
+<?php
+
+/**
+ * This module contains the plain non-curl HTTP fetcher
+ * implementation.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005-2008 Janrain, Inc.
+ * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
+ */
+
+/**
+ * Interface import
+ */
+require_once "Auth/Yadis/HTTPFetcher.php";
+
+/**
+ * This class implements a plain, hand-built socket-based fetcher
+ * which will be used in the event that CURL is unavailable.
+ *
+ * @package OpenID
+ */
+class Auth_Yadis_PlainHTTPFetcher extends Auth_Yadis_HTTPFetcher {
+ /**
+ * Does this fetcher support SSL URLs?
+ */
+ function supportsSSL()
+ {
+ return function_exists('openssl_open');
+ }
+
+ function get($url, $extra_headers = null)
+ {
+ if (!$this->canFetchURL($url)) {
+ return null;
+ }
+
+ $redir = true;
+
+ $stop = time() + $this->timeout;
+ $off = $this->timeout;
+
+ while ($redir && ($off > 0)) {
+
+ $parts = parse_url($url);
+
+ $specify_port = true;
+
+ // Set a default port.
+ if (!array_key_exists('port', $parts)) {
+ $specify_port = false;
+ if ($parts['scheme'] == 'http') {
+ $parts['port'] = 80;
+ } elseif ($parts['scheme'] == 'https') {
+ $parts['port'] = 443;
+ } else {
+ return null;
+ }
+ }
+
+ if (!array_key_exists('path', $parts)) {
+ $parts['path'] = '/';
+ }
+
+ $host = $parts['host'];
+
+ if ($parts['scheme'] == 'https') {
+ $host = 'ssl://' . $host;
+ }
+
+ $user_agent = Auth_OpenID_USER_AGENT;
+
+ $headers = array(
+ "GET ".$parts['path'].
+ (array_key_exists('query', $parts) ?
+ "?".$parts['query'] : "").
+ " HTTP/1.0",
+ "User-Agent: $user_agent",
+ "Host: ".$parts['host'].
+ ($specify_port ? ":".$parts['port'] : ""),
+ "Range: 0-".
+ (1024*Auth_OpenID_FETCHER_MAX_RESPONSE_KB),
+ "Port: ".$parts['port']);
+
+ $errno = 0;
+ $errstr = '';
+
+ if ($extra_headers) {
+ foreach ($extra_headers as $h) {
+ $headers[] = $h;
+ }
+ }
+
+ @$sock = fsockopen($host, $parts['port'], $errno, $errstr,
+ $this->timeout);
+ if ($sock === false) {
+ return false;
+ }
+
+ stream_set_timeout($sock, $this->timeout);
+
+ fputs($sock, implode("\r\n", $headers) . "\r\n\r\n");
+
+ $data = "";
+ $kilobytes = 0;
+ while (!feof($sock) &&
+ $kilobytes < Auth_OpenID_FETCHER_MAX_RESPONSE_KB ) {
+ $data .= fgets($sock, 1024);
+ $kilobytes += 1;
+ }
+
+ fclose($sock);
+
+ // Split response into header and body sections
+ list($headers, $body) = explode("\r\n\r\n", $data, 2);
+ $headers = explode("\r\n", $headers);
+
+ $http_code = explode(" ", $headers[0]);
+ $code = $http_code[1];
+
+ if (in_array($code, array('301', '302'))) {
+ $url = $this->_findRedirect($headers);
+ $redir = true;
+ } else {
+ $redir = false;
+ }
+
+ $off = $stop - time();
+ }
+
+ $new_headers = array();
+
+ foreach ($headers as $header) {
+ if (preg_match("/:/", $header)) {
+ $parts = explode(": ", $header, 2);
+
+ if (count($parts) == 2) {
+ list($name, $value) = $parts;
+ $new_headers[$name] = $value;
+ }
+ }
+
+ }
+
+ return new Auth_Yadis_HTTPResponse($url, $code, $new_headers, $body);
+ }
+
+ function post($url, $body, $extra_headers = null)
+ {
+ if (!$this->canFetchURL($url)) {
+ return null;
+ }
+
+ $parts = parse_url($url);
+
+ $headers = array();
+
+ $post_path = $parts['path'];
+ if (isset($parts['query'])) {
+ $post_path .= '?' . $parts['query'];
+ }
+
+ $headers[] = "POST ".$post_path." HTTP/1.0";
+ $headers[] = "Host: " . $parts['host'];
+ $headers[] = "Content-type: application/x-www-form-urlencoded";
+ $headers[] = "Content-length: " . strval(strlen($body));
+
+ if ($extra_headers &&
+ is_array($extra_headers)) {
+ $headers = array_merge($headers, $extra_headers);
+ }
+
+ // Join all headers together.
+ $all_headers = implode("\r\n", $headers);
+
+ // Add headers, two newlines, and request body.
+ $request = $all_headers . "\r\n\r\n" . $body;
+
+ // Set a default port.
+ if (!array_key_exists('port', $parts)) {
+ if ($parts['scheme'] == 'http') {
+ $parts['port'] = 80;
+ } elseif ($parts['scheme'] == 'https') {
+ $parts['port'] = 443;
+ } else {
+ return null;
+ }
+ }
+
+ if ($parts['scheme'] == 'https') {
+ $parts['host'] = sprintf("ssl://%s", $parts['host']);
+ }
+
+ // Connect to the remote server.
+ $errno = 0;
+ $errstr = '';
+
+ $sock = fsockopen($parts['host'], $parts['port'], $errno, $errstr,
+ $this->timeout);
+
+ if ($sock === false) {
+ return null;
+ }
+
+ stream_set_timeout($sock, $this->timeout);
+
+ // Write the POST request.
+ fputs($sock, $request);
+
+ // Get the response from the server.
+ $response = "";
+ while (!feof($sock)) {
+ if ($data = fgets($sock, 128)) {
+ $response .= $data;
+ } else {
+ break;
+ }
+ }
+
+ // Split the request into headers and body.
+ list($headers, $response_body) = explode("\r\n\r\n", $response, 2);
+
+ $headers = explode("\r\n", $headers);
+
+ // Expect the first line of the headers data to be something
+ // like HTTP/1.1 200 OK. Split the line on spaces and take
+ // the second token, which should be the return code.
+ $http_code = explode(" ", $headers[0]);
+ $code = $http_code[1];
+
+ $new_headers = array();
+
+ foreach ($headers as $header) {
+ if (preg_match("/:/", $header)) {
+ list($name, $value) = explode(": ", $header, 2);
+ $new_headers[$name] = $value;
+ }
+
+ }
+
+ return new Auth_Yadis_HTTPResponse($url, $code,
+ $new_headers, $response_body);
+ }
+}
+
+?> \ No newline at end of file
diff --git a/extlib/Auth/Yadis/XML.php b/extlib/Auth/Yadis/XML.php
new file mode 100644
index 000000000..4854f12bb
--- /dev/null
+++ b/extlib/Auth/Yadis/XML.php
@@ -0,0 +1,374 @@
+<?php
+
+/**
+ * XML-parsing classes to wrap the domxml and DOM extensions for PHP 4
+ * and 5, respectively.
+ *
+ * @package OpenID
+ */
+
+/**
+ * The base class for wrappers for available PHP XML-parsing
+ * extensions. To work with this Yadis library, subclasses of this
+ * class MUST implement the API as defined in the remarks for this
+ * class. Subclasses of Auth_Yadis_XMLParser are used to wrap
+ * particular PHP XML extensions such as 'domxml'. These are used
+ * internally by the library depending on the availability of
+ * supported PHP XML extensions.
+ *
+ * @package OpenID
+ */
+class Auth_Yadis_XMLParser {
+ /**
+ * Initialize an instance of Auth_Yadis_XMLParser with some
+ * XML and namespaces. This SHOULD NOT be overridden by
+ * subclasses.
+ *
+ * @param string $xml_string A string of XML to be parsed.
+ * @param array $namespace_map An array of ($ns_name => $ns_uri)
+ * to be registered with the XML parser. May be empty.
+ * @return boolean $result True if the initialization and
+ * namespace registration(s) succeeded; false otherwise.
+ */
+ function init($xml_string, $namespace_map)
+ {
+ if (!$this->setXML($xml_string)) {
+ return false;
+ }
+
+ foreach ($namespace_map as $prefix => $uri) {
+ if (!$this->registerNamespace($prefix, $uri)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Register a namespace with the XML parser. This should be
+ * overridden by subclasses.
+ *
+ * @param string $prefix The namespace prefix to appear in XML tag
+ * names.
+ *
+ * @param string $uri The namespace URI to be used to identify the
+ * namespace in the XML.
+ *
+ * @return boolean $result True if the registration succeeded;
+ * false otherwise.
+ */
+ function registerNamespace($prefix, $uri)
+ {
+ // Not implemented.
+ }
+
+ /**
+ * Set this parser object's XML payload. This should be
+ * overridden by subclasses.
+ *
+ * @param string $xml_string The XML string to pass to this
+ * object's XML parser.
+ *
+ * @return boolean $result True if the initialization succeeded;
+ * false otherwise.
+ */
+ function setXML($xml_string)
+ {
+ // Not implemented.
+ }
+
+ /**
+ * Evaluate an XPath expression and return the resulting node
+ * list. This should be overridden by subclasses.
+ *
+ * @param string $xpath The XPath expression to be evaluated.
+ *
+ * @param mixed $node A node object resulting from a previous
+ * evalXPath call. This node, if specified, provides the context
+ * for the evaluation of this xpath expression.
+ *
+ * @return array $node_list An array of matching opaque node
+ * objects to be used with other methods of this parser class.
+ */
+ function evalXPath($xpath, $node = null)
+ {
+ // Not implemented.
+ }
+
+ /**
+ * Return the textual content of a specified node.
+ *
+ * @param mixed $node A node object from a previous call to
+ * $this->evalXPath().
+ *
+ * @return string $content The content of this node.
+ */
+ function content($node)
+ {
+ // Not implemented.
+ }
+
+ /**
+ * Return the attributes of a specified node.
+ *
+ * @param mixed $node A node object from a previous call to
+ * $this->evalXPath().
+ *
+ * @return array $attrs An array mapping attribute names to
+ * values.
+ */
+ function attributes($node)
+ {
+ // Not implemented.
+ }
+}
+
+/**
+ * This concrete implementation of Auth_Yadis_XMLParser implements
+ * the appropriate API for the 'domxml' extension which is typically
+ * packaged with PHP 4. This class will be used whenever the 'domxml'
+ * extension is detected. See the Auth_Yadis_XMLParser class for
+ * details on this class's methods.
+ *
+ * @package OpenID
+ */
+class Auth_Yadis_domxml extends Auth_Yadis_XMLParser {
+ function Auth_Yadis_domxml()
+ {
+ $this->xml = null;
+ $this->doc = null;
+ $this->xpath = null;
+ $this->errors = array();
+ }
+
+ function setXML($xml_string)
+ {
+ $this->xml = $xml_string;
+ $this->doc = @domxml_open_mem($xml_string, DOMXML_LOAD_PARSING,
+ $this->errors);
+
+ if (!$this->doc) {
+ return false;
+ }
+
+ $this->xpath = $this->doc->xpath_new_context();
+
+ return true;
+ }
+
+ function registerNamespace($prefix, $uri)
+ {
+ return xpath_register_ns($this->xpath, $prefix, $uri);
+ }
+
+ function &evalXPath($xpath, $node = null)
+ {
+ if ($node) {
+ $result = @$this->xpath->xpath_eval($xpath, $node);
+ } else {
+ $result = @$this->xpath->xpath_eval($xpath);
+ }
+
+ if (!$result) {
+ $n = array();
+ return $n;
+ }
+
+ if (!$result->nodeset) {
+ $n = array();
+ return $n;
+ }
+
+ return $result->nodeset;
+ }
+
+ function content($node)
+ {
+ if ($node) {
+ return $node->get_content();
+ }
+ }
+
+ function attributes($node)
+ {
+ if ($node) {
+ $arr = $node->attributes();
+ $result = array();
+
+ if ($arr) {
+ foreach ($arr as $attrnode) {
+ $result[$attrnode->name] = $attrnode->value;
+ }
+ }
+
+ return $result;
+ }
+ }
+}
+
+/**
+ * This concrete implementation of Auth_Yadis_XMLParser implements
+ * the appropriate API for the 'dom' extension which is typically
+ * packaged with PHP 5. This class will be used whenever the 'dom'
+ * extension is detected. See the Auth_Yadis_XMLParser class for
+ * details on this class's methods.
+ *
+ * @package OpenID
+ */
+class Auth_Yadis_dom extends Auth_Yadis_XMLParser {
+ function Auth_Yadis_dom()
+ {
+ $this->xml = null;
+ $this->doc = null;
+ $this->xpath = null;
+ $this->errors = array();
+ }
+
+ function setXML($xml_string)
+ {
+ $this->xml = $xml_string;
+ $this->doc = new DOMDocument;
+
+ if (!$this->doc) {
+ return false;
+ }
+
+ if (!@$this->doc->loadXML($xml_string)) {
+ return false;
+ }
+
+ $this->xpath = new DOMXPath($this->doc);
+
+ if ($this->xpath) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ function registerNamespace($prefix, $uri)
+ {
+ return $this->xpath->registerNamespace($prefix, $uri);
+ }
+
+ function &evalXPath($xpath, $node = null)
+ {
+ if ($node) {
+ $result = @$this->xpath->query($xpath, $node);
+ } else {
+ $result = @$this->xpath->query($xpath);
+ }
+
+ $n = array();
+
+ if (!$result) {
+ return $n;
+ }
+
+ for ($i = 0; $i < $result->length; $i++) {
+ $n[] = $result->item($i);
+ }
+
+ return $n;
+ }
+
+ function content($node)
+ {
+ if ($node) {
+ return $node->textContent;
+ }
+ }
+
+ function attributes($node)
+ {
+ if ($node) {
+ $arr = $node->attributes;
+ $result = array();
+
+ if ($arr) {
+ for ($i = 0; $i < $arr->length; $i++) {
+ $node = $arr->item($i);
+ $result[$node->nodeName] = $node->nodeValue;
+ }
+ }
+
+ return $result;
+ }
+ }
+}
+
+global $__Auth_Yadis_defaultParser;
+$__Auth_Yadis_defaultParser = null;
+
+/**
+ * Set a default parser to override the extension-driven selection of
+ * available parser classes. This is helpful in a test environment or
+ * one in which multiple parsers can be used but one is more
+ * desirable.
+ *
+ * @param Auth_Yadis_XMLParser $parser An instance of a
+ * Auth_Yadis_XMLParser subclass.
+ */
+function Auth_Yadis_setDefaultParser(&$parser)
+{
+ global $__Auth_Yadis_defaultParser;
+ $__Auth_Yadis_defaultParser =& $parser;
+}
+
+function Auth_Yadis_getSupportedExtensions()
+{
+ return array(
+ 'dom' => array('classname' => 'Auth_Yadis_dom',
+ 'libname' => array('dom.so', 'dom.dll')),
+ 'domxml' => array('classname' => 'Auth_Yadis_domxml',
+ 'libname' => array('domxml.so', 'php_domxml.dll')),
+ );
+}
+
+/**
+ * Returns an instance of a Auth_Yadis_XMLParser subclass based on
+ * the availability of PHP extensions for XML parsing. If
+ * Auth_Yadis_setDefaultParser has been called, the parser used in
+ * that call will be returned instead.
+ */
+function &Auth_Yadis_getXMLParser()
+{
+ global $__Auth_Yadis_defaultParser;
+
+ if (isset($__Auth_Yadis_defaultParser)) {
+ return $__Auth_Yadis_defaultParser;
+ }
+
+ $p = null;
+ $classname = null;
+
+ $extensions = Auth_Yadis_getSupportedExtensions();
+
+ // Return a wrapper for the resident implementation, if any.
+ foreach ($extensions as $name => $params) {
+ if (!extension_loaded($name)) {
+ foreach ($params['libname'] as $libname) {
+ if (@dl($libname)) {
+ $classname = $params['classname'];
+ }
+ }
+ } else {
+ $classname = $params['classname'];
+ }
+ if (isset($classname)) {
+ $p = new $classname();
+ return $p;
+ }
+ }
+
+ if (!isset($p)) {
+ trigger_error('No XML parser was found', E_USER_ERROR);
+ } else {
+ Auth_Yadis_setDefaultParser($p);
+ }
+
+ return $p;
+}
+
+?>
diff --git a/extlib/Auth/Yadis/XRDS.php b/extlib/Auth/Yadis/XRDS.php
new file mode 100644
index 000000000..f14a7948e
--- /dev/null
+++ b/extlib/Auth/Yadis/XRDS.php
@@ -0,0 +1,478 @@
+<?php
+
+/**
+ * This module contains the XRDS parsing code.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005-2008 Janrain, Inc.
+ * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
+ */
+
+/**
+ * Require the XPath implementation.
+ */
+require_once 'Auth/Yadis/XML.php';
+
+/**
+ * This match mode means a given service must match ALL filters passed
+ * to the Auth_Yadis_XRDS::services() call.
+ */
+define('SERVICES_YADIS_MATCH_ALL', 101);
+
+/**
+ * This match mode means a given service must match ANY filters (at
+ * least one) passed to the Auth_Yadis_XRDS::services() call.
+ */
+define('SERVICES_YADIS_MATCH_ANY', 102);
+
+/**
+ * The priority value used for service elements with no priority
+ * specified.
+ */
+define('SERVICES_YADIS_MAX_PRIORITY', pow(2, 30));
+
+/**
+ * XRD XML namespace
+ */
+define('Auth_Yadis_XMLNS_XRD_2_0', 'xri://$xrd*($v*2.0)');
+
+/**
+ * XRDS XML namespace
+ */
+define('Auth_Yadis_XMLNS_XRDS', 'xri://$xrds');
+
+function Auth_Yadis_getNSMap()
+{
+ return array('xrds' => Auth_Yadis_XMLNS_XRDS,
+ 'xrd' => Auth_Yadis_XMLNS_XRD_2_0);
+}
+
+/**
+ * @access private
+ */
+function Auth_Yadis_array_scramble($arr)
+{
+ $result = array();
+
+ while (count($arr)) {
+ $index = array_rand($arr, 1);
+ $result[] = $arr[$index];
+ unset($arr[$index]);
+ }
+
+ return $result;
+}
+
+/**
+ * This class represents a <Service> element in an XRDS document.
+ * Objects of this type are returned by
+ * Auth_Yadis_XRDS::services() and
+ * Auth_Yadis_Yadis::services(). Each object corresponds directly
+ * to a <Service> element in the XRDS and supplies a
+ * getElements($name) method which you should use to inspect the
+ * element's contents. See {@link Auth_Yadis_Yadis} for more
+ * information on the role this class plays in Yadis discovery.
+ *
+ * @package OpenID
+ */
+class Auth_Yadis_Service {
+
+ /**
+ * Creates an empty service object.
+ */
+ function Auth_Yadis_Service()
+ {
+ $this->element = null;
+ $this->parser = null;
+ }
+
+ /**
+ * Return the URIs in the "Type" elements, if any, of this Service
+ * element.
+ *
+ * @return array $type_uris An array of Type URI strings.
+ */
+ function getTypes()
+ {
+ $t = array();
+ foreach ($this->getElements('xrd:Type') as $elem) {
+ $c = $this->parser->content($elem);
+ if ($c) {
+ $t[] = $c;
+ }
+ }
+ return $t;
+ }
+
+ function matchTypes($type_uris)
+ {
+ $result = array();
+
+ foreach ($this->getTypes() as $typ) {
+ if (in_array($typ, $type_uris)) {
+ $result[] = $typ;
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Return the URIs in the "URI" elements, if any, of this Service
+ * element. The URIs are returned sorted in priority order.
+ *
+ * @return array $uris An array of URI strings.
+ */
+ function getURIs()
+ {
+ $uris = array();
+ $last = array();
+
+ foreach ($this->getElements('xrd:URI') as $elem) {
+ $uri_string = $this->parser->content($elem);
+ $attrs = $this->parser->attributes($elem);
+ if ($attrs &&
+ array_key_exists('priority', $attrs)) {
+ $priority = intval($attrs['priority']);
+ if (!array_key_exists($priority, $uris)) {
+ $uris[$priority] = array();
+ }
+
+ $uris[$priority][] = $uri_string;
+ } else {
+ $last[] = $uri_string;
+ }
+ }
+
+ $keys = array_keys($uris);
+ sort($keys);
+
+ // Rebuild array of URIs.
+ $result = array();
+ foreach ($keys as $k) {
+ $new_uris = Auth_Yadis_array_scramble($uris[$k]);
+ $result = array_merge($result, $new_uris);
+ }
+
+ $result = array_merge($result,
+ Auth_Yadis_array_scramble($last));
+
+ return $result;
+ }
+
+ /**
+ * Returns the "priority" attribute value of this <Service>
+ * element, if the attribute is present. Returns null if not.
+ *
+ * @return mixed $result Null or integer, depending on whether
+ * this Service element has a 'priority' attribute.
+ */
+ function getPriority()
+ {
+ $attributes = $this->parser->attributes($this->element);
+
+ if (array_key_exists('priority', $attributes)) {
+ return intval($attributes['priority']);
+ }
+
+ return null;
+ }
+
+ /**
+ * Used to get XML elements from this object's <Service> element.
+ *
+ * This is what you should use to get all custom information out
+ * of this element. This is used by service filter functions to
+ * determine whether a service element contains specific tags,
+ * etc. NOTE: this only considers elements which are direct
+ * children of the <Service> element for this object.
+ *
+ * @param string $name The name of the element to look for
+ * @return array $list An array of elements with the specified
+ * name which are direct children of the <Service> element. The
+ * nodes returned by this function can be passed to $this->parser
+ * methods (see {@link Auth_Yadis_XMLParser}).
+ */
+ function getElements($name)
+ {
+ return $this->parser->evalXPath($name, $this->element);
+ }
+}
+
+/*
+ * Return the expiration date of this XRD element, or None if no
+ * expiration was specified.
+ *
+ * @param $default The value to use as the expiration if no expiration
+ * was specified in the XRD.
+ */
+function Auth_Yadis_getXRDExpiration($xrd_element, $default=null)
+{
+ $expires_element = $xrd_element->$parser->evalXPath('/xrd:Expires');
+ if ($expires_element === null) {
+ return $default;
+ } else {
+ $expires_string = $expires_element->text;
+
+ // Will raise ValueError if the string is not the expected
+ // format
+ $t = strptime($expires_string, "%Y-%m-%dT%H:%M:%SZ");
+
+ if ($t === false) {
+ return false;
+ }
+
+ // [int $hour [, int $minute [, int $second [,
+ // int $month [, int $day [, int $year ]]]]]]
+ return mktime($t['tm_hour'], $t['tm_min'], $t['tm_sec'],
+ $t['tm_mon'], $t['tm_day'], $t['tm_year']);
+ }
+}
+
+/**
+ * This class performs parsing of XRDS documents.
+ *
+ * You should not instantiate this class directly; rather, call
+ * parseXRDS statically:
+ *
+ * <pre> $xrds = Auth_Yadis_XRDS::parseXRDS($xml_string);</pre>
+ *
+ * If the XRDS can be parsed and is valid, an instance of
+ * Auth_Yadis_XRDS will be returned. Otherwise, null will be
+ * returned. This class is used by the Auth_Yadis_Yadis::discover
+ * method.
+ *
+ * @package OpenID
+ */
+class Auth_Yadis_XRDS {
+
+ /**
+ * Instantiate a Auth_Yadis_XRDS object. Requires an XPath
+ * instance which has been used to parse a valid XRDS document.
+ */
+ function Auth_Yadis_XRDS(&$xmlParser, &$xrdNodes)
+ {
+ $this->parser =& $xmlParser;
+ $this->xrdNode = $xrdNodes[count($xrdNodes) - 1];
+ $this->allXrdNodes =& $xrdNodes;
+ $this->serviceList = array();
+ $this->_parse();
+ }
+
+ /**
+ * Parse an XML string (XRDS document) and return either a
+ * Auth_Yadis_XRDS object or null, depending on whether the
+ * XRDS XML is valid.
+ *
+ * @param string $xml_string An XRDS XML string.
+ * @return mixed $xrds An instance of Auth_Yadis_XRDS or null,
+ * depending on the validity of $xml_string
+ */
+ function &parseXRDS($xml_string, $extra_ns_map = null)
+ {
+ $_null = null;
+
+ if (!$xml_string) {
+ return $_null;
+ }
+
+ $parser = Auth_Yadis_getXMLParser();
+
+ $ns_map = Auth_Yadis_getNSMap();
+
+ if ($extra_ns_map && is_array($extra_ns_map)) {
+ $ns_map = array_merge($ns_map, $extra_ns_map);
+ }
+
+ if (!($parser && $parser->init($xml_string, $ns_map))) {
+ return $_null;
+ }
+
+ // Try to get root element.
+ $root = $parser->evalXPath('/xrds:XRDS[1]');
+ if (!$root) {
+ return $_null;
+ }
+
+ if (is_array($root)) {
+ $root = $root[0];
+ }
+
+ $attrs = $parser->attributes($root);
+
+ if (array_key_exists('xmlns:xrd', $attrs) &&
+ $attrs['xmlns:xrd'] != Auth_Yadis_XMLNS_XRDS) {
+ return $_null;
+ } else if (array_key_exists('xmlns', $attrs) &&
+ preg_match('/xri/', $attrs['xmlns']) &&
+ $attrs['xmlns'] != Auth_Yadis_XMLNS_XRD_2_0) {
+ return $_null;
+ }
+
+ // Get the last XRD node.
+ $xrd_nodes = $parser->evalXPath('/xrds:XRDS[1]/xrd:XRD');
+
+ if (!$xrd_nodes) {
+ return $_null;
+ }
+
+ $xrds = new Auth_Yadis_XRDS($parser, $xrd_nodes);
+ return $xrds;
+ }
+
+ /**
+ * @access private
+ */
+ function _addService($priority, $service)
+ {
+ $priority = intval($priority);
+
+ if (!array_key_exists($priority, $this->serviceList)) {
+ $this->serviceList[$priority] = array();
+ }
+
+ $this->serviceList[$priority][] = $service;
+ }
+
+ /**
+ * Creates the service list using nodes from the XRDS XML
+ * document.
+ *
+ * @access private
+ */
+ function _parse()
+ {
+ $this->serviceList = array();
+
+ $services = $this->parser->evalXPath('xrd:Service', $this->xrdNode);
+
+ foreach ($services as $node) {
+ $s =& new Auth_Yadis_Service();
+ $s->element = $node;
+ $s->parser =& $this->parser;
+
+ $priority = $s->getPriority();
+
+ if ($priority === null) {
+ $priority = SERVICES_YADIS_MAX_PRIORITY;
+ }
+
+ $this->_addService($priority, $s);
+ }
+ }
+
+ /**
+ * Returns a list of service objects which correspond to <Service>
+ * elements in the XRDS XML document for this object.
+ *
+ * Optionally, an array of filter callbacks may be given to limit
+ * the list of returned service objects. Furthermore, the default
+ * mode is to return all service objects which match ANY of the
+ * specified filters, but $filter_mode may be
+ * SERVICES_YADIS_MATCH_ALL if you want to be sure that the
+ * returned services match all the given filters. See {@link
+ * Auth_Yadis_Yadis} for detailed usage information on filter
+ * functions.
+ *
+ * @param mixed $filters An array of callbacks to filter the
+ * returned services, or null if all services are to be returned.
+ * @param integer $filter_mode SERVICES_YADIS_MATCH_ALL or
+ * SERVICES_YADIS_MATCH_ANY, depending on whether the returned
+ * services should match ALL or ANY of the specified filters,
+ * respectively.
+ * @return mixed $services An array of {@link
+ * Auth_Yadis_Service} objects if $filter_mode is a valid
+ * mode; null if $filter_mode is an invalid mode (i.e., not
+ * SERVICES_YADIS_MATCH_ANY or SERVICES_YADIS_MATCH_ALL).
+ */
+ function services($filters = null,
+ $filter_mode = SERVICES_YADIS_MATCH_ANY)
+ {
+
+ $pri_keys = array_keys($this->serviceList);
+ sort($pri_keys, SORT_NUMERIC);
+
+ // If no filters are specified, return the entire service
+ // list, ordered by priority.
+ if (!$filters ||
+ (!is_array($filters))) {
+
+ $result = array();
+ foreach ($pri_keys as $pri) {
+ $result = array_merge($result, $this->serviceList[$pri]);
+ }
+
+ return $result;
+ }
+
+ // If a bad filter mode is specified, return null.
+ if (!in_array($filter_mode, array(SERVICES_YADIS_MATCH_ANY,
+ SERVICES_YADIS_MATCH_ALL))) {
+ return null;
+ }
+
+ // Otherwise, use the callbacks in the filter list to
+ // determine which services are returned.
+ $filtered = array();
+
+ foreach ($pri_keys as $priority_value) {
+ $service_obj_list = $this->serviceList[$priority_value];
+
+ foreach ($service_obj_list as $service) {
+
+ $matches = 0;
+
+ foreach ($filters as $filter) {
+ if (call_user_func_array($filter, array($service))) {
+ $matches++;
+
+ if ($filter_mode == SERVICES_YADIS_MATCH_ANY) {
+ $pri = $service->getPriority();
+ if ($pri === null) {
+ $pri = SERVICES_YADIS_MAX_PRIORITY;
+ }
+
+ if (!array_key_exists($pri, $filtered)) {
+ $filtered[$pri] = array();
+ }
+
+ $filtered[$pri][] = $service;
+ break;
+ }
+ }
+ }
+
+ if (($filter_mode == SERVICES_YADIS_MATCH_ALL) &&
+ ($matches == count($filters))) {
+
+ $pri = $service->getPriority();
+ if ($pri === null) {
+ $pri = SERVICES_YADIS_MAX_PRIORITY;
+ }
+
+ if (!array_key_exists($pri, $filtered)) {
+ $filtered[$pri] = array();
+ }
+ $filtered[$pri][] = $service;
+ }
+ }
+ }
+
+ $pri_keys = array_keys($filtered);
+ sort($pri_keys, SORT_NUMERIC);
+
+ $result = array();
+ foreach ($pri_keys as $pri) {
+ $result = array_merge($result, $filtered[$pri]);
+ }
+
+ return $result;
+ }
+}
+
+?> \ No newline at end of file
diff --git a/extlib/Auth/Yadis/XRI.php b/extlib/Auth/Yadis/XRI.php
new file mode 100644
index 000000000..4e3462317
--- /dev/null
+++ b/extlib/Auth/Yadis/XRI.php
@@ -0,0 +1,234 @@
+<?php
+
+/**
+ * Routines for XRI resolution.
+ *
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005-2008 Janrain, Inc.
+ * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
+ */
+
+require_once 'Auth/Yadis/Misc.php';
+require_once 'Auth/Yadis/Yadis.php';
+require_once 'Auth/OpenID.php';
+
+function Auth_Yadis_getDefaultProxy()
+{
+ return 'http://xri.net/';
+}
+
+function Auth_Yadis_getXRIAuthorities()
+{
+ return array('!', '=', '@', '+', '$', '(');
+}
+
+function Auth_Yadis_getEscapeRE()
+{
+ $parts = array();
+ foreach (array_merge(Auth_Yadis_getUCSChars(),
+ Auth_Yadis_getIPrivateChars()) as $pair) {
+ list($m, $n) = $pair;
+ $parts[] = sprintf("%s-%s", chr($m), chr($n));
+ }
+
+ return sprintf('/[%s]/', implode('', $parts));
+}
+
+function Auth_Yadis_getXrefRE()
+{
+ return '/\((.*?)\)/';
+}
+
+function Auth_Yadis_identifierScheme($identifier)
+{
+ if (Auth_Yadis_startswith($identifier, 'xri://') ||
+ ($identifier &&
+ in_array($identifier[0], Auth_Yadis_getXRIAuthorities()))) {
+ return "XRI";
+ } else {
+ return "URI";
+ }
+}
+
+function Auth_Yadis_toIRINormal($xri)
+{
+ if (!Auth_Yadis_startswith($xri, 'xri://')) {
+ $xri = 'xri://' . $xri;
+ }
+
+ return Auth_Yadis_escapeForIRI($xri);
+}
+
+function _escape_xref($xref_match)
+{
+ $xref = $xref_match[0];
+ $xref = str_replace('/', '%2F', $xref);
+ $xref = str_replace('?', '%3F', $xref);
+ $xref = str_replace('#', '%23', $xref);
+ return $xref;
+}
+
+function Auth_Yadis_escapeForIRI($xri)
+{
+ $xri = str_replace('%', '%25', $xri);
+ $xri = preg_replace_callback(Auth_Yadis_getXrefRE(),
+ '_escape_xref', $xri);
+ return $xri;
+}
+
+function Auth_Yadis_toURINormal($xri)
+{
+ return Auth_Yadis_iriToURI(Auth_Yadis_toIRINormal($xri));
+}
+
+function Auth_Yadis_iriToURI($iri)
+{
+ if (1) {
+ return $iri;
+ } else {
+ // According to RFC 3987, section 3.1, "Mapping of IRIs to URIs"
+ return preg_replace_callback(Auth_Yadis_getEscapeRE(),
+ 'Auth_Yadis_pct_escape_unicode', $iri);
+ }
+}
+
+
+function Auth_Yadis_XRIAppendArgs($url, $args)
+{
+ // Append some arguments to an HTTP query. Yes, this is just like
+ // OpenID's appendArgs, but with special seasoning for XRI
+ // queries.
+
+ if (count($args) == 0) {
+ return $url;
+ }
+
+ // Non-empty array; if it is an array of arrays, use multisort;
+ // otherwise use sort.
+ if (array_key_exists(0, $args) &&
+ is_array($args[0])) {
+ // Do nothing here.
+ } else {
+ $keys = array_keys($args);
+ sort($keys);
+ $new_args = array();
+ foreach ($keys as $key) {
+ $new_args[] = array($key, $args[$key]);
+ }
+ $args = $new_args;
+ }
+
+ // According to XRI Resolution section "QXRI query parameters":
+ //
+ // "If the original QXRI had a null query component (only a
+ // leading question mark), or a query component consisting of
+ // only question marks, one additional leading question mark MUST
+ // be added when adding any XRI resolution parameters."
+ if (strpos(rtrim($url, '?'), '?') !== false) {
+ $sep = '&';
+ } else {
+ $sep = '?';
+ }
+
+ return $url . $sep . Auth_OpenID::httpBuildQuery($args);
+}
+
+function Auth_Yadis_providerIsAuthoritative($providerID, $canonicalID)
+{
+ $lastbang = strrpos($canonicalID, '!');
+ $p = substr($canonicalID, 0, $lastbang);
+ return $p == $providerID;
+}
+
+function Auth_Yadis_rootAuthority($xri)
+{
+ // Return the root authority for an XRI.
+
+ $root = null;
+
+ if (Auth_Yadis_startswith($xri, 'xri://')) {
+ $xri = substr($xri, 6);
+ }
+
+ $authority = explode('/', $xri, 2);
+ $authority = $authority[0];
+ if ($authority[0] == '(') {
+ // Cross-reference.
+ // XXX: This is incorrect if someone nests cross-references so
+ // there is another close-paren in there. Hopefully nobody
+ // does that before we have a real xriparse function.
+ // Hopefully nobody does that *ever*.
+ $root = substr($authority, 0, strpos($authority, ')') + 1);
+ } else if (in_array($authority[0], Auth_Yadis_getXRIAuthorities())) {
+ // Other XRI reference.
+ $root = $authority[0];
+ } else {
+ // IRI reference.
+ $_segments = explode("!", $authority);
+ $segments = array();
+ foreach ($_segments as $s) {
+ $segments = array_merge($segments, explode("*", $s));
+ }
+ $root = $segments[0];
+ }
+
+ return Auth_Yadis_XRI($root);
+}
+
+function Auth_Yadis_XRI($xri)
+{
+ if (!Auth_Yadis_startswith($xri, 'xri://')) {
+ $xri = 'xri://' . $xri;
+ }
+ return $xri;
+}
+
+function Auth_Yadis_getCanonicalID($iname, $xrds)
+{
+ // Returns false or a canonical ID value.
+
+ // Now nodes are in reverse order.
+ $xrd_list = array_reverse($xrds->allXrdNodes);
+ $parser =& $xrds->parser;
+ $node = $xrd_list[0];
+
+ $canonicalID_nodes = $parser->evalXPath('xrd:CanonicalID', $node);
+
+ if (!$canonicalID_nodes) {
+ return false;
+ }
+
+ $canonicalID = $canonicalID_nodes[0];
+ $canonicalID = Auth_Yadis_XRI($parser->content($canonicalID));
+
+ $childID = $canonicalID;
+
+ for ($i = 1; $i < count($xrd_list); $i++) {
+ $xrd = $xrd_list[$i];
+
+ $parent_sought = substr($childID, 0, strrpos($childID, '!'));
+ $parentCID = $parser->evalXPath('xrd:CanonicalID', $xrd);
+ if (!$parentCID) {
+ return false;
+ }
+ $parentCID = Auth_Yadis_XRI($parser->content($parentCID[0]));
+
+ if (strcasecmp($parent_sought, $parentCID)) {
+ // raise XRDSFraud.
+ return false;
+ }
+
+ $childID = $parent_sought;
+ }
+
+ $root = Auth_Yadis_rootAuthority($iname);
+ if (!Auth_Yadis_providerIsAuthoritative($root, $childID)) {
+ // raise XRDSFraud.
+ return false;
+ }
+
+ return $canonicalID;
+}
+
+?>
diff --git a/extlib/Auth/Yadis/XRIRes.php b/extlib/Auth/Yadis/XRIRes.php
new file mode 100644
index 000000000..4e8e8d037
--- /dev/null
+++ b/extlib/Auth/Yadis/XRIRes.php
@@ -0,0 +1,72 @@
+<?php
+
+/**
+ * Code for using a proxy XRI resolver.
+ */
+
+require_once 'Auth/Yadis/XRDS.php';
+require_once 'Auth/Yadis/XRI.php';
+
+class Auth_Yadis_ProxyResolver {
+ function Auth_Yadis_ProxyResolver(&$fetcher, $proxy_url = null)
+ {
+ $this->fetcher =& $fetcher;
+ $this->proxy_url = $proxy_url;
+ if (!$this->proxy_url) {
+ $this->proxy_url = Auth_Yadis_getDefaultProxy();
+ }
+ }
+
+ function queryURL($xri, $service_type = null)
+ {
+ // trim off the xri:// prefix
+ $qxri = substr(Auth_Yadis_toURINormal($xri), 6);
+ $hxri = $this->proxy_url . $qxri;
+ $args = array(
+ '_xrd_r' => 'application/xrds+xml'
+ );
+
+ if ($service_type) {
+ $args['_xrd_t'] = $service_type;
+ } else {
+ // Don't perform service endpoint selection.
+ $args['_xrd_r'] .= ';sep=false';
+ }
+
+ $query = Auth_Yadis_XRIAppendArgs($hxri, $args);
+ return $query;
+ }
+
+ function query($xri, $service_types, $filters = array())
+ {
+ $services = array();
+ $canonicalID = null;
+ foreach ($service_types as $service_type) {
+ $url = $this->queryURL($xri, $service_type);
+ $response = $this->fetcher->get($url);
+ if ($response->status != 200 and $response->status != 206) {
+ continue;
+ }
+ $xrds = Auth_Yadis_XRDS::parseXRDS($response->body);
+ if (!$xrds) {
+ continue;
+ }
+ $canonicalID = Auth_Yadis_getCanonicalID($xri,
+ $xrds);
+
+ if ($canonicalID === false) {
+ return null;
+ }
+
+ $some_services = $xrds->services($filters);
+ $services = array_merge($services, $some_services);
+ // TODO:
+ // * If we do get hits for multiple service_types, we're
+ // almost certainly going to have duplicated service
+ // entries and broken priority ordering.
+ }
+ return array($canonicalID, $services);
+ }
+}
+
+?>
diff --git a/extlib/Auth/Yadis/Yadis.php b/extlib/Auth/Yadis/Yadis.php
new file mode 100644
index 000000000..d89f77c6d
--- /dev/null
+++ b/extlib/Auth/Yadis/Yadis.php
@@ -0,0 +1,382 @@
+<?php
+
+/**
+ * The core PHP Yadis implementation.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005-2008 Janrain, Inc.
+ * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
+ */
+
+/**
+ * Need both fetcher types so we can use the right one based on the
+ * presence or absence of CURL.
+ */
+require_once "Auth/Yadis/PlainHTTPFetcher.php";
+require_once "Auth/Yadis/ParanoidHTTPFetcher.php";
+
+/**
+ * Need this for parsing HTML (looking for META tags).
+ */
+require_once "Auth/Yadis/ParseHTML.php";
+
+/**
+ * Need this to parse the XRDS document during Yadis discovery.
+ */
+require_once "Auth/Yadis/XRDS.php";
+
+/**
+ * XRDS (yadis) content type
+ */
+define('Auth_Yadis_CONTENT_TYPE', 'application/xrds+xml');
+
+/**
+ * Yadis header
+ */
+define('Auth_Yadis_HEADER_NAME', 'X-XRDS-Location');
+
+/**
+ * Contains the result of performing Yadis discovery on a URI.
+ *
+ * @package OpenID
+ */
+class Auth_Yadis_DiscoveryResult {
+
+ // The URI that was passed to the fetcher
+ var $request_uri = null;
+
+ // The result of following redirects from the request_uri
+ var $normalized_uri = null;
+
+ // The URI from which the response text was returned (set to
+ // None if there was no XRDS document found)
+ var $xrds_uri = null;
+
+ var $xrds = null;
+
+ // The content-type returned with the response_text
+ var $content_type = null;
+
+ // The document returned from the xrds_uri
+ var $response_text = null;
+
+ // Did the discovery fail miserably?
+ var $failed = false;
+
+ function Auth_Yadis_DiscoveryResult($request_uri)
+ {
+ // Initialize the state of the object
+ // sets all attributes to None except the request_uri
+ $this->request_uri = $request_uri;
+ }
+
+ function fail()
+ {
+ $this->failed = true;
+ }
+
+ function isFailure()
+ {
+ return $this->failed;
+ }
+
+ /**
+ * Returns the list of service objects as described by the XRDS
+ * document, if this yadis object represents a successful Yadis
+ * discovery.
+ *
+ * @return array $services An array of {@link Auth_Yadis_Service}
+ * objects
+ */
+ function services()
+ {
+ if ($this->xrds) {
+ return $this->xrds->services();
+ }
+
+ return null;
+ }
+
+ function usedYadisLocation()
+ {
+ // Was the Yadis protocol's indirection used?
+ return $this->normalized_uri != $this->xrds_uri;
+ }
+
+ function isXRDS()
+ {
+ // Is the response text supposed to be an XRDS document?
+ return ($this->usedYadisLocation() ||
+ $this->content_type == Auth_Yadis_CONTENT_TYPE);
+ }
+}
+
+/**
+ *
+ * Perform the Yadis protocol on the input URL and return an iterable
+ * of resulting endpoint objects.
+ *
+ * input_url: The URL on which to perform the Yadis protocol
+ *
+ * @return: The normalized identity URL and an iterable of endpoint
+ * objects generated by the filter function.
+ *
+ * xrds_parse_func: a callback which will take (uri, xrds_text) and
+ * return an array of service endpoint objects or null. Usually
+ * array('Auth_OpenID_ServiceEndpoint', 'fromXRDS').
+ *
+ * discover_func: if not null, a callback which should take (uri) and
+ * return an Auth_Yadis_Yadis object or null.
+ */
+function Auth_Yadis_getServiceEndpoints($input_url, $xrds_parse_func,
+ $discover_func=null, $fetcher=null)
+{
+ if ($discover_func === null) {
+ $discover_function = array('Auth_Yadis_Yadis', 'discover');
+ }
+
+ $yadis_result = call_user_func_array($discover_func,
+ array($input_url, $fetcher));
+
+ if ($yadis_result === null) {
+ return array($input_url, array());
+ }
+
+ $endpoints = call_user_func_array($xrds_parse_func,
+ array($yadis_result->normalized_uri,
+ $yadis_result->response_text));
+
+ if ($endpoints === null) {
+ $endpoints = array();
+ }
+
+ return array($yadis_result->normalized_uri, $endpoints);
+}
+
+/**
+ * This is the core of the PHP Yadis library. This is the only class
+ * a user needs to use to perform Yadis discovery. This class
+ * performs the discovery AND stores the result of the discovery.
+ *
+ * First, require this library into your program source:
+ *
+ * <pre> require_once "Auth/Yadis/Yadis.php";</pre>
+ *
+ * To perform Yadis discovery, first call the "discover" method
+ * statically with a URI parameter:
+ *
+ * <pre> $http_response = array();
+ * $fetcher = Auth_Yadis_Yadis::getHTTPFetcher();
+ * $yadis_object = Auth_Yadis_Yadis::discover($uri,
+ * $http_response, $fetcher);</pre>
+ *
+ * If the discovery succeeds, $yadis_object will be an instance of
+ * {@link Auth_Yadis_Yadis}. If not, it will be null. The XRDS
+ * document found during discovery should have service descriptions,
+ * which can be accessed by calling
+ *
+ * <pre> $service_list = $yadis_object->services();</pre>
+ *
+ * which returns an array of objects which describe each service.
+ * These objects are instances of Auth_Yadis_Service. Each object
+ * describes exactly one whole Service element, complete with all of
+ * its Types and URIs (no expansion is performed). The common use
+ * case for using the service objects returned by services() is to
+ * write one or more filter functions and pass those to services():
+ *
+ * <pre> $service_list = $yadis_object->services(
+ * array("filterByURI",
+ * "filterByExtension"));</pre>
+ *
+ * The filter functions (whose names appear in the array passed to
+ * services()) take the following form:
+ *
+ * <pre> function myFilter(&$service) {
+ * // Query $service object here. Return true if the service
+ * // matches your query; false if not.
+ * }</pre>
+ *
+ * This is an example of a filter which uses a regular expression to
+ * match the content of URI tags (note that the Auth_Yadis_Service
+ * class provides a getURIs() method which you should use instead of
+ * this contrived example):
+ *
+ * <pre>
+ * function URIMatcher(&$service) {
+ * foreach ($service->getElements('xrd:URI') as $uri) {
+ * if (preg_match("/some_pattern/",
+ * $service->parser->content($uri))) {
+ * return true;
+ * }
+ * }
+ * return false;
+ * }</pre>
+ *
+ * The filter functions you pass will be called for each service
+ * object to determine which ones match the criteria your filters
+ * specify. The default behavior is that if a given service object
+ * matches ANY of the filters specified in the services() call, it
+ * will be returned. You can specify that a given service object will
+ * be returned ONLY if it matches ALL specified filters by changing
+ * the match mode of services():
+ *
+ * <pre> $yadis_object->services(array("filter1", "filter2"),
+ * SERVICES_YADIS_MATCH_ALL);</pre>
+ *
+ * See {@link SERVICES_YADIS_MATCH_ALL} and {@link
+ * SERVICES_YADIS_MATCH_ANY}.
+ *
+ * Services described in an XRDS should have a library which you'll
+ * probably be using. Those libraries are responsible for defining
+ * filters that can be used with the "services()" call. If you need
+ * to write your own filter, see the documentation for {@link
+ * Auth_Yadis_Service}.
+ *
+ * @package OpenID
+ */
+class Auth_Yadis_Yadis {
+
+ /**
+ * Returns an HTTP fetcher object. If the CURL extension is
+ * present, an instance of {@link Auth_Yadis_ParanoidHTTPFetcher}
+ * is returned. If not, an instance of
+ * {@link Auth_Yadis_PlainHTTPFetcher} is returned.
+ *
+ * If Auth_Yadis_CURL_OVERRIDE is defined, this method will always
+ * return a {@link Auth_Yadis_PlainHTTPFetcher}.
+ */
+ function getHTTPFetcher($timeout = 20)
+ {
+ if (Auth_Yadis_Yadis::curlPresent() &&
+ (!defined('Auth_Yadis_CURL_OVERRIDE'))) {
+ $fetcher = new Auth_Yadis_ParanoidHTTPFetcher($timeout);
+ } else {
+ $fetcher = new Auth_Yadis_PlainHTTPFetcher($timeout);
+ }
+ return $fetcher;
+ }
+
+ function curlPresent()
+ {
+ return function_exists('curl_init');
+ }
+
+ /**
+ * @access private
+ */
+ function _getHeader($header_list, $names)
+ {
+ foreach ($header_list as $name => $value) {
+ foreach ($names as $n) {
+ if (strtolower($name) == strtolower($n)) {
+ return $value;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * @access private
+ */
+ function _getContentType($content_type_header)
+ {
+ if ($content_type_header) {
+ $parts = explode(";", $content_type_header);
+ return strtolower($parts[0]);
+ }
+ }
+
+ /**
+ * This should be called statically and will build a Yadis
+ * instance if the discovery process succeeds. This implements
+ * Yadis discovery as specified in the Yadis specification.
+ *
+ * @param string $uri The URI on which to perform Yadis discovery.
+ *
+ * @param array $http_response An array reference where the HTTP
+ * response object will be stored (see {@link
+ * Auth_Yadis_HTTPResponse}.
+ *
+ * @param Auth_Yadis_HTTPFetcher $fetcher An instance of a
+ * Auth_Yadis_HTTPFetcher subclass.
+ *
+ * @param array $extra_ns_map An array which maps namespace names
+ * to namespace URIs to be used when parsing the Yadis XRDS
+ * document.
+ *
+ * @param integer $timeout An optional fetcher timeout, in seconds.
+ *
+ * @return mixed $obj Either null or an instance of
+ * Auth_Yadis_Yadis, depending on whether the discovery
+ * succeeded.
+ */
+ function discover($uri, &$fetcher,
+ $extra_ns_map = null, $timeout = 20)
+ {
+ $result = new Auth_Yadis_DiscoveryResult($uri);
+
+ $request_uri = $uri;
+ $headers = array("Accept: " . Auth_Yadis_CONTENT_TYPE .
+ ', text/html; q=0.3, application/xhtml+xml; q=0.5');
+
+ if ($fetcher === null) {
+ $fetcher = Auth_Yadis_Yadis::getHTTPFetcher($timeout);
+ }
+
+ $response = $fetcher->get($uri, $headers);
+
+ if (!$response || ($response->status != 200 and
+ $response->status != 206)) {
+ $result->fail();
+ return $result;
+ }
+
+ $result->normalized_uri = $response->final_url;
+ $result->content_type = Auth_Yadis_Yadis::_getHeader(
+ $response->headers,
+ array('content-type'));
+
+ if ($result->content_type &&
+ (Auth_Yadis_Yadis::_getContentType($result->content_type) ==
+ Auth_Yadis_CONTENT_TYPE)) {
+ $result->xrds_uri = $result->normalized_uri;
+ } else {
+ $yadis_location = Auth_Yadis_Yadis::_getHeader(
+ $response->headers,
+ array(Auth_Yadis_HEADER_NAME));
+
+ if (!$yadis_location) {
+ $parser = new Auth_Yadis_ParseHTML();
+ $yadis_location = $parser->getHTTPEquiv($response->body);
+ }
+
+ if ($yadis_location) {
+ $result->xrds_uri = $yadis_location;
+
+ $response = $fetcher->get($yadis_location);
+
+ if ((!$response) || ($response->status != 200 and
+ $response->status != 206)) {
+ $result->fail();
+ return $result;
+ }
+
+ $result->content_type = Auth_Yadis_Yadis::_getHeader(
+ $response->headers,
+ array('content-type'));
+ }
+ }
+
+ $result->response_text = $response->body;
+ return $result;
+ }
+}
+
+?>
diff --git a/extlib/DB.php b/extlib/DB.php
new file mode 100644
index 000000000..a511979e6
--- /dev/null
+++ b/extlib/DB.php
@@ -0,0 +1,1489 @@
+<?php
+
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * Database independent query interface
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt. If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category Database
+ * @package DB
+ * @author Stig Bakken <ssb@php.net>
+ * @author Tomas V.V.Cox <cox@idecnet.com>
+ * @author Daniel Convissor <danielc@php.net>
+ * @copyright 1997-2007 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0
+ * @version CVS: $Id: DB.php,v 1.88 2007/08/12 05:27:25 aharvey Exp $
+ * @link http://pear.php.net/package/DB
+ */
+
+/**
+ * Obtain the PEAR class so it can be extended from
+ */
+require_once 'PEAR.php';
+
+
+// {{{ constants
+// {{{ error codes
+
+/**#@+
+ * One of PEAR DB's portable error codes.
+ * @see DB_common::errorCode(), DB::errorMessage()
+ *
+ * {@internal If you add an error code here, make sure you also add a textual
+ * version of it in DB::errorMessage().}}
+ */
+
+/**
+ * The code returned by many methods upon success
+ */
+define('DB_OK', 1);
+
+/**
+ * Unkown error
+ */
+define('DB_ERROR', -1);
+
+/**
+ * Syntax error
+ */
+define('DB_ERROR_SYNTAX', -2);
+
+/**
+ * Tried to insert a duplicate value into a primary or unique index
+ */
+define('DB_ERROR_CONSTRAINT', -3);
+
+/**
+ * An identifier in the query refers to a non-existant object
+ */
+define('DB_ERROR_NOT_FOUND', -4);
+
+/**
+ * Tried to create a duplicate object
+ */
+define('DB_ERROR_ALREADY_EXISTS', -5);
+
+/**
+ * The current driver does not support the action you attempted
+ */
+define('DB_ERROR_UNSUPPORTED', -6);
+
+/**
+ * The number of parameters does not match the number of placeholders
+ */
+define('DB_ERROR_MISMATCH', -7);
+
+/**
+ * A literal submitted did not match the data type expected
+ */
+define('DB_ERROR_INVALID', -8);
+
+/**
+ * The current DBMS does not support the action you attempted
+ */
+define('DB_ERROR_NOT_CAPABLE', -9);
+
+/**
+ * A literal submitted was too long so the end of it was removed
+ */
+define('DB_ERROR_TRUNCATED', -10);
+
+/**
+ * A literal number submitted did not match the data type expected
+ */
+define('DB_ERROR_INVALID_NUMBER', -11);
+
+/**
+ * A literal date submitted did not match the data type expected
+ */
+define('DB_ERROR_INVALID_DATE', -12);
+
+/**
+ * Attempt to divide something by zero
+ */
+define('DB_ERROR_DIVZERO', -13);
+
+/**
+ * A database needs to be selected
+ */
+define('DB_ERROR_NODBSELECTED', -14);
+
+/**
+ * Could not create the object requested
+ */
+define('DB_ERROR_CANNOT_CREATE', -15);
+
+/**
+ * Could not drop the database requested because it does not exist
+ */
+define('DB_ERROR_CANNOT_DROP', -17);
+
+/**
+ * An identifier in the query refers to a non-existant table
+ */
+define('DB_ERROR_NOSUCHTABLE', -18);
+
+/**
+ * An identifier in the query refers to a non-existant column
+ */
+define('DB_ERROR_NOSUCHFIELD', -19);
+
+/**
+ * The data submitted to the method was inappropriate
+ */
+define('DB_ERROR_NEED_MORE_DATA', -20);
+
+/**
+ * The attempt to lock the table failed
+ */
+define('DB_ERROR_NOT_LOCKED', -21);
+
+/**
+ * The number of columns doesn't match the number of values
+ */
+define('DB_ERROR_VALUE_COUNT_ON_ROW', -22);
+
+/**
+ * The DSN submitted has problems
+ */
+define('DB_ERROR_INVALID_DSN', -23);
+
+/**
+ * Could not connect to the database
+ */
+define('DB_ERROR_CONNECT_FAILED', -24);
+
+/**
+ * The PHP extension needed for this DBMS could not be found
+ */
+define('DB_ERROR_EXTENSION_NOT_FOUND',-25);
+
+/**
+ * The present user has inadequate permissions to perform the task requestd
+ */
+define('DB_ERROR_ACCESS_VIOLATION', -26);
+
+/**
+ * The database requested does not exist
+ */
+define('DB_ERROR_NOSUCHDB', -27);
+
+/**
+ * Tried to insert a null value into a column that doesn't allow nulls
+ */
+define('DB_ERROR_CONSTRAINT_NOT_NULL',-29);
+/**#@-*/
+
+
+// }}}
+// {{{ prepared statement-related
+
+
+/**#@+
+ * Identifiers for the placeholders used in prepared statements.
+ * @see DB_common::prepare()
+ */
+
+/**
+ * Indicates a scalar (<kbd>?</kbd>) placeholder was used
+ *
+ * Quote and escape the value as necessary.
+ */
+define('DB_PARAM_SCALAR', 1);
+
+/**
+ * Indicates an opaque (<kbd>&</kbd>) placeholder was used
+ *
+ * The value presented is a file name. Extract the contents of that file
+ * and place them in this column.
+ */
+define('DB_PARAM_OPAQUE', 2);
+
+/**
+ * Indicates a misc (<kbd>!</kbd>) placeholder was used
+ *
+ * The value should not be quoted or escaped.
+ */
+define('DB_PARAM_MISC', 3);
+/**#@-*/
+
+
+// }}}
+// {{{ binary data-related
+
+
+/**#@+
+ * The different ways of returning binary data from queries.
+ */
+
+/**
+ * Sends the fetched data straight through to output
+ */
+define('DB_BINMODE_PASSTHRU', 1);
+
+/**
+ * Lets you return data as usual
+ */
+define('DB_BINMODE_RETURN', 2);
+
+/**
+ * Converts the data to hex format before returning it
+ *
+ * For example the string "123" would become "313233".
+ */
+define('DB_BINMODE_CONVERT', 3);
+/**#@-*/
+
+
+// }}}
+// {{{ fetch modes
+
+
+/**#@+
+ * Fetch Modes.
+ * @see DB_common::setFetchMode()
+ */
+
+/**
+ * Indicates the current default fetch mode should be used
+ * @see DB_common::$fetchmode
+ */
+define('DB_FETCHMODE_DEFAULT', 0);
+
+/**
+ * Column data indexed by numbers, ordered from 0 and up
+ */
+define('DB_FETCHMODE_ORDERED', 1);
+
+/**
+ * Column data indexed by column names
+ */
+define('DB_FETCHMODE_ASSOC', 2);
+
+/**
+ * Column data as object properties
+ */
+define('DB_FETCHMODE_OBJECT', 3);
+
+/**
+ * For multi-dimensional results, make the column name the first level
+ * of the array and put the row number in the second level of the array
+ *
+ * This is flipped from the normal behavior, which puts the row numbers
+ * in the first level of the array and the column names in the second level.
+ */
+define('DB_FETCHMODE_FLIPPED', 4);
+/**#@-*/
+
+/**#@+
+ * Old fetch modes. Left here for compatibility.
+ */
+define('DB_GETMODE_ORDERED', DB_FETCHMODE_ORDERED);
+define('DB_GETMODE_ASSOC', DB_FETCHMODE_ASSOC);
+define('DB_GETMODE_FLIPPED', DB_FETCHMODE_FLIPPED);
+/**#@-*/
+
+
+// }}}
+// {{{ tableInfo() && autoPrepare()-related
+
+
+/**#@+
+ * The type of information to return from the tableInfo() method.
+ *
+ * Bitwised constants, so they can be combined using <kbd>|</kbd>
+ * and removed using <kbd>^</kbd>.
+ *
+ * @see DB_common::tableInfo()
+ *
+ * {@internal Since the TABLEINFO constants are bitwised, if more of them are
+ * added in the future, make sure to adjust DB_TABLEINFO_FULL accordingly.}}
+ */
+define('DB_TABLEINFO_ORDER', 1);
+define('DB_TABLEINFO_ORDERTABLE', 2);
+define('DB_TABLEINFO_FULL', 3);
+/**#@-*/
+
+
+/**#@+
+ * The type of query to create with the automatic query building methods.
+ * @see DB_common::autoPrepare(), DB_common::autoExecute()
+ */
+define('DB_AUTOQUERY_INSERT', 1);
+define('DB_AUTOQUERY_UPDATE', 2);
+/**#@-*/
+
+
+// }}}
+// {{{ portability modes
+
+
+/**#@+
+ * Portability Modes.
+ *
+ * Bitwised constants, so they can be combined using <kbd>|</kbd>
+ * and removed using <kbd>^</kbd>.
+ *
+ * @see DB_common::setOption()
+ *
+ * {@internal Since the PORTABILITY constants are bitwised, if more of them are
+ * added in the future, make sure to adjust DB_PORTABILITY_ALL accordingly.}}
+ */
+
+/**
+ * Turn off all portability features
+ */
+define('DB_PORTABILITY_NONE', 0);
+
+/**
+ * Convert names of tables and fields to lower case
+ * when using the get*(), fetch*() and tableInfo() methods
+ */
+define('DB_PORTABILITY_LOWERCASE', 1);
+
+/**
+ * Right trim the data output by get*() and fetch*()
+ */
+define('DB_PORTABILITY_RTRIM', 2);
+
+/**
+ * Force reporting the number of rows deleted
+ */
+define('DB_PORTABILITY_DELETE_COUNT', 4);
+
+/**
+ * Enable hack that makes numRows() work in Oracle
+ */
+define('DB_PORTABILITY_NUMROWS', 8);
+
+/**
+ * Makes certain error messages in certain drivers compatible
+ * with those from other DBMS's
+ *
+ * + mysql, mysqli: change unique/primary key constraints
+ * DB_ERROR_ALREADY_EXISTS -> DB_ERROR_CONSTRAINT
+ *
+ * + odbc(access): MS's ODBC driver reports 'no such field' as code
+ * 07001, which means 'too few parameters.' When this option is on
+ * that code gets mapped to DB_ERROR_NOSUCHFIELD.
+ */
+define('DB_PORTABILITY_ERRORS', 16);
+
+/**
+ * Convert null values to empty strings in data output by
+ * get*() and fetch*()
+ */
+define('DB_PORTABILITY_NULL_TO_EMPTY', 32);
+
+/**
+ * Turn on all portability features
+ */
+define('DB_PORTABILITY_ALL', 63);
+/**#@-*/
+
+// }}}
+
+
+// }}}
+// {{{ class DB
+
+/**
+ * Database independent query interface
+ *
+ * The main "DB" class is simply a container class with some static
+ * methods for creating DB objects as well as some utility functions
+ * common to all parts of DB.
+ *
+ * The object model of DB is as follows (indentation means inheritance):
+ * <pre>
+ * DB The main DB class. This is simply a utility class
+ * with some "static" methods for creating DB objects as
+ * well as common utility functions for other DB classes.
+ *
+ * DB_common The base for each DB implementation. Provides default
+ * | implementations (in OO lingo virtual methods) for
+ * | the actual DB implementations as well as a bunch of
+ * | query utility functions.
+ * |
+ * +-DB_mysql The DB implementation for MySQL. Inherits DB_common.
+ * When calling DB::factory or DB::connect for MySQL
+ * connections, the object returned is an instance of this
+ * class.
+ * </pre>
+ *
+ * @category Database
+ * @package DB
+ * @author Stig Bakken <ssb@php.net>
+ * @author Tomas V.V.Cox <cox@idecnet.com>
+ * @author Daniel Convissor <danielc@php.net>
+ * @copyright 1997-2007 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0
+ * @version Release: 1.7.13
+ * @link http://pear.php.net/package/DB
+ */
+class DB
+{
+ // {{{ &factory()
+
+ /**
+ * Create a new DB object for the specified database type but don't
+ * connect to the database
+ *
+ * @param string $type the database type (eg "mysql")
+ * @param array $options an associative array of option names and values
+ *
+ * @return object a new DB object. A DB_Error object on failure.
+ *
+ * @see DB_common::setOption()
+ */
+ function &factory($type, $options = false)
+ {
+ if (!is_array($options)) {
+ $options = array('persistent' => $options);
+ }
+
+ if (isset($options['debug']) && $options['debug'] >= 2) {
+ // expose php errors with sufficient debug level
+ include_once "DB/{$type}.php";
+ } else {
+ @include_once "DB/{$type}.php";
+ }
+
+ $classname = "DB_${type}";
+
+ if (!class_exists($classname)) {
+ $tmp = PEAR::raiseError(null, DB_ERROR_NOT_FOUND, null, null,
+ "Unable to include the DB/{$type}.php"
+ . " file for '$dsn'",
+ 'DB_Error', true);
+ return $tmp;
+ }
+
+ @$obj = new $classname;
+
+ foreach ($options as $option => $value) {
+ $test = $obj->setOption($option, $value);
+ if (DB::isError($test)) {
+ return $test;
+ }
+ }
+
+ return $obj;
+ }
+
+ // }}}
+ // {{{ &connect()
+
+ /**
+ * Create a new DB object including a connection to the specified database
+ *
+ * Example 1.
+ * <code>
+ * require_once 'DB.php';
+ *
+ * $dsn = 'pgsql://user:password@host/database';
+ * $options = array(
+ * 'debug' => 2,
+ * 'portability' => DB_PORTABILITY_ALL,
+ * );
+ *
+ * $db =& DB::connect($dsn, $options);
+ * if (PEAR::isError($db)) {
+ * die($db->getMessage());
+ * }
+ * </code>
+ *
+ * @param mixed $dsn the string "data source name" or array in the
+ * format returned by DB::parseDSN()
+ * @param array $options an associative array of option names and values
+ *
+ * @return object a new DB object. A DB_Error object on failure.
+ *
+ * @uses DB_dbase::connect(), DB_fbsql::connect(), DB_ibase::connect(),
+ * DB_ifx::connect(), DB_msql::connect(), DB_mssql::connect(),
+ * DB_mysql::connect(), DB_mysqli::connect(), DB_oci8::connect(),
+ * DB_odbc::connect(), DB_pgsql::connect(), DB_sqlite::connect(),
+ * DB_sybase::connect()
+ *
+ * @uses DB::parseDSN(), DB_common::setOption(), PEAR::isError()
+ */
+ function &connect($dsn, $options = array())
+ {
+ $dsninfo = DB::parseDSN($dsn);
+ $type = $dsninfo['phptype'];
+
+ if (!is_array($options)) {
+ /*
+ * For backwards compatibility. $options used to be boolean,
+ * indicating whether the connection should be persistent.
+ */
+ $options = array('persistent' => $options);
+ }
+
+ if (isset($options['debug']) && $options['debug'] >= 2) {
+ // expose php errors with sufficient debug level
+ include_once "DB/${type}.php";
+ } else {
+ @include_once "DB/${type}.php";
+ }
+
+ $classname = "DB_${type}";
+ if (!class_exists($classname)) {
+ $tmp = PEAR::raiseError(null, DB_ERROR_NOT_FOUND, null, null,
+ "Unable to include the DB/{$type}.php"
+ . " file for '$dsn'",
+ 'DB_Error', true);
+ return $tmp;
+ }
+
+ @$obj = new $classname;
+
+ foreach ($options as $option => $value) {
+ $test = $obj->setOption($option, $value);
+ if (DB::isError($test)) {
+ return $test;
+ }
+ }
+
+ $err = $obj->connect($dsninfo, $obj->getOption('persistent'));
+ if (DB::isError($err)) {
+ if (is_array($dsn)) {
+ $err->addUserInfo(DB::getDSNString($dsn, true));
+ } else {
+ $err->addUserInfo($dsn);
+ }
+ return $err;
+ }
+
+ return $obj;
+ }
+
+ // }}}
+ // {{{ apiVersion()
+
+ /**
+ * Return the DB API version
+ *
+ * @return string the DB API version number
+ */
+ function apiVersion()
+ {
+ return '1.7.13';
+ }
+
+ // }}}
+ // {{{ isError()
+
+ /**
+ * Determines if a variable is a DB_Error object
+ *
+ * @param mixed $value the variable to check
+ *
+ * @return bool whether $value is DB_Error object
+ */
+ function isError($value)
+ {
+ return is_a($value, 'DB_Error');
+ }
+
+ // }}}
+ // {{{ isConnection()
+
+ /**
+ * Determines if a value is a DB_<driver> object
+ *
+ * @param mixed $value the value to test
+ *
+ * @return bool whether $value is a DB_<driver> object
+ */
+ function isConnection($value)
+ {
+ return (is_object($value) &&
+ is_subclass_of($value, 'db_common') &&
+ method_exists($value, 'simpleQuery'));
+ }
+
+ // }}}
+ // {{{ isManip()
+
+ /**
+ * Tell whether a query is a data manipulation or data definition query
+ *
+ * Examples of data manipulation queries are INSERT, UPDATE and DELETE.
+ * Examples of data definition queries are CREATE, DROP, ALTER, GRANT,
+ * REVOKE.
+ *
+ * @param string $query the query
+ *
+ * @return boolean whether $query is a data manipulation query
+ */
+ function isManip($query)
+ {
+ $manips = 'INSERT|UPDATE|DELETE|REPLACE|'
+ . 'CREATE|DROP|'
+ . 'LOAD DATA|SELECT .* INTO .* FROM|COPY|'
+ . 'ALTER|GRANT|REVOKE|'
+ . 'LOCK|UNLOCK';
+ if (preg_match('/^\s*"?(' . $manips . ')\s+/i', $query)) {
+ return true;
+ }
+ return false;
+ }
+
+ // }}}
+ // {{{ errorMessage()
+
+ /**
+ * Return a textual error message for a DB error code
+ *
+ * @param integer $value the DB error code
+ *
+ * @return string the error message or false if the error code was
+ * not recognized
+ */
+ function errorMessage($value)
+ {
+ static $errorMessages;
+ if (!isset($errorMessages)) {
+ $errorMessages = array(
+ DB_ERROR => 'unknown error',
+ DB_ERROR_ACCESS_VIOLATION => 'insufficient permissions',
+ DB_ERROR_ALREADY_EXISTS => 'already exists',
+ DB_ERROR_CANNOT_CREATE => 'can not create',
+ DB_ERROR_CANNOT_DROP => 'can not drop',
+ DB_ERROR_CONNECT_FAILED => 'connect failed',
+ DB_ERROR_CONSTRAINT => 'constraint violation',
+ DB_ERROR_CONSTRAINT_NOT_NULL=> 'null value violates not-null constraint',
+ DB_ERROR_DIVZERO => 'division by zero',
+ DB_ERROR_EXTENSION_NOT_FOUND=> 'extension not found',
+ DB_ERROR_INVALID => 'invalid',
+ DB_ERROR_INVALID_DATE => 'invalid date or time',
+ DB_ERROR_INVALID_DSN => 'invalid DSN',
+ DB_ERROR_INVALID_NUMBER => 'invalid number',
+ DB_ERROR_MISMATCH => 'mismatch',
+ DB_ERROR_NEED_MORE_DATA => 'insufficient data supplied',
+ DB_ERROR_NODBSELECTED => 'no database selected',
+ DB_ERROR_NOSUCHDB => 'no such database',
+ DB_ERROR_NOSUCHFIELD => 'no such field',
+ DB_ERROR_NOSUCHTABLE => 'no such table',
+ DB_ERROR_NOT_CAPABLE => 'DB backend not capable',
+ DB_ERROR_NOT_FOUND => 'not found',
+ DB_ERROR_NOT_LOCKED => 'not locked',
+ DB_ERROR_SYNTAX => 'syntax error',
+ DB_ERROR_UNSUPPORTED => 'not supported',
+ DB_ERROR_TRUNCATED => 'truncated',
+ DB_ERROR_VALUE_COUNT_ON_ROW => 'value count on row',
+ DB_OK => 'no error',
+ );
+ }
+
+ if (DB::isError($value)) {
+ $value = $value->getCode();
+ }
+
+ return isset($errorMessages[$value]) ? $errorMessages[$value]
+ : $errorMessages[DB_ERROR];
+ }
+
+ // }}}
+ // {{{ parseDSN()
+
+ /**
+ * Parse a data source name
+ *
+ * Additional keys can be added by appending a URI query string to the
+ * end of the DSN.
+ *
+ * The format of the supplied DSN is in its fullest form:
+ * <code>
+ * phptype(dbsyntax)://username:password@protocol+hostspec/database?option=8&another=true
+ * </code>
+ *
+ * Most variations are allowed:
+ * <code>
+ * phptype://username:password@protocol+hostspec:110//usr/db_file.db?mode=0644
+ * phptype://username:password@hostspec/database_name
+ * phptype://username:password@hostspec
+ * phptype://username@hostspec
+ * phptype://hostspec/database
+ * phptype://hostspec
+ * phptype(dbsyntax)
+ * phptype
+ * </code>
+ *
+ * @param string $dsn Data Source Name to be parsed
+ *
+ * @return array an associative array with the following keys:
+ * + phptype: Database backend used in PHP (mysql, odbc etc.)
+ * + dbsyntax: Database used with regards to SQL syntax etc.
+ * + protocol: Communication protocol to use (tcp, unix etc.)
+ * + hostspec: Host specification (hostname[:port])
+ * + database: Database to use on the DBMS server
+ * + username: User name for login
+ * + password: Password for login
+ */
+ function parseDSN($dsn)
+ {
+ $parsed = array(
+ 'phptype' => false,
+ 'dbsyntax' => false,
+ 'username' => false,
+ 'password' => false,
+ 'protocol' => false,
+ 'hostspec' => false,
+ 'port' => false,
+ 'socket' => false,
+ 'database' => false,
+ );
+
+ if (is_array($dsn)) {
+ $dsn = array_merge($parsed, $dsn);
+ if (!$dsn['dbsyntax']) {
+ $dsn['dbsyntax'] = $dsn['phptype'];
+ }
+ return $dsn;
+ }
+
+ // Find phptype and dbsyntax
+ if (($pos = strpos($dsn, '://')) !== false) {
+ $str = substr($dsn, 0, $pos);
+ $dsn = substr($dsn, $pos + 3);
+ } else {
+ $str = $dsn;
+ $dsn = null;
+ }
+
+ // Get phptype and dbsyntax
+ // $str => phptype(dbsyntax)
+ if (preg_match('|^(.+?)\((.*?)\)$|', $str, $arr)) {
+ $parsed['phptype'] = $arr[1];
+ $parsed['dbsyntax'] = !$arr[2] ? $arr[1] : $arr[2];
+ } else {
+ $parsed['phptype'] = $str;
+ $parsed['dbsyntax'] = $str;
+ }
+
+ if (!count($dsn)) {
+ return $parsed;
+ }
+
+ // Get (if found): username and password
+ // $dsn => username:password@protocol+hostspec/database
+ if (($at = strrpos($dsn,'@')) !== false) {
+ $str = substr($dsn, 0, $at);
+ $dsn = substr($dsn, $at + 1);
+ if (($pos = strpos($str, ':')) !== false) {
+ $parsed['username'] = rawurldecode(substr($str, 0, $pos));
+ $parsed['password'] = rawurldecode(substr($str, $pos + 1));
+ } else {
+ $parsed['username'] = rawurldecode($str);
+ }
+ }
+
+ // Find protocol and hostspec
+
+ if (preg_match('|^([^(]+)\((.*?)\)/?(.*?)$|', $dsn, $match)) {
+ // $dsn => proto(proto_opts)/database
+ $proto = $match[1];
+ $proto_opts = $match[2] ? $match[2] : false;
+ $dsn = $match[3];
+
+ } else {
+ // $dsn => protocol+hostspec/database (old format)
+ if (strpos($dsn, '+') !== false) {
+ list($proto, $dsn) = explode('+', $dsn, 2);
+ }
+ if (strpos($dsn, '/') !== false) {
+ list($proto_opts, $dsn) = explode('/', $dsn, 2);
+ } else {
+ $proto_opts = $dsn;
+ $dsn = null;
+ }
+ }
+
+ // process the different protocol options
+ $parsed['protocol'] = (!empty($proto)) ? $proto : 'tcp';
+ $proto_opts = rawurldecode($proto_opts);
+ if (strpos($proto_opts, ':') !== false) {
+ list($proto_opts, $parsed['port']) = explode(':', $proto_opts);
+ }
+ if ($parsed['protocol'] == 'tcp') {
+ $parsed['hostspec'] = $proto_opts;
+ } elseif ($parsed['protocol'] == 'unix') {
+ $parsed['socket'] = $proto_opts;
+ }
+
+ // Get dabase if any
+ // $dsn => database
+ if ($dsn) {
+ if (($pos = strpos($dsn, '?')) === false) {
+ // /database
+ $parsed['database'] = rawurldecode($dsn);
+ } else {
+ // /database?param1=value1&param2=value2
+ $parsed['database'] = rawurldecode(substr($dsn, 0, $pos));
+ $dsn = substr($dsn, $pos + 1);
+ if (strpos($dsn, '&') !== false) {
+ $opts = explode('&', $dsn);
+ } else { // database?param1=value1
+ $opts = array($dsn);
+ }
+ foreach ($opts as $opt) {
+ list($key, $value) = explode('=', $opt);
+ if (!isset($parsed[$key])) {
+ // don't allow params overwrite
+ $parsed[$key] = rawurldecode($value);
+ }
+ }
+ }
+ }
+
+ return $parsed;
+ }
+
+ // }}}
+ // {{{ getDSNString()
+
+ /**
+ * Returns the given DSN in a string format suitable for output.
+ *
+ * @param array|string the DSN to parse and format
+ * @param boolean true to hide the password, false to include it
+ * @return string
+ */
+ function getDSNString($dsn, $hidePassword) {
+ /* Calling parseDSN will ensure that we have all the array elements
+ * defined, and means that we deal with strings and array in the same
+ * manner. */
+ $dsnArray = DB::parseDSN($dsn);
+
+ if ($hidePassword) {
+ $dsnArray['password'] = 'PASSWORD';
+ }
+
+ /* Protocol is special-cased, as using the default "tcp" along with an
+ * Oracle TNS connection string fails. */
+ if (is_string($dsn) && strpos($dsn, 'tcp') === false && $dsnArray['protocol'] == 'tcp') {
+ $dsnArray['protocol'] = false;
+ }
+
+ // Now we just have to construct the actual string. This is ugly.
+ $dsnString = $dsnArray['phptype'];
+ if ($dsnArray['dbsyntax']) {
+ $dsnString .= '('.$dsnArray['dbsyntax'].')';
+ }
+ $dsnString .= '://'
+ .$dsnArray['username']
+ .':'
+ .$dsnArray['password']
+ .'@'
+ .$dsnArray['protocol'];
+ if ($dsnArray['socket']) {
+ $dsnString .= '('.$dsnArray['socket'].')';
+ }
+ if ($dsnArray['protocol'] && $dsnArray['hostspec']) {
+ $dsnString .= '+';
+ }
+ $dsnString .= $dsnArray['hostspec'];
+ if ($dsnArray['port']) {
+ $dsnString .= ':'.$dsnArray['port'];
+ }
+ $dsnString .= '/'.$dsnArray['database'];
+
+ /* Option handling. Unfortunately, parseDSN simply places options into
+ * the top-level array, so we'll first get rid of the fields defined by
+ * DB and see what's left. */
+ unset($dsnArray['phptype'],
+ $dsnArray['dbsyntax'],
+ $dsnArray['username'],
+ $dsnArray['password'],
+ $dsnArray['protocol'],
+ $dsnArray['socket'],
+ $dsnArray['hostspec'],
+ $dsnArray['port'],
+ $dsnArray['database']
+ );
+ if (count($dsnArray) > 0) {
+ $dsnString .= '?';
+ $i = 0;
+ foreach ($dsnArray as $key => $value) {
+ if (++$i > 1) {
+ $dsnString .= '&';
+ }
+ $dsnString .= $key.'='.$value;
+ }
+ }
+
+ return $dsnString;
+ }
+
+ // }}}
+}
+
+// }}}
+// {{{ class DB_Error
+
+/**
+ * DB_Error implements a class for reporting portable database error
+ * messages
+ *
+ * @category Database
+ * @package DB
+ * @author Stig Bakken <ssb@php.net>
+ * @copyright 1997-2007 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0
+ * @version Release: 1.7.13
+ * @link http://pear.php.net/package/DB
+ */
+class DB_Error extends PEAR_Error
+{
+ // {{{ constructor
+
+ /**
+ * DB_Error constructor
+ *
+ * @param mixed $code DB error code, or string with error message
+ * @param int $mode what "error mode" to operate in
+ * @param int $level what error level to use for $mode &
+ * PEAR_ERROR_TRIGGER
+ * @param mixed $debuginfo additional debug info, such as the last query
+ *
+ * @see PEAR_Error
+ */
+ function DB_Error($code = DB_ERROR, $mode = PEAR_ERROR_RETURN,
+ $level = E_USER_NOTICE, $debuginfo = null)
+ {
+ if (is_int($code)) {
+ $this->PEAR_Error('DB Error: ' . DB::errorMessage($code), $code,
+ $mode, $level, $debuginfo);
+ } else {
+ $this->PEAR_Error("DB Error: $code", DB_ERROR,
+ $mode, $level, $debuginfo);
+ }
+ }
+
+ // }}}
+}
+
+// }}}
+// {{{ class DB_result
+
+/**
+ * This class implements a wrapper for a DB result set
+ *
+ * A new instance of this class will be returned by the DB implementation
+ * after processing a query that returns data.
+ *
+ * @category Database
+ * @package DB
+ * @author Stig Bakken <ssb@php.net>
+ * @copyright 1997-2007 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0
+ * @version Release: 1.7.13
+ * @link http://pear.php.net/package/DB
+ */
+class DB_result
+{
+ // {{{ properties
+
+ /**
+ * Should results be freed automatically when there are no more rows?
+ * @var boolean
+ * @see DB_common::$options
+ */
+ var $autofree;
+
+ /**
+ * A reference to the DB_<driver> object
+ * @var object
+ */
+ var $dbh;
+
+ /**
+ * The current default fetch mode
+ * @var integer
+ * @see DB_common::$fetchmode
+ */
+ var $fetchmode;
+
+ /**
+ * The name of the class into which results should be fetched when
+ * DB_FETCHMODE_OBJECT is in effect
+ *
+ * @var string
+ * @see DB_common::$fetchmode_object_class
+ */
+ var $fetchmode_object_class;
+
+ /**
+ * The number of rows to fetch from a limit query
+ * @var integer
+ */
+ var $limit_count = null;
+
+ /**
+ * The row to start fetching from in limit queries
+ * @var integer
+ */
+ var $limit_from = null;
+
+ /**
+ * The execute parameters that created this result
+ * @var array
+ * @since Property available since Release 1.7.0
+ */
+ var $parameters;
+
+ /**
+ * The query string that created this result
+ *
+ * Copied here incase it changes in $dbh, which is referenced
+ *
+ * @var string
+ * @since Property available since Release 1.7.0
+ */
+ var $query;
+
+ /**
+ * The query result resource id created by PHP
+ * @var resource
+ */
+ var $result;
+
+ /**
+ * The present row being dealt with
+ * @var integer
+ */
+ var $row_counter = null;
+
+ /**
+ * The prepared statement resource id created by PHP in $dbh
+ *
+ * This resource is only available when the result set was created using
+ * a driver's native execute() method, not PEAR DB's emulated one.
+ *
+ * Copied here incase it changes in $dbh, which is referenced
+ *
+ * {@internal Mainly here because the InterBase/Firebird API is only
+ * able to retrieve data from result sets if the statemnt handle is
+ * still in scope.}}
+ *
+ * @var resource
+ * @since Property available since Release 1.7.0
+ */
+ var $statement;
+
+
+ // }}}
+ // {{{ constructor
+
+ /**
+ * This constructor sets the object's properties
+ *
+ * @param object &$dbh the DB object reference
+ * @param resource $result the result resource id
+ * @param array $options an associative array with result options
+ *
+ * @return void
+ */
+ function DB_result(&$dbh, $result, $options = array())
+ {
+ $this->autofree = $dbh->options['autofree'];
+ $this->dbh = &$dbh;
+ $this->fetchmode = $dbh->fetchmode;
+ $this->fetchmode_object_class = $dbh->fetchmode_object_class;
+ $this->parameters = $dbh->last_parameters;
+ $this->query = $dbh->last_query;
+ $this->result = $result;
+ $this->statement = empty($dbh->last_stmt) ? null : $dbh->last_stmt;
+ foreach ($options as $key => $value) {
+ $this->setOption($key, $value);
+ }
+ }
+
+ /**
+ * Set options for the DB_result object
+ *
+ * @param string $key the option to set
+ * @param mixed $value the value to set the option to
+ *
+ * @return void
+ */
+ function setOption($key, $value = null)
+ {
+ switch ($key) {
+ case 'limit_from':
+ $this->limit_from = $value;
+ break;
+ case 'limit_count':
+ $this->limit_count = $value;
+ }
+ }
+
+ // }}}
+ // {{{ fetchRow()
+
+ /**
+ * Fetch a row of data and return it by reference into an array
+ *
+ * The type of array returned can be controlled either by setting this
+ * method's <var>$fetchmode</var> parameter or by changing the default
+ * fetch mode setFetchMode() before calling this method.
+ *
+ * There are two options for standardizing the information returned
+ * from databases, ensuring their values are consistent when changing
+ * DBMS's. These portability options can be turned on when creating a
+ * new DB object or by using setOption().
+ *
+ * + <var>DB_PORTABILITY_LOWERCASE</var>
+ * convert names of fields to lower case
+ *
+ * + <var>DB_PORTABILITY_RTRIM</var>
+ * right trim the data
+ *
+ * @param int $fetchmode the constant indicating how to format the data
+ * @param int $rownum the row number to fetch (index starts at 0)
+ *
+ * @return mixed an array or object containing the row's data,
+ * NULL when the end of the result set is reached
+ * or a DB_Error object on failure.
+ *
+ * @see DB_common::setOption(), DB_common::setFetchMode()
+ */
+ function &fetchRow($fetchmode = DB_FETCHMODE_DEFAULT, $rownum = null)
+ {
+ if ($fetchmode === DB_FETCHMODE_DEFAULT) {
+ $fetchmode = $this->fetchmode;
+ }
+ if ($fetchmode === DB_FETCHMODE_OBJECT) {
+ $fetchmode = DB_FETCHMODE_ASSOC;
+ $object_class = $this->fetchmode_object_class;
+ }
+ if (is_null($rownum) && $this->limit_from !== null) {
+ if ($this->row_counter === null) {
+ $this->row_counter = $this->limit_from;
+ // Skip rows
+ if ($this->dbh->features['limit'] === false) {
+ $i = 0;
+ while ($i++ < $this->limit_from) {
+ $this->dbh->fetchInto($this->result, $arr, $fetchmode);
+ }
+ }
+ }
+ if ($this->row_counter >= ($this->limit_from + $this->limit_count))
+ {
+ if ($this->autofree) {
+ $this->free();
+ }
+ $tmp = null;
+ return $tmp;
+ }
+ if ($this->dbh->features['limit'] === 'emulate') {
+ $rownum = $this->row_counter;
+ }
+ $this->row_counter++;
+ }
+ $res = $this->dbh->fetchInto($this->result, $arr, $fetchmode, $rownum);
+ if ($res === DB_OK) {
+ if (isset($object_class)) {
+ // The default mode is specified in the
+ // DB_common::fetchmode_object_class property
+ if ($object_class == 'stdClass') {
+ $arr = (object) $arr;
+ } else {
+ $arr = new $object_class($arr);
+ }
+ }
+ return $arr;
+ }
+ if ($res == null && $this->autofree) {
+ $this->free();
+ }
+ return $res;
+ }
+
+ // }}}
+ // {{{ fetchInto()
+
+ /**
+ * Fetch a row of data into an array which is passed by reference
+ *
+ * The type of array returned can be controlled either by setting this
+ * method's <var>$fetchmode</var> parameter or by changing the default
+ * fetch mode setFetchMode() before calling this method.
+ *
+ * There are two options for standardizing the information returned
+ * from databases, ensuring their values are consistent when changing
+ * DBMS's. These portability options can be turned on when creating a
+ * new DB object or by using setOption().
+ *
+ * + <var>DB_PORTABILITY_LOWERCASE</var>
+ * convert names of fields to lower case
+ *
+ * + <var>DB_PORTABILITY_RTRIM</var>
+ * right trim the data
+ *
+ * @param array &$arr the variable where the data should be placed
+ * @param int $fetchmode the constant indicating how to format the data
+ * @param int $rownum the row number to fetch (index starts at 0)
+ *
+ * @return mixed DB_OK if a row is processed, NULL when the end of the
+ * result set is reached or a DB_Error object on failure
+ *
+ * @see DB_common::setOption(), DB_common::setFetchMode()
+ */
+ function fetchInto(&$arr, $fetchmode = DB_FETCHMODE_DEFAULT, $rownum = null)
+ {
+ if ($fetchmode === DB_FETCHMODE_DEFAULT) {
+ $fetchmode = $this->fetchmode;
+ }
+ if ($fetchmode === DB_FETCHMODE_OBJECT) {
+ $fetchmode = DB_FETCHMODE_ASSOC;
+ $object_class = $this->fetchmode_object_class;
+ }
+ if (is_null($rownum) && $this->limit_from !== null) {
+ if ($this->row_counter === null) {
+ $this->row_counter = $this->limit_from;
+ // Skip rows
+ if ($this->dbh->features['limit'] === false) {
+ $i = 0;
+ while ($i++ < $this->limit_from) {
+ $this->dbh->fetchInto($this->result, $arr, $fetchmode);
+ }
+ }
+ }
+ if ($this->row_counter >= (
+ $this->limit_from + $this->limit_count))
+ {
+ if ($this->autofree) {
+ $this->free();
+ }
+ return null;
+ }
+ if ($this->dbh->features['limit'] === 'emulate') {
+ $rownum = $this->row_counter;
+ }
+
+ $this->row_counter++;
+ }
+ $res = $this->dbh->fetchInto($this->result, $arr, $fetchmode, $rownum);
+ if ($res === DB_OK) {
+ if (isset($object_class)) {
+ // default mode specified in the
+ // DB_common::fetchmode_object_class property
+ if ($object_class == 'stdClass') {
+ $arr = (object) $arr;
+ } else {
+ $arr = new $object_class($arr);
+ }
+ }
+ return DB_OK;
+ }
+ if ($res == null && $this->autofree) {
+ $this->free();
+ }
+ return $res;
+ }
+
+ // }}}
+ // {{{ numCols()
+
+ /**
+ * Get the the number of columns in a result set
+ *
+ * @return int the number of columns. A DB_Error object on failure.
+ */
+ function numCols()
+ {
+ return $this->dbh->numCols($this->result);
+ }
+
+ // }}}
+ // {{{ numRows()
+
+ /**
+ * Get the number of rows in a result set
+ *
+ * @return int the number of rows. A DB_Error object on failure.
+ */
+ function numRows()
+ {
+ if ($this->dbh->features['numrows'] === 'emulate'
+ && $this->dbh->options['portability'] & DB_PORTABILITY_NUMROWS)
+ {
+ if ($this->dbh->features['prepare']) {
+ $res = $this->dbh->query($this->query, $this->parameters);
+ } else {
+ $res = $this->dbh->query($this->query);
+ }
+ if (DB::isError($res)) {
+ return $res;
+ }
+ $i = 0;
+ while ($res->fetchInto($tmp, DB_FETCHMODE_ORDERED)) {
+ $i++;
+ }
+ $count = $i;
+ } else {
+ $count = $this->dbh->numRows($this->result);
+ }
+
+ /* fbsql is checked for here because limit queries are implemented
+ * using a TOP() function, which results in fbsql_num_rows still
+ * returning the total number of rows that would have been returned,
+ * rather than the real number. As a result, we'll just do the limit
+ * calculations for fbsql in the same way as a database with emulated
+ * limits. Unfortunately, we can't just do this in DB_fbsql::numRows()
+ * because that only gets the result resource, rather than the full
+ * DB_Result object. */
+ if (($this->dbh->features['limit'] === 'emulate'
+ && $this->limit_from !== null)
+ || $this->dbh->phptype == 'fbsql') {
+ $limit_count = is_null($this->limit_count) ? $count : $this->limit_count;
+ if ($count < $this->limit_from) {
+ $count = 0;
+ } elseif ($count < ($this->limit_from + $limit_count)) {
+ $count -= $this->limit_from;
+ } else {
+ $count = $limit_count;
+ }
+ }
+
+ return $count;
+ }
+
+ // }}}
+ // {{{ nextResult()
+
+ /**
+ * Get the next result if a batch of queries was executed
+ *
+ * @return bool true if a new result is available or false if not
+ */
+ function nextResult()
+ {
+ return $this->dbh->nextResult($this->result);
+ }
+
+ // }}}
+ // {{{ free()
+
+ /**
+ * Frees the resources allocated for this result set
+ *
+ * @return bool true on success. A DB_Error object on failure.
+ */
+ function free()
+ {
+ $err = $this->dbh->freeResult($this->result);
+ if (DB::isError($err)) {
+ return $err;
+ }
+ $this->result = false;
+ $this->statement = false;
+ return true;
+ }
+
+ // }}}
+ // {{{ tableInfo()
+
+ /**
+ * @see DB_common::tableInfo()
+ * @deprecated Method deprecated some time before Release 1.2
+ */
+ function tableInfo($mode = null)
+ {
+ if (is_string($mode)) {
+ return $this->dbh->raiseError(DB_ERROR_NEED_MORE_DATA);
+ }
+ return $this->dbh->tableInfo($this, $mode);
+ }
+
+ // }}}
+ // {{{ getQuery()
+
+ /**
+ * Determine the query string that created this result
+ *
+ * @return string the query string
+ *
+ * @since Method available since Release 1.7.0
+ */
+ function getQuery()
+ {
+ return $this->query;
+ }
+
+ // }}}
+ // {{{ getRowCounter()
+
+ /**
+ * Tells which row number is currently being processed
+ *
+ * @return integer the current row being looked at. Starts at 1.
+ */
+ function getRowCounter()
+ {
+ return $this->row_counter;
+ }
+
+ // }}}
+}
+
+// }}}
+// {{{ class DB_row
+
+/**
+ * PEAR DB Row Object
+ *
+ * The object contains a row of data from a result set. Each column's data
+ * is placed in a property named for the column.
+ *
+ * @category Database
+ * @package DB
+ * @author Stig Bakken <ssb@php.net>
+ * @copyright 1997-2007 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0
+ * @version Release: 1.7.13
+ * @link http://pear.php.net/package/DB
+ * @see DB_common::setFetchMode()
+ */
+class DB_row
+{
+ // {{{ constructor
+
+ /**
+ * The constructor places a row's data into properties of this object
+ *
+ * @param array the array containing the row's data
+ *
+ * @return void
+ */
+ function DB_row(&$arr)
+ {
+ foreach ($arr as $key => $value) {
+ $this->$key = &$arr[$key];
+ }
+ }
+
+ // }}}
+}
+
+// }}}
+
+/*
+ * Local variables:
+ * tab-width: 4
+ * c-basic-offset: 4
+ * End:
+ */
+
+?>
diff --git a/extlib/DB/DataObject.php b/extlib/DB/DataObject.php
new file mode 100644
index 000000000..b1a1a4e21
--- /dev/null
+++ b/extlib/DB/DataObject.php
@@ -0,0 +1,4152 @@
+<?php
+/**
+ * Object Based Database Query Builder and data store
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt. If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category Database
+ * @package DB_DataObject
+ * @author Alan Knowles <alan@akbkhome.com>
+ * @copyright 1997-2006 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0
+ * @version CVS: $Id: DataObject.php,v 1.439 2008/01/30 02:14:06 alan_k Exp $
+ * @link http://pear.php.net/package/DB_DataObject
+ */
+
+
+/* ===========================================================================
+ *
+ * !!!!!!!!!!!!! W A R N I N G !!!!!!!!!!!
+ *
+ * THIS MAY SEGFAULT PHP IF YOU ARE USING THE ZEND OPTIMIZER (to fix it,
+ * just add "define('DB_DATAOBJECT_NO_OVERLOAD',true);" before you include
+ * this file. reducing the optimization level may also solve the segfault.
+ * ===========================================================================
+ */
+
+/**
+ * The main "DB_DataObject" class is really a base class for your own tables classes
+ *
+ * // Set up the class by creating an ini file (refer to the manual for more details
+ * [DB_DataObject]
+ * database = mysql:/username:password@host/database
+ * schema_location = /home/myapplication/database
+ * class_location = /home/myapplication/DBTables/
+ * clase_prefix = DBTables_
+ *
+ *
+ * //Start and initialize...................... - dont forget the &
+ * $config = parse_ini_file('example.ini',true);
+ * $options = &PEAR::getStaticProperty('DB_DataObject','options');
+ * $options = $config['DB_DataObject'];
+ *
+ * // example of a class (that does not use the 'auto generated tables data')
+ * class mytable extends DB_DataObject {
+ * // mandatory - set the table
+ * var $_database_dsn = "mysql://username:password@localhost/database";
+ * var $__table = "mytable";
+ * function table() {
+ * return array(
+ * 'id' => 1, // integer or number
+ * 'name' => 2, // string
+ * );
+ * }
+ * function keys() {
+ * return array('id');
+ * }
+ * }
+ *
+ * // use in the application
+ *
+ *
+ * Simple get one row
+ *
+ * $instance = new mytable;
+ * $instance->get("id",12);
+ * echo $instance->somedata;
+ *
+ *
+ * Get multiple rows
+ *
+ * $instance = new mytable;
+ * $instance->whereAdd("ID > 12");
+ * $instance->whereAdd("ID < 14");
+ * $instance->find();
+ * while ($instance->fetch()) {
+ * echo $instance->somedata;
+ * }
+
+
+/**
+ * Needed classes
+ * - we use getStaticProperty from PEAR pretty extensively (cant remove it ATM)
+ */
+
+require_once 'PEAR.php';
+
+/**
+ * We are duping fetchmode constants to be compatible with
+ * both DB and MDB2
+ */
+define('DB_DATAOBJECT_FETCHMODE_ORDERED',1);
+define('DB_DATAOBJECT_FETCHMODE_ASSOC',2);
+
+
+
+
+
+/**
+ * these are constants for the get_table array
+ * user to determine what type of escaping is required around the object vars.
+ */
+define('DB_DATAOBJECT_INT', 1); // does not require ''
+define('DB_DATAOBJECT_STR', 2); // requires ''
+
+define('DB_DATAOBJECT_DATE', 4); // is date #TODO
+define('DB_DATAOBJECT_TIME', 8); // is time #TODO
+define('DB_DATAOBJECT_BOOL', 16); // is boolean #TODO
+define('DB_DATAOBJECT_TXT', 32); // is long text #TODO
+define('DB_DATAOBJECT_BLOB', 64); // is blob type
+
+
+define('DB_DATAOBJECT_NOTNULL', 128); // not null col.
+define('DB_DATAOBJECT_MYSQLTIMESTAMP' , 256); // mysql timestamps (ignored by update/insert)
+/*
+ * Define this before you include DataObjects.php to disable overload - if it segfaults due to Zend optimizer..
+ */
+//define('DB_DATAOBJECT_NO_OVERLOAD',true)
+
+
+/**
+ * Theses are the standard error codes, most methods will fail silently - and return false
+ * to access the error message either use $table->_lastError
+ * or $last_error = PEAR::getStaticProperty('DB_DataObject','lastError');
+ * the code is $last_error->code, and the message is $last_error->message (a standard PEAR error)
+ */
+
+define('DB_DATAOBJECT_ERROR_INVALIDARGS', -1); // wrong args to function
+define('DB_DATAOBJECT_ERROR_NODATA', -2); // no data available
+define('DB_DATAOBJECT_ERROR_INVALIDCONFIG', -3); // something wrong with the config
+define('DB_DATAOBJECT_ERROR_NOCLASS', -4); // no class exists
+define('DB_DATAOBJECT_ERROR_INVALID_CALL' ,-7); // overlad getter/setter failure
+
+/**
+ * Used in methods like delete() and count() to specify that the method should
+ * build the condition only out of the whereAdd's and not the object parameters.
+ */
+define('DB_DATAOBJECT_WHEREADD_ONLY', true);
+
+/**
+ *
+ * storage for connection and result objects,
+ * it is done this way so that print_r()'ing the is smaller, and
+ * it reduces the memory size of the object.
+ * -- future versions may use $this->_connection = & PEAR object..
+ * although will need speed tests to see how this affects it.
+ * - includes sub arrays
+ * - connections = md5 sum mapp to pear db object
+ * - results = [id] => map to pear db object
+ * - resultseq = sequence id for results & results field
+ * - resultfields = [id] => list of fields return from query (for use with toArray())
+ * - ini = mapping of database to ini file results
+ * - links = mapping of database to links file
+ * - lasterror = pear error objects for last error event.
+ * - config = aliased view of PEAR::getStaticPropery('DB_DataObject','options') * done for performance.
+ * - array of loaded classes by autoload method - to stop it doing file access request over and over again!
+ */
+$GLOBALS['_DB_DATAOBJECT']['RESULTS'] = array();
+$GLOBALS['_DB_DATAOBJECT']['RESULTSEQ'] = 1;
+$GLOBALS['_DB_DATAOBJECT']['RESULTFIELDS'] = array();
+$GLOBALS['_DB_DATAOBJECT']['CONNECTIONS'] = array();
+$GLOBALS['_DB_DATAOBJECT']['INI'] = array();
+$GLOBALS['_DB_DATAOBJECT']['LINKS'] = array();
+$GLOBALS['_DB_DATAOBJECT']['SEQUENCE'] = array();
+$GLOBALS['_DB_DATAOBJECT']['LASTERROR'] = null;
+$GLOBALS['_DB_DATAOBJECT']['CONFIG'] = array();
+$GLOBALS['_DB_DATAOBJECT']['CACHE'] = array();
+$GLOBALS['_DB_DATAOBJECT']['OVERLOADED'] = false;
+$GLOBALS['_DB_DATAOBJECT']['QUERYENDTIME'] = 0;
+
+
+
+// this will be horrifically slow!!!!
+// NOTE: Overload SEGFAULTS ON PHP4 + Zend Optimizer (see define before..)
+// these two are BC/FC handlers for call in PHP4/5
+
+if ( substr(phpversion(),0,1) == 5) {
+ class DB_DataObject_Overload
+ {
+ function __call($method,$args)
+ {
+ $return = null;
+ $this->_call($method,$args,$return);
+ return $return;
+ }
+ function __sleep()
+ {
+ return array_keys(get_object_vars($this)) ;
+ }
+ }
+} else {
+ if (version_compare(phpversion(),'4.3.10','eq') && !defined('DB_DATAOBJECT_NO_OVERLOAD')) {
+ trigger_error(
+ "overload does not work with PHP4.3.10, either upgrade
+ (snaps.php.net) or more recent version
+ or define DB_DATAOBJECT_NO_OVERLOAD as per the manual.
+ ",E_USER_ERROR);
+ }
+
+ if (!function_exists('clone')) {
+ // emulate clone - as per php_compact, slow but really the correct behaviour..
+ eval('function clone($t) { $r = $t; if (method_exists($r,"__clone")) { $r->__clone(); } return $r; }');
+ }
+ eval('
+ class DB_DataObject_Overload {
+ function __call($method,$args,&$return) {
+ return $this->_call($method,$args,$return);
+ }
+ }
+ ');
+}
+
+
+
+
+
+
+ /*
+ *
+ * @package DB_DataObject
+ * @author Alan Knowles <alan@akbkhome.com>
+ * @since PHP 4.0
+ */
+
+class DB_DataObject extends DB_DataObject_Overload
+{
+ /**
+ * The Version - use this to check feature changes
+ *
+ * @access private
+ * @var string
+ */
+ var $_DB_DataObject_version = "1.8.8";
+
+ /**
+ * The Database table (used by table extends)
+ *
+ * @access private
+ * @var string
+ */
+ var $__table = ''; // database table
+
+ /**
+ * The Number of rows returned from a query
+ *
+ * @access public
+ * @var int
+ */
+ var $N = 0; // Number of rows returned from a query
+
+ /* ============================================================= */
+ /* Major Public Methods */
+ /* (designed to be optionally then called with parent::method()) */
+ /* ============================================================= */
+
+
+ /**
+ * Get a result using key, value.
+ *
+ * for example
+ * $object->get("ID",1234);
+ * Returns Number of rows located (usually 1) for success,
+ * and puts all the table columns into this classes variables
+ *
+ * see the fetch example on how to extend this.
+ *
+ * if no value is entered, it is assumed that $key is a value
+ * and get will then use the first key in keys()
+ * to obtain the key.
+ *
+ * @param string $k column
+ * @param string $v value
+ * @access public
+ * @return int No. of rows
+ */
+ function get($k = null, $v = null)
+ {
+ global $_DB_DATAOBJECT;
+ if (empty($_DB_DATAOBJECT['CONFIG'])) {
+ DB_DataObject::_loadConfig();
+ }
+ $keys = array();
+
+ if ($v === null) {
+ $v = $k;
+ $keys = $this->keys();
+ if (!$keys) {
+ $this->raiseError("No Keys available for {$this->__table}", DB_DATAOBJECT_ERROR_INVALIDCONFIG);
+ return false;
+ }
+ $k = $keys[0];
+ }
+ if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
+ $this->debug("$k $v " .print_r($keys,true), "GET");
+ }
+
+ if ($v === null) {
+ $this->raiseError("No Value specified for get", DB_DATAOBJECT_ERROR_INVALIDARGS);
+ return false;
+ }
+ $this->$k = $v;
+ return $this->find(1);
+ }
+
+ /**
+ * An autoloading, caching static get method using key, value (based on get)
+ *
+ * Usage:
+ * $object = DB_DataObject::staticGet("DbTable_mytable",12);
+ * or
+ * $object = DB_DataObject::staticGet("DbTable_mytable","name","fred");
+ *
+ * or write it into your extended class:
+ * function &staticGet($k,$v=NULL) { return DB_DataObject::staticGet("This_Class",$k,$v); }
+ *
+ * @param string $class class name
+ * @param string $k column (or value if using keys)
+ * @param string $v value (optional)
+ * @access public
+ * @return object
+ */
+ function &staticGet($class, $k, $v = null)
+ {
+ $lclass = strtolower($class);
+ global $_DB_DATAOBJECT;
+ if (empty($_DB_DATAOBJECT['CONFIG'])) {
+ DB_DataObject::_loadConfig();
+ }
+
+
+
+ $key = "$k:$v";
+ if ($v === null) {
+ $key = $k;
+ }
+ if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
+ DB_DataObject::debug("$class $key","STATIC GET - TRY CACHE");
+ }
+ if (!empty($_DB_DATAOBJECT['CACHE'][$lclass][$key])) {
+ return $_DB_DATAOBJECT['CACHE'][$lclass][$key];
+ }
+ if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
+ DB_DataObject::debug("$class $key","STATIC GET - NOT IN CACHE");
+ }
+
+ $obj = DB_DataObject::factory(substr($class,strlen($_DB_DATAOBJECT['CONFIG']['class_prefix'])));
+ if (PEAR::isError($obj)) {
+ DB_DataObject::raiseError("could not autoload $class", DB_DATAOBJECT_ERROR_NOCLASS);
+ $r = false;
+ return $r;
+ }
+
+ if (!isset($_DB_DATAOBJECT['CACHE'][$lclass])) {
+ $_DB_DATAOBJECT['CACHE'][$lclass] = array();
+ }
+ if (!$obj->get($k,$v)) {
+ DB_DataObject::raiseError("No Data return from get $k $v", DB_DATAOBJECT_ERROR_NODATA);
+
+ $r = false;
+ return $r;
+ }
+ $_DB_DATAOBJECT['CACHE'][$lclass][$key] = $obj;
+ return $_DB_DATAOBJECT['CACHE'][$lclass][$key];
+ }
+
+ /**
+ * find results, either normal or crosstable
+ *
+ * for example
+ *
+ * $object = new mytable();
+ * $object->ID = 1;
+ * $object->find();
+ *
+ *
+ * will set $object->N to number of rows, and expects next command to fetch rows
+ * will return $object->N
+ *
+ * @param boolean $n Fetch first result
+ * @access public
+ * @return mixed (number of rows returned, or true if numRows fetching is not supported)
+ */
+ function find($n = false)
+ {
+ global $_DB_DATAOBJECT;
+ if ($this->_query === false) {
+ $this->raiseError(
+ "You cannot do two queries on the same object (copy it before finding)",
+ DB_DATAOBJECT_ERROR_INVALIDARGS);
+ return false;
+ }
+
+ if (empty($_DB_DATAOBJECT['CONFIG'])) {
+ DB_DataObject::_loadConfig();
+ }
+
+ if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
+ $this->debug($n, "find",1);
+ }
+ if (!$this->__table) {
+ // xdebug can backtrace this!
+ trigger_error("NO \$__table SPECIFIED in class definition",E_USER_ERROR);
+ }
+ $this->N = 0;
+ $query_before = $this->_query;
+ $this->_build_condition($this->table()) ;
+
+ $quoteIdentifiers = !empty($_DB_DATAOBJECT['CONFIG']['quote_identifiers']);
+ $this->_connect();
+ $DB = &$_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5];
+
+ /* We are checking for method modifyLimitQuery as it is PEAR DB specific */
+ $sql = 'SELECT ' .
+ $this->_query['data_select'] . " \n" .
+ ' FROM ' . ($quoteIdentifiers ? $DB->quoteIdentifier($this->__table) : $this->__table) . " \n" .
+ $this->_join . " \n" .
+ $this->_query['condition'] . " \n" .
+ $this->_query['group_by'] . " \n" .
+ $this->_query['having'] . " \n" .
+ $this->_query['order_by'] . " \n";
+
+ if ((!isset($_DB_DATAOBJECT['CONFIG']['db_driver'])) ||
+ ($_DB_DATAOBJECT['CONFIG']['db_driver'] == 'DB')) {
+ /* PEAR DB specific */
+
+ if (isset($this->_query['limit_start']) && strlen($this->_query['limit_start'] . $this->_query['limit_count'])) {
+ $sql = $DB->modifyLimitQuery($sql,$this->_query['limit_start'], $this->_query['limit_count']);
+ }
+ } else {
+ /* theoretically MDB2! */
+ if (isset($this->_query['limit_start']) && strlen($this->_query['limit_start'] . $this->_query['limit_count'])) {
+ $DB->setLimit($this->_query['limit_count'],$this->_query['limit_start']);
+ }
+ }
+
+
+ $this->_query($sql);
+
+ if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
+ $this->debug("CHECK autofetchd $n", "find", 1);
+ }
+
+ // find(true)
+
+ $ret = $this->N;
+ if (!$ret && !empty($_DB_DATAOBJECT['RESULTS'][$this->_DB_resultid])) {
+ // clear up memory if nothing found!?
+ unset($_DB_DATAOBJECT['RESULTS'][$this->_DB_resultid]);
+ }
+
+ if ($n && $this->N > 0 ) {
+ if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
+ $this->debug("ABOUT TO AUTOFETCH", "find", 1);
+ }
+ $fs = $this->fetch();
+ // if fetch returns false (eg. failed), then the backend doesnt support numRows (eg. ret=true)
+ // - hence find() also returns false..
+ $ret = ($ret === true) ? $fs : $ret;
+ }
+ if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
+ $this->debug("DONE", "find", 1);
+ }
+ $this->_query = $query_before;
+ return $ret;
+ }
+
+ /**
+ * fetches next row into this objects var's
+ *
+ * returns 1 on success 0 on failure
+ *
+ *
+ *
+ * Example
+ * $object = new mytable();
+ * $object->name = "fred";
+ * $object->find();
+ * $store = array();
+ * while ($object->fetch()) {
+ * echo $this->ID;
+ * $store[] = $object; // builds an array of object lines.
+ * }
+ *
+ * to add features to a fetch
+ * function fetch () {
+ * $ret = parent::fetch();
+ * $this->date_formated = date('dmY',$this->date);
+ * return $ret;
+ * }
+ *
+ * @access public
+ * @return boolean on success
+ */
+ function fetch()
+ {
+
+ global $_DB_DATAOBJECT;
+ if (empty($_DB_DATAOBJECT['CONFIG'])) {
+ DB_DataObject::_loadConfig();
+ }
+ if (empty($this->N)) {
+ if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
+ $this->debug("No data returned from FIND (eg. N is 0)","FETCH", 3);
+ }
+ return false;
+ }
+
+ if (empty($_DB_DATAOBJECT['RESULTS'][$this->_DB_resultid]) ||
+ !is_object($result = &$_DB_DATAOBJECT['RESULTS'][$this->_DB_resultid]))
+ {
+ if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
+ $this->debug('fetched on object after fetch completed (no results found)');
+ }
+ return false;
+ }
+
+
+ $array = $result->fetchRow(DB_DATAOBJECT_FETCHMODE_ASSOC);
+ if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
+ $this->debug(serialize($array),"FETCH");
+ }
+
+ // fetched after last row..
+ if ($array === null) {
+ if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
+ $t= explode(' ',microtime());
+
+ $this->debug("Last Data Fetch'ed after " .
+ ($t[0]+$t[1]- $_DB_DATAOBJECT['QUERYENDTIME'] ) .
+ " seconds",
+ "FETCH", 1);
+ }
+ // reduce the memory usage a bit... (but leave the id in, so count() works ok on it)
+ unset($_DB_DATAOBJECT['RESULTS'][$this->_DB_resultid]);
+
+ // we need to keep a copy of resultfields locally so toArray() still works
+ // however we dont want to keep it in the global cache..
+
+ if (!empty($_DB_DATAOBJECT['RESULTFIELDS'][$this->_DB_resultid])) {
+ $this->_resultFields = $_DB_DATAOBJECT['RESULTFIELDS'][$this->_DB_resultid];
+ unset($_DB_DATAOBJECT['RESULTFIELDS'][$this->_DB_resultid]);
+ }
+ // this is probably end of data!!
+ //DB_DataObject::raiseError("fetch: no data returned", DB_DATAOBJECT_ERROR_NODATA);
+ return false;
+ }
+ // make sure resultFields is always empty..
+ $this->_resultFields = false;
+
+ if (!isset($_DB_DATAOBJECT['RESULTFIELDS'][$this->_DB_resultid])) {
+ // note: we dont declare this to keep the print_r size down.
+ $_DB_DATAOBJECT['RESULTFIELDS'][$this->_DB_resultid]= array_flip(array_keys($array));
+ }
+
+ foreach($array as $k=>$v) {
+ $kk = str_replace(".", "_", $k);
+ $kk = str_replace(" ", "_", $kk);
+ if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
+ $this->debug("$kk = ". $array[$k], "fetchrow LINE", 3);
+ }
+ $this->$kk = $array[$k];
+ }
+
+ // set link flag
+ $this->_link_loaded=false;
+ if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
+ $this->debug("{$this->__table} DONE", "fetchrow",2);
+ }
+ if (($this->_query !== false) && empty($_DB_DATAOBJECT['CONFIG']['keep_query_after_fetch'])) {
+ $this->_query = false;
+ }
+ return true;
+ }
+
+ /**
+ * Adds a condition to the WHERE statement, defaults to AND
+ *
+ * $object->whereAdd(); //reset or cleaer ewhwer
+ * $object->whereAdd("ID > 20");
+ * $object->whereAdd("age > 20","OR");
+ *
+ * @param string $cond condition
+ * @param string $logic optional logic "OR" (defaults to "AND")
+ * @access public
+ * @return string|PEAR::Error - previous condition or Error when invalid args found
+ */
+ function whereAdd($cond = false, $logic = 'AND')
+ {
+ // for PHP5.2.3 - there is a bug with setting array properties of an object.
+ $_query = $this->_query;
+
+ if (!isset($this->_query) || ($_query === false)) {
+ return $this->raiseError(
+ "You cannot do two queries on the same object (clone it before finding)",
+ DB_DATAOBJECT_ERROR_INVALIDARGS);
+ }
+
+ if ($cond === false) {
+ $r = $this->_query['condition'];
+ $_query['condition'] = '';
+ $this->_query = $_query;
+ return preg_replace('/^\s+WHERE\s+/','',$r);
+ }
+ // check input...= 0 or ' ' == error!
+ if (!trim($cond)) {
+ return $this->raiseError("WhereAdd: No Valid Arguments", DB_DATAOBJECT_ERROR_INVALIDARGS);
+ }
+ $r = $_query['condition'];
+ if ($_query['condition']) {
+ $_query['condition'] .= " {$logic} ( {$cond} )";
+ $this->_query = $_query;
+ return $r;
+ }
+ $_query['condition'] = " WHERE ( {$cond} ) ";
+ $this->_query = $_query;
+ return $r;
+ }
+
+ /**
+ * Adds a order by condition
+ *
+ * $object->orderBy(); //clears order by
+ * $object->orderBy("ID");
+ * $object->orderBy("ID,age");
+ *
+ * @param string $order Order
+ * @access public
+ * @return none|PEAR::Error - invalid args only
+ */
+ function orderBy($order = false)
+ {
+ if ($this->_query === false) {
+ $this->raiseError(
+ "You cannot do two queries on the same object (copy it before finding)",
+ DB_DATAOBJECT_ERROR_INVALIDARGS);
+ return false;
+ }
+ if ($order === false) {
+ $this->_query['order_by'] = '';
+ return;
+ }
+ // check input...= 0 or ' ' == error!
+ if (!trim($order)) {
+ return $this->raiseError("orderBy: No Valid Arguments", DB_DATAOBJECT_ERROR_INVALIDARGS);
+ }
+
+ if (!$this->_query['order_by']) {
+ $this->_query['order_by'] = " ORDER BY {$order} ";
+ return;
+ }
+ $this->_query['order_by'] .= " , {$order}";
+ }
+
+ /**
+ * Adds a group by condition
+ *
+ * $object->groupBy(); //reset the grouping
+ * $object->groupBy("ID DESC");
+ * $object->groupBy("ID,age");
+ *
+ * @param string $group Grouping
+ * @access public
+ * @return none|PEAR::Error - invalid args only
+ */
+ function groupBy($group = false)
+ {
+ if ($this->_query === false) {
+ $this->raiseError(
+ "You cannot do two queries on the same object (copy it before finding)",
+ DB_DATAOBJECT_ERROR_INVALIDARGS);
+ return false;
+ }
+ if ($group === false) {
+ $this->_query['group_by'] = '';
+ return;
+ }
+ // check input...= 0 or ' ' == error!
+ if (!trim($group)) {
+ return $this->raiseError("groupBy: No Valid Arguments", DB_DATAOBJECT_ERROR_INVALIDARGS);
+ }
+
+
+ if (!$this->_query['group_by']) {
+ $this->_query['group_by'] = " GROUP BY {$group} ";
+ return;
+ }
+ $this->_query['group_by'] .= " , {$group}";
+ }
+
+ /**
+ * Adds a having clause
+ *
+ * $object->having(); //reset the grouping
+ * $object->having("sum(value) > 0 ");
+ *
+ * @param string $having condition
+ * @access public
+ * @return none|PEAR::Error - invalid args only
+ */
+ function having($having = false)
+ {
+ if ($this->_query === false) {
+ $this->raiseError(
+ "You cannot do two queries on the same object (copy it before finding)",
+ DB_DATAOBJECT_ERROR_INVALIDARGS);
+ return false;
+ }
+ if ($having === false) {
+ $this->_query['having'] = '';
+ return;
+ }
+ // check input...= 0 or ' ' == error!
+ if (!trim($having)) {
+ return $this->raiseError("Having: No Valid Arguments", DB_DATAOBJECT_ERROR_INVALIDARGS);
+ }
+
+
+ if (!$this->_query['having']) {
+ $this->_query['having'] = " HAVING {$having} ";
+ return;
+ }
+ $this->_query['having'] .= " AND {$having}";
+ }
+
+ /**
+ * Sets the Limit
+ *
+ * $boject->limit(); // clear limit
+ * $object->limit(12);
+ * $object->limit(12,10);
+ *
+ * Note this will emit an error on databases other than mysql/postgress
+ * as there is no 'clean way' to implement it. - you should consider refering to
+ * your database manual to decide how you want to implement it.
+ *
+ * @param string $a limit start (or number), or blank to reset
+ * @param string $b number
+ * @access public
+ * @return none|PEAR::Error - invalid args only
+ */
+ function limit($a = null, $b = null)
+ {
+ if ($this->_query === false) {
+ $this->raiseError(
+ "You cannot do two queries on the same object (copy it before finding)",
+ DB_DATAOBJECT_ERROR_INVALIDARGS);
+ return false;
+ }
+
+ if ($a === null) {
+ $this->_query['limit_start'] = '';
+ $this->_query['limit_count'] = '';
+ return;
+ }
+ // check input...= 0 or ' ' == error!
+ if ((!is_int($a) && ((string)((int)$a) !== (string)$a))
+ || (($b !== null) && (!is_int($b) && ((string)((int)$b) !== (string)$b)))) {
+ return $this->raiseError("limit: No Valid Arguments", DB_DATAOBJECT_ERROR_INVALIDARGS);
+ }
+ global $_DB_DATAOBJECT;
+ $this->_connect();
+ $DB = &$_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5];
+
+ $this->_query['limit_start'] = ($b == null) ? 0 : (int)$a;
+ $this->_query['limit_count'] = ($b == null) ? (int)$a : (int)$b;
+
+ }
+
+ /**
+ * Adds a select columns
+ *
+ * $object->selectAdd(); // resets select to nothing!
+ * $object->selectAdd("*"); // default select
+ * $object->selectAdd("unixtime(DATE) as udate");
+ * $object->selectAdd("DATE");
+ *
+ * to prepend distict:
+ * $object->selectAdd('distinct ' . $object->selectAdd());
+ *
+ * @param string $k
+ * @access public
+ * @return mixed null or old string if you reset it.
+ */
+ function selectAdd($k = null)
+ {
+ if ($this->_query === false) {
+ $this->raiseError(
+ "You cannot do two queries on the same object (copy it before finding)",
+ DB_DATAOBJECT_ERROR_INVALIDARGS);
+ return false;
+ }
+ if ($k === null) {
+ $old = $this->_query['data_select'];
+ $this->_query['data_select'] = '';
+ return $old;
+ }
+
+ // check input...= 0 or ' ' == error!
+ if (!trim($k)) {
+ return $this->raiseError("selectAdd: No Valid Arguments", DB_DATAOBJECT_ERROR_INVALIDARGS);
+ }
+
+ if ($this->_query['data_select']) {
+ $this->_query['data_select'] .= ', ';
+ }
+ $this->_query['data_select'] .= " $k ";
+ }
+ /**
+ * Adds multiple Columns or objects to select with formating.
+ *
+ * $object->selectAs(null); // adds "table.colnameA as colnameA,table.colnameB as colnameB,......"
+ * // note with null it will also clear the '*' default select
+ * $object->selectAs(array('a','b'),'%s_x'); // adds "a as a_x, b as b_x"
+ * $object->selectAs(array('a','b'),'ddd_%s','ccc'); // adds "ccc.a as ddd_a, ccc.b as ddd_b"
+ * $object->selectAdd($object,'prefix_%s'); // calls $object->get_table and adds it all as
+ * objectTableName.colnameA as prefix_colnameA
+ *
+ * @param array|object|null the array or object to take column names from.
+ * @param string format in sprintf format (use %s for the colname)
+ * @param string table name eg. if you have joinAdd'd or send $from as an array.
+ * @access public
+ * @return void
+ */
+ function selectAs($from = null,$format = '%s',$tableName=false)
+ {
+ global $_DB_DATAOBJECT;
+
+ if ($this->_query === false) {
+ $this->raiseError(
+ "You cannot do two queries on the same object (copy it before finding)",
+ DB_DATAOBJECT_ERROR_INVALIDARGS);
+ return false;
+ }
+
+ if ($from === null) {
+ // blank the '*'
+ $this->selectAdd();
+ $from = $this;
+ }
+
+
+ $table = $this->__table;
+ if (is_object($from)) {
+ $table = $from->__table;
+ $from = array_keys($from->table());
+ }
+
+ if ($tableName !== false) {
+ $table = $tableName;
+ }
+ $s = '%s';
+ if (!empty($_DB_DATAOBJECT['CONFIG']['quote_identifiers'])) {
+ $this->_connect();
+ $DB = &$_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5];
+ $s = $DB->quoteIdentifier($s);
+ $format = $DB->quoteIdentifier($format);
+ }
+ foreach ($from as $k) {
+ $this->selectAdd(sprintf("{$s}.{$s} as {$format}",$table,$k,$k));
+ }
+ $this->_query['data_select'] .= "\n";
+ }
+ /**
+ * Insert the current objects variables into the database
+ *
+ * Returns the ID of the inserted element (if auto increment or sequences are used.)
+ *
+ * for example
+ *
+ * Designed to be extended
+ *
+ * $object = new mytable();
+ * $object->name = "fred";
+ * echo $object->insert();
+ *
+ * @access public
+ * @return mixed false on failure, int when auto increment or sequence used, otherwise true on success
+ */
+ function insert()
+ {
+ global $_DB_DATAOBJECT;
+
+ // we need to write to the connection (For nextid) - so us the real
+ // one not, a copyied on (as ret-by-ref fails with overload!)
+
+ if (!isset($_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5])) {
+ $this->_connect();
+ }
+
+ $quoteIdentifiers = !empty($_DB_DATAOBJECT['CONFIG']['quote_identifiers']);
+
+ $DB = &$_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5];
+
+ $items = isset($_DB_DATAOBJECT['INI'][$this->_database][$this->__table]) ?
+ $_DB_DATAOBJECT['INI'][$this->_database][$this->__table] : $this->table();
+
+ if (!$items) {
+ $this->raiseError("insert:No table definition for {$this->__table}",
+ DB_DATAOBJECT_ERROR_INVALIDCONFIG);
+ return false;
+ }
+ $options = &$_DB_DATAOBJECT['CONFIG'];
+
+
+ $datasaved = 1;
+ $leftq = '';
+ $rightq = '';
+
+ $seqKeys = isset($_DB_DATAOBJECT['SEQUENCE'][$this->_database][$this->__table]) ?
+ $_DB_DATAOBJECT['SEQUENCE'][$this->_database][$this->__table] :
+ $this->sequenceKey();
+
+ $key = isset($seqKeys[0]) ? $seqKeys[0] : false;
+ $useNative = isset($seqKeys[1]) ? $seqKeys[1] : false;
+ $seq = isset($seqKeys[2]) ? $seqKeys[2] : false;
+
+ $dbtype = $_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5]->dsn["phptype"];
+
+
+ // nativeSequences or Sequences..
+
+ // big check for using sequences
+
+ if (($key !== false) && !$useNative) {
+
+ if (!$seq) {
+ $keyvalue = $DB->nextId($this->__table);
+ } else {
+ $f = $DB->getOption('seqname_format');
+ $DB->setOption('seqname_format','%s');
+ $keyvalue = $DB->nextId($seq);
+ $DB->setOption('seqname_format',$f);
+ }
+ if (PEAR::isError($keyvalue)) {
+ $this->raiseError($keyvalue->toString(), DB_DATAOBJECT_ERROR_INVALIDCONFIG);
+ return false;
+ }
+ $this->$key = $keyvalue;
+ }
+
+
+
+ foreach($items as $k => $v) {
+
+ // if we are using autoincrement - skip the column...
+ if ($key && ($k == $key) && $useNative) {
+ continue;
+ }
+
+
+ if (!isset($this->$k)) {
+ continue;
+ }
+ // dont insert data into mysql timestamps
+ // use query() if you really want to do this!!!!
+ if ($v & DB_DATAOBJECT_MYSQLTIMESTAMP) {
+ continue;
+ }
+
+ if ($leftq) {
+ $leftq .= ', ';
+ $rightq .= ', ';
+ }
+
+ $leftq .= ($quoteIdentifiers ? ($DB->quoteIdentifier($k) . ' ') : "$k ");
+
+ if (is_a($this->$k,'DB_DataObject_Cast')) {
+ $value = $this->$k->toString($v,$DB);
+ if (PEAR::isError($value)) {
+ $this->raiseError($value->toString() ,DB_DATAOBJECT_ERROR_INVALIDARGS);
+ return false;
+ }
+ $rightq .= $value;
+ continue;
+ }
+
+
+
+ if (!isset($options['disable_null_strings']) && is_string($this->$k) && (strtolower($this->$k) === 'null') && !($v & DB_DATAOBJECT_NOTNULL)) {
+ $rightq .= " NULL ";
+ continue;
+ }
+ // DATE is empty... on a col. that can be null..
+ // note: this may be usefull for time as well..
+ if (!$this->$k &&
+ (($v & DB_DATAOBJECT_DATE) || ($v & DB_DATAOBJECT_TIME)) &&
+ !($v & DB_DATAOBJECT_NOTNULL)) {
+
+ $rightq .= " NULL ";
+ continue;
+ }
+
+
+ if ($v & DB_DATAOBJECT_STR) {
+ $rightq .= $this->_quote((string) (
+ ($v & DB_DATAOBJECT_BOOL) ?
+ // this is thanks to the braindead idea of postgres to
+ // use t/f for boolean.
+ (($this->$k === 'f') ? 0 : (int)(bool) $this->$k) :
+ $this->$k
+ )) . " ";
+ continue;
+ }
+ if (is_numeric($this->$k)) {
+ $rightq .=" {$this->$k} ";
+ continue;
+ }
+ /* flag up string values - only at debug level... !!!??? */
+ if (is_object($this->$k) || is_array($this->$k)) {
+ $this->debug('ODD DATA: ' .$k . ' ' . print_r($this->$k,true),'ERROR');
+ }
+
+ // at present we only cast to integers
+ // - V2 may store additional data about float/int
+ $rightq .= ' ' . intval($this->$k) . ' ';
+
+ }
+
+ // not sure why we let empty insert here.. - I guess to generate a blank row..
+
+
+ if ($leftq || $useNative) {
+ $table = ($quoteIdentifiers ? $DB->quoteIdentifier($this->__table) : $this->__table);
+
+ $r = $this->_query("INSERT INTO {$table} ($leftq) VALUES ($rightq) ");
+
+
+
+ if (PEAR::isError($r)) {
+ $this->raiseError($r);
+ return false;
+ }
+
+ if ($r < 1) {
+ return 0;
+ }
+
+
+ // now do we have an integer key!
+
+ if ($key && $useNative) {
+ switch ($dbtype) {
+ case 'mysql':
+ case 'mysqli':
+ $method = "{$dbtype}_insert_id";
+ $this->$key = $method(
+ $_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5]->connection
+ );
+ break;
+
+ case 'mssql':
+ // note this is not really thread safe - you should wrapp it with
+ // transactions = eg.
+ // $db->query('BEGIN');
+ // $db->insert();
+ // $db->query('COMMIT');
+ $db_driver = empty($options['db_driver']) ? 'DB' : $options['db_driver'];
+ $method = ($db_driver == 'DB') ? 'getOne' : 'queryOne';
+ $mssql_key = $DB->$method("SELECT @@IDENTITY");
+ if (PEAR::isError($mssql_key)) {
+ $this->raiseError($mssql_key);
+ return false;
+ }
+ $this->$key = $mssql_key;
+ break;
+
+ case 'pgsql':
+ if (!$seq) {
+ $seq = $DB->getSequenceName(strtolower($this->__table));
+ }
+ $db_driver = empty($options['db_driver']) ? 'DB' : $options['db_driver'];
+ $method = ($db_driver == 'DB') ? 'getOne' : 'queryOne';
+ $pgsql_key = $DB->$method("SELECT currval('".$seq . "')");
+
+
+ if (PEAR::isError($pgsql_key)) {
+ $this->raiseError($pgsql_key);
+ return false;
+ }
+ $this->$key = $pgsql_key;
+ break;
+
+ case 'ifx':
+ $this->$key = array_shift (
+ ifx_fetch_row (
+ ifx_query(
+ "select DBINFO('sqlca.sqlerrd1') FROM systables where tabid=1",
+ $_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5]->connection,
+ IFX_SCROLL
+ ),
+ "FIRST"
+ )
+ );
+ break;
+
+ }
+
+ }
+
+ if (isset($_DB_DATAOBJECT['CACHE'][strtolower(get_class($this))])) {
+ $this->_clear_cache();
+ }
+ if ($key) {
+ return $this->$key;
+ }
+ return true;
+ }
+ $this->raiseError("insert: No Data specifed for query", DB_DATAOBJECT_ERROR_NODATA);
+ return false;
+ }
+
+ /**
+ * Updates current objects variables into the database
+ * uses the keys() to decide how to update
+ * Returns the true on success
+ *
+ * for example
+ *
+ * $object = DB_DataObject::factory('mytable');
+ * $object->get("ID",234);
+ * $object->email="testing@test.com";
+ * if(!$object->update())
+ * echo "UPDATE FAILED";
+ *
+ * to only update changed items :
+ * $dataobject->get(132);
+ * $original = $dataobject; // clone/copy it..
+ * $dataobject->setFrom($_POST);
+ * if ($dataobject->validate()) {
+ * $dataobject->update($original);
+ * } // otherwise an error...
+ *
+ * performing global updates:
+ * $object = DB_DataObject::factory('mytable');
+ * $object->status = "dead";
+ * $object->whereAdd('age > 150');
+ * $object->update(DB_DATAOBJECT_WHEREADD_ONLY);
+ *
+ * @param object dataobject (optional) | DB_DATAOBJECT_WHEREADD_ONLY - used to only update changed items.
+ * @access public
+ * @return int rows affected or false on failure
+ */
+ function update($dataObject = false)
+ {
+ global $_DB_DATAOBJECT;
+ // connect will load the config!
+ $this->_connect();
+
+
+ $original_query = $this->_query;
+
+ $items = isset($_DB_DATAOBJECT['INI'][$this->_database][$this->__table]) ?
+ $_DB_DATAOBJECT['INI'][$this->_database][$this->__table] : $this->table();
+
+ // only apply update against sequence key if it is set?????
+
+ $seq = $this->sequenceKey();
+ if ($seq[0] !== false) {
+ $keys = array($seq[0]);
+ if (empty($this->{$keys[0]}) && $dataObject !== true) {
+ $this->raiseError("update: trying to perform an update without
+ the key set, and argument to update is not
+ DB_DATAOBJECT_WHEREADD_ONLY
+ ", DB_DATAOBJECT_ERROR_INVALIDARGS);
+ return false;
+ }
+ } else {
+ $keys = $this->keys();
+ }
+
+
+ if (!$items) {
+ $this->raiseError("update:No table definition for {$this->__table}", DB_DATAOBJECT_ERROR_INVALIDCONFIG);
+ return false;
+ }
+ $datasaved = 1;
+ $settings = '';
+ $this->_connect();
+
+ $DB = &$_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5];
+ $dbtype = $DB->dsn["phptype"];
+ $quoteIdentifiers = !empty($_DB_DATAOBJECT['CONFIG']['quote_identifiers']);
+ $options = $_DB_DATAOBJECT['CONFIG'];
+
+
+ foreach($items as $k => $v) {
+ if (!isset($this->$k)) {
+ continue;
+ }
+ // ignore stuff thats
+
+ // dont write things that havent changed..
+ if (($dataObject !== false) && isset($dataObject->$k) && ($dataObject->$k === $this->$k)) {
+ continue;
+ }
+
+ // - dont write keys to left.!!!
+ if (in_array($k,$keys)) {
+ continue;
+ }
+
+ // dont insert data into mysql timestamps
+ // use query() if you really want to do this!!!!
+ if ($v & DB_DATAOBJECT_MYSQLTIMESTAMP) {
+ continue;
+ }
+
+
+ if ($settings) {
+ $settings .= ', ';
+ }
+
+ $kSql = ($quoteIdentifiers ? $DB->quoteIdentifier($k) : $k);
+
+ if (is_a($this->$k,'DB_DataObject_Cast')) {
+ $value = $this->$k->toString($v,$DB);
+ if (PEAR::isError($value)) {
+ $this->raiseError($value->getMessage() ,DB_DATAOBJECT_ERROR_INVALIDARG);
+ return false;
+ }
+ $settings .= "$kSql = $value ";
+ continue;
+ }
+
+ // special values ... at least null is handled...
+ if (!isset($options['disable_null_strings']) && (strtolower($this->$k) === 'null') && !($v & DB_DATAOBJECT_NOTNULL)) {
+ $settings .= "$kSql = NULL ";
+ continue;
+ }
+ // DATE is empty... on a col. that can be null..
+ // note: this may be usefull for time as well..
+ if (!$this->$k &&
+ (($v & DB_DATAOBJECT_DATE) || ($v & DB_DATAOBJECT_TIME)) &&
+ !($v & DB_DATAOBJECT_NOTNULL)) {
+
+ $settings .= "$kSql = NULL ";
+ continue;
+ }
+
+
+ if ($v & DB_DATAOBJECT_STR) {
+ $settings .= "$kSql = ". $this->_quote((string) (
+ ($v & DB_DATAOBJECT_BOOL) ?
+ // this is thanks to the braindead idea of postgres to
+ // use t/f for boolean.
+ (($this->$k === 'f') ? 0 : (int)(bool) $this->$k) :
+ $this->$k
+ )) . ' ';
+ continue;
+ }
+ if (is_numeric($this->$k)) {
+ $settings .= "$kSql = {$this->$k} ";
+ continue;
+ }
+ // at present we only cast to integers
+ // - V2 may store additional data about float/int
+ $settings .= "$kSql = " . intval($this->$k) . ' ';
+ }
+
+
+ if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
+ $this->debug("got keys as ".serialize($keys),3);
+ }
+ if ($dataObject !== true) {
+ $this->_build_condition($items,$keys);
+ } else {
+ // prevent wiping out of data!
+ if (empty($this->_query['condition'])) {
+ $this->raiseError("update: global table update not available
+ do \$do->whereAdd('1=1'); if you really want to do that.
+ ", DB_DATAOBJECT_ERROR_INVALIDARGS);
+ return false;
+ }
+ }
+
+
+
+ // echo " $settings, $this->condition ";
+ if ($settings && isset($this->_query) && $this->_query['condition']) {
+
+ $table = ($quoteIdentifiers ? $DB->quoteIdentifier($this->__table) : $this->__table);
+
+ $r = $this->_query("UPDATE {$table} SET {$settings} {$this->_query['condition']} ");
+
+ // restore original query conditions.
+ $this->_query = $original_query;
+
+ if (PEAR::isError($r)) {
+ $this->raiseError($r);
+ return false;
+ }
+ if ($r < 1) {
+ return 0;
+ }
+
+ $this->_clear_cache();
+ return $r;
+ }
+ // restore original query conditions.
+ $this->_query = $original_query;
+
+ // if you manually specified a dataobject, and there where no changes - then it's ok..
+ if ($dataObject !== false) {
+ return true;
+ }
+
+ $this->raiseError(
+ "update: No Data specifed for query $settings , {$this->_query['condition']}",
+ DB_DATAOBJECT_ERROR_NODATA);
+ return false;
+ }
+
+ /**
+ * Deletes items from table which match current objects variables
+ *
+ * Returns the true on success
+ *
+ * for example
+ *
+ * Designed to be extended
+ *
+ * $object = new mytable();
+ * $object->ID=123;
+ * echo $object->delete(); // builds a conditon
+ *
+ * $object = new mytable();
+ * $object->whereAdd('age > 12');
+ * $object->limit(1);
+ * $object->orderBy('age DESC');
+ * $object->delete(true); // dont use object vars, use the conditions, limit and order.
+ *
+ * @param bool $useWhere (optional) If DB_DATAOBJECT_WHEREADD_ONLY is passed in then
+ * we will build the condition only using the whereAdd's. Default is to
+ * build the condition only using the object parameters.
+ *
+ * @access public
+ * @return mixed True on success, false on failure, 0 on no data affected
+ */
+ function delete($useWhere = false)
+ {
+ global $_DB_DATAOBJECT;
+ // connect will load the config!
+ $this->_connect();
+ $DB = &$_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5];
+ $quoteIdentifiers = !empty($_DB_DATAOBJECT['CONFIG']['quote_identifiers']);
+
+ $extra_cond = ' ' . (isset($this->_query['order_by']) ? $this->_query['order_by'] : '');
+
+ if (!$useWhere) {
+
+ $keys = $this->keys();
+ $this->_query = array(); // as it's probably unset!
+ $this->_query['condition'] = ''; // default behaviour not to use where condition
+ $this->_build_condition($this->table(),$keys);
+ // if primary keys are not set then use data from rest of object.
+ if (!$this->_query['condition']) {
+ $this->_build_condition($this->table(),array(),$keys);
+ }
+ $extra_cond = '';
+ }
+
+
+ // don't delete without a condition
+ if (($this->_query !== false) && $this->_query['condition']) {
+
+ $table = ($quoteIdentifiers ? $DB->quoteIdentifier($this->__table) : $this->__table);
+ $sql = "DELETE FROM {$table} {$this->_query['condition']}{$extra_cond}";
+
+ // add limit..
+
+ if (isset($this->_query['limit_start']) && strlen($this->_query['limit_start'] . $this->_query['limit_count'])) {
+
+ if (!isset($_DB_DATAOBJECT['CONFIG']['db_driver']) ||
+ ($_DB_DATAOBJECT['CONFIG']['db_driver'] == 'DB')) {
+ // pear DB
+ $sql = $DB->modifyLimitQuery($sql,$this->_query['limit_start'], $this->_query['limit_count']);
+
+ } else {
+ // MDB2
+ $DB->setLimit( $this->_query['limit_count'],$this->_query['limit_start']);
+ }
+
+ }
+
+
+ $r = $this->_query($sql);
+
+
+ if (PEAR::isError($r)) {
+ $this->raiseError($r);
+ return false;
+ }
+ if ($r < 1) {
+ return 0;
+ }
+ $this->_clear_cache();
+ return $r;
+ } else {
+ $this->raiseError("delete: No condition specifed for query", DB_DATAOBJECT_ERROR_NODATA);
+ return false;
+ }
+ }
+
+ /**
+ * fetches a specific row into this object variables
+ *
+ * Not recommended - better to use fetch()
+ *
+ * Returens true on success
+ *
+ * @param int $row row
+ * @access public
+ * @return boolean true on success
+ */
+ function fetchRow($row = null)
+ {
+ global $_DB_DATAOBJECT;
+ if (empty($_DB_DATAOBJECT['CONFIG'])) {
+ $this->_loadConfig();
+ }
+ if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
+ $this->debug("{$this->__table} $row of {$this->N}", "fetchrow",3);
+ }
+ if (!$this->__table) {
+ $this->raiseError("fetchrow: No table", DB_DATAOBJECT_ERROR_INVALIDCONFIG);
+ return false;
+ }
+ if ($row === null) {
+ $this->raiseError("fetchrow: No row specified", DB_DATAOBJECT_ERROR_INVALIDARGS);
+ return false;
+ }
+ if (!$this->N) {
+ $this->raiseError("fetchrow: No results avaiable", DB_DATAOBJECT_ERROR_NODATA);
+ return false;
+ }
+ if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
+ $this->debug("{$this->__table} $row of {$this->N}", "fetchrow",3);
+ }
+
+
+ $result = &$_DB_DATAOBJECT['RESULTS'][$this->_DB_resultid];
+ $array = $result->fetchrow(DB_DATAOBJECT_FETCHMODE_ASSOC,$row);
+ if (!is_array($array)) {
+ $this->raiseError("fetchrow: No results available", DB_DATAOBJECT_ERROR_NODATA);
+ return false;
+ }
+
+ foreach($array as $k => $v) {
+ $kk = str_replace(".", "_", $k);
+ if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
+ $this->debug("$kk = ". $array[$k], "fetchrow LINE", 3);
+ }
+ $this->$kk = $array[$k];
+ }
+
+ if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
+ $this->debug("{$this->__table} DONE", "fetchrow", 3);
+ }
+ return true;
+ }
+
+ /**
+ * Find the number of results from a simple query
+ *
+ * for example
+ *
+ * $object = new mytable();
+ * $object->name = "fred";
+ * echo $object->count();
+ * echo $object->count(true); // dont use object vars.
+ * echo $object->count('distinct mycol'); count distinct mycol.
+ * echo $object->count('distinct mycol',true); // dont use object vars.
+ * echo $object->count('distinct'); // count distinct id (eg. the primary key)
+ *
+ *
+ * @param bool|string (optional)
+ * (true|false => see below not on whereAddonly)
+ * (string)
+ * "DISTINCT" => does a distinct count on the tables 'key' column
+ * otherwise => normally it counts primary keys - you can use
+ * this to do things like $do->count('distinct mycol');
+ *
+ * @param bool $whereAddOnly (optional) If DB_DATAOBJECT_WHEREADD_ONLY is passed in then
+ * we will build the condition only using the whereAdd's. Default is to
+ * build the condition using the object parameters as well.
+ *
+ * @access public
+ * @return int
+ */
+ function count($countWhat = false,$whereAddOnly = false)
+ {
+ global $_DB_DATAOBJECT;
+
+ if (is_bool($countWhat)) {
+ $whereAddOnly = $countWhat;
+ }
+
+ $t = clone($this);
+ $items = $t->table();
+
+ $quoteIdentifiers = !empty($_DB_DATAOBJECT['CONFIG']['quote_identifiers']);
+
+
+ if (!isset($t->_query)) {
+ $this->raiseError(
+ "You cannot do run count after you have run fetch()",
+ DB_DATAOBJECT_ERROR_INVALIDARGS);
+ return false;
+ }
+ $this->_connect();
+ $DB = &$_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5];
+
+
+ if (!$whereAddOnly && $items) {
+ $t->_build_condition($items);
+ }
+ $keys = $this->keys();
+
+ if (!$keys[0] && !is_string($countWhat)) {
+ $this->raiseError(
+ "You cannot do run count without keys - use \$do->keys('id');",
+ DB_DATAOBJECT_ERROR_INVALIDARGS,PEAR_ERROR_DIE);
+ return false;
+
+ }
+ $table = ($quoteIdentifiers ? $DB->quoteIdentifier($this->__table) : $this->__table);
+ $key_col = ($quoteIdentifiers ? $DB->quoteIdentifier($keys[0]) : $keys[0]);
+ $as = ($quoteIdentifiers ? $DB->quoteIdentifier('DATAOBJECT_NUM') : 'DATAOBJECT_NUM');
+
+ // support distinct on default keys.
+ $countWhat = (strtoupper($countWhat) == 'DISTINCT') ?
+ "DISTINCT {$table}.{$key_col}" : $countWhat;
+
+ $countWhat = is_string($countWhat) ? $countWhat : "{$table}.{$key_col}";
+
+ $r = $t->_query(
+ "SELECT count({$countWhat}) as $as
+ FROM $table {$t->_join} {$t->_query['condition']}");
+ if (PEAR::isError($r)) {
+ return false;
+ }
+
+ $result = &$_DB_DATAOBJECT['RESULTS'][$t->_DB_resultid];
+ $l = $result->fetchRow(DB_DATAOBJECT_FETCHMODE_ORDERED);
+ // free the results - essential on oracle.
+ $t->free();
+
+ return (int) $l[0];
+ }
+
+ /**
+ * sends raw query to database
+ *
+ * Since _query has to be a private 'non overwriteable method', this is a relay
+ *
+ * @param string $string SQL Query
+ * @access public
+ * @return void or DB_Error
+ */
+ function query($string)
+ {
+ return $this->_query($string);
+ }
+
+
+ /**
+ * an escape wrapper around DB->escapeSimple()
+ * can be used when adding manual queries or clauses
+ * eg.
+ * $object->query("select * from xyz where abc like '". $object->escape($_GET['name']) . "'");
+ *
+ * @param string $string value to be escaped
+ * @param bool $likeEscape escapes % and _ as well. - so like queries can be protected.
+ * @access public
+ * @return string
+ */
+ function escape($string, $likeEscape=false)
+ {
+ global $_DB_DATAOBJECT;
+ $this->_connect();
+ $DB = &$_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5];
+ // mdb2 uses escape...
+ $dd = empty($_DB_DATAOBJECT['CONFIG']['db_driver']) ? 'DB' : $_DB_DATAOBJECT['CONFIG']['db_driver'];
+ $ret = ($dd == 'DB') ? $DB->escapeSimple($string) : $DB->escape($string);
+ if ($likeEscape) {
+ $ret = str_replace(array('_','%'), array('\_','\%'), $ret);
+ }
+ return $ret;
+
+ }
+
+ /* ==================================================== */
+ /* Major Private Vars */
+ /* ==================================================== */
+
+ /**
+ * The Database connection dsn (as described in the PEAR DB)
+ * only used really if you are writing a very simple application/test..
+ * try not to use this - it is better stored in configuration files..
+ *
+ * @access private
+ * @var string
+ */
+ var $_database_dsn = '';
+
+ /**
+ * The Database connection id (md5 sum of databasedsn)
+ *
+ * @access private
+ * @var string
+ */
+ var $_database_dsn_md5 = '';
+
+ /**
+ * The Database name
+ * created in __connection
+ *
+ * @access private
+ * @var string
+ */
+ var $_database = '';
+
+
+
+ /**
+ * The QUERY rules
+ * This replaces alot of the private variables
+ * used to build a query, it is unset after find() is run.
+ *
+ *
+ *
+ * @access private
+ * @var array
+ */
+ var $_query = array(
+ 'condition' => '', // the WHERE condition
+ 'group_by' => '', // the GROUP BY condition
+ 'order_by' => '', // the ORDER BY condition
+ 'having' => '', // the HAVING condition
+ 'limit_start' => '', // the LIMIT condition
+ 'limit_count' => '', // the LIMIT condition
+ 'data_select' => '*', // the columns to be SELECTed
+ );
+
+
+
+
+ /**
+ * Database result id (references global $_DB_DataObject[results]
+ *
+ * @access private
+ * @var integer
+ */
+ var $_DB_resultid;
+
+ /**
+ * ResultFields - on the last call to fetch(), resultfields is sent here,
+ * so we can clean up the memory.
+ *
+ * @access public
+ * @var array
+ */
+ var $_resultFields = false;
+
+
+ /* ============================================================== */
+ /* Table definition layer (started of very private but 'came out'*/
+ /* ============================================================== */
+
+ /**
+ * Autoload or manually load the table definitions
+ *
+ *
+ * usage :
+ * DB_DataObject::databaseStructure( 'databasename',
+ * parse_ini_file('mydb.ini',true),
+ * parse_ini_file('mydb.link.ini',true));
+ *
+ * obviously you dont have to use ini files.. (just return array similar to ini files..)
+ *
+ * It should append to the table structure array
+ *
+ *
+ * @param optional string name of database to assign / read
+ * @param optional array structure of database, and keys
+ * @param optional array table links
+ *
+ * @access public
+ * @return true or PEAR:error on wrong paramenters.. or false if no file exists..
+ * or the array(tablename => array(column_name=>type)) if called with 1 argument.. (databasename)
+ */
+ function databaseStructure()
+ {
+
+ global $_DB_DATAOBJECT;
+
+ // Assignment code
+
+ if ($args = func_get_args()) {
+
+ if (count($args) == 1) {
+
+ // this returns all the tables and their structure..
+ if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
+ $this->debug("Loading Generator as databaseStructure called with args",1);
+ }
+
+ $x = new DB_DataObject;
+ $x->_database = $args[0];
+ $this->_connect();
+ $DB = &$_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5];
+
+ $tables = $DB->getListOf('tables');
+ class_exists('DB_DataObject_Generator') ? '' :
+ require_once 'DB/DataObject/Generator.php';
+
+ foreach($tables as $table) {
+ $y = new DB_DataObject_Generator;
+ $y->fillTableSchema($x->_database,$table);
+ }
+ return $_DB_DATAOBJECT['INI'][$x->_database];
+ } else {
+
+ $_DB_DATAOBJECT['INI'][$args[0]] = isset($_DB_DATAOBJECT['INI'][$args[0]]) ?
+ $_DB_DATAOBJECT['INI'][$args[0]] + $args[1] : $args[1];
+
+ if (isset($args[1])) {
+ $_DB_DATAOBJECT['LINKS'][$args[0]] = isset($_DB_DATAOBJECT['LINKS'][$args[0]]) ?
+ $_DB_DATAOBJECT['LINKS'][$args[0]] + $args[2] : $args[2];
+ }
+ return true;
+ }
+
+ }
+
+
+
+ if (!$this->_database) {
+ $this->_connect();
+ }
+
+ // loaded already?
+ if (!empty($_DB_DATAOBJECT['INI'][$this->_database])) {
+
+ // database loaded - but this is table is not available..
+ if (
+ empty($_DB_DATAOBJECT['INI'][$this->_database][$this->__table])
+ && !empty($_DB_DATAOBJECT['CONFIG']['proxy'])
+ ) {
+ if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
+ $this->debug("Loading Generator to fetch Schema",1);
+ }
+ class_exists('DB_DataObject_Generator') ? '' :
+ require_once 'DB/DataObject/Generator.php';
+
+
+ $x = new DB_DataObject_Generator;
+ $x->fillTableSchema($this->_database,$this->__table);
+ }
+ return true;
+ }
+
+
+ if (empty($_DB_DATAOBJECT['CONFIG'])) {
+ DB_DataObject::_loadConfig();
+ }
+
+ // if you supply this with arguments, then it will take those
+ // as the database and links array...
+
+ $schemas = isset($_DB_DATAOBJECT['CONFIG']['schema_location']) ?
+ array("{$_DB_DATAOBJECT['CONFIG']['schema_location']}/{$this->_database}.ini") :
+ array() ;
+
+ if (isset($_DB_DATAOBJECT['CONFIG']["ini_{$this->_database}"])) {
+ $schemas = is_array($_DB_DATAOBJECT['CONFIG']["ini_{$this->_database}"]) ?
+ $_DB_DATAOBJECT['CONFIG']["ini_{$this->_database}"] :
+ explode(PATH_SEPARATOR,$_DB_DATAOBJECT['CONFIG']["ini_{$this->_database}"]);
+ }
+
+
+
+ foreach ($schemas as $ini) {
+ if (file_exists($ini) && is_file($ini)) {
+ $_DB_DATAOBJECT['INI'][$this->_database] = parse_ini_file($ini, true);
+ if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
+ if (!is_readable ($ini)) {
+ $this->debug("ini file is not readable: $ini","databaseStructure",1);
+ } else {
+ $this->debug("Loaded ini file: $ini","databaseStructure",1);
+ }
+ }
+ } else {
+ if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
+ $this->debug("Missing ini file: $ini","databaseStructure",1);
+ }
+ }
+
+ }
+ // now have we loaded the structure..
+
+ if (!empty($_DB_DATAOBJECT['INI'][$this->_database][$this->__table])) {
+ return true;
+ }
+ // - if not try building it..
+ if (!empty($_DB_DATAOBJECT['CONFIG']['proxy'])) {
+ class_exists('DB_DataObject_Generator') ? '' :
+ require_once 'DB/DataObject/Generator.php';
+
+ $x = new DB_DataObject_Generator;
+ $x->fillTableSchema($this->_database,$this->__table);
+ // should this fail!!!???
+ return true;
+ }
+ $this->debug("Cant find database schema: {$this->_database}/{$this->__table} \n".
+ "in links file data: " . print_r($_DB_DATAOBJECT['INI'],true),"databaseStructure",5);
+ // we have to die here!! - it causes chaos if we dont (including looping forever!)
+ $this->raiseError( "Unable to load schema for database and table (turn debugging up to 5 for full error message)", DB_DATAOBJECT_ERROR_INVALIDARGS, PEAR_ERROR_DIE);
+ return false;
+
+
+ }
+
+
+
+
+ /**
+ * Return or assign the name of the current table
+ *
+ *
+ * @param string optinal table name to set
+ * @access public
+ * @return string The name of the current table
+ */
+ function tableName()
+ {
+ $args = func_get_args();
+ if (count($args)) {
+ $this->__table = $args[0];
+ }
+ return $this->__table;
+ }
+
+ /**
+ * Return or assign the name of the current database
+ *
+ * @param string optional database name to set
+ * @access public
+ * @return string The name of the current database
+ */
+ function database()
+ {
+ $args = func_get_args();
+ if (count($args)) {
+ $this->_database = $args[0];
+ }
+ return $this->_database;
+ }
+
+ /**
+ * get/set an associative array of table columns
+ *
+ * @access public
+ * @param array key=>type array
+ * @return array (associative)
+ */
+ function table()
+ {
+
+ // for temporary storage of database fields..
+ // note this is not declared as we dont want to bloat the print_r output
+ $args = func_get_args();
+ if (count($args)) {
+ $this->_database_fields = $args[0];
+ }
+ if (isset($this->_database_fields)) {
+ return $this->_database_fields;
+ }
+
+
+ global $_DB_DATAOBJECT;
+ if (!isset($_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5])) {
+ $this->_connect();
+ }
+
+ if (isset($_DB_DATAOBJECT['INI'][$this->_database][$this->__table])) {
+ return $_DB_DATAOBJECT['INI'][$this->_database][$this->__table];
+ }
+
+ $this->databaseStructure();
+
+
+ $ret = array();
+ if (isset($_DB_DATAOBJECT['INI'][$this->_database][$this->__table])) {
+ $ret = $_DB_DATAOBJECT['INI'][$this->_database][$this->__table];
+ }
+
+ return $ret;
+ }
+
+ /**
+ * get/set an array of table primary keys
+ *
+ * set usage: $do->keys('id','code');
+ *
+ * This is defined in the table definition if it gets it wrong,
+ * or you do not want to use ini tables, you can override this.
+ * @param string optional set the key
+ * @param * optional set more keys
+ * @access private
+ * @return array
+ */
+ function keys()
+ {
+ // for temporary storage of database fields..
+ // note this is not declared as we dont want to bloat the print_r output
+ $args = func_get_args();
+ if (count($args)) {
+ $this->_database_keys = $args;
+ }
+ if (isset($this->_database_keys)) {
+ return $this->_database_keys;
+ }
+
+ global $_DB_DATAOBJECT;
+ if (!isset($_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5])) {
+ $this->_connect();
+ }
+ if (isset($_DB_DATAOBJECT['INI'][$this->_database][$this->__table."__keys"])) {
+ return array_keys($_DB_DATAOBJECT['INI'][$this->_database][$this->__table."__keys"]);
+ }
+ $this->databaseStructure();
+
+ if (isset($_DB_DATAOBJECT['INI'][$this->_database][$this->__table."__keys"])) {
+ return array_keys($_DB_DATAOBJECT['INI'][$this->_database][$this->__table."__keys"]);
+ }
+ return array();
+ }
+ /**
+ * get/set an sequence key
+ *
+ * by default it returns the first key from keys()
+ * set usage: $do->sequenceKey('id',true);
+ *
+ * override this to return array(false,false) if table has no real sequence key.
+ *
+ * @param string optional the key sequence/autoinc. key
+ * @param boolean optional use native increment. default false
+ * @param false|string optional native sequence name
+ * @access private
+ * @return array (column,use_native,sequence_name)
+ */
+ function sequenceKey()
+ {
+ global $_DB_DATAOBJECT;
+
+ // call setting
+ if (!$this->_database) {
+ $this->_connect();
+ }
+
+ if (!isset($_DB_DATAOBJECT['SEQUENCE'][$this->_database])) {
+ $_DB_DATAOBJECT['SEQUENCE'][$this->_database] = array();
+ }
+
+
+ $args = func_get_args();
+ if (count($args)) {
+ $args[1] = isset($args[1]) ? $args[1] : false;
+ $args[2] = isset($args[2]) ? $args[2] : false;
+ $_DB_DATAOBJECT['SEQUENCE'][$this->_database][$this->__table] = $args;
+ }
+ if (isset($_DB_DATAOBJECT['SEQUENCE'][$this->_database][$this->__table])) {
+ return $_DB_DATAOBJECT['SEQUENCE'][$this->_database][$this->__table];
+ }
+ // end call setting (eg. $do->sequenceKeys(a,b,c); )
+
+
+
+
+ $keys = $this->keys();
+ if (!$keys) {
+ return $_DB_DATAOBJECT['SEQUENCE'][$this->_database][$this->__table]
+ = array(false,false,false);
+ }
+
+
+ $table = isset($_DB_DATAOBJECT['INI'][$this->_database][$this->__table]) ?
+ $_DB_DATAOBJECT['INI'][$this->_database][$this->__table] : $this->table();
+
+ $dbtype = $_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5]->dsn['phptype'];
+
+ $usekey = $keys[0];
+
+
+
+ $seqname = false;
+
+ if (!empty($_DB_DATAOBJECT['CONFIG']['sequence_'.$this->__table])) {
+ $usekey = $_DB_DATAOBJECT['CONFIG']['sequence_'.$this->__table];
+ if (strpos($usekey,':') !== false) {
+ list($usekey,$seqname) = explode(':',$usekey);
+ }
+ }
+
+
+ // if the key is not an integer - then it's not a sequence or native
+ if (empty($table[$usekey]) || !($table[$usekey] & DB_DATAOBJECT_INT)) {
+ return $_DB_DATAOBJECT['SEQUENCE'][$this->_database][$this->__table] = array(false,false,false);
+ }
+
+
+ if (!empty($_DB_DATAOBJECT['CONFIG']['ignore_sequence_keys'])) {
+ $ignore = $_DB_DATAOBJECT['CONFIG']['ignore_sequence_keys'];
+ if (is_string($ignore) && (strtoupper($ignore) == 'ALL')) {
+ return $_DB_DATAOBJECT['SEQUENCE'][$this->_database][$this->__table] = array(false,false,$seqname);
+ }
+ if (is_string($ignore)) {
+ $ignore = $_DB_DATAOBJECT['CONFIG']['ignore_sequence_keys'] = explode(',',$ignore);
+ }
+ if (in_array($this->__table,$ignore)) {
+ return $_DB_DATAOBJECT['SEQUENCE'][$this->_database][$this->__table] = array(false,false,$seqname);
+ }
+ }
+
+
+ $realkeys = $_DB_DATAOBJECT['INI'][$this->_database][$this->__table."__keys"];
+
+ // if you are using an old ini file - go back to old behaviour...
+ if (is_numeric($realkeys[$usekey])) {
+ $realkeys[$usekey] = 'N';
+ }
+
+ // multiple unique primary keys without a native sequence...
+ if (($realkeys[$usekey] == 'K') && (count($keys) > 1)) {
+ return $_DB_DATAOBJECT['SEQUENCE'][$this->_database][$this->__table] = array(false,false,$seqname);
+ }
+ // use native sequence keys...
+ // technically postgres native here...
+ // we need to get the new improved tabledata sorted out first.
+
+ if ( in_array($dbtype , array('psql', 'mysql', 'mysqli', 'mssql', 'ifx')) &&
+ ($table[$usekey] & DB_DATAOBJECT_INT) &&
+ isset($realkeys[$usekey]) && ($realkeys[$usekey] == 'N')
+ ) {
+ return $_DB_DATAOBJECT['SEQUENCE'][$this->_database][$this->__table] = array($usekey,true,$seqname);
+ }
+ // if not a native autoinc, and we have not assumed all primary keys are sequence
+ if (($realkeys[$usekey] != 'N') &&
+ !empty($_DB_DATAOBJECT['CONFIG']['dont_use_pear_sequences'])) {
+ return array(false,false,false);
+ }
+ // I assume it's going to try and be a nextval DB sequence.. (not native)
+ return $_DB_DATAOBJECT['SEQUENCE'][$this->_database][$this->__table] = array($usekey,false,$seqname);
+ }
+
+
+
+ /* =========================================================== */
+ /* Major Private Methods - the core part! */
+ /* =========================================================== */
+
+
+
+ /**
+ * clear the cache values for this class - normally done on insert/update etc.
+ *
+ * @access private
+ * @return void
+ */
+ function _clear_cache()
+ {
+ global $_DB_DATAOBJECT;
+
+ $class = strtolower(get_class($this));
+
+ if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
+ $this->debug("Clearing Cache for ".$class,1);
+ }
+
+ if (!empty($_DB_DATAOBJECT['CACHE'][$class])) {
+ unset($_DB_DATAOBJECT['CACHE'][$class]);
+ }
+ }
+
+
+ /**
+ * backend wrapper for quoting, as MDB2 and DB do it differently...
+ *
+ * @access private
+ * @return string quoted
+ */
+
+ function _quote($str)
+ {
+ global $_DB_DATAOBJECT;
+ return (empty($_DB_DATAOBJECT['CONFIG']['db_driver']) ||
+ ($_DB_DATAOBJECT['CONFIG']['db_driver'] == 'DB'))
+ ? $_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5]->quoteSmart($str)
+ : $_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5]->quote($str);
+ }
+
+
+ /**
+ * connects to the database
+ *
+ *
+ * TODO: tidy this up - This has grown to support a number of connection options like
+ * a) dynamic changing of ini file to change which database to connect to
+ * b) multi data via the table_{$table} = dsn ini option
+ * c) session based storage.
+ *
+ * @access private
+ * @return true | PEAR::error
+ */
+ function _connect()
+ {
+ global $_DB_DATAOBJECT;
+ if (empty($_DB_DATAOBJECT['CONFIG'])) {
+ $this->_loadConfig();
+ }
+ // Set database driver for reference
+ $db_driver = empty($_DB_DATAOBJECT['CONFIG']['db_driver']) ? 'DB' : $_DB_DATAOBJECT['CONFIG']['db_driver'];
+ // is it already connected ?
+
+ if ($this->_database_dsn_md5 && !empty($_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5])) {
+ if (PEAR::isError($_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5])) {
+ return $this->raiseError(
+ $_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5]->message,
+ $_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5]->code, PEAR_ERROR_DIE
+ );
+
+ }
+
+ if (!$this->_database) {
+ $this->_database = $_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5]->dsn['database'];
+ $hasGetDatabase = method_exists($_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5], 'getDatabase');
+
+ $this->_database = ($db_driver != 'DB' && $hasGetDatabase)
+ ? $_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5]->getDatabase()
+ : $_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5]->dsn['database'];
+
+
+
+ if (($_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5]->dsn['phptype'] == 'sqlite')
+ && is_file($this->_database)) {
+ $this->_database = basename($this->_database);
+ }
+ if ($_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5]->dsn['phptype'] == 'ibase') {
+ $this->_database = substr(basename($this->_database), 0, -4);
+ }
+
+ }
+ // theoretically we have a md5, it's listed in connections and it's not an error.
+ // so everything is ok!
+ return true;
+
+ }
+
+ // it's not currently connected!
+ // try and work out what to use for the dsn !
+
+ $options= &$_DB_DATAOBJECT['CONFIG'];
+ $dsn = isset($this->_database_dsn) ? $this->_database_dsn : null;
+
+ if (!$dsn) {
+ if (!$this->_database) {
+ $this->_database = isset($options["table_{$this->__table}"]) ? $options["table_{$this->__table}"] : null;
+ }
+ if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
+ $this->debug("Checking for database database_{$this->_database} in options","CONNECT");
+ }
+
+ if ($this->_database && !empty($options["database_{$this->_database}"])) {
+
+ $dsn = $options["database_{$this->_database}"];
+ } else if (!empty($options['database'])) {
+ $dsn = $options['database'];
+ }
+ }
+
+ // if still no database...
+ if (!$dsn) {
+ return $this->raiseError(
+ "No database name / dsn found anywhere",
+ DB_DATAOBJECT_ERROR_INVALIDCONFIG, PEAR_ERROR_DIE
+ );
+
+ }
+
+
+ if (is_string($dsn)) {
+ $this->_database_dsn_md5 = md5($dsn);
+ } else {
+ /// support array based dsn's
+ $this->_database_dsn_md5 = md5(serialize($dsn));
+ }
+
+ if (!empty($_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5])) {
+ if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
+ $this->debug("USING CACHED CONNECTION", "CONNECT",3);
+ }
+ if (!$this->_database) {
+
+ $hasGetDatabase = method_exists($_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5], 'getDatabase');
+ $this->_database = ($db_driver != 'DB' && $hasGetDatabase)
+ ? $_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5]->getDatabase()
+ : $_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5]->dsn['database'];
+
+ if (($_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5]->dsn['phptype'] == 'sqlite')
+ && is_file($this->_database))
+ {
+ $this->_database = basename($this->_database);
+ }
+ }
+ return true;
+ }
+ if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
+ $this->debug("NEW CONNECTION", "CONNECT",3);
+ /* actualy make a connection */
+ $this->debug(print_r($dsn,true) ." {$this->_database_dsn_md5}", "CONNECT",3);
+ }
+
+ // Note this is verbose deliberatly!
+
+ if ($db_driver == 'DB') {
+
+ /* PEAR DB connect */
+
+ // this allows the setings of compatibility on DB
+ $db_options = PEAR::getStaticProperty('DB','options');
+ require_once 'DB.php';
+ if ($db_options) {
+ $_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5] = &DB::connect($dsn,$db_options);
+ } else {
+ $_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5] = &DB::connect($dsn);
+ }
+
+ } else {
+ /* assumption is MDB2 */
+ require_once 'MDB2.php';
+ // this allows the setings of compatibility on MDB2
+ $db_options = PEAR::getStaticProperty('MDB2','options');
+ $db_options = is_array($db_options) ? $db_options : array();
+ $db_options['portability'] = isset($db_options['portability'] )
+ ? $db_options['portability'] : MDB2_PORTABILITY_ALL ^ MDB2_PORTABILITY_FIX_CASE;
+ $_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5] = &MDB2::connect($dsn,$db_options);
+
+ }
+
+
+ if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
+ $this->debug(serialize($_DB_DATAOBJECT['CONNECTIONS']), "CONNECT",5);
+ }
+ if (PEAR::isError($_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5])) {
+ $this->debug($_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5]->toString(), "CONNECT FAILED",5);
+ return $this->raiseError(
+ "Connect failed, turn on debugging to 5 see why",
+ $_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5]->code, PEAR_ERROR_DIE
+ );
+
+ }
+
+ if (!$this->_database) {
+ $hasGetDatabase = method_exists($_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5], 'getDatabase');
+
+ $this->_database = ($db_driver != 'DB' && $hasGetDatabase)
+ ? $_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5]->getDatabase()
+ : $_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5]->dsn['database'];
+
+
+ if (($_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5]->dsn['phptype'] == 'sqlite')
+ && is_file($this->_database))
+ {
+ $this->_database = basename($this->_database);
+ }
+ }
+
+ // Oracle need to optimize for portibility - not sure exactly what this does though :)
+ $c = &$_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5];
+
+ return true;
+ }
+
+ /**
+ * sends query to database - this is the private one that must work
+ * - internal functions use this rather than $this->query()
+ *
+ * @param string $string
+ * @access private
+ * @return mixed none or PEAR_Error
+ */
+ function _query($string)
+ {
+ global $_DB_DATAOBJECT;
+ $this->_connect();
+
+
+ $DB = &$_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5];
+
+ $options = &$_DB_DATAOBJECT['CONFIG'];
+
+ $_DB_driver = empty($_DB_DATAOBJECT['CONFIG']['db_driver']) ?
+ 'DB': $_DB_DATAOBJECT['CONFIG']['db_driver'];
+
+ if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
+ $this->debug($string,$log="QUERY");
+
+ }
+
+ if (strtoupper($string) == 'BEGIN') {
+ if ($_DB_driver == 'DB') {
+ $DB->autoCommit(false);
+ } else {
+ $DB->beginTransaction();
+ }
+ // db backend adds begin anyway from now on..
+ return true;
+ }
+ if (strtoupper($string) == 'COMMIT') {
+ $res = $DB->commit();
+ if ($_DB_driver == 'DB') {
+ $DB->autoCommit(true);
+ }
+ return $res;
+ }
+
+ if (strtoupper($string) == 'ROLLBACK') {
+ $DB->rollback();
+ if ($_DB_driver == 'DB') {
+ $DB->autoCommit(true);
+ }
+ return true;
+ }
+
+
+ if (!empty($options['debug_ignore_updates']) &&
+ (strtolower(substr(trim($string), 0, 6)) != 'select') &&
+ (strtolower(substr(trim($string), 0, 4)) != 'show') &&
+ (strtolower(substr(trim($string), 0, 8)) != 'describe')) {
+
+ $this->debug('Disabling Update as you are in debug mode');
+ return $this->raiseError("Disabling Update as you are in debug mode", null) ;
+
+ }
+ //if (@$_DB_DATAOBJECT['CONFIG']['debug'] > 1) {
+ // this will only work when PEAR:DB supports it.
+ //$this->debug($DB->getAll('explain ' .$string,DB_DATAOBJECT_FETCHMODE_ASSOC), $log="sql",2);
+ //}
+
+ // some sim
+ $t= explode(' ',microtime());
+ $_DB_DATAOBJECT['QUERYENDTIME'] = $time = $t[0]+$t[1];
+
+
+ if ($_DB_driver == 'DB') {
+ $result = $DB->query($string);
+ } else {
+ switch (strtolower(substr(trim($string),0,6))) {
+
+ case 'insert':
+ case 'update':
+ case 'delete':
+ $result = $DB->exec($string);
+ break;
+
+ default:
+ $result = $DB->query($string);
+ break;
+ }
+ }
+
+
+
+ if (is_a($result,'PEAR_Error')) {
+ if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
+ $this->debug($result->toString(), "Query Error",1 );
+ }
+ return $this->raiseError($result);
+ }
+ if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
+ $t= explode(' ',microtime());
+ $_DB_DATAOBJECT['QUERYENDTIME'] = $t[0]+$t[1];
+ $this->debug('QUERY DONE IN '.($t[0]+$t[1]-$time)." seconds", 'query',1);
+ }
+ switch (strtolower(substr(trim($string),0,6))) {
+ case 'insert':
+ case 'update':
+ case 'delete':
+ if ($_DB_driver == 'DB') {
+ // pear DB specific
+ return $DB->affectedRows();
+ }
+ return $result;
+ }
+ if (is_object($result)) {
+ // lets hope that copying the result object is OK!
+
+ $_DB_resultid = $GLOBALS['_DB_DATAOBJECT']['RESULTSEQ']++;
+ $_DB_DATAOBJECT['RESULTS'][$_DB_resultid] = $result;
+ $this->_DB_resultid = $_DB_resultid;
+ }
+ $this->N = 0;
+ if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
+ $this->debug(serialize($result), 'RESULT',5);
+ }
+ if (method_exists($result, 'numrows')) {
+ if ($_DB_driver == 'DB') {
+ $DB->expectError(DB_ERROR_UNSUPPORTED);
+ } else {
+ $DB->expectError(MDB2_ERROR_UNSUPPORTED);
+ }
+ $this->N = $result->numrows();
+ if (is_a($this->N,'PEAR_Error')) {
+ $this->N = true;
+ }
+ $DB->popExpect();
+ }
+ }
+
+ /**
+ * Builds the WHERE based on the values of of this object
+ *
+ * @param mixed $keys
+ * @param array $filter (used by update to only uses keys in this filter list).
+ * @param array $negative_filter (used by delete to prevent deleting using the keys mentioned..)
+ * @access private
+ * @return string
+ */
+ function _build_condition($keys, $filter = array(),$negative_filter=array())
+ {
+ global $_DB_DATAOBJECT;
+ $this->_connect();
+ $DB = &$_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5];
+
+ $quoteIdentifiers = !empty($_DB_DATAOBJECT['CONFIG']['quote_identifiers']);
+ $options = $_DB_DATAOBJECT['CONFIG'];
+
+ // if we dont have query vars.. - reset them.
+ if ($this->_query === false) {
+ $x = new DB_DataObject;
+ $this->_query= $x->_query;
+ }
+
+ foreach($keys as $k => $v) {
+ // index keys is an indexed array
+ /* these filter checks are a bit suspicious..
+ - need to check that update really wants to work this way */
+
+ if ($filter) {
+ if (!in_array($k, $filter)) {
+ continue;
+ }
+ }
+ if ($negative_filter) {
+ if (in_array($k, $negative_filter)) {
+ continue;
+ }
+ }
+ if (!isset($this->$k)) {
+ continue;
+ }
+
+ $kSql = $quoteIdentifiers
+ ? ( $DB->quoteIdentifier($this->__table) . '.' . $DB->quoteIdentifier($k) )
+ : "{$this->__table}.{$k}";
+
+
+
+ if (is_a($this->$k,'DB_DataObject_Cast')) {
+ $dbtype = $DB->dsn["phptype"];
+ $value = $this->$k->toString($v,$DB);
+ if (PEAR::isError($value)) {
+ $this->raiseError($value->getMessage() ,DB_DATAOBJECT_ERROR_INVALIDARG);
+ return false;
+ }
+ if ((strtolower($value) === 'null') && !($v & DB_DATAOBJECT_NOTNULL)) {
+ $this->whereAdd(" $kSql IS NULL");
+ continue;
+ }
+ $this->whereAdd(" $kSql = $value");
+ continue;
+ }
+
+ if (!isset($options['disable_null_strings']) && (strtolower($this->$k) === 'null') && !($v & DB_DATAOBJECT_NOTNULL)) {
+ $this->whereAdd(" $kSql IS NULL");
+ continue;
+ }
+
+
+ if ($v & DB_DATAOBJECT_STR) {
+ $this->whereAdd(" $kSql = " . $this->_quote((string) (
+ ($v & DB_DATAOBJECT_BOOL) ?
+ // this is thanks to the braindead idea of postgres to
+ // use t/f for boolean.
+ (($this->$k === 'f') ? 0 : (int)(bool) $this->$k) :
+ $this->$k
+ )) );
+ continue;
+ }
+ if (is_numeric($this->$k)) {
+ $this->whereAdd(" $kSql = {$this->$k}");
+ continue;
+ }
+ /* this is probably an error condition! */
+ $this->whereAdd(" $kSql = ".intval($this->$k));
+ }
+ }
+
+ /**
+ * autoload Class relating to a table
+ * (depreciated - use ::factory)
+ *
+ * @param string $table table
+ * @access private
+ * @return string classname on Success
+ */
+ function staticAutoloadTable($table)
+ {
+ global $_DB_DATAOBJECT;
+ if (empty($_DB_DATAOBJECT['CONFIG'])) {
+ DB_DataObject::_loadConfig();
+ }
+ $p = isset($_DB_DATAOBJECT['CONFIG']['class_prefix']) ?
+ $_DB_DATAOBJECT['CONFIG']['class_prefix'] : '';
+ $class = $p . preg_replace('/[^A-Z0-9]/i','_',ucfirst($table));
+
+ $ce = substr(phpversion(),0,1) > 4 ? class_exists($class,false) : class_exists($class);
+ $class = $ce ? $class : DB_DataObject::_autoloadClass($class);
+ return $class;
+ }
+
+
+ /**
+ * classic factory method for loading a table class
+ * usage: $do = DB_DataObject::factory('person')
+ * WARNING - this may emit a include error if the file does not exist..
+ * use @ to silence it (if you are sure it is acceptable)
+ * eg. $do = @DB_DataObject::factory('person')
+ *
+ * table name will eventually be databasename/table
+ * - and allow modular dataobjects to be written..
+ * (this also helps proxy creation)
+ *
+ *
+ * @param string $table tablename (use blank to create a new instance of the same class.)
+ * @access private
+ * @return DataObject|PEAR_Error
+ */
+
+
+
+ function factory($table = '') {
+ global $_DB_DATAOBJECT;
+ if (empty($_DB_DATAOBJECT['CONFIG'])) {
+ DB_DataObject::_loadConfig();
+ }
+
+ if ($table === '') {
+ if (is_a($this,'DB_DataObject') && strlen($this->__table)) {
+ $table = $this->__table;
+ } else {
+ return DB_DataObject::raiseError(
+ "factory did not recieve a table name",
+ DB_DATAOBJECT_ERROR_INVALIDARGS);
+ }
+ }
+
+
+ $p = isset($_DB_DATAOBJECT['CONFIG']['class_prefix']) ?
+ $_DB_DATAOBJECT['CONFIG']['class_prefix'] : '';
+ $class = $p . preg_replace('/[^A-Z0-9]/i','_',ucfirst($table));
+
+ $ce = substr(phpversion(),0,1) > 4 ? class_exists($class,false) : class_exists($class);
+ $class = $ce ? $class : DB_DataObject::_autoloadClass($class);
+
+ // proxy = full|light
+ if (!$class && isset($_DB_DATAOBJECT['CONFIG']['proxy'])) {
+ $proxyMethod = 'getProxy'.$_DB_DATAOBJECT['CONFIG']['proxy'];
+ class_exists('DB_DataObject_Generator') ? '' :
+ require_once 'DB/DataObject/Generator.php';
+
+ $d = new DB_DataObject;
+
+ $d->__table = $table;
+ if (is_a($ret = $d->_connect(), 'PEAR_Error')) {
+ return $ret;
+ }
+
+ $x = new DB_DataObject_Generator;
+ return $x->$proxyMethod( $d->_database, $table);
+ }
+
+ if (!$class) {
+ return DB_DataObject::raiseError(
+ "factory could not find class $class from $table",
+ DB_DATAOBJECT_ERROR_INVALIDCONFIG);
+ }
+
+ return new $class;
+ }
+ /**
+ * autoload Class
+ *
+ * @param string $class Class
+ * @access private
+ * @return string classname on Success
+ */
+ function _autoloadClass($class)
+ {
+ global $_DB_DATAOBJECT;
+
+ if (empty($_DB_DATAOBJECT['CONFIG'])) {
+ DB_DataObject::_loadConfig();
+ }
+ $class_prefix = empty($_DB_DATAOBJECT['CONFIG']['class_prefix']) ?
+ '' : $_DB_DATAOBJECT['CONFIG']['class_prefix'];
+
+ $table = substr($class,strlen($class_prefix));
+
+ // only include the file if it exists - and barf badly if it has parse errors :)
+ if (!empty($_DB_DATAOBJECT['CONFIG']['proxy']) || empty($_DB_DATAOBJECT['CONFIG']['class_location'])) {
+ return false;
+ }
+
+
+ if (strpos($_DB_DATAOBJECT['CONFIG']['class_location'],'%s') !== false) {
+ $file = sprintf($_DB_DATAOBJECT['CONFIG']['class_location'], preg_replace('/[^A-Z0-9]/i','_',ucfirst($table)));
+ } else {
+ $file = $_DB_DATAOBJECT['CONFIG']['class_location'].'/'.preg_replace('/[^A-Z0-9]/i','_',ucfirst($table)).".php";
+ }
+
+ if (!file_exists($file)) {
+ $found = false;
+ foreach(explode(PATH_SEPARATOR, ini_get('include_path')) as $p) {
+ if (file_exists("$p/$file")) {
+ $file = "$p/$file";
+ $found = true;
+ break;
+ }
+ }
+ if (!$found) {
+ DB_DataObject::raiseError(
+ "autoload:Could not find class {$class} using class_location value",
+ DB_DATAOBJECT_ERROR_INVALIDCONFIG);
+ return false;
+ }
+ }
+
+ include_once $file;
+
+
+ $ce = substr(phpversion(),0,1) > 4 ? class_exists($class,false) : class_exists($class);
+
+ if (!$ce) {
+ DB_DataObject::raiseError(
+ "autoload:Could not autoload {$class}",
+ DB_DATAOBJECT_ERROR_INVALIDCONFIG);
+ return false;
+ }
+ return $class;
+ }
+
+
+
+ /**
+ * Have the links been loaded?
+ * if they have it contains a array of those variables.
+ *
+ * @access private
+ * @var boolean | array
+ */
+ var $_link_loaded = false;
+
+ /**
+ * Get the links associate array as defined by the links.ini file.
+ *
+ *
+ * Experimental... -
+ * Should look a bit like
+ * [local_col_name] => "related_tablename:related_col_name"
+ *
+ *
+ * @return array|null
+ * array = if there are links defined for this table.
+ * empty array - if there is a links.ini file, but no links on this table
+ * null - if no links.ini exists for this database (hence try auto_links).
+ * @access public
+ * @see DB_DataObject::getLinks(), DB_DataObject::getLink()
+ */
+
+ function links()
+ {
+ global $_DB_DATAOBJECT;
+ if (empty($_DB_DATAOBJECT['CONFIG'])) {
+ $this->_loadConfig();
+ }
+ // have to connect.. -> otherwise things break later.
+ $this->_connect();
+
+ if (isset($_DB_DATAOBJECT['LINKS'][$this->_database][$this->__table])) {
+ return $_DB_DATAOBJECT['LINKS'][$this->_database][$this->__table];
+ }
+
+
+
+
+
+ // attempt to load links file here..
+
+ if (!isset($_DB_DATAOBJECT['LINKS'][$this->_database])) {
+ $schemas = isset($_DB_DATAOBJECT['CONFIG']['schema_location']) ?
+ array("{$_DB_DATAOBJECT['CONFIG']['schema_location']}/{$this->_database}.ini") :
+ array() ;
+
+ if (isset($_DB_DATAOBJECT['CONFIG']["ini_{$this->_database}"])) {
+ $schemas = is_array($_DB_DATAOBJECT['CONFIG']["ini_{$this->_database}"]) ?
+ $_DB_DATAOBJECT['CONFIG']["ini_{$this->_database}"] :
+ explode(PATH_SEPARATOR,$_DB_DATAOBJECT['CONFIG']["ini_{$this->_database}"]);
+ }
+
+
+
+ foreach ($schemas as $ini) {
+
+ $links =
+ isset($_DB_DATAOBJECT['CONFIG']["links_{$this->_database}"]) ?
+ $_DB_DATAOBJECT['CONFIG']["links_{$this->_database}"] :
+ str_replace('.ini','.links.ini',$ini);
+
+ if (empty($_DB_DATAOBJECT['LINKS'][$this->_database]) && file_exists($links) && is_file($links)) {
+ /* not sure why $links = ... here - TODO check if that works */
+ $_DB_DATAOBJECT['LINKS'][$this->_database] = parse_ini_file($links, true);
+ if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
+ $this->debug("Loaded links.ini file: $links","links",1);
+ }
+ } else {
+ if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
+ $this->debug("Missing links.ini file: $links","links",1);
+ }
+ }
+ }
+ }
+
+
+ // if there is no link data at all on the file!
+ // we return null.
+ if (!isset($_DB_DATAOBJECT['LINKS'][$this->_database])) {
+ return null;
+ }
+
+ if (isset($_DB_DATAOBJECT['LINKS'][$this->_database][$this->__table])) {
+ return $_DB_DATAOBJECT['LINKS'][$this->_database][$this->__table];
+ }
+
+ return array();
+ }
+ /**
+ * load related objects
+ *
+ * There are two ways to use this, one is to set up a <dbname>.links.ini file
+ * into a static property named <dbname>.links and specifies the table joins,
+ * the other highly dependent on naming columns 'correctly' :)
+ * using colname = xxxxx_yyyyyy
+ * xxxxxx = related table; (yyyyy = user defined..)
+ * looks up table xxxxx, for value id=$this->xxxxx
+ * stores it in $this->_xxxxx_yyyyy
+ * you can change what object vars the links are stored in by
+ * changeing the format parameter
+ *
+ *
+ * @param string format (default _%s) where %s is the table name.
+ * @author Tim White <tim@cyface.com>
+ * @access public
+ * @return boolean , true on success
+ */
+ function getLinks($format = '_%s')
+ {
+
+ // get table will load the options.
+ if ($this->_link_loaded) {
+ return true;
+ }
+ $this->_link_loaded = false;
+ $cols = $this->table();
+ $links = $this->links();
+
+ $loaded = array();
+
+ if ($links) {
+ foreach($links as $key => $match) {
+ list($table,$link) = explode(':', $match);
+ $k = sprintf($format, str_replace('.', '_', $key));
+ // makes sure that '.' is the end of the key;
+ if ($p = strpos($key,'.')) {
+ $key = substr($key, 0, $p);
+ }
+
+ $this->$k = $this->getLink($key, $table, $link);
+
+ if (is_object($this->$k)) {
+ $loaded[] = $k;
+ }
+ }
+ $this->_link_loaded = $loaded;
+ return true;
+ }
+ // this is the autonaming stuff..
+ // it sends the column name down to getLink and lets that sort it out..
+ // if there is a links file then it is not used!
+ // IT IS DEPRECIATED!!!! - USE
+ if (!is_null($links)) {
+ return false;
+ }
+
+
+ foreach (array_keys($cols) as $key) {
+ if (!($p = strpos($key, '_'))) {
+ continue;
+ }
+ // does the table exist.
+ $k =sprintf($format, $key);
+ $this->$k = $this->getLink($key);
+ if (is_object($this->$k)) {
+ $loaded[] = $k;
+ }
+ }
+ $this->_link_loaded = $loaded;
+ return true;
+ }
+
+ /**
+ * return name from related object
+ *
+ * There are two ways to use this, one is to set up a <dbname>.links.ini file
+ * into a static property named <dbname>.links and specifies the table joins,
+ * the other is highly dependant on naming columns 'correctly' :)
+ *
+ * NOTE: the naming convention is depreciated!!! - use links.ini
+ *
+ * using colname = xxxxx_yyyyyy
+ * xxxxxx = related table; (yyyyy = user defined..)
+ * looks up table xxxxx, for value id=$this->xxxxx
+ * stores it in $this->_xxxxx_yyyyy
+ *
+ * you can also use $this->getLink('thisColumnName','otherTable','otherTableColumnName')
+ *
+ *
+ * @param string $row either row or row.xxxxx
+ * @param string $table name of table to look up value in
+ * @param string $link name of column in other table to match
+ * @author Tim White <tim@cyface.com>
+ * @access public
+ * @return mixed object on success
+ */
+ function getLink($row, $table = null, $link = false)
+ {
+
+
+ // GUESS THE LINKED TABLE.. (if found - recursevly call self)
+
+ if ($table === null) {
+ $links = $this->links();
+
+ if (is_array($links)) {
+
+ if ($links[$row]) {
+ list($table,$link) = explode(':', $links[$row]);
+ if ($p = strpos($row,".")) {
+ $row = substr($row,0,$p);
+ }
+ return $this->getLink($row,$table,$link);
+
+ }
+
+ $this->raiseError(
+ "getLink: $row is not defined as a link (normally this is ok)",
+ DB_DATAOBJECT_ERROR_NODATA);
+
+ $r = false;
+ return $r;// technically a possible error condition?
+
+ }
+ // use the old _ method - this shouldnt happen if called via getLinks()
+ if (!($p = strpos($row, '_'))) {
+ $r = null;
+ return $r;
+ }
+ $table = substr($row, 0, $p);
+ return $this->getLink($row, $table);
+
+
+ }
+
+
+
+ if (!isset($this->$row)) {
+ $this->raiseError("getLink: row not set $row", DB_DATAOBJECT_ERROR_NODATA);
+ return false;
+ }
+
+ // check to see if we know anything about this table..
+
+ $obj = $this->factory($table);
+
+ if (!is_a($obj,'DB_DataObject')) {
+ $this->raiseError(
+ "getLink:Could not find class for row $row, table $table",
+ DB_DATAOBJECT_ERROR_INVALIDCONFIG);
+ return false;
+ }
+ if ($link) {
+ if ($obj->get($link, $this->$row)) {
+ $obj->free();
+ return $obj;
+ }
+ return false;
+ }
+
+ if ($obj->get($this->$row)) {
+ $obj->free();
+ return $obj;
+ }
+ return false;
+
+ }
+
+ /**
+ * IS THIS SUPPORTED/USED ANYMORE????
+ *return a list of options for a linked table
+ *
+ * This is highly dependant on naming columns 'correctly' :)
+ * using colname = xxxxx_yyyyyy
+ * xxxxxx = related table; (yyyyy = user defined..)
+ * looks up table xxxxx, for value id=$this->xxxxx
+ * stores it in $this->_xxxxx_yyyyy
+ *
+ * @access public
+ * @return array of results (empty array on failure)
+ */
+ function &getLinkArray($row, $table = null)
+ {
+
+ $ret = array();
+ if (!$table) {
+ $links = $this->links();
+
+ if (is_array($links)) {
+ if (!isset($links[$row])) {
+ // failed..
+ return $ret;
+ }
+ list($table,$link) = explode(':',$links[$row]);
+ } else {
+ if (!($p = strpos($row,'_'))) {
+ return $ret;
+ }
+ $table = substr($row,0,$p);
+ }
+ }
+
+ $c = $this->factory($table);
+
+ if (!is_a($c,'DB_DataObject')) {
+ $this->raiseError(
+ "getLinkArray:Could not find class for row $row, table $table",
+ DB_DATAOBJECT_ERROR_INVALIDCONFIG
+ );
+ return $ret;
+ }
+
+ // if the user defined method list exists - use it...
+ if (method_exists($c, 'listFind')) {
+ $c->listFind($this->id);
+ } else {
+ $c->find();
+ }
+ while ($c->fetch()) {
+ $ret[] = $c;
+ }
+ return $ret;
+ }
+
+ /**
+ * The JOIN condition
+ *
+ * @access private
+ * @var string
+ */
+ var $_join = '';
+
+ /**
+ * joinAdd - adds another dataobject to this, building a joined query.
+ *
+ * example (requires links.ini to be set up correctly)
+ * // get all the images for product 24
+ * $i = new DataObject_Image();
+ * $pi = new DataObjects_Product_image();
+ * $pi->product_id = 24; // set the product id to 24
+ * $i->joinAdd($pi); // add the product_image connectoin
+ * $i->find();
+ * while ($i->fetch()) {
+ * // do stuff
+ * }
+ * // an example with 2 joins
+ * // get all the images linked with products or productgroups
+ * $i = new DataObject_Image();
+ * $pi = new DataObject_Product_image();
+ * $pgi = new DataObject_Productgroup_image();
+ * $i->joinAdd($pi);
+ * $i->joinAdd($pgi);
+ * $i->find();
+ * while ($i->fetch()) {
+ * // do stuff
+ * }
+ *
+ *
+ * @param optional $obj object |array the joining object (no value resets the join)
+ * If you use an array here it should be in the format:
+ * array('local_column','remotetable:remote_column');
+ * if remotetable does not have a definition, you should
+ * use @ to hide the include error message..
+ *
+ *
+ * @param optional $joinType string | array
+ * 'LEFT'|'INNER'|'RIGHT'|'' Inner is default, '' indicates
+ * just select ... from a,b,c with no join and
+ * links are added as where items.
+ *
+ * If second Argument is array, it is assumed to be an associative
+ * array with arguments matching below = eg.
+ * 'joinType' => 'INNER',
+ * 'joinAs' => '...'
+ * 'joinCol' => ....
+ * 'useWhereAsOn' => false,
+ *
+ * @param optional $joinAs string if you want to select the table as anther name
+ * useful when you want to select multiple columsn
+ * from a secondary table.
+
+ * @param optional $joinCol string The column on This objects table to match (needed
+ * if this table links to the child object in
+ * multiple places eg.
+ * user->friend (is a link to another user)
+ * user->mother (is a link to another user..)
+ *
+ * optional 'useWhereAsOn' bool default false;
+ * convert the where argments from the object being added
+ * into ON arguments.
+ *
+ *
+ * @return none
+ * @access public
+ * @author Stijn de Reede <sjr@gmx.co.uk>
+ */
+ function joinAdd($obj = false, $joinType='INNER', $joinAs=false, $joinCol=false)
+ {
+ global $_DB_DATAOBJECT;
+ if ($obj === false) {
+ $this->_join = '';
+ return;
+ }
+
+
+ $useWhereAsOn = false;
+ // support for 2nd argument as an array of options
+ if (is_array($joinType)) {
+ // new options can now go in here... (dont forget to document them)
+ $useWhereAsOn = !empty($joinType['useWhereAsOn']);
+ $joinCol = isset($joinType['joinCol']) ? $joinType['joinCol'] : $joinCol;
+ $joinAs = isset($joinType['joinAs']) ? $joinType['joinAs'] : $joinAs;
+ $joinType = isset($joinType['joinType']) ? $joinType['joinType'] : 'INNER';
+ }
+ // support for array as first argument
+ // this assumes that you dont have a links.ini for the specified table.
+ // and it doesnt exist as am extended dataobject!! - experimental.
+
+ $ofield = false; // object field
+ $tfield = false; // this field
+ $toTable = false;
+ if (is_array($obj)) {
+ $tfield = $obj[0];
+ list($toTable,$ofield) = explode(':',$obj[1]);
+ $obj = DB_DataObject::factory($toTable);
+
+ if (!$obj || is_a($obj,'PEAR_Error')) {
+ $obj = new DB_DataObject;
+ $obj->__table = $toTable;
+ }
+ $obj->_connect();
+ // set the table items to nothing.. - eg. do not try and match
+ // things in the child table...???
+ $items = array();
+ }
+
+ if (!is_object($obj) || !is_a($obj,'DB_DataObject')) {
+ return $this->raiseError("joinAdd: called without an object", DB_DATAOBJECT_ERROR_NODATA,PEAR_ERROR_DIE);
+ }
+ /* make sure $this->_database is set. */
+ $this->_connect();
+ $DB = &$_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5];
+
+
+
+
+ /* look up the links for obj table */
+ //print_r($obj->links());
+ if (!$ofield && ($olinks = $obj->links())) {
+
+ foreach ($olinks as $k => $v) {
+ /* link contains {this column} = {linked table}:{linked column} */
+ $ar = explode(':', $v);
+
+ // Feature Request #4266 - Allow joins with multiple keys
+
+ $links_key_array = strpos($k,',');
+ if ($links_key_array !== false) {
+ $k = explode(',', $k);
+ }
+
+ $ar_array = strpos($ar[1],',');
+ if ($ar_array !== false) {
+ $ar[1] = explode(',', $ar[1]);
+ }
+
+ if ($ar[0] == $this->__table) {
+
+ // you have explictly specified the column
+ // and the col is listed here..
+ // not sure if 1:1 table could cause probs here..
+
+ if ($joinCol !== false) {
+ $this->raiseError(
+ "joinAdd: You cannot target a join column in the " .
+ "'link from' table ({$obj->__table}). " .
+ "Either remove the fourth argument to joinAdd() ".
+ "({$joinCol}), or alter your links.ini file.",
+ DB_DATAOBJECT_ERROR_NODATA);
+ return false;
+ }
+
+ $ofield = $k;
+ $tfield = $ar[1];
+ break;
+ }
+ }
+ }
+
+ /* otherwise see if there are any links from this table to the obj. */
+ //print_r($this->links());
+ if (($ofield === false) && ($links = $this->links())) {
+ foreach ($links as $k => $v) {
+ /* link contains {this column} = {linked table}:{linked column} */
+ $ar = explode(':', $v);
+ // Feature Request #4266 - Allow joins with multiple keys
+ if (strpos($k, ',') !== false) {
+ $k = explode(',', $k);
+ }
+ if (strpos($ar[1], ',') !== false) {
+ $ar[1] = explode(',', $ar[1]);
+ }
+
+ if ($ar[0] == $obj->__table) {
+ if ($joinCol !== false) {
+ if ($k == $joinCol) {
+ $tfield = $k;
+ $ofield = $ar[1];
+ break;
+ } else {
+ continue;
+ }
+ } else {
+ $tfield = $k;
+ $ofield = $ar[1];
+ break;
+ }
+ }
+ }
+ }
+ // finally if these two table have column names that match do a join by default on them
+
+ if (($ofield === false) && $joinCol) {
+ $ofield = $joinCol;
+ $tfield = $joinCol;
+
+ }
+ /* did I find a conneciton between them? */
+
+ if ($ofield === false) {
+ $this->raiseError(
+ "joinAdd: {$obj->__table} has no link with {$this->__table}",
+ DB_DATAOBJECT_ERROR_NODATA);
+ return false;
+ }
+ $joinType = strtoupper($joinType);
+
+ // we default to joining as the same name (this is remvoed later..)
+
+ if ($joinAs === false) {
+ $joinAs = $obj->__table;
+ }
+
+ $quoteIdentifiers = !empty($_DB_DATAOBJECT['CONFIG']['quote_identifiers']);
+ $options = $_DB_DATAOBJECT['CONFIG'];
+
+ // not sure how portable adding database prefixes is..
+ $objTable = $quoteIdentifiers ?
+ $DB->quoteIdentifier($obj->__table) :
+ $obj->__table ;
+
+ $dbPrefix = '';
+ if (strlen($obj->_database) && in_array($DB->dsn['phptype'],array('mysql','mysqli'))) {
+ $dbPrefix = ($quoteIdentifiers
+ ? $DB->quoteIdentifier($obj->_database)
+ : $obj->_database) . '.';
+ }
+
+ // if they are the same, then dont add a prefix...
+ if ($obj->_database == $this->_database) {
+ $dbPrefix = '';
+ }
+ // as far as we know only mysql supports database prefixes..
+ // prefixing the database name is now the default behaviour,
+ // as it enables joining mutiple columns from multiple databases...
+
+ // prefix database (quoted if neccessary..)
+ $objTable = $dbPrefix . $objTable;
+
+ $cond = '';
+
+ // if obj only a dataobject - eg. no extended class has been defined..
+ // it obvioulsy cant work out what child elements might exist...
+ // until we get on the fly querying of tables..
+ // note: we have already checked that it is_a(db_dataobject earlier)
+ if ( strtolower(get_class($obj)) != 'db_dataobject') {
+
+ // now add where conditions for anything that is set in the object
+
+
+
+ $items = $obj->table();
+ // will return an array if no items..
+
+ // only fail if we where expecting it to work (eg. not joined on a array)
+
+ if (!$items) {
+ $this->raiseError(
+ "joinAdd: No table definition for {$obj->__table}",
+ DB_DATAOBJECT_ERROR_INVALIDCONFIG);
+ return false;
+ }
+
+ foreach($items as $k => $v) {
+ if (!isset($obj->$k)) {
+ continue;
+ }
+
+ $kSql = ($quoteIdentifiers ? $DB->quoteIdentifier($k) : $k);
+
+
+ if ($v & DB_DATAOBJECT_STR) {
+ $obj->whereAdd("{$joinAs}.{$kSql} = " . $this->_quote((string) (
+ ($v & DB_DATAOBJECT_BOOL) ?
+ // this is thanks to the braindead idea of postgres to
+ // use t/f for boolean.
+ (($obj->$k === 'f') ? 0 : (int)(bool) $obj->$k) :
+ $obj->$k
+ )));
+ continue;
+ }
+ if (is_numeric($obj->$k)) {
+ $obj->whereAdd("{$joinAs}.{$kSql} = {$obj->$k}");
+ continue;
+ }
+
+ if (is_a($obj->$k,'DB_DataObject_Cast')) {
+ $value = $obj->$k->toString($v,$DB);
+ if (PEAR::isError($value)) {
+ $this->raiseError($value->getMessage() ,DB_DATAOBJECT_ERROR_INVALIDARG);
+ return false;
+ }
+ if (!isset($options['disable_null_strings']) && strtolower($value) === 'null') {
+ $obj->whereAdd("{$joinAs}.{$kSql} IS NULL");
+ continue;
+ } else {
+ $obj->whereAdd("{$joinAs}.{$kSql} = $value");
+ continue;
+ }
+ }
+
+
+ /* this is probably an error condition! */
+ $obj->whereAdd("{$joinAs}.{$kSql} = 0");
+ }
+ if ($this->_query === false) {
+ $this->raiseError(
+ "joinAdd can not be run from a object that has had a query run on it,
+ clone the object or create a new one and use setFrom()",
+ DB_DATAOBJECT_ERROR_INVALIDARGS);
+ return false;
+ }
+ }
+
+ // and finally merge the whereAdd from the child..
+ if ($obj->_query['condition']) {
+ $cond = preg_replace('/^\sWHERE/i','',$obj->_query['condition']);
+
+ if (!$useWhereAsOn) {
+ $this->whereAdd($cond);
+ }
+ }
+
+
+
+
+ // nested (join of joined objects..)
+ $appendJoin = '';
+ if ($obj->_join) {
+ // postgres allows nested queries, with ()'s
+ // not sure what the results are with other databases..
+ // may be unpredictable..
+ if (in_array($DB->dsn["phptype"],array('pgsql'))) {
+ $objTable = "($objTable {$obj->_join})";
+ } else {
+ $appendJoin = $obj->_join;
+ }
+ }
+
+
+ // fix for #2216
+ // add the joinee object's conditions to the ON clause instead of the WHERE clause
+ if ($useWhereAsOn && strlen($cond)) {
+ $appendJoin = ' AND ' . $cond . ' ' . $appendJoin;
+ }
+
+
+
+ $table = $this->__table;
+
+ if ($quoteIdentifiers) {
+ $joinAs = $DB->quoteIdentifier($joinAs);
+ $table = $DB->quoteIdentifier($table);
+ $ofield = (is_array($ofield)) ? array_map(array($DB, 'quoteIdentifier'), $ofield) : $DB->quoteIdentifier($ofield);
+ $tfield = (is_array($tfield)) ? array_map(array($DB, 'quoteIdentifier'), $tfield) : $DB->quoteIdentifier($tfield);
+ }
+ // add database prefix if they are different databases
+
+
+ $fullJoinAs = '';
+ $addJoinAs = ($quoteIdentifiers ? $DB->quoteIdentifier($obj->__table) : $obj->__table) != $joinAs;
+ if ($addJoinAs) {
+ // join table a AS b - is only supported by a few databases and is probably not needed
+ // , however since it makes the whole Statement alot clearer we are leaving it in
+ // for those databases.
+ $fullJoinAs = in_array($DB->dsn["phptype"],array('mysql','mysqli','pgsql')) ? "AS {$joinAs}" : $joinAs;
+ } else {
+ // if
+ $joinAs = $dbPrefix . $joinAs;
+ }
+
+
+ switch ($joinType) {
+ case 'INNER':
+ case 'LEFT':
+ case 'RIGHT': // others??? .. cross, left outer, right outer, natural..?
+
+ // Feature Request #4266 - Allow joins with multiple keys
+ $this->_join .= "\n {$joinType} JOIN {$objTable} {$fullJoinAs}";
+ if (is_array($ofield)) {
+ $key_count = count($ofield);
+ for($i = 0; $i < $key_count; $i++) {
+ if ($i == 0) {
+ $this->_join .= " ON ({$joinAs}.{$ofield[$i]}={$table}.{$tfield[$i]}) ";
+ }
+ else {
+ $this->_join .= " AND {$joinAs}.{$ofield[$i]}={$table}.{$tfield[$i]} ";
+ }
+ }
+ $this->_join .= ' ' . $appendJoin . ' ';
+ } else {
+ $this->_join .= " ON ({$joinAs}.{$ofield}={$table}.{$tfield}) {$appendJoin} ";
+ }
+
+ break;
+
+ case '': // this is just a standard multitable select..
+ $this->_join .= "\n , {$objTable} {$fullJoinAs} {$appendJoin}";
+ $this->whereAdd("{$joinAs}.{$ofield}={$table}.{$tfield}");
+ }
+
+
+ return true;
+
+ }
+
+ /**
+ * Copies items that are in the table definitions from an
+ * array or object into the current object
+ * will not override key values.
+ *
+ *
+ * @param array | object $from
+ * @param string $format eg. map xxxx_name to $object->name using 'xxxx_%s' (defaults to %s - eg. name -> $object->name
+ * @param boolean $skipEmpty (dont assign empty values if a column is empty (eg. '' / 0 etc...)
+ * @access public
+ * @return true on success or array of key=>setValue error message
+ */
+ function setFrom($from, $format = '%s', $skipEmpty=false)
+ {
+ global $_DB_DATAOBJECT;
+ $keys = $this->keys();
+ $items = $this->table();
+ if (!$items) {
+ $this->raiseError(
+ "setFrom:Could not find table definition for {$this->__table}",
+ DB_DATAOBJECT_ERROR_INVALIDCONFIG);
+ return;
+ }
+ $overload_return = array();
+ foreach (array_keys($items) as $k) {
+ if (in_array($k,$keys)) {
+ continue; // dont overwrite keys
+ }
+ if (!$k) {
+ continue; // ignore empty keys!!! what
+ }
+ if (is_object($from) && isset($from->{sprintf($format,$k)})) {
+ $kk = (strtolower($k) == 'from') ? '_from' : $k;
+ if (method_exists($this,'set'.$kk)) {
+ $ret = $this->{'set'.$kk}($from->{sprintf($format,$k)});
+ if (is_string($ret)) {
+ $overload_return[$k] = $ret;
+ }
+ continue;
+ }
+ $this->$k = $from->{sprintf($format,$k)};
+ continue;
+ }
+
+ if (is_object($from)) {
+ continue;
+ }
+
+ if (empty($from[$k]) && $skipEmpty) {
+ continue;
+ }
+
+ if (!isset($from[sprintf($format,$k)])) {
+ continue;
+ }
+
+ $kk = (strtolower($k) == 'from') ? '_from' : $k;
+ if (method_exists($this,'set'. $kk)) {
+ $ret = $this->{'set'.$kk}($from[sprintf($format,$k)]);
+ if (is_string($ret)) {
+ $overload_return[$k] = $ret;
+ }
+ continue;
+ }
+ if (is_object($from[sprintf($format,$k)])) {
+ continue;
+ }
+ if (is_array($from[sprintf($format,$k)])) {
+ continue;
+ }
+ $ret = $this->fromValue($k,$from[sprintf($format,$k)]);
+ if ($ret !== true) {
+ $overload_return[$k] = 'Not A Valid Value';
+ }
+ //$this->$k = $from[sprintf($format,$k)];
+ }
+ if ($overload_return) {
+ return $overload_return;
+ }
+ return true;
+ }
+
+ /**
+ * Returns an associative array from the current data
+ * (kind of oblivates the idea behind DataObjects, but
+ * is usefull if you use it with things like QuickForms.
+ *
+ * you can use the format to return things like user[key]
+ * by sending it $object->toArray('user[%s]')
+ *
+ * will also return links converted to arrays.
+ *
+ * @param string sprintf format for array
+ * @param bool empty only return elemnts that have a value set.
+ *
+ * @access public
+ * @return array of key => value for row
+ */
+
+ function toArray($format = '%s', $hideEmpty = false)
+ {
+ global $_DB_DATAOBJECT;
+ $ret = array();
+ $rf = ($this->_resultFields !== false) ? $this->_resultFields :
+ (isset($_DB_DATAOBJECT['RESULTFIELDS'][$this->_DB_resultid]) ? $_DB_DATAOBJECT['RESULTFIELDS'][$this->_DB_resultid] : false);
+ $ar = ($rf !== false) ?
+ array_merge($_DB_DATAOBJECT['RESULTFIELDS'][$this->_DB_resultid],$this->table()) :
+ $this->table();
+
+ foreach($ar as $k=>$v) {
+
+ if (!isset($this->$k)) {
+ if (!$hideEmpty) {
+ $ret[sprintf($format,$k)] = '';
+ }
+ continue;
+ }
+ // call the overloaded getXXXX() method. - except getLink and getLinks
+ if (method_exists($this,'get'.$k) && !in_array(strtolower($k),array('links','link'))) {
+ $ret[sprintf($format,$k)] = $this->{'get'.$k}();
+ continue;
+ }
+ // should this call toValue() ???
+ $ret[sprintf($format,$k)] = $this->$k;
+ }
+ if (!$this->_link_loaded) {
+ return $ret;
+ }
+ foreach($this->_link_loaded as $k) {
+ $ret[sprintf($format,$k)] = $this->$k->toArray();
+
+ }
+
+ return $ret;
+ }
+
+ /**
+ * validate the values of the object (usually prior to inserting/updating..)
+ *
+ * Note: This was always intended as a simple validation routine.
+ * It lacks understanding of field length, whether you are inserting or updating (and hence null key values)
+ *
+ * This should be moved to another class: DB_DataObject_Validate
+ * FEEL FREE TO SEND ME YOUR VERSION FOR CONSIDERATION!!!
+ *
+ * Usage:
+ * if (is_array($ret = $obj->validate())) { ... there are problems with the data ... }
+ *
+ * Logic:
+ * - defaults to only testing strings/numbers if numbers or strings are the correct type and null values are correct
+ * - validate Column methods : "validate{ROWNAME}()" are called if they are defined.
+ * These methods should return
+ * true = everything ok
+ * false|object = something is wrong!
+ *
+ * - This method loads and uses the PEAR Validate Class.
+ *
+ *
+ * @access public
+ * @return array of validation results (where key=>value, value=false|object if it failed) or true (if they all succeeded)
+ */
+ function validate()
+ {
+ global $_DB_DATAOBJECT;
+ require_once 'Validate.php';
+ $table = $this->table();
+ $ret = array();
+ $seq = $this->sequenceKey();
+ $options = $_DB_DATAOBJECT['CONFIG'];
+ foreach($table as $key => $val) {
+
+
+ // call user defined validation always...
+ $method = "Validate" . ucfirst($key);
+ if (method_exists($this, $method)) {
+ $ret[$key] = $this->$method();
+ continue;
+ }
+
+ // if not null - and it's not set.......
+
+ if (!isset($this->$key) && ($val & DB_DATAOBJECT_NOTNULL)) {
+ // dont check empty sequence key values..
+ if (($key == $seq[0]) && ($seq[1] == true)) {
+ continue;
+ }
+ $ret[$key] = false;
+ continue;
+ }
+
+
+ if (!isset($options['disable_null_strings']) && is_string($this->$key) && (strtolower($this->$key) == 'null')) {
+ if ($val & DB_DATAOBJECT_NOTNULL) {
+ $this->debug("'null' field used for '$key', but it is defined as NOT NULL", 'VALIDATION', 4);
+ $ret[$key] = false;
+ continue;
+ }
+ continue;
+ }
+
+ // ignore things that are not set. ?
+
+ if (!isset($this->$key)) {
+ continue;
+ }
+
+ // if the string is empty.. assume it is ok..
+ if (!is_object($this->$key) && !is_array($this->$key) && !strlen((string) $this->$key)) {
+ continue;
+ }
+
+ // dont try and validate cast objects - assume they are problably ok..
+ if (is_object($this->$key) && is_a($this->$key,'DB_DataObject_Cast')) {
+ continue;
+ }
+ // at this point if you have set something to an object, and it's not expected
+ // the Validate will probably break!!... - rightly so! (your design is broken,
+ // so issuing a runtime error like PEAR_Error is probably not appropriate..
+
+ switch (true) {
+ // todo: date time.....
+ case ($val & DB_DATAOBJECT_STR):
+ $ret[$key] = Validate::string($this->$key, VALIDATE_PUNCTUATION . VALIDATE_NAME);
+ continue;
+ case ($val & DB_DATAOBJECT_INT):
+ $ret[$key] = Validate::number($this->$key, array('decimal'=>'.'));
+ continue;
+ }
+ }
+ // if any of the results are false or an object (eg. PEAR_Error).. then return the array..
+ foreach ($ret as $key => $val) {
+ if ($val !== true) {
+ return $ret;
+ }
+ }
+ return true; // everything is OK.
+ }
+
+ /**
+ * Gets the DB object related to an object - so you can use funky peardb stuf with it :)
+ *
+ * @access public
+ * @return object The DB connection
+ */
+ function &getDatabaseConnection()
+ {
+ global $_DB_DATAOBJECT;
+
+ if (($e = $this->_connect()) !== true) {
+ return $e;
+ }
+ if (!isset($_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5])) {
+ $r = false;
+ return $r;
+ }
+ return $_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5];
+ }
+
+
+ /**
+ * Gets the DB result object related to the objects active query
+ * - so you can use funky pear stuff with it - like pager for example.. :)
+ *
+ * @access public
+ * @return object The DB result object
+ */
+
+ function &getDatabaseResult()
+ {
+ global $_DB_DATAOBJECT;
+ $this->_connect();
+ if (!isset($_DB_DATAOBJECT['RESULTS'][$this->_DB_resultid])) {
+ $r = false;
+ return $r;
+ }
+ return $_DB_DATAOBJECT['RESULTS'][$this->_DB_resultid];
+ }
+
+ /**
+ * Overload Extension support
+ * - enables setCOLNAME/getCOLNAME
+ * if you define a set/get method for the item it will be called.
+ * otherwise it will just return/set the value.
+ * NOTE this currently means that a few Names are NO-NO's
+ * eg. links,link,linksarray, from, Databaseconnection,databaseresult
+ *
+ * note
+ * - set is automatically called by setFrom.
+ * - get is automatically called by toArray()
+ *
+ * setters return true on success. = strings on failure
+ * getters return the value!
+ *
+ * this fires off trigger_error - if any problems.. pear_error,
+ * has problems with 4.3.2RC2 here
+ *
+ * @access public
+ * @return true?
+ * @see overload
+ */
+
+
+ function _call($method,$params,&$return) {
+
+ //$this->debug("ATTEMPTING OVERLOAD? $method");
+ // ignore constructors : - mm
+ if (strtolower($method) == strtolower(get_class($this))) {
+ return true;
+ }
+ $type = strtolower(substr($method,0,3));
+ $class = get_class($this);
+ if (($type != 'set') && ($type != 'get')) {
+ return false;
+ }
+
+
+
+ // deal with naming conflick of setFrom = this is messy ATM!
+
+ if (strtolower($method) == 'set_from') {
+ $return = $this->toValue('from',isset($params[0]) ? $params[0] : null);
+ return true;
+ }
+
+ $element = substr($method,3);
+
+ // dont you just love php's case insensitivity!!!!
+
+ $array = array_keys(get_class_vars($class));
+ /* php5 version which segfaults on 5.0.3 */
+ if (class_exists('ReflectionClass')) {
+ $reflection = new ReflectionClass($class);
+ $array = array_keys($reflection->getdefaultProperties());
+ }
+
+ if (!in_array($element,$array)) {
+ // munge case
+ foreach($array as $k) {
+ $case[strtolower($k)] = $k;
+ }
+ if ((substr(phpversion(),0,1) == 5) && isset($case[strtolower($element)])) {
+ trigger_error("PHP5 set/get calls should match the case of the variable",E_USER_WARNING);
+ $element = strtolower($element);
+ }
+
+ // does it really exist?
+ if (!isset($case[$element])) {
+ return false;
+ }
+ // use the mundged case
+ $element = $case[$element]; // real case !
+ }
+
+
+ if ($type == 'get') {
+ $return = $this->toValue($element,isset($params[0]) ? $params[0] : null);
+ return true;
+ }
+
+
+ $return = $this->fromValue($element, $params[0]);
+
+ return true;
+
+
+ }
+
+
+ /**
+ * standard set* implementation.
+ *
+ * takes data and uses it to set dates/strings etc.
+ * normally called from __call..
+ *
+ * Current supports
+ * date = using (standard time format, or unixtimestamp).... so you could create a method :
+ * function setLastread($string) { $this->fromValue('lastread',strtotime($string)); }
+ *
+ * time = using strtotime
+ * datetime = using same as date - accepts iso standard or unixtimestamp.
+ * string = typecast only..
+ *
+ * TODO: add formater:: eg. d/m/Y for date! ???
+ *
+ * @param string column of database
+ * @param mixed value to assign
+ *
+ * @return true| false (False on error)
+ * @access public
+ * @see DB_DataObject::_call
+ */
+
+
+ function fromValue($col,$value)
+ {
+ global $_DB_DATAOBJECT;
+ $options = $_DB_DATAOBJECT['CONFIG'];
+ $cols = $this->table();
+ // dont know anything about this col..
+ if (!isset($cols[$col])) {
+ $this->$col = $value;
+ return true;
+ }
+ //echo "FROM VALUE $col, {$cols[$col]}, $value\n";
+ switch (true) {
+ // set to null and column is can be null...
+ case (!isset($options['disable_null_strings']) && (strtolower($value) == 'null') && (!($cols[$col] & DB_DATAOBJECT_NOTNULL))):
+ case (is_object($value) && is_a($value,'DB_DataObject_Cast')):
+ $this->$col = $value;
+ return true;
+
+ // fail on setting null on a not null field..
+ case (!isset($options['disable_null_strings']) && (strtolower($value) == 'null') && ($cols[$col] & DB_DATAOBJECT_NOTNULL)):
+ return false;
+
+ case (($cols[$col] & DB_DATAOBJECT_DATE) && ($cols[$col] & DB_DATAOBJECT_TIME)):
+ // empty values get set to '' (which is inserted/updated as NULl
+ if (!$value) {
+ $this->$col = '';
+ }
+
+ if (is_numeric($value)) {
+ $this->$col = date('Y-m-d H:i:s', $value);
+ return true;
+ }
+
+ // eak... - no way to validate date time otherwise...
+ $this->$col = (string) $value;
+ return true;
+
+ case ($cols[$col] & DB_DATAOBJECT_DATE):
+ // empty values get set to '' (which is inserted/updated as NULl
+
+ if (!$value) {
+ $this->$col = '';
+ return true;
+ }
+
+ if (is_numeric($value)) {
+ $this->$col = date('Y-m-d',$value);
+ return true;
+ }
+
+ // try date!!!!
+ require_once 'Date.php';
+ $x = new Date($value);
+ $this->$col = $x->format("%Y-%m-%d");
+ return true;
+
+ case ($cols[$col] & DB_DATAOBJECT_TIME):
+ // empty values get set to '' (which is inserted/updated as NULl
+ if (!$value) {
+ $this->$col = '';
+ }
+
+ $guess = strtotime($value);
+ if ($guess != -1) {
+ $this->$col = date('H:i:s', $guess);
+ return $return = true;
+ }
+ // otherwise an error in type...
+ return false;
+
+ case ($cols[$col] & DB_DATAOBJECT_STR):
+
+ $this->$col = (string) $value;
+ return true;
+
+ // todo : floats numerics and ints...
+ default:
+ $this->$col = $value;
+ return true;
+ }
+
+
+
+ }
+ /**
+ * standard get* implementation.
+ *
+ * with formaters..
+ * supported formaters:
+ * date/time : %d/%m/%Y (eg. php strftime) or pear::Date
+ * numbers : %02d (eg. sprintf)
+ * NOTE you will get unexpected results with times like 0000-00-00 !!!
+ *
+ *
+ *
+ * @param string column of database
+ * @param format foramt
+ *
+ * @return true Description
+ * @access public
+ * @see DB_DataObject::_call(),strftime(),Date::format()
+ */
+ function toValue($col,$format = null)
+ {
+ if (is_null($format)) {
+ return $this->$col;
+ }
+ $cols = $this->table();
+ switch (true) {
+ case (($cols[$col] & DB_DATAOBJECT_DATE) && ($cols[$col] & DB_DATAOBJECT_TIME)):
+ if (!$this->$col) {
+ return '';
+ }
+ $guess = strtotime($this->$col);
+ if ($guess != -1) {
+ return strftime($format, $guess);
+ }
+ // eak... - no way to validate date time otherwise...
+ return $this->$col;
+ case ($cols[$col] & DB_DATAOBJECT_DATE):
+ if (!$this->$col) {
+ return '';
+ }
+ $guess = strtotime($this->$col);
+ if ($guess != -1) {
+ return strftime($format,$guess);
+ }
+ // try date!!!!
+ require_once 'Date.php';
+ $x = new Date($this->$col);
+ return $x->format($format);
+
+ case ($cols[$col] & DB_DATAOBJECT_TIME):
+ if (!$this->$col) {
+ return '';
+ }
+ $guess = strtotime($this->$col);
+ if ($guess > -1) {
+ return strftime($format, $guess);
+ }
+ // otherwise an error in type...
+ return $this->$col;
+
+ case ($cols[$col] & DB_DATAOBJECT_MYSQLTIMESTAMP):
+ if (!$this->$col) {
+ return '';
+ }
+ require_once 'Date.php';
+
+ $x = new Date($this->$col);
+
+ return $x->format($format);
+
+
+ case ($cols[$col] & DB_DATAOBJECT_BOOL):
+
+ if ($cols[$col] & DB_DATAOBJECT_STR) {
+ // it's a 't'/'f' !
+ return ($this->$col === 't');
+ }
+ return (bool) $this->$col;
+
+
+ default:
+ return sprintf($format,$this->col);
+ }
+
+
+ }
+
+
+ /* ----------------------- Debugger ------------------ */
+
+ /**
+ * Debugger. - use this in your extended classes to output debugging information.
+ *
+ * Uses DB_DataObject::DebugLevel(x) to turn it on
+ *
+ * @param string $message - message to output
+ * @param string $logtype - bold at start
+ * @param string $level - output level
+ * @access public
+ * @return none
+ */
+ function debug($message, $logtype = 0, $level = 1)
+ {
+ global $_DB_DATAOBJECT;
+
+ if (empty($_DB_DATAOBJECT['CONFIG']['debug']) ||
+ (is_numeric($_DB_DATAOBJECT['CONFIG']['debug']) && $_DB_DATAOBJECT['CONFIG']['debug'] < $level)) {
+ return;
+ }
+ // this is a bit flaky due to php's wonderfull class passing around crap..
+ // but it's about as good as it gets..
+ $class = (isset($this) && is_a($this,'DB_DataObject')) ? get_class($this) : 'DB_DataObject';
+
+ if (!is_string($message)) {
+ $message = print_r($message,true);
+ }
+ if (!is_numeric( $_DB_DATAOBJECT['CONFIG']['debug']) && is_callable( $_DB_DATAOBJECT['CONFIG']['debug'])) {
+ return call_user_func($_DB_DATAOBJECT['CONFIG']['debug'], $class, $message, $logtype, $level);
+ }
+
+ if (!ini_get('html_errors')) {
+ echo "$class : $logtype : $message\n";
+ flush();
+ return;
+ }
+ if (!is_string($message)) {
+ $message = print_r($message,true);
+ }
+ $colorize = ($logtype == 'ERROR') ? '<font color="red">' : '<font>';
+ echo "<code>{$colorize}<B>$class: $logtype:</B> ". nl2br(htmlspecialchars($message)) . "</font></code><BR>\n";
+ }
+
+ /**
+ * sets and returns debug level
+ * eg. DB_DataObject::debugLevel(4);
+ *
+ * @param int $v level
+ * @access public
+ * @return none
+ */
+ function debugLevel($v = null)
+ {
+ global $_DB_DATAOBJECT;
+ if (empty($_DB_DATAOBJECT['CONFIG'])) {
+ DB_DataObject::_loadConfig();
+ }
+ if ($v !== null) {
+ $r = isset($_DB_DATAOBJECT['CONFIG']['debug']) ? $_DB_DATAOBJECT['CONFIG']['debug'] : 0;
+ $_DB_DATAOBJECT['CONFIG']['debug'] = $v;
+ return $r;
+ }
+ return isset($_DB_DATAOBJECT['CONFIG']['debug']) ? $_DB_DATAOBJECT['CONFIG']['debug'] : 0;
+ }
+
+ /**
+ * Last Error that has occured
+ * - use $this->_lastError or
+ * $last_error = &PEAR::getStaticProperty('DB_DataObject','lastError');
+ *
+ * @access public
+ * @var object PEAR_Error (or false)
+ */
+ var $_lastError = false;
+
+ /**
+ * Default error handling is to create a pear error, but never return it.
+ * if you need to handle errors you should look at setting the PEAR_Error callback
+ * this is due to the fact it would wreck havoc on the internal methods!
+ *
+ * @param int $message message
+ * @param int $type type
+ * @param int $behaviour behaviour (die or continue!);
+ * @access public
+ * @return error object
+ */
+ function raiseError($message, $type = null, $behaviour = null)
+ {
+ global $_DB_DATAOBJECT;
+
+ if ($behaviour == PEAR_ERROR_DIE && !empty($_DB_DATAOBJECT['CONFIG']['dont_die'])) {
+ $behaviour = null;
+ }
+ $error = &PEAR::getStaticProperty('DB_DataObject','lastError');
+
+ // this will never work totally with PHP's object model.
+ // as this is passed on static calls (like staticGet in our case)
+
+ if (isset($this) && is_object($this) && is_subclass_of($this,'db_dataobject')) {
+ $this->_lastError = $error;
+ }
+
+ $_DB_DATAOBJECT['LASTERROR'] = $error;
+
+ // no checks for production here?....... - we log errors before we throw them.
+ DB_DataObject::debug($message,'ERROR',1);
+
+
+ if (PEAR::isError($message)) {
+ $error = $message;
+ } else {
+ require_once 'DB/DataObject/Error.php';
+ $error = PEAR::raiseError($message, $type, $behaviour,
+ $opts=null, $userinfo=null, 'DB_DataObject_Error'
+ );
+ }
+
+ return $error;
+ }
+
+ /**
+ * Define the global $_DB_DATAOBJECT['CONFIG'] as an alias to PEAR::getStaticProperty('DB_DataObject','options');
+ *
+ * After Profiling DB_DataObject, I discoved that the debug calls where taking
+ * considerable time (well 0.1 ms), so this should stop those calls happening. as
+ * all calls to debug are wrapped with direct variable queries rather than actually calling the funciton
+ * THIS STILL NEEDS FURTHER INVESTIGATION
+ *
+ * @access public
+ * @return object an error object
+ */
+ function _loadConfig()
+ {
+ global $_DB_DATAOBJECT;
+
+ $_DB_DATAOBJECT['CONFIG'] = &PEAR::getStaticProperty('DB_DataObject','options');
+
+
+ }
+ /**
+ * Free global arrays associated with this object.
+ *
+ *
+ * @access public
+ * @return none
+ */
+ function free()
+ {
+ global $_DB_DATAOBJECT;
+
+ if (isset($_DB_DATAOBJECT['RESULTFIELDS'][$this->_DB_resultid])) {
+ unset($_DB_DATAOBJECT['RESULTFIELDS'][$this->_DB_resultid]);
+ }
+ if (isset($_DB_DATAOBJECT['RESULTS'][$this->_DB_resultid])) {
+ unset($_DB_DATAOBJECT['RESULTS'][$this->_DB_resultid]);
+ }
+ // clear the staticGet cache as well.
+ $this->_clear_cache();
+ // this is a huge bug in DB!
+ if (isset($_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5])) {
+ $_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5]->num_rows = array();
+ }
+
+ }
+
+
+ /* ---- LEGACY BC METHODS - NOT DOCUMENTED - See Documentation on New Methods. ---*/
+
+ function _get_table() { return $this->table(); }
+ function _get_keys() { return $this->keys(); }
+
+
+
+
+}
+// technially 4.3.2RC1 was broken!!
+// looks like 4.3.3 may have problems too....
+if (!defined('DB_DATAOBJECT_NO_OVERLOAD')) {
+
+ if ((phpversion() != '4.3.2-RC1') && (version_compare( phpversion(), "4.3.1") > 0)) {
+ if (version_compare( phpversion(), "5") < 0) {
+ overload('DB_DataObject');
+ }
+ $GLOBALS['_DB_DATAOBJECT']['OVERLOADED'] = true;
+ }
+}
+
diff --git a/extlib/DB/DataObject/Cast.php b/extlib/DB/DataObject/Cast.php
new file mode 100644
index 000000000..616abb55e
--- /dev/null
+++ b/extlib/DB/DataObject/Cast.php
@@ -0,0 +1,546 @@
+<?php
+/**
+ * Prototype Castable Object.. for DataObject queries
+ *
+ * Storage for Data that may be cast into a variety of formats.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt. If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category Database
+ * @package DB_DataObject
+ * @author Alan Knowles <alan@akbkhome.com>
+ * @copyright 1997-2006 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0
+ * @version CVS: $Id: Cast.php,v 1.15 2005/07/07 05:30:53 alan_k Exp $
+ * @link http://pear.php.net/package/DB_DataObject
+ */
+
+/**
+*
+* Common usages:
+* // blobs
+* $data = DB_DataObject_Cast::blob($somefile);
+* $data = DB_DataObject_Cast::string($somefile);
+* $dataObject->someblobfield = $data
+*
+* // dates?
+* $d1 = new DB_DataObject_Cast::date('12/12/2000');
+* $d2 = new DB_DataObject_Cast::date(2000,12,30);
+* $d3 = new DB_DataObject_Cast::date($d1->year, $d1->month+30, $d1->day+30);
+*
+* // time, datetime.. ?????????
+*
+* // raw sql????
+* $data = DB_DataObject_Cast::sql('cast("123123",datetime)');
+* $data = DB_DataObject_Cast::sql('NULL');
+*
+* // int's/string etc. are proably pretty pointless..!!!!
+*
+*
+* inside DB_DataObject,
+* if (is_a($v,'db_dataobject_class')) {
+* $value .= $v->toString(DB_DATAOBJECT_INT,'mysql');
+* }
+*
+*
+*
+*
+
+*/
+class DB_DataObject_Cast {
+
+ /**
+ * Type of data Stored in the object..
+ *
+ * @var string (date|blob|.....?)
+ * @access public
+ */
+ var $type;
+
+ /**
+ * Data For date representation
+ *
+ * @var int day/month/year
+ * @access public
+ */
+ var $day;
+ var $month;
+ var $year;
+
+
+ /**
+ * Generic Data..
+ *
+ * @var string
+ * @access public
+ */
+
+ var $value;
+
+
+
+ /**
+ * Blob consructor
+ *
+ * create a Cast object from some raw data.. (binary)
+ *
+ *
+ * @param string (with binary data!)
+ *
+ * @return object DB_DataObject_Cast
+ * @access public
+ */
+
+ function blob($value) {
+ $r = new DB_DataObject_Cast;
+ $r->type = 'blob';
+ $r->value = $value;
+ return $r;
+ }
+
+
+ /**
+ * String consructor (actually use if for ints and everything else!!!
+ *
+ * create a Cast object from some string (not binary)
+ *
+ *
+ * @param string (with binary data!)
+ *
+ * @return object DB_DataObject_Cast
+ * @access public
+ */
+
+ function string($value) {
+ $r = new DB_DataObject_Cast;
+ $r->type = 'string';
+ $r->value = $value;
+ return $r;
+ }
+
+ /**
+ * SQL constructor (for raw SQL insert)
+ *
+ * create a Cast object from some sql
+ *
+ * @param string (with binary data!)
+ *
+ * @return object DB_DataObject_Cast
+ * @access public
+ */
+
+ function sql($value)
+ {
+ $r = new DB_DataObject_Cast;
+ $r->type = 'sql';
+ $r->value = $value;
+ return $r;
+ }
+
+
+ /**
+ * Date Constructor
+ *
+ * create a Cast object from some string (not binary)
+ * NO VALIDATION DONE, although some crappy re-calcing done!
+ *
+ * @param vargs... accepts
+ * dd/mm
+ * dd/mm/yyyy
+ * yyyy-mm
+ * yyyy-mm-dd
+ * array(yyyy,dd)
+ * array(yyyy,dd,mm)
+ *
+ *
+ *
+ * @return object DB_DataObject_Cast
+ * @access public
+ */
+
+ function date()
+ {
+ $args = func_get_args();
+ switch(count($args)) {
+ case 0: // no args = today!
+ $bits = explode('-',date('Y-m-d'));
+ break;
+ case 1: // one arg = a string
+
+ if (strpos($args[0],'/') !== false) {
+ $bits = array_reverse(explode('/',$args[0]));
+ } else {
+ $bits = explode('-',$args[0]);
+ }
+ break;
+ default: // 2 or more..
+ $bits = $args;
+ }
+ if (count($bits) == 1) { // if YYYY set day = 1st..
+ $bits[] = 1;
+ }
+
+ if (count($bits) == 2) { // if YYYY-DD set day = 1st..
+ $bits[] = 1;
+ }
+
+ // if year < 1970 we cant use system tools to check it...
+ // so we make a few best gueses....
+ // basically do date calculations for the year 2000!!!
+ // fix me if anyone has more time...
+ if (($bits[0] < 1975) || ($bits[0] > 2030)) {
+ $oldyear = $bits[0];
+ $bits = explode('-',date('Y-m-d',mktime(1,1,1,$bits[1],$bits[2],2000)));
+ $bits[0] = ($bits[0] - 2000) + $oldyear;
+ } else {
+ // now mktime
+ $bits = explode('-',date('Y-m-d',mktime(1,1,1,$bits[1],$bits[2],$bits[0])));
+ }
+ $r = new DB_DataObject_Cast;
+ $r->type = 'date';
+ list($r->year,$r->month,$r->day) = $bits;
+ return $r;
+ }
+
+
+
+ /**
+ * Data For time representation ** does not handle timezones!!
+ *
+ * @var int hour/minute/second
+ * @access public
+ */
+ var $hour;
+ var $minute;
+ var $second;
+
+
+ /**
+ * DateTime Constructor
+ *
+ * create a Cast object from a Date/Time
+ * Maybe should accept a Date object.!
+ * NO VALIDATION DONE, although some crappy re-calcing done!
+ *
+ * @param vargs... accepts
+ * noargs (now)
+ * yyyy-mm-dd HH:MM:SS (Iso)
+ * array(yyyy,mm,dd,HH,MM,SS)
+ *
+ *
+ * @return object DB_DataObject_Cast
+ * @access public
+ * @author therion 5 at hotmail
+ */
+
+ function dateTime()
+ {
+ $args = func_get_args();
+ switch(count($args)) {
+ case 0: // no args = now!
+ $datetime = date('Y-m-d G:i:s', mktime());
+
+ case 1:
+ // continue on from 0 args.
+ if (!isset($datetime)) {
+ $datetime = $args[0];
+ }
+
+ $parts = explode(' ', $datetime);
+ $bits = explode('-', $parts[0]);
+ $bits = array_merge($bits, explode(':', $parts[1]));
+ break;
+
+ default: // 2 or more..
+ $bits = $args;
+
+ }
+
+ if (count($bits) != 6) {
+ // PEAR ERROR?
+ return false;
+ }
+
+ $r = DB_DataObject_Cast::date($bits[0], $bits[1], $bits[2]);
+ if (!$r) {
+ return $r; // pass thru error (False) - doesnt happen at present!
+ }
+ // change the type!
+ $r->type = 'datetime';
+
+ // should we mathematically sort this out..
+ // (or just assume that no-one's dumb enough to enter 26:90:90 as a time!
+ $r->hour = $bits[3];
+ $r->minute = $bits[4];
+ $r->second = $bits[5];
+ return $r;
+
+ }
+
+
+
+ /**
+ * time Constructor
+ *
+ * create a Cast object from a Date/Time
+ * Maybe should accept a Date object.!
+ * NO VALIDATION DONE, and no-recalcing done!
+ *
+ * @param vargs... accepts
+ * noargs (now)
+ * HH:MM:SS (Iso)
+ * array(HH,MM,SS)
+ *
+ *
+ * @return object DB_DataObject_Cast
+ * @access public
+ * @author therion 5 at hotmail
+ */
+ function time()
+ {
+ $args = func_get_args();
+ switch (count($args)) {
+ case 0: // no args = now!
+ $time = date('G:i:s', mktime());
+
+ case 1:
+ // continue on from 0 args.
+ if (!isset($time)) {
+ $time = $args[0];
+ }
+ $bits = explode(':', $time);
+ break;
+
+ default: // 2 or more..
+ $bits = $args;
+
+ }
+
+ if (count($bits) != 3) {
+ return false;
+ }
+
+ // now take data from bits into object fields
+ $r = new DB_DataObject_Cast;
+ $r->type = 'time';
+ $r->hour = $bits[0];
+ $r->minute = $bits[1];
+ $r->second = $bits[2];
+ return $r;
+
+ }
+
+
+
+ /**
+ * get the string to use in the SQL statement for this...
+ *
+ *
+ * @param int $to Type (DB_DATAOBJECT_*
+ * @param object $db DB Connection Object
+ *
+ *
+ * @return string
+ * @access public
+ */
+
+ function toString($to=false,$db)
+ {
+ // if $this->type is not set, we are in serious trouble!!!!
+ // values for to:
+ $method = 'toStringFrom'.$this->type;
+ return $this->$method($to,$db);
+ }
+
+ /**
+ * get the string to use in the SQL statement from a blob of binary data
+ * ** Suppots only blob->postgres::bytea
+ *
+ * @param int $to Type (DB_DATAOBJECT_*
+ * @param object $db DB Connection Object
+ *
+ *
+ * @return string
+ * @access public
+ */
+ function toStringFromBlob($to,$db)
+ {
+ // first weed out invalid casts..
+ // in blobs can only be cast to blobs.!
+
+ // perhaps we should support TEXT fields???
+
+ if (!($to & DB_DATAOBJECT_BLOB)) {
+ return PEAR::raiseError('Invalid Cast from a DB_DataObject_Cast::blob to something other than a blob!');
+ }
+
+ switch ($db->dsn["phptype"]) {
+ case 'pgsql':
+ return "'".pg_escape_bytea($this->value)."'::bytea";
+
+ case 'mysql':
+ return "'".mysql_real_escape_string($this->value,$db->connection)."'";
+
+ case 'mysqli':
+ // this is funny - the parameter order is reversed ;)
+ return "'".mysqli_real_escape_string($db->connection, $this->value)."'";
+
+
+
+ default:
+ return PEAR::raiseError("DB_DataObject_Cast cant handle blobs for Database:{$db->dsn['phptype']} Yet");
+ }
+
+ }
+
+ /**
+ * get the string to use in the SQL statement for a blob from a string!
+ * ** Suppots only string->postgres::bytea
+ *
+ *
+ * @param int $to Type (DB_DATAOBJECT_*
+ * @param object $db DB Connection Object
+ *
+ *
+ * @return string
+ * @access public
+ */
+ function toStringFromString($to,$db)
+ {
+ // first weed out invalid casts..
+ // in blobs can only be cast to blobs.!
+
+ // perhaps we should support TEXT fields???
+ //
+
+ if (!($to & DB_DATAOBJECT_BLOB)) {
+ return PEAR::raiseError('Invalid Cast from a DB_DataObject_Cast::string to something other than a blob!'.
+ ' (why not just use native features)');
+ }
+
+ switch ($db->dsn['phptype']) {
+ case 'pgsql':
+ return "'".pg_escape_string($this->value)."'::bytea";
+
+ case 'mysql':
+ return "'".mysql_real_escape_string($this->value,$db->connection)."'";
+
+
+ case 'mysqli':
+ return "'".mysqli_real_escape_string($db->connection, $this->value)."'";
+
+
+ default:
+ return PEAR::raiseError("DB_DataObject_Cast cant handle blobs for Database:{$db->dsn['phptype']} Yet");
+ }
+
+ }
+
+
+ /**
+ * get the string to use in the SQL statement for a date
+ *
+ *
+ *
+ * @param int $to Type (DB_DATAOBJECT_*
+ * @param object $db DB Connection Object
+ *
+ *
+ * @return string
+ * @access public
+ */
+ function toStringFromDate($to,$db)
+ {
+ // first weed out invalid casts..
+ // in blobs can only be cast to blobs.!
+ // perhaps we should support TEXT fields???
+ //
+
+ if (($to !== false) && !($to & DB_DATAOBJECT_DATE)) {
+ return PEAR::raiseError('Invalid Cast from a DB_DataObject_Cast::string to something other than a date!'.
+ ' (why not just use native features)');
+ }
+ return "'{$this->year}-{$this->month}-{$this->day}'";
+ }
+
+ /**
+ * get the string to use in the SQL statement for a datetime
+ *
+ *
+ *
+ * @param int $to Type (DB_DATAOBJECT_*
+ * @param object $db DB Connection Object
+ *
+ *
+ * @return string
+ * @access public
+ * @author therion 5 at hotmail
+ */
+
+ function toStringFromDateTime($to,$db)
+ {
+ // first weed out invalid casts..
+ // in blobs can only be cast to blobs.!
+ // perhaps we should support TEXT fields???
+ if (($to !== false) &&
+ !($to & (DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME))) {
+ return PEAR::raiseError('Invalid Cast from a ' .
+ ' DB_DataObject_Cast::dateTime to something other than a datetime!' .
+ ' (try using native features)');
+ }
+ return "'{$this->year}-{$this->month}-{$this->day} {$this->hour}:{$this->minute}:{$this->second}'";
+ }
+
+ /**
+ * get the string to use in the SQL statement for a time
+ *
+ *
+ *
+ * @param int $to Type (DB_DATAOBJECT_*
+ * @param object $db DB Connection Object
+ *
+ *
+ * @return string
+ * @access public
+ * @author therion 5 at hotmail
+ */
+
+ function toStringFromTime($to,$db)
+ {
+ // first weed out invalid casts..
+ // in blobs can only be cast to blobs.!
+ // perhaps we should support TEXT fields???
+ if (($to !== false) && !($to & DB_DATAOBJECT_TIME)) {
+ return PEAR::raiseError('Invalid Cast from a' .
+ ' DB_DataObject_Cast::time to something other than a time!'.
+ ' (try using native features)');
+ }
+ return "'{$this->hour}:{$this->minute}:{$this->second}'";
+ }
+
+ /**
+ * get the string to use in the SQL statement for a raw sql statement.
+ *
+ * @param int $to Type (DB_DATAOBJECT_*
+ * @param object $db DB Connection Object
+ *
+ *
+ * @return string
+ * @access public
+ */
+ function toStringFromSql($to,$db)
+ {
+ return $this->value;
+ }
+
+
+
+
+}
+
diff --git a/extlib/DB/DataObject/Error.php b/extlib/DB/DataObject/Error.php
new file mode 100644
index 000000000..05a741408
--- /dev/null
+++ b/extlib/DB/DataObject/Error.php
@@ -0,0 +1,53 @@
+<?php
+/**
+ * DataObjects error handler, loaded on demand...
+ *
+ * DB_DataObject_Error is a quick wrapper around pear error, so you can distinguish the
+ * error code source.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt. If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category Database
+ * @package DB_DataObject
+ * @author Alan Knowles <alan@akbkhome.com>
+ * @copyright 1997-2006 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0
+ * @version CVS: $Id: Error.php,v 1.3 2005/03/23 02:35:35 alan_k Exp $
+ * @link http://pear.php.net/package/DB_DataObject
+ */
+
+
+class DB_DataObject_Error extends PEAR_Error
+{
+
+ /**
+ * DB_DataObject_Error constructor.
+ *
+ * @param mixed $code DB error code, or string with error message.
+ * @param integer $mode what "error mode" to operate in
+ * @param integer $level what error level to use for $mode & PEAR_ERROR_TRIGGER
+ * @param mixed $debuginfo additional debug info, such as the last query
+ *
+ * @access public
+ *
+ * @see PEAR_Error
+ */
+ function DB_DataObject_Error($message = '', $code = DB_ERROR, $mode = PEAR_ERROR_RETURN,
+ $level = E_USER_NOTICE)
+ {
+ $this->PEAR_Error('DB_DataObject Error: ' . $message, $code, $mode, $level);
+
+ }
+
+
+ // todo : - support code -> message handling, and translated error messages...
+
+
+
+}
diff --git a/extlib/DB/DataObject/Generator.php b/extlib/DB/DataObject/Generator.php
new file mode 100644
index 000000000..de16af692
--- /dev/null
+++ b/extlib/DB/DataObject/Generator.php
@@ -0,0 +1,1553 @@
+<?php
+/**
+ * Generation tools for DB_DataObject
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt. If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category Database
+ * @package DB_DataObject
+ * @author Alan Knowles <alan@akbkhome.com>
+ * @copyright 1997-2006 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0
+ * @version CVS: $Id: Generator.php,v 1.141 2008/01/30 02:29:39 alan_k Exp $
+ * @link http://pear.php.net/package/DB_DataObject
+ */
+
+ /*
+ * Security Notes:
+ * This class uses eval to create classes on the fly.
+ * The table name and database name are used to check the database before writing the
+ * class definitions, we now check for quotes and semi-colon's in both variables
+ * so I cant see how it would be possible to generate code even if
+ * for some crazy reason you took the classname and table name from User Input.
+ *
+ * If you consider that wrong, or can prove it.. let me know!
+ */
+
+ /**
+ *
+ * Config _$ptions
+ * [DB_DataObject_Generator]
+ * ; optional default = DB/DataObject.php
+ * extends_location =
+ * ; optional default = DB_DataObject
+ * extends =
+ * ; alter the extends field when updating a class (defaults to only replacing DB_DataObject)
+ * generator_class_rewrite = ANY|specific_name // default is DB_DataObject
+ *
+ */
+
+/**
+ * Needed classes
+ * We lazy load here, due to problems with the tests not setting up include path correctly.
+ * FIXME!
+ */
+class_exists('DB_DataObject') ? '' : require_once 'DB/DataObject.php';
+//require_once('Config.php');
+
+/**
+ * Generator class
+ *
+ * @package DB_DataObject
+ */
+class DB_DataObject_Generator extends DB_DataObject
+{
+ /* =========================================================== */
+ /* Utility functions - for building db config files */
+ /* =========================================================== */
+
+ /**
+ * Array of table names
+ *
+ * @var array
+ * @access private
+ */
+ var $tables;
+
+ /**
+ * associative array table -> array of table row objects
+ *
+ * @var array
+ * @access private
+ */
+ var $_definitions;
+
+ /**
+ * active table being output
+ *
+ * @var string
+ * @access private
+ */
+ var $table; // active tablename
+
+
+ /**
+ * The 'starter' = call this to start the process
+ *
+ * @access public
+ * @return none
+ */
+ function start()
+ {
+ $options = &PEAR::getStaticProperty('DB_DataObject','options');
+ $db_driver = empty($options['db_driver']) ? 'DB' : $options['db_driver'];
+
+ $databases = array();
+ foreach($options as $k=>$v) {
+ if (substr($k,0,9) == 'database_') {
+ $databases[substr($k,9)] = $v;
+ }
+ }
+
+ if (isset($options['database'])) {
+ if ($db_driver == 'DB') {
+ require_once 'DB.php';
+ $dsn = DB::parseDSN($options['database']);
+ } else {
+ require_once 'MDB2.php';
+ $dsn = MDB2::parseDSN($options['database']);
+ }
+
+ if (!isset($database[$dsn['database']])) {
+ $databases[$dsn['database']] = $options['database'];
+ }
+ }
+
+ foreach($databases as $databasename => $database) {
+ if (!$database) {
+ continue;
+ }
+ $this->debug("CREATING FOR $databasename\n");
+ $class = get_class($this);
+ $t = new $class;
+ $t->_database_dsn = $database;
+
+
+ $t->_database = $databasename;
+ if ($db_driver == 'DB') {
+ require_once 'DB.php';
+ $dsn = DB::parseDSN($database);
+ } else {
+ require_once 'MDB2.php';
+ $dsn = MDB2::parseDSN($database);
+ }
+
+ if (($dsn['phptype'] == 'sqlite') && is_file($databasename)) {
+ $t->_database = basename($t->_database);
+ }
+ $t->_createTableList();
+
+ foreach(get_class_methods($class) as $method) {
+ if (substr($method,0,8 ) != 'generate') {
+ continue;
+ }
+ $this->debug("calling $method");
+ $t->$method();
+ }
+ }
+ $this->debug("DONE\n\n");
+ }
+
+ /**
+ * Output File was config object, now just string
+ * Used to generate the Tables
+ *
+ * @var string outputbuffer for table definitions
+ * @access private
+ */
+ var $_newConfig;
+
+ /**
+ * Build a list of tables;
+ * and store it in $this->tables and $this->_definitions[tablename];
+ *
+ * @access private
+ * @return none
+ */
+ function _createTableList()
+ {
+ $this->_connect();
+ $options = &PEAR::getStaticProperty('DB_DataObject','options');
+
+ $__DB= &$GLOBALS['_DB_DATAOBJECT']['CONNECTIONS'][$this->_database_dsn_md5];
+
+ $db_driver = empty($options['db_driver']) ? 'DB' : $options['db_driver'];
+ $is_MDB2 = ($db_driver != 'DB') ? true : false;
+
+ if (is_a($__DB , 'PEAR_Error')) {
+ return PEAR::raiseError($__DB->toString(), null, PEAR_ERROR_DIE);
+ }
+
+ if (!$is_MDB2) {
+ // try getting a list of schema tables first. (postgres)
+ $__DB->expectError(DB_ERROR_UNSUPPORTED);
+ $this->tables = $__DB->getListOf('schema.tables');
+ $__DB->popExpect();
+ } else {
+ /**
+ * set portability and some modules to fetch the informations
+ */
+ $__DB->setOption('portability', MDB2_PORTABILITY_ALL ^ MDB2_PORTABILITY_FIX_CASE);
+ $__DB->loadModule('Manager');
+ $__DB->loadModule('Reverse');
+ }
+
+ if ((empty($this->tables) || is_a($this->tables , 'PEAR_Error'))) {
+ //if that fails fall back to clasic tables list.
+ if (!$is_MDB2) {
+ // try getting a list of schema tables first. (postgres)
+ $__DB->expectError(DB_ERROR_UNSUPPORTED);
+ $this->tables = $__DB->getListOf('tables');
+ $__DB->popExpect();
+ } else {
+ $this->tables = $__DB->manager->listTables();
+ $sequences = $__DB->manager->listSequences();
+ foreach ($sequences as $k => $v) {
+ $this->tables[] = $__DB->getSequenceName($v);
+ }
+ }
+ }
+
+ if (is_a($this->tables , 'PEAR_Error')) {
+ return PEAR::raiseError($this->tables->toString(), null, PEAR_ERROR_DIE);
+ }
+
+ // build views as well if asked to.
+ if (!empty($options['build_views'])) {
+ if (!$is_MDB2) {
+ $views = $__DB->getListOf('views');
+ } else {
+ $views = $__DB->manager->listViews();
+ }
+ if (is_a($views,'PEAR_Error')) {
+ return PEAR::raiseError(
+ 'Error getting Views (check the PEAR bug database for the fix to DB), ' .
+ $views->toString(),
+ null,
+ PEAR_ERROR_DIE
+ );
+ }
+ $this->tables = array_merge ($this->tables, $views);
+ }
+
+ // declare a temporary table to be filled with matching tables names
+ $tmp_table = array();
+
+
+ foreach($this->tables as $table) {
+ if (isset($options['generator_include_regex']) &&
+ !preg_match($options['generator_include_regex'],$table)) {
+ continue;
+ } else if (isset($options['generator_exclude_regex']) &&
+ preg_match($options['generator_exclude_regex'],$table)) {
+ continue;
+ }
+ // postgres strip the schema bit from the
+ if (!empty($options['generator_strip_schema'])) {
+ $bits = explode('.', $table,2);
+ $table = $bits[0];
+ if (count($bits) > 1) {
+ $table = $bits[1];
+ }
+ }
+ $quotedTable = !empty($options['quote_identifiers_tableinfo']) ?
+ $__DB->quoteIdentifier($table) : $table;
+
+ if (!$is_MDB2) {
+
+ $defs = $__DB->tableInfo($quotedTable);
+ } else {
+ $defs = $__DB->reverse->tableInfo($quotedTable);
+ // rename the length value, so it matches db's return.
+ foreach ($defs as $k => $v) {
+ if (!isset($defs[$k]['length'])) {
+ continue;
+ }
+ $defs[$k]['len'] = $defs[$k]['length'];
+ }
+ }
+
+ if (is_a($defs,'PEAR_Error')) {
+ // running in debug mode should pick this up as a big warning..
+ $this->raiseError('Error reading tableInfo, '. $defs->toString());
+ continue;
+ }
+ // cast all definitions to objects - as we deal with that better.
+
+
+
+ foreach($defs as $def) {
+ if (!is_array($def)) {
+ continue;
+ }
+
+ $this->_definitions[$table][] = (object) $def;
+
+ }
+ // we find a matching table, just store it into a temporary array
+ $tmp_table[] = $table;
+
+
+ }
+ // the temporary table array is now the right one (tables names matching
+ // with regex expressions have been removed)
+ $this->tables = $tmp_table;
+ //print_r($this->_definitions);
+ }
+
+ /**
+ * Auto generation of table data.
+ *
+ * it will output to db_oo_{database} the table definitions
+ *
+ * @access private
+ * @return none
+ */
+ function generateDefinitions()
+ {
+ $this->debug("Generating Definitions file: ");
+ if (!$this->tables) {
+ $this->debug("-- NO TABLES -- \n");
+ return;
+ }
+
+ $options = &PEAR::getStaticProperty('DB_DataObject','options');
+
+
+ //$this->_newConfig = new Config('IniFile');
+ $this->_newConfig = '';
+ foreach($this->tables as $this->table) {
+ $this->_generateDefinitionsTable();
+ }
+ $this->_connect();
+ // dont generate a schema if location is not set
+ // it's created on the fly!
+ if (empty($options['schema_location']) && empty($options["ini_{$this->_database}"]) ) {
+ return;
+ }
+ if (!empty($options['generator_no_ini'])) { // built in ini files..
+ return;
+ }
+ $base = @$options['schema_location'];
+ if (isset($options["ini_{$this->_database}"])) {
+ $file = $options["ini_{$this->_database}"];
+ } else {
+ $file = "{$base}/{$this->_database}.ini";
+ }
+
+ if (!file_exists(dirname($file))) {
+ require_once 'System.php';
+ System::mkdir(array('-p','-m',0755,dirname($file)));
+ }
+ $this->debug("Writing ini as {$file}\n");
+ //touch($file);
+ $tmpname = tempnam(session_save_path(),'DataObject_');
+ //print_r($this->_newConfig);
+ $fh = fopen($tmpname,'w');
+ fwrite($fh,$this->_newConfig);
+ fclose($fh);
+ $perms = file_exists($file) ? fileperms($file) : 0755;
+ // windows can fail doing this. - not a perfect solution but otherwise it's getting really kludgy..
+
+ if (!@rename($tmpname, $file)) {
+ unlink($file);
+ rename($tmpname, $file);
+ }
+ chmod($file,$perms);
+ //$ret = $this->_newConfig->writeInput($file,false);
+
+ //if (PEAR::isError($ret) ) {
+ // return PEAR::raiseError($ret->message,null,PEAR_ERROR_DIE);
+ // }
+ }
+
+ /**
+ * generate Foreign Keys (for links.ini)
+ * Currenly only works with mysql / mysqli
+ * to use, you must set option: generate_links=true
+ *
+ * @author Pascal Schöni
+ */
+ function generateForeignKeys()
+ {
+ $options = PEAR::getStaticProperty('DB_DataObject','options');
+ if (empty($options['generate_links'])) {
+ return false;
+ }
+ $__DB = &$GLOBALS['_DB_DATAOBJECT']['CONNECTIONS'][$this->_database_dsn_md5];
+ if (!in_array($__DB->phptype, array('mysql','mysqli'))) {
+ echo "WARNING: cant handle non-mysql introspection for defaults.";
+ return; // cant handle non-mysql introspection for defaults.
+ }
+
+ $DB = $this->getDatabaseConnection();
+
+ $fk = array();
+
+ foreach($this->tables as $this->table) {
+ $res =& $DB->query('SHOW CREATE TABLE ' . $this->table);
+ if (PEAR::isError($res)) {
+ die($res->getMessage());
+ }
+
+ $text = $res->fetchRow(DB_FETCHMODE_DEFAULT, 0);
+ $treffer = array();
+ // Extract FOREIGN KEYS
+ preg_match_all(
+ "/FOREIGN KEY \(`(\w*)`\) REFERENCES `(\w*)` \(`(\w*)`\)/i",
+ $text[1],
+ $treffer,
+ PREG_SET_ORDER);
+
+ if (count($treffer) < 1) {
+ continue;
+ }
+ for ($i = 0; $i < count($treffer); $i++) {
+ $fk[$this->table][$treffer[$i][1]] = $treffer[$i][2] . ":" . $treffer[$i][3];
+ }
+
+ }
+
+ $links_ini = "";
+
+ foreach($fk as $table => $details) {
+ $links_ini .= "[$table]\n";
+ foreach ($details as $col => $ref) {
+ $links_ini .= "$col = $ref\n";
+ }
+ $links_ini .= "\n";
+ }
+
+ // dont generate a schema if location is not set
+ // it's created on the fly!
+ $options = PEAR::getStaticProperty('DB_DataObject','options');
+
+ if (empty($options['schema_location'])) {
+ return;
+ }
+
+
+ $file = "{$options['schema_location']}/{$this->_database}.links.ini";
+
+ if (!file_exists(dirname($file))) {
+ require_once 'System.php';
+ System::mkdir(array('-p','-m',0755,dirname($file)));
+ }
+
+ $this->debug("Writing ini as {$file}\n");
+
+ //touch($file); // not sure why this is needed?
+ $tmpname = tempnam(session_save_path(),'DataObject_');
+
+ $fh = fopen($tmpname,'w');
+ fwrite($fh,$links_ini);
+ fclose($fh);
+ $perms = file_exists($file) ? fileperms($file) : 0755;
+ // windows can fail doing this. - not a perfect solution but otherwise it's getting really kludgy..
+ if (!@rename($tmpname, $file)) {
+ unlink($file);
+ rename($tmpname, $file);
+ }
+ chmod($file, $perms);
+ }
+
+
+ /**
+ * The table geneation part
+ *
+ * @access private
+ * @return tabledef and keys array.
+ */
+ function _generateDefinitionsTable()
+ {
+ global $_DB_DATAOBJECT;
+
+ $defs = $this->_definitions[$this->table];
+ $this->_newConfig .= "\n[{$this->table}]\n";
+ $keys_out = "\n[{$this->table}__keys]\n";
+ $keys_out_primary = '';
+ $keys_out_secondary = '';
+ if (@$_DB_DATAOBJECT['CONFIG']['debug'] > 2) {
+ echo "TABLE STRUCTURE FOR {$this->table}\n";
+ print_r($defs);
+ }
+ $DB = $this->getDatabaseConnection();
+ $dbtype = $DB->phptype;
+
+ $ret = array(
+ 'table' => array(),
+ 'keys' => array(),
+ );
+
+ $ret_keys_primary = array();
+ $ret_keys_secondary = array();
+
+
+
+ foreach($defs as $t) {
+
+ $n=0;
+ $write_ini = true;
+
+
+ switch (strtoupper($t->type)) {
+
+ case 'INT':
+ case 'INT2': // postgres
+ case 'INT4': // postgres
+ case 'INT8': // postgres
+ case 'SERIAL4': // postgres
+ case 'SERIAL8': // postgres
+ case 'INTEGER':
+ case 'TINYINT':
+ case 'SMALLINT':
+ case 'MEDIUMINT':
+ case 'BIGINT':
+ $type = DB_DATAOBJECT_INT;
+ if ($t->len == 1) {
+ $type += DB_DATAOBJECT_BOOL;
+ }
+ break;
+
+ case 'REAL':
+ case 'DOUBLE':
+ case 'DOUBLE PRECISION': // double precision (firebird)
+ case 'FLOAT':
+ case 'FLOAT4': // real (postgres)
+ case 'FLOAT8': // double precision (postgres)
+ case 'DECIMAL':
+ case 'MONEY': // mssql and maybe others
+ case 'NUMERIC':
+ case 'NUMBER': // oci8
+ $type = DB_DATAOBJECT_INT; // should really by FLOAT!!! / MONEY...
+ break;
+
+ case 'YEAR':
+ $type = DB_DATAOBJECT_INT;
+ break;
+
+ case 'BIT':
+ case 'BOOL':
+ case 'BOOLEAN':
+
+ $type = DB_DATAOBJECT_BOOL;
+ // postgres needs to quote '0'
+ if ($dbtype == 'pgsql') {
+ $type += DB_DATAOBJECT_STR;
+ }
+ break;
+
+ case 'STRING':
+ case 'CHAR':
+ case 'VARCHAR':
+ case 'VARCHAR2':
+ case 'TINYTEXT':
+
+ case 'ENUM':
+ case 'SET': // not really but oh well
+ case 'TIMESTAMPTZ': // postgres
+ case 'BPCHAR': // postgres
+ case 'INTERVAL': // postgres (eg. '12 days')
+
+ case 'CIDR': // postgres IP net spec
+ case 'INET': // postgres IP
+ case 'MACADDR': // postgress network Mac address.
+
+ case 'INTEGER[]': // postgres type
+ case 'BOOLEAN[]': // postgres type
+
+ $type = DB_DATAOBJECT_STR;
+ break;
+
+ case 'TEXT':
+ case 'MEDIUMTEXT':
+ case 'LONGTEXT':
+
+ $type = DB_DATAOBJECT_STR + DB_DATAOBJECT_TXT;
+ break;
+
+
+ case 'DATE':
+ $type = DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE;
+ break;
+
+ case 'TIME':
+ $type = DB_DATAOBJECT_STR + DB_DATAOBJECT_TIME;
+ break;
+
+
+ case 'DATETIME':
+
+ $type = DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME;
+ break;
+
+ case 'TIMESTAMP': // do other databases use this???
+
+ $type = ($dbtype == 'mysql') ?
+ DB_DATAOBJECT_MYSQLTIMESTAMP :
+ DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME;
+ break;
+
+
+ case 'TINYBLOB':
+ case 'BLOB': /// these should really be ignored!!!???
+ case 'MEDIUMBLOB':
+ case 'LONGBLOB':
+ case 'BYTEA': // postgres blob support..
+ $type = DB_DATAOBJECT_STR + DB_DATAOBJECT_BLOB;
+ break;
+ default:
+ echo "*****************************************************************\n".
+ "** WARNING UNKNOWN TYPE **\n".
+ "** Found column '{$t->name}', of type '{$t->type}' **\n".
+ "** Please submit a bug, describe what type you expect this **\n".
+ "** column to be **\n".
+ "** ---------POSSIBLE FIX / WORKAROUND -------------------------**\n".
+ "** Try using MDB2 as the backend - eg set the config option **\n".
+ "** db_driver = MDB2 **\n".
+ "*****************************************************************\n";
+ $write_ini = false;
+ break;
+ }
+
+ if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $t->name)) {
+ echo "*****************************************************************\n".
+ "** WARNING COLUMN NAME UNUSABLE **\n".
+ "** Found column '{$t->name}', of type '{$t->type}' **\n".
+ "** Since this column name can't be converted to a php variable **\n".
+ "** name, and the whole idea of mapping would result in a mess **\n".
+ "** This column has been ignored... **\n".
+ "*****************************************************************\n";
+ continue;
+ }
+
+ if (!strlen(trim($t->name))) {
+ continue; // is this a bug?
+ }
+
+ if (preg_match('/not[ _]null/i',$t->flags)) {
+ $type += DB_DATAOBJECT_NOTNULL;
+ }
+
+
+ if (in_array($t->name,array('null','yes','no','true','false'))) {
+ echo "*****************************************************************\n".
+ "** WARNING **\n".
+ "** Found column '{$t->name}', which is invalid in an .ini file **\n".
+ "** This line will not be writen to the file - you will have **\n".
+ "** define the keys()/method manually. **\n".
+ "*****************************************************************\n";
+ $write_ini = false;
+ } else {
+ $this->_newConfig .= "{$t->name} = $type\n";
+ }
+
+ $ret['table'][$t->name] = $type;
+ // i've no idea if this will work well on other databases?
+ // only use primary key or nextval(), cause the setFrom blocks you setting all key items...
+ // if no keys exist fall back to using unique
+ //echo "\n{$t->name} => {$t->flags}\n";
+ if (preg_match("/(auto_increment|nextval\()/i",rawurldecode($t->flags))
+ || (isset($t->autoincrement) && ($t->autoincrement === true))) {
+
+ // native sequences = 2
+ if ($write_ini) {
+ $keys_out_primary .= "{$t->name} = N\n";
+ }
+ $ret_keys_primary[$t->name] = 'N';
+
+ } else if (preg_match("/(primary|unique)/i",$t->flags)) {
+ // keys.. = 1
+ $key_type = 'K';
+ if (!preg_match("/(primary)/i",$t->flags)) {
+ $key_type = 'U';
+ }
+
+ if ($write_ini) {
+ $keys_out_secondary .= "{$t->name} = {$key_type}\n";
+ }
+ $ret_keys_secondary[$t->name] = $key_type;
+ }
+
+
+ }
+
+ $this->_newConfig .= $keys_out . (empty($keys_out_primary) ? $keys_out_secondary : $keys_out_primary);
+ $ret['keys'] = empty($keys_out_primary) ? $ret_keys_secondary : $ret_keys_primary;
+
+ if (@$_DB_DATAOBJECT['CONFIG']['debug'] > 2) {
+ print_r(array("dump for {$this->table}", $ret));
+ }
+
+ return $ret;
+
+
+ }
+
+ /**
+ * Convert a table name into a class name -> override this if you want a different mapping
+ *
+ * @access public
+ * @return string class name;
+ */
+
+
+ function getClassNameFromTableName($table)
+ {
+ $options = &PEAR::getStaticProperty('DB_DataObject','options');
+ $class_prefix = empty($options['class_prefix']) ? '' : $options['class_prefix'];
+ return $class_prefix.preg_replace('/[^A-Z0-9]/i','_',ucfirst(trim($this->table)));
+ }
+
+
+ /**
+ * Convert a table name into a file name -> override this if you want a different mapping
+ *
+ * @access public
+ * @return string file name;
+ */
+
+
+ function getFileNameFromTableName($table)
+ {
+ $options = &PEAR::getStaticProperty('DB_DataObject','options');
+ $base = $options['class_location'];
+ if (strpos($base,'%s') !== false) {
+ $base = dirname($base);
+ }
+ if (!file_exists($base)) {
+ require_once 'System.php';
+ System::mkdir(array('-p',$base));
+ }
+ if (strpos($options['class_location'],'%s') !== false) {
+ $outfilename = sprintf($options['class_location'],
+ preg_replace('/[^A-Z0-9]/i','_',ucfirst($this->table)));
+ } else {
+ $outfilename = "{$base}/".preg_replace('/[^A-Z0-9]/i','_',ucfirst($this->table)).".php";
+ }
+ return $outfilename;
+
+ }
+
+
+ /**
+ * Convert a column name into a method name (usually prefixed by get/set/validateXXXXX)
+ *
+ * @access public
+ * @return string method name;
+ */
+
+
+ function getMethodNameFromColumnName($col)
+ {
+ return ucfirst($col);
+ }
+
+
+
+
+ /*
+ * building the class files
+ * for each of the tables output a file!
+ */
+ function generateClasses()
+ {
+ //echo "Generating Class files: \n";
+ $options = &PEAR::getStaticProperty('DB_DataObject','options');
+
+
+ if ($extends = @$options['extends']) {
+ $this->_extends = $extends;
+ $this->_extendsFile = $options['extends_location'];
+ }
+
+ foreach($this->tables as $this->table) {
+ $this->table = trim($this->table);
+ $this->classname = $this->getClassNameFromTableName($this->table);
+ $i = '';
+ $outfilename = $this->getFileNameFromTableName($this->table);
+
+ $oldcontents = '';
+ if (file_exists($outfilename)) {
+ // file_get_contents???
+ $oldcontents = implode('',file($outfilename));
+ }
+
+ $out = $this->_generateClassTable($oldcontents);
+ $this->debug( "writing $this->classname\n");
+ $tmpname = tempnam(session_save_path(),'DataObject_');
+
+ $fh = fopen($tmpname, "w");
+ fputs($fh,$out);
+ fclose($fh);
+ $perms = file_exists($outfilename) ? fileperms($outfilename) : 0755;
+
+ // windows can fail doing this. - not a perfect solution but otherwise it's getting really kludgy..
+ if (!@rename($tmpname, $outfilename)) {
+ unlink($outfilename);
+ rename($tmpname, $outfilename);
+ }
+
+ chmod($outfilename, $perms);
+ }
+ //echo $out;
+ }
+
+ /**
+ * class being extended (can be overridden by [DB_DataObject_Generator] extends=xxxx
+ *
+ * @var string
+ * @access private
+ */
+ var $_extends = 'DB_DataObject';
+
+ /**
+ * line to use for require('DB/DataObject.php');
+ *
+ * @var string
+ * @access private
+ */
+ var $_extendsFile = "DB/DataObject.php";
+
+ /**
+ * class being generated
+ *
+ * @var string
+ * @access private
+ */
+ var $_className;
+
+ /**
+ * The table class geneation part - single file.
+ *
+ * @access private
+ * @return none
+ */
+ function _generateClassTable($input = '')
+ {
+ // title = expand me!
+ $foot = "";
+ $head = "<?php\n/**\n * Table Definition for {$this->table}\n";
+ $head .= $this->derivedHookPageLevelDocBlock();
+ $head .= " */\n";
+ $head .= $this->derivedHookExtendsDocBlock();
+
+
+ // requires
+ $head .= "require_once '{$this->_extendsFile}';\n\n";
+ // add dummy class header in...
+ // class
+ $head .= $this->derivedHookClassDocBlock();
+ $head .= "class {$this->classname} extends {$this->_extends} \n{";
+
+ $body = "\n ###START_AUTOCODE\n";
+ $body .= " /* the code below is auto generated do not remove the above tag */\n\n";
+ // table
+ $padding = (30 - strlen($this->table));
+ $padding = ($padding < 2) ? 2 : $padding;
+
+ $p = str_repeat(' ',$padding) ;
+
+ $options = &PEAR::getStaticProperty('DB_DataObject','options');
+
+
+ $var = (substr(phpversion(),0,1) > 4) ? 'public' : 'var';
+ $var = !empty($options['generator_var_keyword']) ? $options['generator_var_keyword'] : $var;
+
+
+ $body .= " {$var} \$__table = '{$this->table}'; {$p}// table name\n";
+
+
+ // if we are using the option database_{databasename} = dsn
+ // then we should add var $_database = here
+ // as database names may not always match..
+
+
+
+
+ if (isset($options["database_{$this->_database}"])) {
+ $body .= " {$var} \$_database = '{$this->_database}'; {$p}// database name (used with database_{*} config)\n";
+ }
+
+
+ if (!empty($options['generator_novars'])) {
+ $var = '//'.$var;
+ }
+
+ $defs = $this->_definitions[$this->table];
+
+ // show nice information!
+ $connections = array();
+ $sets = array();
+ foreach($defs as $t) {
+ if (!strlen(trim($t->name))) {
+ continue;
+ }
+ if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $t->name)) {
+ echo "*****************************************************************\n".
+ "** WARNING COLUMN NAME UNUSABLE **\n".
+ "** Found column '{$t->name}', of type '{$t->type}' **\n".
+ "** Since this column name can't be converted to a php variable **\n".
+ "** name, and the whole idea of mapping would result in a mess **\n".
+ "** This column has been ignored... **\n".
+ "*****************************************************************\n";
+ continue;
+ }
+
+
+ $padding = (30 - strlen($t->name));
+ if ($padding < 2) $padding =2;
+ $p = str_repeat(' ',$padding) ;
+
+ $body .=" {$var} \${$t->name}; {$p}// {$t->type}({$t->len}) {$t->flags}\n";
+
+ // can not do set as PEAR::DB table info doesnt support it.
+ //if (substr($t->Type,0,3) == "set")
+ // $sets[$t->Field] = "array".substr($t->Type,3);
+ $body .= $this->derivedHookVar($t,$padding);
+ }
+
+ // THIS IS TOTALLY BORKED old FC creation
+ // IT WILL BE REMOVED!!!!! in DataObjects 1.6
+ // grep -r __clone * to find all it's uses
+ // and replace them with $x = clone($y);
+ // due to the change in the PHP5 clone design.
+
+ if ( substr(phpversion(),0,1) < 5) {
+ $body .= "\n";
+ $body .= " /* ZE2 compatibility trick*/\n";
+ $body .= " function __clone() { return \$this;}\n";
+ }
+
+ // simple creation tools ! (static stuff!)
+ $body .= "\n";
+ $body .= " /* Static get */\n";
+ $body .= " function staticGet(\$k,\$v=NULL) { return DB_DataObject::staticGet('{$this->classname}',\$k,\$v); }\n";
+
+ // generate getter and setter methods
+ $body .= $this->_generateGetters($input);
+ $body .= $this->_generateSetters($input);
+
+ /*
+ theoretically there is scope here to introduce 'list' methods
+ based up 'xxxx_up' column!!! for heiracitcal trees..
+ */
+
+ // set methods
+ //foreach ($sets as $k=>$v) {
+ // $kk = strtoupper($k);
+ // $body .=" function getSets{$k}() { return {$v}; }\n";
+ //}
+
+ if (!empty($options['generator_no_ini'])) {
+ $def = $this->_generateDefinitionsTable(); // simplify this!?
+ $body .= $this->_generateTableFunction($def['table']);
+ $body .= $this->_generateKeysFunction($def['keys']);
+ $body .= $this->_generateSequenceKeyFunction($def);
+ $body .= $this->_generateDefaultsFunction($this->table, $def['table']);
+ } else if (!empty($options['generator_add_defaults'])) {
+ // I dont really like doing it this way (adding another option)
+ // but it helps on older projects.
+ $def = $this->_generateDefinitionsTable(); // simplify this!?
+ $body .= $this->_generateDefaultsFunction($this->table,$def['table']);
+
+ }
+ $body .= $this->derivedHookFunctions($input);
+
+ $body .= "\n /* the code above is auto generated do not remove the tag below */";
+ $body .= "\n ###END_AUTOCODE\n";
+
+
+ // stubs..
+
+ if (!empty($options['generator_add_validate_stubs'])) {
+ foreach($defs as $t) {
+ if (!strlen(trim($t->name))) {
+ continue;
+ }
+ $validate_fname = 'validate' . $this->getMethodNameFromColumnName($t->name);
+ // dont re-add it..
+ if (preg_match('/\s+function\s+' . $validate_fname . '\s*\(/i', $input)) {
+ continue;
+ }
+ $body .= "\n function {$validate_fname}()\n {\n return false;\n }\n";
+ }
+ }
+
+
+
+
+ $foot .= "}\n";
+ $full = $head . $body . $foot;
+
+ if (!$input) {
+ return $full;
+ }
+ if (!preg_match('/(\n|\r\n)\s*###START_AUTOCODE(\n|\r\n)/s',$input)) {
+ return $full;
+ }
+ if (!preg_match('/(\n|\r\n)\s*###END_AUTOCODE(\n|\r\n)/s',$input)) {
+ return $full;
+ }
+
+
+ /* this will only replace extends DB_DataObject by default,
+ unless use set generator_class_rewrite to ANY or a name*/
+
+ $class_rewrite = 'DB_DataObject';
+ $options = &PEAR::getStaticProperty('DB_DataObject','options');
+ if (empty($options['generator_class_rewrite']) || !($class_rewrite = $options['generator_class_rewrite'])) {
+ $class_rewrite = 'DB_DataObject';
+ }
+ if ($class_rewrite == 'ANY') {
+ $class_rewrite = '[a-z_]+';
+ }
+
+ $input = preg_replace(
+ '/(\n|\r\n)class\s*[a-z0-9_]+\s*extends\s*' .$class_rewrite . '\s*(\n|\r\n)\{(\n|\r\n)/si',
+ "\nclass {$this->classname} extends {$this->_extends} \n{\n",
+ $input);
+
+ $ret = preg_replace(
+ '/(\n|\r\n)\s*###START_AUTOCODE(\n|\r\n).*(\n|\r\n)\s*###END_AUTOCODE(\n|\r\n)/s',
+ $body,$input);
+
+ if (!strlen($ret)) {
+ return PEAR::raiseError(
+ "PREG_REPLACE failed to replace body, - you probably need to set these in your php.ini\n".
+ "pcre.backtrack_limit=1000000\n".
+ "pcre.recursion_limit=1000000\n"
+ ,null, PEAR_ERROR_DIE);
+ }
+
+ return $ret;
+ }
+
+ /**
+ * hook to add extra methods to all classes
+ *
+ * called once for each class, use with $this->table and
+ * $this->_definitions[$this->table], to get data out of the current table,
+ * use it to add extra methods to the default classes.
+ *
+ * @access public
+ * @return string added to class eg. functions.
+ */
+ function derivedHookFunctions($input = "")
+ {
+ // This is so derived generator classes can generate functions
+ // It MUST NOT be changed here!!!
+ return "";
+ }
+
+ /**
+ * hook for var lines
+ * called each time a var line is generated, override to add extra var
+ * lines
+ *
+ * @param object t containing type,len,flags etc. from tableInfo call
+ * @param int padding number of spaces
+ * @access public
+ * @return string added to class eg. functions.
+ */
+ function derivedHookVar(&$t,$padding)
+ {
+ // This is so derived generator classes can generate variabels
+ // It MUST NOT be changed here!!!
+ return "";
+ }
+
+ /**
+ * hook to add extra page-level (in terms of phpDocumentor) DocBlock
+ *
+ * called once for each class, use it add extra page-level docs
+ * @access public
+ * @return string added to class eg. functions.
+ */
+ function derivedHookPageLevelDocBlock() {
+ return '';
+ }
+
+ /**
+ * hook to add extra doc block (in terms of phpDocumentor) to extend string
+ *
+ * called once for each class, use it add extra comments to extends
+ * string (require_once...)
+ * @access public
+ * @return string added to class eg. functions.
+ */
+ function derivedHookExtendsDocBlock() {
+ return '';
+ }
+
+ /**
+ * hook to add extra class level DocBlock (in terms of phpDocumentor)
+ *
+ * called once for each class, use it add extra comments to class
+ * string (require_once...)
+ * @access public
+ * @return string added to class eg. functions.
+ */
+ function derivedHookClassDocBlock() {
+ return '';
+ }
+
+ /**
+
+ /**
+ * getProxyFull - create a class definition on the fly and instantate it..
+ *
+ * similar to generated files - but also evals the class definitoin code.
+ *
+ *
+ * @param string database name
+ * @param string table name of table to create proxy for.
+ *
+ *
+ * @return object Instance of class. or PEAR Error
+ * @access public
+ */
+ function getProxyFull($database,$table)
+ {
+
+ if ($err = $this->fillTableSchema($database,$table)) {
+ return $err;
+ }
+
+
+ $options = &PEAR::getStaticProperty('DB_DataObject','options');
+ $class_prefix = empty($options['class_prefix']) ? '' : $options['class_prefix'];
+
+ if ($extends = @$options['extends']) {
+ $this->_extends = $extends;
+ $this->_extendsFile = $options['extends_location'];
+ }
+ $classname = $this->classname = $this->getClassNameFromTableName($this->table);
+
+ $out = $this->_generateClassTable();
+ //echo $out;
+ eval('?>'.$out);
+ return new $classname;
+
+ }
+
+ /**
+ * fillTableSchema - set the database schema on the fly
+ *
+ *
+ *
+ * @param string database name
+ * @param string table name of table to create schema info for
+ *
+ * @return none | PEAR::error()
+ * @access public
+ */
+ function fillTableSchema($database,$table)
+ {
+ global $_DB_DATAOBJECT;
+ // a little bit of sanity testing.
+ if ((false !== strpos($database,"'")) || (false !== strpos($database,";"))) {
+ return PEAR::raiseError("Error: Database name contains a quote or semi-colon", null, PEAR_ERROR_DIE);
+ }
+
+ $this->_database = $database;
+
+ $this->_connect();
+ $table = trim($table);
+
+ // a little bit of sanity testing.
+ if ((false !== strpos($table,"'")) || (false !== strpos($table,";"))) {
+ return PEAR::raiseError("Error: Table contains a quote or semi-colon", null, PEAR_ERROR_DIE);
+ }
+ $__DB= &$GLOBALS['_DB_DATAOBJECT']['CONNECTIONS'][$this->_database_dsn_md5];
+
+
+ $options = PEAR::getStaticProperty('DB_DataObject','options');
+ $db_driver = empty($options['db_driver']) ? 'DB' : $options['db_driver'];
+ $is_MDB2 = ($db_driver != 'DB') ? true : false;
+
+ if (!$is_MDB2) {
+ // try getting a list of schema tables first. (postgres)
+ $__DB->expectError(DB_ERROR_UNSUPPORTED);
+ $this->tables = $__DB->getListOf('schema.tables');
+ $__DB->popExpect();
+ } else {
+ /**
+ * set portability and some modules to fetch the informations
+ */
+ $__DB->setOption('portability', MDB2_PORTABILITY_ALL ^ MDB2_PORTABILITY_FIX_CASE);
+ $__DB->loadModule('Manager');
+ $__DB->loadModule('Reverse');
+ }
+ $quotedTable = !empty($options['quote_identifiers']) ?
+ $__DB->quoteIdentifier($table) : $table;
+
+ if (!$is_MDB2) {
+ $defs = $__DB->tableInfo($quotedTable);
+ } else {
+ $defs = $__DB->reverse->tableInfo($quotedTable);
+ foreach ($defs as $k => $v) {
+ if (!isset($defs[$k]['length'])) {
+ continue;
+ }
+ $defs[$k]['len'] = $defs[$k]['length'];
+ }
+ }
+
+
+
+
+ if (PEAR::isError($defs)) {
+ return $defs;
+ }
+ if (@$_DB_DATAOBJECT['CONFIG']['debug'] > 2) {
+ $this->debug("getting def for $database/$table",'fillTable');
+ $this->debug(print_r($defs,true),'defs');
+ }
+ // cast all definitions to objects - as we deal with that better.
+
+
+ foreach($defs as $def) {
+ if (is_array($def)) {
+ $this->_definitions[$table][] = (object) $def;
+ }
+ }
+
+ $this->table = trim($table);
+ $ret = $this->_generateDefinitionsTable();
+
+ $_DB_DATAOBJECT['INI'][$database][$table] = $ret['table'];
+ $_DB_DATAOBJECT['INI'][$database][$table.'__keys'] = $ret['keys'];
+ return false;
+
+ }
+
+ /**
+ * Generate getter methods for class definition
+ *
+ * @param string $input Existing class contents
+ * @return string
+ * @access public
+ */
+ function _generateGetters($input)
+ {
+
+ $options = &PEAR::getStaticProperty('DB_DataObject','options');
+ $getters = '';
+
+ // only generate if option is set to true
+ if (empty($options['generate_getters'])) {
+ return '';
+ }
+
+ // remove auto-generated code from input to be able to check if the method exists outside of the auto-code
+ $input = preg_replace('/(\n|\r\n)\s*###START_AUTOCODE(\n|\r\n).*(\n|\r\n)\s*###END_AUTOCODE(\n|\r\n)/s', '', $input);
+
+ $getters .= "\n\n";
+ $defs = $this->_definitions[$this->table];
+
+ // loop through properties and create getter methods
+ foreach ($defs = $defs as $t) {
+
+ // build mehtod name
+ $methodName = 'get' . $this->getMethodNameFromColumnName($t->name);
+
+ if (!strlen(trim($t->name)) || preg_match("/function[\s]+[&]?$methodName\(/i", $input)) {
+ continue;
+ }
+
+ $getters .= " /**\n";
+ $getters .= " * Getter for \${$t->name}\n";
+ $getters .= " *\n";
+ $getters .= (stristr($t->flags, 'multiple_key')) ? " * @return object\n"
+ : " * @return {$t->type}\n";
+ $getters .= " * @access public\n";
+ $getters .= " */\n";
+ $getters .= (substr(phpversion(),0,1) > 4) ? ' public '
+ : ' ';
+ $getters .= "function $methodName() {\n";
+ $getters .= " return \$this->{$t->name};\n";
+ $getters .= " }\n\n";
+ }
+
+
+ return $getters;
+ }
+
+
+ /**
+ * Generate setter methods for class definition
+ *
+ * @param string Existing class contents
+ * @return string
+ * @access public
+ */
+ function _generateSetters($input)
+ {
+
+ $options = &PEAR::getStaticProperty('DB_DataObject','options');
+ $setters = '';
+
+ // only generate if option is set to true
+ if (empty($options['generate_setters'])) {
+ return '';
+ }
+
+ // remove auto-generated code from input to be able to check if the method exists outside of the auto-code
+ $input = preg_replace('/(\n|\r\n)\s*###START_AUTOCODE(\n|\r\n).*(\n|\r\n)\s*###END_AUTOCODE(\n|\r\n)/s', '', $input);
+
+ $setters .= "\n";
+ $defs = $this->_definitions[$this->table];
+
+ // loop through properties and create setter methods
+ foreach ($defs = $defs as $t) {
+
+ // build mehtod name
+ $methodName = 'set' . $this->getMethodNameFromColumnName($t->name);
+
+ if (!strlen(trim($t->name)) || preg_match("/function[\s]+[&]?$methodName\(/i", $input)) {
+ continue;
+ }
+
+ $setters .= " /**\n";
+ $setters .= " * Setter for \${$t->name}\n";
+ $setters .= " *\n";
+ $setters .= " * @param mixed input value\n";
+ $setters .= " * @access public\n";
+ $setters .= " */\n";
+ $setters .= (substr(phpversion(),0,1) > 4) ? ' public '
+ : ' ';
+ $setters .= "function $methodName(\$value) {\n";
+ $setters .= " \$this->{$t->name} = \$value;\n";
+ $setters .= " }\n\n";
+ }
+
+
+ return $setters;
+ }
+ /**
+ * Generate table Function - used when generator_no_ini is set.
+ *
+ * @param array table array.
+ * @return string
+ * @access public
+ */
+ function _generateTableFunction($def)
+ {
+ $defines = explode(',','INT,STR,DATE,TIME,BOOL,TXT,BLOB,NOTNULL,MYSQLTIMESTAMP');
+
+ $ret = "\n" .
+ " function table()\n" .
+ " {\n" .
+ " return array(\n";
+
+ foreach($def as $k=>$v) {
+ $str = '0';
+ foreach($defines as $dn) {
+ if ($v & constant('DB_DATAOBJECT_' . $dn)) {
+ $str .= ' + DB_DATAOBJECT_' . $dn;
+ }
+ }
+ if (strlen($str) > 1) {
+ $str = substr($str,3); // strip the 0 +
+ }
+ // hopefully addslashes is good enough here!!!
+ $ret .= ' \''.addslashes($k).'\' => ' . $str . ",\n";
+ }
+ return $ret . " );\n" .
+ " }\n";
+
+
+
+ }
+ /**
+ * Generate keys Function - used generator_no_ini is set.
+ *
+ * @param array keys array.
+ * @return string
+ * @access public
+ */
+ function _generateKeysFunction($def)
+ {
+
+ $ret = "\n" .
+ " function keys()\n" .
+ " {\n" .
+ " return array(";
+
+ foreach($def as $k=>$type) {
+ // hopefully addslashes is good enough here!!!
+ $ret .= '\''.addslashes($k).'\', ';
+ }
+ $ret = preg_replace('#, $#', '', $ret);
+ return $ret . ");\n" .
+ " }\n";
+
+
+
+ }
+ /**
+ * Generate sequenceKey Function - used generator_no_ini is set.
+ *
+ * @param array table and key definition.
+ * @return string
+ * @access public
+ */
+ function _generateSequenceKeyFunction($def)
+ {
+
+ //print_r($def);
+ // DB_DataObject::debugLevel(5);
+ global $_DB_DATAOBJECT;
+ // print_r($def);
+
+
+ $dbtype = $_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5]->dsn['phptype'];
+ $realkeys = $def['keys'];
+ $keys = array_keys($realkeys);
+ $usekey = isset($keys[0]) ? $keys[0] : false;
+ $table = $def['table'];
+
+
+ $seqname = false;
+
+
+
+
+ $ar = array(false,false,false);
+ if ($usekey !== false) {
+ if (!empty($_DB_DATAOBJECT['CONFIG']['sequence_'.$this->__table])) {
+ $usekey = $_DB_DATAOBJECT['CONFIG']['sequence_'.$this->__table];
+ if (strpos($usekey,':') !== false) {
+ list($usekey,$seqname) = explode(':',$usekey);
+ }
+ }
+
+ if (in_array($dbtype , array( 'mysql', 'mysqli', 'mssql', 'ifx')) &&
+ ($table[$usekey] & DB_DATAOBJECT_INT) &&
+ isset($realkeys[$usekey]) && ($realkeys[$usekey] == 'N')
+ ) {
+ // use native sequence keys.
+ $ar = array($usekey,true,$seqname);
+ } else {
+ // use generated sequence keys..
+ if ($table[$usekey] & DB_DATAOBJECT_INT) {
+ $ar = array($usekey,false,$seqname);
+ }
+ }
+ }
+
+
+
+
+ $ret = "\n" .
+ " function sequenceKey() // keyname, use native, native name\n" .
+ " {\n" .
+ " return array(";
+ foreach($ar as $v) {
+ switch (gettype($v)) {
+ case 'boolean':
+ $ret .= ($v ? 'true' : 'false') . ', ';
+ break;
+
+ case 'string':
+ $ret .= "'" . $v . "', ";
+ break;
+
+ default: // eak
+ $ret .= "null, ";
+
+ }
+ }
+ $ret = preg_replace('#, $#', '', $ret);
+ return $ret . ");\n" .
+ " }\n";
+
+ }
+ /**
+ * Generate defaults Function - used generator_add_defaults or generator_no_ini is set.
+ * Only supports mysql and mysqli ... welcome ideas for more..
+ *
+ *
+ * @param array table and key definition.
+ * @return string
+ * @access public
+ */
+ function _generateDefaultsFunction($table,$defs)
+ {
+ $__DB= &$GLOBALS['_DB_DATAOBJECT']['CONNECTIONS'][$this->_database_dsn_md5];
+ if (!in_array($__DB->phptype, array('mysql','mysqli'))) {
+ return; // cant handle non-mysql introspection for defaults.
+ }
+ $options = PEAR::getStaticProperty('DB_DataObject','options');
+ $db_driver = empty($options['db_driver']) ? 'DB' : $options['db_driver'];
+ $method = $db_driver == 'DB' ? 'getAll' : 'queryAll';
+ $res = $__DB->$method('DESCRIBE ' . $table,DB_FETCHMODE_ASSOC);
+ $defaults = array();
+ foreach($res as $ar) {
+ // this is initially very dumb... -> and it may mess up..
+ $type = $defs[$ar['Field']];
+
+ switch (true) {
+
+ case (is_null( $ar['Default'])):
+ $defaults[$ar['Field']] = 'null';
+ break;
+
+ case ($type & DB_DATAOBJECT_DATE):
+ case ($type & DB_DATAOBJECT_TIME):
+ case ($type & DB_DATAOBJECT_MYSQLTIMESTAMP): // not supported yet..
+ break;
+
+ case ($type & DB_DATAOBJECT_BOOL):
+ $defaults[$ar['Field']] = (int)(boolean) $ar['Default'];
+ break;
+
+
+ case ($type & DB_DATAOBJECT_STR):
+ $defaults[$ar['Field']] = "'" . addslashes($ar['Default']) . "'";
+ break;
+
+
+ default: // hopefully eveything else... - numbers etc.
+ if (!strlen($ar['Default'])) {
+ continue;
+ }
+ if (is_numeric($ar['Default'])) {
+ $defaults[$ar['Field']] = $ar['Default'];
+ }
+ break;
+
+ }
+ //var_dump(array($ar['Field'], $ar['Default'], $defaults[$ar['Field']]));
+ }
+ if (empty($defaults)) {
+ return;
+ }
+
+ $ret = "\n" .
+ " function defaults() // column default values \n" .
+ " {\n" .
+ " return array(\n";
+ foreach($defaults as $k=>$v) {
+ $ret .= ' \''.addslashes($k).'\' => ' . $v . ",\n";
+ }
+ return $ret . " );\n" .
+ " }\n";
+
+
+
+
+ }
+
+
+
+
+
+}
diff --git a/extlib/DB/DataObject/createTables.php b/extlib/DB/DataObject/createTables.php
new file mode 100755
index 000000000..c0659574e
--- /dev/null
+++ b/extlib/DB/DataObject/createTables.php
@@ -0,0 +1,59 @@
+#!/usr/bin/php -q
+<?php
+// +----------------------------------------------------------------------+
+// | PHP Version 4 |
+// +----------------------------------------------------------------------+
+// | Copyright (c) 1997-2003 The PHP Group |
+// +----------------------------------------------------------------------+
+// | This source file is subject to version 2.02 of the PHP license, |
+// | that is bundled with this package in the file LICENSE, and is |
+// | available at through the world-wide-web at |
+// | http://www.php.net/license/2_02.txt. |
+// | If you did not receive a copy of the PHP license and are unable to |
+// | obtain it through the world-wide-web, please send a note to |
+// | license@php.net so we can mail you a copy immediately. |
+// +----------------------------------------------------------------------+
+// | Author: Alan Knowles <alan@akbkhome.com>
+// +----------------------------------------------------------------------+
+//
+// $Id: createTables.php,v 1.24 2006/01/13 01:27:55 alan_k Exp $
+//
+
+// since this version doesnt use overload,
+// and I assume anyone using custom generators should add this..
+
+define('DB_DATAOBJECT_NO_OVERLOAD',1);
+
+//require_once 'DB/DataObject/Generator.php';
+require_once 'DB/DataObject/Generator.php';
+
+if (!ini_get('register_argc_argv')) {
+ PEAR::raiseError("\nERROR: You must turn register_argc_argv On in you php.ini file for this to work\neg.\n\nregister_argc_argv = On\n\n", null, PEAR_ERROR_DIE);
+ exit;
+}
+
+if (!@$_SERVER['argv'][1]) {
+ PEAR::raiseError("\nERROR: createTable.php usage:\n\nC:\php\pear\DB\DataObjects\createTable.php example.ini\n\n", null, PEAR_ERROR_DIE);
+ exit;
+}
+
+$config = parse_ini_file($_SERVER['argv'][1], true);
+foreach($config as $class=>$values) {
+ $options = &PEAR::getStaticProperty($class,'options');
+ $options = $values;
+}
+
+
+$options = &PEAR::getStaticProperty('DB_DataObject','options');
+if (empty($options)) {
+ PEAR::raiseError("\nERROR: could not read ini file\n\n", null, PEAR_ERROR_DIE);
+ exit;
+}
+set_time_limit(0);
+
+// use debug level from file if set..
+DB_DataObject::debugLevel(isset($options['debug']) ? $options['debug'] : 1);
+
+$generator = new DB_DataObject_Generator;
+$generator->start();
+
diff --git a/extlib/DB/common.php b/extlib/DB/common.php
new file mode 100644
index 000000000..c51323d25
--- /dev/null
+++ b/extlib/DB/common.php
@@ -0,0 +1,2262 @@
+<?php
+
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * Contains the DB_common base class
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt. If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category Database
+ * @package DB
+ * @author Stig Bakken <ssb@php.net>
+ * @author Tomas V.V. Cox <cox@idecnet.com>
+ * @author Daniel Convissor <danielc@php.net>
+ * @copyright 1997-2007 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0
+ * @version CVS: $Id: common.php,v 1.144 2007/11/26 22:54:03 aharvey Exp $
+ * @link http://pear.php.net/package/DB
+ */
+
+/**
+ * Obtain the PEAR class so it can be extended from
+ */
+require_once 'PEAR.php';
+
+/**
+ * DB_common is the base class from which each database driver class extends
+ *
+ * All common methods are declared here. If a given DBMS driver contains
+ * a particular method, that method will overload the one here.
+ *
+ * @category Database
+ * @package DB
+ * @author Stig Bakken <ssb@php.net>
+ * @author Tomas V.V. Cox <cox@idecnet.com>
+ * @author Daniel Convissor <danielc@php.net>
+ * @copyright 1997-2007 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0
+ * @version Release: 1.7.14RC1
+ * @link http://pear.php.net/package/DB
+ */
+class DB_common extends PEAR
+{
+ // {{{ properties
+
+ /**
+ * The current default fetch mode
+ * @var integer
+ */
+ var $fetchmode = DB_FETCHMODE_ORDERED;
+
+ /**
+ * The name of the class into which results should be fetched when
+ * DB_FETCHMODE_OBJECT is in effect
+ *
+ * @var string
+ */
+ var $fetchmode_object_class = 'stdClass';
+
+ /**
+ * Was a connection present when the object was serialized()?
+ * @var bool
+ * @see DB_common::__sleep(), DB_common::__wake()
+ */
+ var $was_connected = null;
+
+ /**
+ * The most recently executed query
+ * @var string
+ */
+ var $last_query = '';
+
+ /**
+ * Run-time configuration options
+ *
+ * The 'optimize' option has been deprecated. Use the 'portability'
+ * option instead.
+ *
+ * @var array
+ * @see DB_common::setOption()
+ */
+ var $options = array(
+ 'result_buffering' => 500,
+ 'persistent' => false,
+ 'ssl' => false,
+ 'debug' => 0,
+ 'seqname_format' => '%s_seq',
+ 'autofree' => false,
+ 'portability' => DB_PORTABILITY_NONE,
+ 'optimize' => 'performance', // Deprecated. Use 'portability'.
+ );
+
+ /**
+ * The parameters from the most recently executed query
+ * @var array
+ * @since Property available since Release 1.7.0
+ */
+ var $last_parameters = array();
+
+ /**
+ * The elements from each prepared statement
+ * @var array
+ */
+ var $prepare_tokens = array();
+
+ /**
+ * The data types of the various elements in each prepared statement
+ * @var array
+ */
+ var $prepare_types = array();
+
+ /**
+ * The prepared queries
+ * @var array
+ */
+ var $prepared_queries = array();
+
+ /**
+ * Flag indicating that the last query was a manipulation query.
+ * @access protected
+ * @var boolean
+ */
+ var $_last_query_manip = false;
+
+ /**
+ * Flag indicating that the next query <em>must</em> be a manipulation
+ * query.
+ * @access protected
+ * @var boolean
+ */
+ var $_next_query_manip = false;
+
+
+ // }}}
+ // {{{ DB_common
+
+ /**
+ * This constructor calls <kbd>$this->PEAR('DB_Error')</kbd>
+ *
+ * @return void
+ */
+ function DB_common()
+ {
+ $this->PEAR('DB_Error');
+ }
+
+ // }}}
+ // {{{ __sleep()
+
+ /**
+ * Automatically indicates which properties should be saved
+ * when PHP's serialize() function is called
+ *
+ * @return array the array of properties names that should be saved
+ */
+ function __sleep()
+ {
+ if ($this->connection) {
+ // Don't disconnect(), people use serialize() for many reasons
+ $this->was_connected = true;
+ } else {
+ $this->was_connected = false;
+ }
+ if (isset($this->autocommit)) {
+ return array('autocommit',
+ 'dbsyntax',
+ 'dsn',
+ 'features',
+ 'fetchmode',
+ 'fetchmode_object_class',
+ 'options',
+ 'was_connected',
+ );
+ } else {
+ return array('dbsyntax',
+ 'dsn',
+ 'features',
+ 'fetchmode',
+ 'fetchmode_object_class',
+ 'options',
+ 'was_connected',
+ );
+ }
+ }
+
+ // }}}
+ // {{{ __wakeup()
+
+ /**
+ * Automatically reconnects to the database when PHP's unserialize()
+ * function is called
+ *
+ * The reconnection attempt is only performed if the object was connected
+ * at the time PHP's serialize() function was run.
+ *
+ * @return void
+ */
+ function __wakeup()
+ {
+ if ($this->was_connected) {
+ $this->connect($this->dsn, $this->options);
+ }
+ }
+
+ // }}}
+ // {{{ __toString()
+
+ /**
+ * Automatic string conversion for PHP 5
+ *
+ * @return string a string describing the current PEAR DB object
+ *
+ * @since Method available since Release 1.7.0
+ */
+ function __toString()
+ {
+ $info = strtolower(get_class($this));
+ $info .= ': (phptype=' . $this->phptype .
+ ', dbsyntax=' . $this->dbsyntax .
+ ')';
+ if ($this->connection) {
+ $info .= ' [connected]';
+ }
+ return $info;
+ }
+
+ // }}}
+ // {{{ toString()
+
+ /**
+ * DEPRECATED: String conversion method
+ *
+ * @return string a string describing the current PEAR DB object
+ *
+ * @deprecated Method deprecated in Release 1.7.0
+ */
+ function toString()
+ {
+ return $this->__toString();
+ }
+
+ // }}}
+ // {{{ quoteString()
+
+ /**
+ * DEPRECATED: Quotes a string so it can be safely used within string
+ * delimiters in a query
+ *
+ * @param string $string the string to be quoted
+ *
+ * @return string the quoted string
+ *
+ * @see DB_common::quoteSmart(), DB_common::escapeSimple()
+ * @deprecated Method deprecated some time before Release 1.2
+ */
+ function quoteString($string)
+ {
+ $string = $this->quote($string);
+ if ($string{0} == "'") {
+ return substr($string, 1, -1);
+ }
+ return $string;
+ }
+
+ // }}}
+ // {{{ quote()
+
+ /**
+ * DEPRECATED: Quotes a string so it can be safely used in a query
+ *
+ * @param string $string the string to quote
+ *
+ * @return string the quoted string or the string <samp>NULL</samp>
+ * if the value submitted is <kbd>null</kbd>.
+ *
+ * @see DB_common::quoteSmart(), DB_common::escapeSimple()
+ * @deprecated Deprecated in release 1.6.0
+ */
+ function quote($string = null)
+ {
+ return ($string === null) ? 'NULL'
+ : "'" . str_replace("'", "''", $string) . "'";
+ }
+
+ // }}}
+ // {{{ quoteIdentifier()
+
+ /**
+ * Quotes a string so it can be safely used as a table or column name
+ *
+ * Delimiting style depends on which database driver is being used.
+ *
+ * NOTE: just because you CAN use delimited identifiers doesn't mean
+ * you SHOULD use them. In general, they end up causing way more
+ * problems than they solve.
+ *
+ * Portability is broken by using the following characters inside
+ * delimited identifiers:
+ * + backtick (<kbd>`</kbd>) -- due to MySQL
+ * + double quote (<kbd>"</kbd>) -- due to Oracle
+ * + brackets (<kbd>[</kbd> or <kbd>]</kbd>) -- due to Access
+ *
+ * Delimited identifiers are known to generally work correctly under
+ * the following drivers:
+ * + mssql
+ * + mysql
+ * + mysqli
+ * + oci8
+ * + odbc(access)
+ * + odbc(db2)
+ * + pgsql
+ * + sqlite
+ * + sybase (must execute <kbd>set quoted_identifier on</kbd> sometime
+ * prior to use)
+ *
+ * InterBase doesn't seem to be able to use delimited identifiers
+ * via PHP 4. They work fine under PHP 5.
+ *
+ * @param string $str the identifier name to be quoted
+ *
+ * @return string the quoted identifier
+ *
+ * @since Method available since Release 1.6.0
+ */
+ function quoteIdentifier($str)
+ {
+ return '"' . str_replace('"', '""', $str) . '"';
+ }
+
+ // }}}
+ // {{{ quoteSmart()
+
+ /**
+ * Formats input so it can be safely used in a query
+ *
+ * The output depends on the PHP data type of input and the database
+ * type being used.
+ *
+ * @param mixed $in the data to be formatted
+ *
+ * @return mixed the formatted data. The format depends on the input's
+ * PHP type:
+ * <ul>
+ * <li>
+ * <kbd>input</kbd> -> <samp>returns</samp>
+ * </li>
+ * <li>
+ * <kbd>null</kbd> -> the string <samp>NULL</samp>
+ * </li>
+ * <li>
+ * <kbd>integer</kbd> or <kbd>double</kbd> -> the unquoted number
+ * </li>
+ * <li>
+ * <kbd>bool</kbd> -> output depends on the driver in use
+ * Most drivers return integers: <samp>1</samp> if
+ * <kbd>true</kbd> or <samp>0</samp> if
+ * <kbd>false</kbd>.
+ * Some return strings: <samp>TRUE</samp> if
+ * <kbd>true</kbd> or <samp>FALSE</samp> if
+ * <kbd>false</kbd>.
+ * Finally one returns strings: <samp>T</samp> if
+ * <kbd>true</kbd> or <samp>F</samp> if
+ * <kbd>false</kbd>. Here is a list of each DBMS,
+ * the values returned and the suggested column type:
+ * <ul>
+ * <li>
+ * <kbd>dbase</kbd> -> <samp>T/F</samp>
+ * (<kbd>Logical</kbd>)
+ * </li>
+ * <li>
+ * <kbd>fbase</kbd> -> <samp>TRUE/FALSE</samp>
+ * (<kbd>BOOLEAN</kbd>)
+ * </li>
+ * <li>
+ * <kbd>ibase</kbd> -> <samp>1/0</samp>
+ * (<kbd>SMALLINT</kbd>) [1]
+ * </li>
+ * <li>
+ * <kbd>ifx</kbd> -> <samp>1/0</samp>
+ * (<kbd>SMALLINT</kbd>) [1]
+ * </li>
+ * <li>
+ * <kbd>msql</kbd> -> <samp>1/0</samp>
+ * (<kbd>INTEGER</kbd>)
+ * </li>
+ * <li>
+ * <kbd>mssql</kbd> -> <samp>1/0</samp>
+ * (<kbd>BIT</kbd>)
+ * </li>
+ * <li>
+ * <kbd>mysql</kbd> -> <samp>1/0</samp>
+ * (<kbd>TINYINT(1)</kbd>)
+ * </li>
+ * <li>
+ * <kbd>mysqli</kbd> -> <samp>1/0</samp>
+ * (<kbd>TINYINT(1)</kbd>)
+ * </li>
+ * <li>
+ * <kbd>oci8</kbd> -> <samp>1/0</samp>
+ * (<kbd>NUMBER(1)</kbd>)
+ * </li>
+ * <li>
+ * <kbd>odbc</kbd> -> <samp>1/0</samp>
+ * (<kbd>SMALLINT</kbd>) [1]
+ * </li>
+ * <li>
+ * <kbd>pgsql</kbd> -> <samp>TRUE/FALSE</samp>
+ * (<kbd>BOOLEAN</kbd>)
+ * </li>
+ * <li>
+ * <kbd>sqlite</kbd> -> <samp>1/0</samp>
+ * (<kbd>INTEGER</kbd>)
+ * </li>
+ * <li>
+ * <kbd>sybase</kbd> -> <samp>1/0</samp>
+ * (<kbd>TINYINT(1)</kbd>)
+ * </li>
+ * </ul>
+ * [1] Accommodate the lowest common denominator because not all
+ * versions of have <kbd>BOOLEAN</kbd>.
+ * </li>
+ * <li>
+ * other (including strings and numeric strings) ->
+ * the data with single quotes escaped by preceeding
+ * single quotes, backslashes are escaped by preceeding
+ * backslashes, then the whole string is encapsulated
+ * between single quotes
+ * </li>
+ * </ul>
+ *
+ * @see DB_common::escapeSimple()
+ * @since Method available since Release 1.6.0
+ */
+ function quoteSmart($in)
+ {
+ if (is_int($in)) {
+ return $in;
+ } elseif (is_float($in)) {
+ return $this->quoteFloat($in);
+ } elseif (is_bool($in)) {
+ return $this->quoteBoolean($in);
+ } elseif (is_null($in)) {
+ return 'NULL';
+ } else {
+ if ($this->dbsyntax == 'access'
+ && preg_match('/^#.+#$/', $in))
+ {
+ return $this->escapeSimple($in);
+ }
+ return "'" . $this->escapeSimple($in) . "'";
+ }
+ }
+
+ // }}}
+ // {{{ quoteBoolean()
+
+ /**
+ * Formats a boolean value for use within a query in a locale-independent
+ * manner.
+ *
+ * @param boolean the boolean value to be quoted.
+ * @return string the quoted string.
+ * @see DB_common::quoteSmart()
+ * @since Method available since release 1.7.8.
+ */
+ function quoteBoolean($boolean) {
+ return $boolean ? '1' : '0';
+ }
+
+ // }}}
+ // {{{ quoteFloat()
+
+ /**
+ * Formats a float value for use within a query in a locale-independent
+ * manner.
+ *
+ * @param float the float value to be quoted.
+ * @return string the quoted string.
+ * @see DB_common::quoteSmart()
+ * @since Method available since release 1.7.8.
+ */
+ function quoteFloat($float) {
+ return "'".$this->escapeSimple(str_replace(',', '.', strval(floatval($float))))."'";
+ }
+
+ // }}}
+ // {{{ escapeSimple()
+
+ /**
+ * Escapes a string according to the current DBMS's standards
+ *
+ * In SQLite, this makes things safe for inserts/updates, but may
+ * cause problems when performing text comparisons against columns
+ * containing binary data. See the
+ * {@link http://php.net/sqlite_escape_string PHP manual} for more info.
+ *
+ * @param string $str the string to be escaped
+ *
+ * @return string the escaped string
+ *
+ * @see DB_common::quoteSmart()
+ * @since Method available since Release 1.6.0
+ */
+ function escapeSimple($str)
+ {
+ return str_replace("'", "''", $str);
+ }
+
+ // }}}
+ // {{{ provides()
+
+ /**
+ * Tells whether the present driver supports a given feature
+ *
+ * @param string $feature the feature you're curious about
+ *
+ * @return bool whether this driver supports $feature
+ */
+ function provides($feature)
+ {
+ return $this->features[$feature];
+ }
+
+ // }}}
+ // {{{ setFetchMode()
+
+ /**
+ * Sets the fetch mode that should be used by default for query results
+ *
+ * @param integer $fetchmode DB_FETCHMODE_ORDERED, DB_FETCHMODE_ASSOC
+ * or DB_FETCHMODE_OBJECT
+ * @param string $object_class the class name of the object to be returned
+ * by the fetch methods when the
+ * DB_FETCHMODE_OBJECT mode is selected.
+ * If no class is specified by default a cast
+ * to object from the assoc array row will be
+ * done. There is also the posibility to use
+ * and extend the 'DB_row' class.
+ *
+ * @see DB_FETCHMODE_ORDERED, DB_FETCHMODE_ASSOC, DB_FETCHMODE_OBJECT
+ */
+ function setFetchMode($fetchmode, $object_class = 'stdClass')
+ {
+ switch ($fetchmode) {
+ case DB_FETCHMODE_OBJECT:
+ $this->fetchmode_object_class = $object_class;
+ case DB_FETCHMODE_ORDERED:
+ case DB_FETCHMODE_ASSOC:
+ $this->fetchmode = $fetchmode;
+ break;
+ default:
+ return $this->raiseError('invalid fetchmode mode');
+ }
+ }
+
+ // }}}
+ // {{{ setOption()
+
+ /**
+ * Sets run-time configuration options for PEAR DB
+ *
+ * Options, their data types, default values and description:
+ * <ul>
+ * <li>
+ * <var>autofree</var> <kbd>boolean</kbd> = <samp>false</samp>
+ * <br />should results be freed automatically when there are no
+ * more rows?
+ * </li><li>
+ * <var>result_buffering</var> <kbd>integer</kbd> = <samp>500</samp>
+ * <br />how many rows of the result set should be buffered?
+ * <br />In mysql: mysql_unbuffered_query() is used instead of
+ * mysql_query() if this value is 0. (Release 1.7.0)
+ * <br />In oci8: this value is passed to ocisetprefetch().
+ * (Release 1.7.0)
+ * </li><li>
+ * <var>debug</var> <kbd>integer</kbd> = <samp>0</samp>
+ * <br />debug level
+ * </li><li>
+ * <var>persistent</var> <kbd>boolean</kbd> = <samp>false</samp>
+ * <br />should the connection be persistent?
+ * </li><li>
+ * <var>portability</var> <kbd>integer</kbd> = <samp>DB_PORTABILITY_NONE</samp>
+ * <br />portability mode constant (see below)
+ * </li><li>
+ * <var>seqname_format</var> <kbd>string</kbd> = <samp>%s_seq</samp>
+ * <br />the sprintf() format string used on sequence names. This
+ * format is applied to sequence names passed to
+ * createSequence(), nextID() and dropSequence().
+ * </li><li>
+ * <var>ssl</var> <kbd>boolean</kbd> = <samp>false</samp>
+ * <br />use ssl to connect?
+ * </li>
+ * </ul>
+ *
+ * -----------------------------------------
+ *
+ * PORTABILITY MODES
+ *
+ * These modes are bitwised, so they can be combined using <kbd>|</kbd>
+ * and removed using <kbd>^</kbd>. See the examples section below on how
+ * to do this.
+ *
+ * <samp>DB_PORTABILITY_NONE</samp>
+ * turn off all portability features
+ *
+ * This mode gets automatically turned on if the deprecated
+ * <var>optimize</var> option gets set to <samp>performance</samp>.
+ *
+ *
+ * <samp>DB_PORTABILITY_LOWERCASE</samp>
+ * convert names of tables and fields to lower case when using
+ * <kbd>get*()</kbd>, <kbd>fetch*()</kbd> and <kbd>tableInfo()</kbd>
+ *
+ * This mode gets automatically turned on in the following databases
+ * if the deprecated option <var>optimize</var> gets set to
+ * <samp>portability</samp>:
+ * + oci8
+ *
+ *
+ * <samp>DB_PORTABILITY_RTRIM</samp>
+ * right trim the data output by <kbd>get*()</kbd> <kbd>fetch*()</kbd>
+ *
+ *
+ * <samp>DB_PORTABILITY_DELETE_COUNT</samp>
+ * force reporting the number of rows deleted
+ *
+ * Some DBMS's don't count the number of rows deleted when performing
+ * simple <kbd>DELETE FROM tablename</kbd> queries. This portability
+ * mode tricks such DBMS's into telling the count by adding
+ * <samp>WHERE 1=1</samp> to the end of <kbd>DELETE</kbd> queries.
+ *
+ * This mode gets automatically turned on in the following databases
+ * if the deprecated option <var>optimize</var> gets set to
+ * <samp>portability</samp>:
+ * + fbsql
+ * + mysql
+ * + mysqli
+ * + sqlite
+ *
+ *
+ * <samp>DB_PORTABILITY_NUMROWS</samp>
+ * enable hack that makes <kbd>numRows()</kbd> work in Oracle
+ *
+ * This mode gets automatically turned on in the following databases
+ * if the deprecated option <var>optimize</var> gets set to
+ * <samp>portability</samp>:
+ * + oci8
+ *
+ *
+ * <samp>DB_PORTABILITY_ERRORS</samp>
+ * makes certain error messages in certain drivers compatible
+ * with those from other DBMS's
+ *
+ * + mysql, mysqli: change unique/primary key constraints
+ * DB_ERROR_ALREADY_EXISTS -> DB_ERROR_CONSTRAINT
+ *
+ * + odbc(access): MS's ODBC driver reports 'no such field' as code
+ * 07001, which means 'too few parameters.' When this option is on
+ * that code gets mapped to DB_ERROR_NOSUCHFIELD.
+ * DB_ERROR_MISMATCH -> DB_ERROR_NOSUCHFIELD
+ *
+ * <samp>DB_PORTABILITY_NULL_TO_EMPTY</samp>
+ * convert null values to empty strings in data output by get*() and
+ * fetch*(). Needed because Oracle considers empty strings to be null,
+ * while most other DBMS's know the difference between empty and null.
+ *
+ *
+ * <samp>DB_PORTABILITY_ALL</samp>
+ * turn on all portability features
+ *
+ * -----------------------------------------
+ *
+ * Example 1. Simple setOption() example
+ * <code>
+ * $db->setOption('autofree', true);
+ * </code>
+ *
+ * Example 2. Portability for lowercasing and trimming
+ * <code>
+ * $db->setOption('portability',
+ * DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_RTRIM);
+ * </code>
+ *
+ * Example 3. All portability options except trimming
+ * <code>
+ * $db->setOption('portability',
+ * DB_PORTABILITY_ALL ^ DB_PORTABILITY_RTRIM);
+ * </code>
+ *
+ * @param string $option option name
+ * @param mixed $value value for the option
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ *
+ * @see DB_common::$options
+ */
+ function setOption($option, $value)
+ {
+ if (isset($this->options[$option])) {
+ $this->options[$option] = $value;
+
+ /*
+ * Backwards compatibility check for the deprecated 'optimize'
+ * option. Done here in case settings change after connecting.
+ */
+ if ($option == 'optimize') {
+ if ($value == 'portability') {
+ switch ($this->phptype) {
+ case 'oci8':
+ $this->options['portability'] =
+ DB_PORTABILITY_LOWERCASE |
+ DB_PORTABILITY_NUMROWS;
+ break;
+ case 'fbsql':
+ case 'mysql':
+ case 'mysqli':
+ case 'sqlite':
+ $this->options['portability'] =
+ DB_PORTABILITY_DELETE_COUNT;
+ break;
+ }
+ } else {
+ $this->options['portability'] = DB_PORTABILITY_NONE;
+ }
+ }
+
+ return DB_OK;
+ }
+ return $this->raiseError("unknown option $option");
+ }
+
+ // }}}
+ // {{{ getOption()
+
+ /**
+ * Returns the value of an option
+ *
+ * @param string $option the option name you're curious about
+ *
+ * @return mixed the option's value
+ */
+ function getOption($option)
+ {
+ if (isset($this->options[$option])) {
+ return $this->options[$option];
+ }
+ return $this->raiseError("unknown option $option");
+ }
+
+ // }}}
+ // {{{ prepare()
+
+ /**
+ * Prepares a query for multiple execution with execute()
+ *
+ * Creates a query that can be run multiple times. Each time it is run,
+ * the placeholders, if any, will be replaced by the contents of
+ * execute()'s $data argument.
+ *
+ * Three types of placeholders can be used:
+ * + <kbd>?</kbd> scalar value (i.e. strings, integers). The system
+ * will automatically quote and escape the data.
+ * + <kbd>!</kbd> value is inserted 'as is'
+ * + <kbd>&</kbd> requires a file name. The file's contents get
+ * inserted into the query (i.e. saving binary
+ * data in a db)
+ *
+ * Example 1.
+ * <code>
+ * $sth = $db->prepare('INSERT INTO tbl (a, b, c) VALUES (?, !, &)');
+ * $data = array(
+ * "John's text",
+ * "'it''s good'",
+ * 'filename.txt'
+ * );
+ * $res = $db->execute($sth, $data);
+ * </code>
+ *
+ * Use backslashes to escape placeholder characters if you don't want
+ * them to be interpreted as placeholders:
+ * <pre>
+ * "UPDATE foo SET col=? WHERE col='over \& under'"
+ * </pre>
+ *
+ * With some database backends, this is emulated.
+ *
+ * {@internal ibase and oci8 have their own prepare() methods.}}
+ *
+ * @param string $query the query to be prepared
+ *
+ * @return mixed DB statement resource on success. A DB_Error object
+ * on failure.
+ *
+ * @see DB_common::execute()
+ */
+ function prepare($query)
+ {
+ $tokens = preg_split('/((?<!\\\)[&?!])/', $query, -1,
+ PREG_SPLIT_DELIM_CAPTURE);
+ $token = 0;
+ $types = array();
+ $newtokens = array();
+
+ foreach ($tokens as $val) {
+ switch ($val) {
+ case '?':
+ $types[$token++] = DB_PARAM_SCALAR;
+ break;
+ case '&':
+ $types[$token++] = DB_PARAM_OPAQUE;
+ break;
+ case '!':
+ $types[$token++] = DB_PARAM_MISC;
+ break;
+ default:
+ $newtokens[] = preg_replace('/\\\([&?!])/', "\\1", $val);
+ }
+ }
+
+ $this->prepare_tokens[] = &$newtokens;
+ end($this->prepare_tokens);
+
+ $k = key($this->prepare_tokens);
+ $this->prepare_types[$k] = $types;
+ $this->prepared_queries[$k] = implode(' ', $newtokens);
+
+ return $k;
+ }
+
+ // }}}
+ // {{{ autoPrepare()
+
+ /**
+ * Automaticaly generates an insert or update query and pass it to prepare()
+ *
+ * @param string $table the table name
+ * @param array $table_fields the array of field names
+ * @param int $mode a type of query to make:
+ * DB_AUTOQUERY_INSERT or DB_AUTOQUERY_UPDATE
+ * @param string $where for update queries: the WHERE clause to
+ * append to the SQL statement. Don't
+ * include the "WHERE" keyword.
+ *
+ * @return resource the query handle
+ *
+ * @uses DB_common::prepare(), DB_common::buildManipSQL()
+ */
+ function autoPrepare($table, $table_fields, $mode = DB_AUTOQUERY_INSERT,
+ $where = false)
+ {
+ $query = $this->buildManipSQL($table, $table_fields, $mode, $where);
+ if (DB::isError($query)) {
+ return $query;
+ }
+ return $this->prepare($query);
+ }
+
+ // }}}
+ // {{{ autoExecute()
+
+ /**
+ * Automaticaly generates an insert or update query and call prepare()
+ * and execute() with it
+ *
+ * @param string $table the table name
+ * @param array $fields_values the associative array where $key is a
+ * field name and $value its value
+ * @param int $mode a type of query to make:
+ * DB_AUTOQUERY_INSERT or DB_AUTOQUERY_UPDATE
+ * @param string $where for update queries: the WHERE clause to
+ * append to the SQL statement. Don't
+ * include the "WHERE" keyword.
+ *
+ * @return mixed a new DB_result object for successful SELECT queries
+ * or DB_OK for successul data manipulation queries.
+ * A DB_Error object on failure.
+ *
+ * @uses DB_common::autoPrepare(), DB_common::execute()
+ */
+ function autoExecute($table, $fields_values, $mode = DB_AUTOQUERY_INSERT,
+ $where = false)
+ {
+ $sth = $this->autoPrepare($table, array_keys($fields_values), $mode,
+ $where);
+ if (DB::isError($sth)) {
+ return $sth;
+ }
+ $ret = $this->execute($sth, array_values($fields_values));
+ $this->freePrepared($sth);
+ return $ret;
+
+ }
+
+ // }}}
+ // {{{ buildManipSQL()
+
+ /**
+ * Produces an SQL query string for autoPrepare()
+ *
+ * Example:
+ * <pre>
+ * buildManipSQL('table_sql', array('field1', 'field2', 'field3'),
+ * DB_AUTOQUERY_INSERT);
+ * </pre>
+ *
+ * That returns
+ * <samp>
+ * INSERT INTO table_sql (field1,field2,field3) VALUES (?,?,?)
+ * </samp>
+ *
+ * NOTES:
+ * - This belongs more to a SQL Builder class, but this is a simple
+ * facility.
+ * - Be carefull! If you don't give a $where param with an UPDATE
+ * query, all the records of the table will be updated!
+ *
+ * @param string $table the table name
+ * @param array $table_fields the array of field names
+ * @param int $mode a type of query to make:
+ * DB_AUTOQUERY_INSERT or DB_AUTOQUERY_UPDATE
+ * @param string $where for update queries: the WHERE clause to
+ * append to the SQL statement. Don't
+ * include the "WHERE" keyword.
+ *
+ * @return string the sql query for autoPrepare()
+ */
+ function buildManipSQL($table, $table_fields, $mode, $where = false)
+ {
+ if (count($table_fields) == 0) {
+ return $this->raiseError(DB_ERROR_NEED_MORE_DATA);
+ }
+ $first = true;
+ switch ($mode) {
+ case DB_AUTOQUERY_INSERT:
+ $values = '';
+ $names = '';
+ foreach ($table_fields as $value) {
+ if ($first) {
+ $first = false;
+ } else {
+ $names .= ',';
+ $values .= ',';
+ }
+ $names .= $value;
+ $values .= '?';
+ }
+ return "INSERT INTO $table ($names) VALUES ($values)";
+ case DB_AUTOQUERY_UPDATE:
+ $set = '';
+ foreach ($table_fields as $value) {
+ if ($first) {
+ $first = false;
+ } else {
+ $set .= ',';
+ }
+ $set .= "$value = ?";
+ }
+ $sql = "UPDATE $table SET $set";
+ if ($where) {
+ $sql .= " WHERE $where";
+ }
+ return $sql;
+ default:
+ return $this->raiseError(DB_ERROR_SYNTAX);
+ }
+ }
+
+ // }}}
+ // {{{ execute()
+
+ /**
+ * Executes a DB statement prepared with prepare()
+ *
+ * Example 1.
+ * <code>
+ * $sth = $db->prepare('INSERT INTO tbl (a, b, c) VALUES (?, !, &)');
+ * $data = array(
+ * "John's text",
+ * "'it''s good'",
+ * 'filename.txt'
+ * );
+ * $res = $db->execute($sth, $data);
+ * </code>
+ *
+ * @param resource $stmt a DB statement resource returned from prepare()
+ * @param mixed $data array, string or numeric data to be used in
+ * execution of the statement. Quantity of items
+ * passed must match quantity of placeholders in
+ * query: meaning 1 placeholder for non-array
+ * parameters or 1 placeholder per array element.
+ *
+ * @return mixed a new DB_result object for successful SELECT queries
+ * or DB_OK for successul data manipulation queries.
+ * A DB_Error object on failure.
+ *
+ * {@internal ibase and oci8 have their own execute() methods.}}
+ *
+ * @see DB_common::prepare()
+ */
+ function &execute($stmt, $data = array())
+ {
+ $realquery = $this->executeEmulateQuery($stmt, $data);
+ if (DB::isError($realquery)) {
+ return $realquery;
+ }
+ $result = $this->simpleQuery($realquery);
+
+ if ($result === DB_OK || DB::isError($result)) {
+ return $result;
+ } else {
+ $tmp = new DB_result($this, $result);
+ return $tmp;
+ }
+ }
+
+ // }}}
+ // {{{ executeEmulateQuery()
+
+ /**
+ * Emulates executing prepared statements if the DBMS not support them
+ *
+ * @param resource $stmt a DB statement resource returned from execute()
+ * @param mixed $data array, string or numeric data to be used in
+ * execution of the statement. Quantity of items
+ * passed must match quantity of placeholders in
+ * query: meaning 1 placeholder for non-array
+ * parameters or 1 placeholder per array element.
+ *
+ * @return mixed a string containing the real query run when emulating
+ * prepare/execute. A DB_Error object on failure.
+ *
+ * @access protected
+ * @see DB_common::execute()
+ */
+ function executeEmulateQuery($stmt, $data = array())
+ {
+ $stmt = (int)$stmt;
+ $data = (array)$data;
+ $this->last_parameters = $data;
+
+ if (count($this->prepare_types[$stmt]) != count($data)) {
+ $this->last_query = $this->prepared_queries[$stmt];
+ return $this->raiseError(DB_ERROR_MISMATCH);
+ }
+
+ $realquery = $this->prepare_tokens[$stmt][0];
+
+ $i = 0;
+ foreach ($data as $value) {
+ if ($this->prepare_types[$stmt][$i] == DB_PARAM_SCALAR) {
+ $realquery .= $this->quoteSmart($value);
+ } elseif ($this->prepare_types[$stmt][$i] == DB_PARAM_OPAQUE) {
+ $fp = @fopen($value, 'rb');
+ if (!$fp) {
+ return $this->raiseError(DB_ERROR_ACCESS_VIOLATION);
+ }
+ $realquery .= $this->quoteSmart(fread($fp, filesize($value)));
+ fclose($fp);
+ } else {
+ $realquery .= $value;
+ }
+
+ $realquery .= $this->prepare_tokens[$stmt][++$i];
+ }
+
+ return $realquery;
+ }
+
+ // }}}
+ // {{{ executeMultiple()
+
+ /**
+ * Performs several execute() calls on the same statement handle
+ *
+ * $data must be an array indexed numerically
+ * from 0, one execute call is done for every "row" in the array.
+ *
+ * If an error occurs during execute(), executeMultiple() does not
+ * execute the unfinished rows, but rather returns that error.
+ *
+ * @param resource $stmt query handle from prepare()
+ * @param array $data numeric array containing the
+ * data to insert into the query
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ *
+ * @see DB_common::prepare(), DB_common::execute()
+ */
+ function executeMultiple($stmt, $data)
+ {
+ foreach ($data as $value) {
+ $res = $this->execute($stmt, $value);
+ if (DB::isError($res)) {
+ return $res;
+ }
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ freePrepared()
+
+ /**
+ * Frees the internal resources associated with a prepared query
+ *
+ * @param resource $stmt the prepared statement's PHP resource
+ * @param bool $free_resource should the PHP resource be freed too?
+ * Use false if you need to get data
+ * from the result set later.
+ *
+ * @return bool TRUE on success, FALSE if $result is invalid
+ *
+ * @see DB_common::prepare()
+ */
+ function freePrepared($stmt, $free_resource = true)
+ {
+ $stmt = (int)$stmt;
+ if (isset($this->prepare_tokens[$stmt])) {
+ unset($this->prepare_tokens[$stmt]);
+ unset($this->prepare_types[$stmt]);
+ unset($this->prepared_queries[$stmt]);
+ return true;
+ }
+ return false;
+ }
+
+ // }}}
+ // {{{ modifyQuery()
+
+ /**
+ * Changes a query string for various DBMS specific reasons
+ *
+ * It is defined here to ensure all drivers have this method available.
+ *
+ * @param string $query the query string to modify
+ *
+ * @return string the modified query string
+ *
+ * @access protected
+ * @see DB_mysql::modifyQuery(), DB_oci8::modifyQuery(),
+ * DB_sqlite::modifyQuery()
+ */
+ function modifyQuery($query)
+ {
+ return $query;
+ }
+
+ // }}}
+ // {{{ modifyLimitQuery()
+
+ /**
+ * Adds LIMIT clauses to a query string according to current DBMS standards
+ *
+ * It is defined here to assure that all implementations
+ * have this method defined.
+ *
+ * @param string $query the query to modify
+ * @param int $from the row to start to fetching (0 = the first row)
+ * @param int $count the numbers of rows to fetch
+ * @param mixed $params array, string or numeric data to be used in
+ * execution of the statement. Quantity of items
+ * passed must match quantity of placeholders in
+ * query: meaning 1 placeholder for non-array
+ * parameters or 1 placeholder per array element.
+ *
+ * @return string the query string with LIMIT clauses added
+ *
+ * @access protected
+ */
+ function modifyLimitQuery($query, $from, $count, $params = array())
+ {
+ return $query;
+ }
+
+ // }}}
+ // {{{ query()
+
+ /**
+ * Sends a query to the database server
+ *
+ * The query string can be either a normal statement to be sent directly
+ * to the server OR if <var>$params</var> are passed the query can have
+ * placeholders and it will be passed through prepare() and execute().
+ *
+ * @param string $query the SQL query or the statement to prepare
+ * @param mixed $params array, string or numeric data to be used in
+ * execution of the statement. Quantity of items
+ * passed must match quantity of placeholders in
+ * query: meaning 1 placeholder for non-array
+ * parameters or 1 placeholder per array element.
+ *
+ * @return mixed a new DB_result object for successful SELECT queries
+ * or DB_OK for successul data manipulation queries.
+ * A DB_Error object on failure.
+ *
+ * @see DB_result, DB_common::prepare(), DB_common::execute()
+ */
+ function &query($query, $params = array())
+ {
+ if (sizeof($params) > 0) {
+ $sth = $this->prepare($query);
+ if (DB::isError($sth)) {
+ return $sth;
+ }
+ $ret = $this->execute($sth, $params);
+ $this->freePrepared($sth, false);
+ return $ret;
+ } else {
+ $this->last_parameters = array();
+ $result = $this->simpleQuery($query);
+ if ($result === DB_OK || DB::isError($result)) {
+ return $result;
+ } else {
+ $tmp = new DB_result($this, $result);
+ return $tmp;
+ }
+ }
+ }
+
+ // }}}
+ // {{{ limitQuery()
+
+ /**
+ * Generates and executes a LIMIT query
+ *
+ * @param string $query the query
+ * @param intr $from the row to start to fetching (0 = the first row)
+ * @param int $count the numbers of rows to fetch
+ * @param mixed $params array, string or numeric data to be used in
+ * execution of the statement. Quantity of items
+ * passed must match quantity of placeholders in
+ * query: meaning 1 placeholder for non-array
+ * parameters or 1 placeholder per array element.
+ *
+ * @return mixed a new DB_result object for successful SELECT queries
+ * or DB_OK for successul data manipulation queries.
+ * A DB_Error object on failure.
+ */
+ function &limitQuery($query, $from, $count, $params = array())
+ {
+ $query = $this->modifyLimitQuery($query, $from, $count, $params);
+ if (DB::isError($query)){
+ return $query;
+ }
+ $result = $this->query($query, $params);
+ if (is_a($result, 'DB_result')) {
+ $result->setOption('limit_from', $from);
+ $result->setOption('limit_count', $count);
+ }
+ return $result;
+ }
+
+ // }}}
+ // {{{ getOne()
+
+ /**
+ * Fetches the first column of the first row from a query result
+ *
+ * Takes care of doing the query and freeing the results when finished.
+ *
+ * @param string $query the SQL query
+ * @param mixed $params array, string or numeric data to be used in
+ * execution of the statement. Quantity of items
+ * passed must match quantity of placeholders in
+ * query: meaning 1 placeholder for non-array
+ * parameters or 1 placeholder per array element.
+ *
+ * @return mixed the returned value of the query.
+ * A DB_Error object on failure.
+ */
+ function &getOne($query, $params = array())
+ {
+ $params = (array)$params;
+ // modifyLimitQuery() would be nice here, but it causes BC issues
+ if (sizeof($params) > 0) {
+ $sth = $this->prepare($query);
+ if (DB::isError($sth)) {
+ return $sth;
+ }
+ $res = $this->execute($sth, $params);
+ $this->freePrepared($sth);
+ } else {
+ $res = $this->query($query);
+ }
+
+ if (DB::isError($res)) {
+ return $res;
+ }
+
+ $err = $res->fetchInto($row, DB_FETCHMODE_ORDERED);
+ $res->free();
+
+ if ($err !== DB_OK) {
+ return $err;
+ }
+
+ return $row[0];
+ }
+
+ // }}}
+ // {{{ getRow()
+
+ /**
+ * Fetches the first row of data returned from a query result
+ *
+ * Takes care of doing the query and freeing the results when finished.
+ *
+ * @param string $query the SQL query
+ * @param mixed $params array, string or numeric data to be used in
+ * execution of the statement. Quantity of items
+ * passed must match quantity of placeholders in
+ * query: meaning 1 placeholder for non-array
+ * parameters or 1 placeholder per array element.
+ * @param int $fetchmode the fetch mode to use
+ *
+ * @return array the first row of results as an array.
+ * A DB_Error object on failure.
+ */
+ function &getRow($query, $params = array(),
+ $fetchmode = DB_FETCHMODE_DEFAULT)
+ {
+ // compat check, the params and fetchmode parameters used to
+ // have the opposite order
+ if (!is_array($params)) {
+ if (is_array($fetchmode)) {
+ if ($params === null) {
+ $tmp = DB_FETCHMODE_DEFAULT;
+ } else {
+ $tmp = $params;
+ }
+ $params = $fetchmode;
+ $fetchmode = $tmp;
+ } elseif ($params !== null) {
+ $fetchmode = $params;
+ $params = array();
+ }
+ }
+ // modifyLimitQuery() would be nice here, but it causes BC issues
+ if (sizeof($params) > 0) {
+ $sth = $this->prepare($query);
+ if (DB::isError($sth)) {
+ return $sth;
+ }
+ $res = $this->execute($sth, $params);
+ $this->freePrepared($sth);
+ } else {
+ $res = $this->query($query);
+ }
+
+ if (DB::isError($res)) {
+ return $res;
+ }
+
+ $err = $res->fetchInto($row, $fetchmode);
+
+ $res->free();
+
+ if ($err !== DB_OK) {
+ return $err;
+ }
+
+ return $row;
+ }
+
+ // }}}
+ // {{{ getCol()
+
+ /**
+ * Fetches a single column from a query result and returns it as an
+ * indexed array
+ *
+ * @param string $query the SQL query
+ * @param mixed $col which column to return (integer [column number,
+ * starting at 0] or string [column name])
+ * @param mixed $params array, string or numeric data to be used in
+ * execution of the statement. Quantity of items
+ * passed must match quantity of placeholders in
+ * query: meaning 1 placeholder for non-array
+ * parameters or 1 placeholder per array element.
+ *
+ * @return array the results as an array. A DB_Error object on failure.
+ *
+ * @see DB_common::query()
+ */
+ function &getCol($query, $col = 0, $params = array())
+ {
+ $params = (array)$params;
+ if (sizeof($params) > 0) {
+ $sth = $this->prepare($query);
+
+ if (DB::isError($sth)) {
+ return $sth;
+ }
+
+ $res = $this->execute($sth, $params);
+ $this->freePrepared($sth);
+ } else {
+ $res = $this->query($query);
+ }
+
+ if (DB::isError($res)) {
+ return $res;
+ }
+
+ $fetchmode = is_int($col) ? DB_FETCHMODE_ORDERED : DB_FETCHMODE_ASSOC;
+
+ if (!is_array($row = $res->fetchRow($fetchmode))) {
+ $ret = array();
+ } else {
+ if (!array_key_exists($col, $row)) {
+ $ret = $this->raiseError(DB_ERROR_NOSUCHFIELD);
+ } else {
+ $ret = array($row[$col]);
+ while (is_array($row = $res->fetchRow($fetchmode))) {
+ $ret[] = $row[$col];
+ }
+ }
+ }
+
+ $res->free();
+
+ if (DB::isError($row)) {
+ $ret = $row;
+ }
+
+ return $ret;
+ }
+
+ // }}}
+ // {{{ getAssoc()
+
+ /**
+ * Fetches an entire query result and returns it as an
+ * associative array using the first column as the key
+ *
+ * If the result set contains more than two columns, the value
+ * will be an array of the values from column 2-n. If the result
+ * set contains only two columns, the returned value will be a
+ * scalar with the value of the second column (unless forced to an
+ * array with the $force_array parameter). A DB error code is
+ * returned on errors. If the result set contains fewer than two
+ * columns, a DB_ERROR_TRUNCATED error is returned.
+ *
+ * For example, if the table "mytable" contains:
+ *
+ * <pre>
+ * ID TEXT DATE
+ * --------------------------------
+ * 1 'one' 944679408
+ * 2 'two' 944679408
+ * 3 'three' 944679408
+ * </pre>
+ *
+ * Then the call getAssoc('SELECT id,text FROM mytable') returns:
+ * <pre>
+ * array(
+ * '1' => 'one',
+ * '2' => 'two',
+ * '3' => 'three',
+ * )
+ * </pre>
+ *
+ * ...while the call getAssoc('SELECT id,text,date FROM mytable') returns:
+ * <pre>
+ * array(
+ * '1' => array('one', '944679408'),
+ * '2' => array('two', '944679408'),
+ * '3' => array('three', '944679408')
+ * )
+ * </pre>
+ *
+ * If the more than one row occurs with the same value in the
+ * first column, the last row overwrites all previous ones by
+ * default. Use the $group parameter if you don't want to
+ * overwrite like this. Example:
+ *
+ * <pre>
+ * getAssoc('SELECT category,id,name FROM mytable', false, null,
+ * DB_FETCHMODE_ASSOC, true) returns:
+ *
+ * array(
+ * '1' => array(array('id' => '4', 'name' => 'number four'),
+ * array('id' => '6', 'name' => 'number six')
+ * ),
+ * '9' => array(array('id' => '4', 'name' => 'number four'),
+ * array('id' => '6', 'name' => 'number six')
+ * )
+ * )
+ * </pre>
+ *
+ * Keep in mind that database functions in PHP usually return string
+ * values for results regardless of the database's internal type.
+ *
+ * @param string $query the SQL query
+ * @param bool $force_array used only when the query returns
+ * exactly two columns. If true, the values
+ * of the returned array will be one-element
+ * arrays instead of scalars.
+ * @param mixed $params array, string or numeric data to be used in
+ * execution of the statement. Quantity of
+ * items passed must match quantity of
+ * placeholders in query: meaning 1
+ * placeholder for non-array parameters or
+ * 1 placeholder per array element.
+ * @param int $fetchmode the fetch mode to use
+ * @param bool $group if true, the values of the returned array
+ * is wrapped in another array. If the same
+ * key value (in the first column) repeats
+ * itself, the values will be appended to
+ * this array instead of overwriting the
+ * existing values.
+ *
+ * @return array the associative array containing the query results.
+ * A DB_Error object on failure.
+ */
+ function &getAssoc($query, $force_array = false, $params = array(),
+ $fetchmode = DB_FETCHMODE_DEFAULT, $group = false)
+ {
+ $params = (array)$params;
+ if (sizeof($params) > 0) {
+ $sth = $this->prepare($query);
+
+ if (DB::isError($sth)) {
+ return $sth;
+ }
+
+ $res = $this->execute($sth, $params);
+ $this->freePrepared($sth);
+ } else {
+ $res = $this->query($query);
+ }
+
+ if (DB::isError($res)) {
+ return $res;
+ }
+ if ($fetchmode == DB_FETCHMODE_DEFAULT) {
+ $fetchmode = $this->fetchmode;
+ }
+ $cols = $res->numCols();
+
+ if ($cols < 2) {
+ $tmp = $this->raiseError(DB_ERROR_TRUNCATED);
+ return $tmp;
+ }
+
+ $results = array();
+
+ if ($cols > 2 || $force_array) {
+ // return array values
+ // XXX this part can be optimized
+ if ($fetchmode == DB_FETCHMODE_ASSOC) {
+ while (is_array($row = $res->fetchRow(DB_FETCHMODE_ASSOC))) {
+ reset($row);
+ $key = current($row);
+ unset($row[key($row)]);
+ if ($group) {
+ $results[$key][] = $row;
+ } else {
+ $results[$key] = $row;
+ }
+ }
+ } elseif ($fetchmode == DB_FETCHMODE_OBJECT) {
+ while ($row = $res->fetchRow(DB_FETCHMODE_OBJECT)) {
+ $arr = get_object_vars($row);
+ $key = current($arr);
+ if ($group) {
+ $results[$key][] = $row;
+ } else {
+ $results[$key] = $row;
+ }
+ }
+ } else {
+ while (is_array($row = $res->fetchRow(DB_FETCHMODE_ORDERED))) {
+ // we shift away the first element to get
+ // indices running from 0 again
+ $key = array_shift($row);
+ if ($group) {
+ $results[$key][] = $row;
+ } else {
+ $results[$key] = $row;
+ }
+ }
+ }
+ if (DB::isError($row)) {
+ $results = $row;
+ }
+ } else {
+ // return scalar values
+ while (is_array($row = $res->fetchRow(DB_FETCHMODE_ORDERED))) {
+ if ($group) {
+ $results[$row[0]][] = $row[1];
+ } else {
+ $results[$row[0]] = $row[1];
+ }
+ }
+ if (DB::isError($row)) {
+ $results = $row;
+ }
+ }
+
+ $res->free();
+
+ return $results;
+ }
+
+ // }}}
+ // {{{ getAll()
+
+ /**
+ * Fetches all of the rows from a query result
+ *
+ * @param string $query the SQL query
+ * @param mixed $params array, string or numeric data to be used in
+ * execution of the statement. Quantity of
+ * items passed must match quantity of
+ * placeholders in query: meaning 1
+ * placeholder for non-array parameters or
+ * 1 placeholder per array element.
+ * @param int $fetchmode the fetch mode to use:
+ * + DB_FETCHMODE_ORDERED
+ * + DB_FETCHMODE_ASSOC
+ * + DB_FETCHMODE_ORDERED | DB_FETCHMODE_FLIPPED
+ * + DB_FETCHMODE_ASSOC | DB_FETCHMODE_FLIPPED
+ *
+ * @return array the nested array. A DB_Error object on failure.
+ */
+ function &getAll($query, $params = array(),
+ $fetchmode = DB_FETCHMODE_DEFAULT)
+ {
+ // compat check, the params and fetchmode parameters used to
+ // have the opposite order
+ if (!is_array($params)) {
+ if (is_array($fetchmode)) {
+ if ($params === null) {
+ $tmp = DB_FETCHMODE_DEFAULT;
+ } else {
+ $tmp = $params;
+ }
+ $params = $fetchmode;
+ $fetchmode = $tmp;
+ } elseif ($params !== null) {
+ $fetchmode = $params;
+ $params = array();
+ }
+ }
+
+ if (sizeof($params) > 0) {
+ $sth = $this->prepare($query);
+
+ if (DB::isError($sth)) {
+ return $sth;
+ }
+
+ $res = $this->execute($sth, $params);
+ $this->freePrepared($sth);
+ } else {
+ $res = $this->query($query);
+ }
+
+ if ($res === DB_OK || DB::isError($res)) {
+ return $res;
+ }
+
+ $results = array();
+ while (DB_OK === $res->fetchInto($row, $fetchmode)) {
+ if ($fetchmode & DB_FETCHMODE_FLIPPED) {
+ foreach ($row as $key => $val) {
+ $results[$key][] = $val;
+ }
+ } else {
+ $results[] = $row;
+ }
+ }
+
+ $res->free();
+
+ if (DB::isError($row)) {
+ $tmp = $this->raiseError($row);
+ return $tmp;
+ }
+ return $results;
+ }
+
+ // }}}
+ // {{{ autoCommit()
+
+ /**
+ * Enables or disables automatic commits
+ *
+ * @param bool $onoff true turns it on, false turns it off
+ *
+ * @return int DB_OK on success. A DB_Error object if the driver
+ * doesn't support auto-committing transactions.
+ */
+ function autoCommit($onoff = false)
+ {
+ return $this->raiseError(DB_ERROR_NOT_CAPABLE);
+ }
+
+ // }}}
+ // {{{ commit()
+
+ /**
+ * Commits the current transaction
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ */
+ function commit()
+ {
+ return $this->raiseError(DB_ERROR_NOT_CAPABLE);
+ }
+
+ // }}}
+ // {{{ rollback()
+
+ /**
+ * Reverts the current transaction
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ */
+ function rollback()
+ {
+ return $this->raiseError(DB_ERROR_NOT_CAPABLE);
+ }
+
+ // }}}
+ // {{{ numRows()
+
+ /**
+ * Determines the number of rows in a query result
+ *
+ * @param resource $result the query result idenifier produced by PHP
+ *
+ * @return int the number of rows. A DB_Error object on failure.
+ */
+ function numRows($result)
+ {
+ return $this->raiseError(DB_ERROR_NOT_CAPABLE);
+ }
+
+ // }}}
+ // {{{ affectedRows()
+
+ /**
+ * Determines the number of rows affected by a data maniuplation query
+ *
+ * 0 is returned for queries that don't manipulate data.
+ *
+ * @return int the number of rows. A DB_Error object on failure.
+ */
+ function affectedRows()
+ {
+ return $this->raiseError(DB_ERROR_NOT_CAPABLE);
+ }
+
+ // }}}
+ // {{{ getSequenceName()
+
+ /**
+ * Generates the name used inside the database for a sequence
+ *
+ * The createSequence() docblock contains notes about storing sequence
+ * names.
+ *
+ * @param string $sqn the sequence's public name
+ *
+ * @return string the sequence's name in the backend
+ *
+ * @access protected
+ * @see DB_common::createSequence(), DB_common::dropSequence(),
+ * DB_common::nextID(), DB_common::setOption()
+ */
+ function getSequenceName($sqn)
+ {
+ return sprintf($this->getOption('seqname_format'),
+ preg_replace('/[^a-z0-9_.]/i', '_', $sqn));
+ }
+
+ // }}}
+ // {{{ nextId()
+
+ /**
+ * Returns the next free id in a sequence
+ *
+ * @param string $seq_name name of the sequence
+ * @param boolean $ondemand when true, the seqence is automatically
+ * created if it does not exist
+ *
+ * @return int the next id number in the sequence.
+ * A DB_Error object on failure.
+ *
+ * @see DB_common::createSequence(), DB_common::dropSequence(),
+ * DB_common::getSequenceName()
+ */
+ function nextId($seq_name, $ondemand = true)
+ {
+ return $this->raiseError(DB_ERROR_NOT_CAPABLE);
+ }
+
+ // }}}
+ // {{{ createSequence()
+
+ /**
+ * Creates a new sequence
+ *
+ * The name of a given sequence is determined by passing the string
+ * provided in the <var>$seq_name</var> argument through PHP's sprintf()
+ * function using the value from the <var>seqname_format</var> option as
+ * the sprintf()'s format argument.
+ *
+ * <var>seqname_format</var> is set via setOption().
+ *
+ * @param string $seq_name name of the new sequence
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ *
+ * @see DB_common::dropSequence(), DB_common::getSequenceName(),
+ * DB_common::nextID()
+ */
+ function createSequence($seq_name)
+ {
+ return $this->raiseError(DB_ERROR_NOT_CAPABLE);
+ }
+
+ // }}}
+ // {{{ dropSequence()
+
+ /**
+ * Deletes a sequence
+ *
+ * @param string $seq_name name of the sequence to be deleted
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ *
+ * @see DB_common::createSequence(), DB_common::getSequenceName(),
+ * DB_common::nextID()
+ */
+ function dropSequence($seq_name)
+ {
+ return $this->raiseError(DB_ERROR_NOT_CAPABLE);
+ }
+
+ // }}}
+ // {{{ raiseError()
+
+ /**
+ * Communicates an error and invoke error callbacks, etc
+ *
+ * Basically a wrapper for PEAR::raiseError without the message string.
+ *
+ * @param mixed integer error code, or a PEAR error object (all
+ * other parameters are ignored if this parameter is
+ * an object
+ * @param int error mode, see PEAR_Error docs
+ * @param mixed if error mode is PEAR_ERROR_TRIGGER, this is the
+ * error level (E_USER_NOTICE etc). If error mode is
+ * PEAR_ERROR_CALLBACK, this is the callback function,
+ * either as a function name, or as an array of an
+ * object and method name. For other error modes this
+ * parameter is ignored.
+ * @param string extra debug information. Defaults to the last
+ * query and native error code.
+ * @param mixed native error code, integer or string depending the
+ * backend
+ * @param mixed dummy parameter for E_STRICT compatibility with
+ * PEAR::raiseError
+ * @param mixed dummy parameter for E_STRICT compatibility with
+ * PEAR::raiseError
+ *
+ * @return object the PEAR_Error object
+ *
+ * @see PEAR_Error
+ */
+ function &raiseError($code = DB_ERROR, $mode = null, $options = null,
+ $userinfo = null, $nativecode = null, $dummy1 = null,
+ $dummy2 = null)
+ {
+ // The error is yet a DB error object
+ if (is_object($code)) {
+ // because we the static PEAR::raiseError, our global
+ // handler should be used if it is set
+ if ($mode === null && !empty($this->_default_error_mode)) {
+ $mode = $this->_default_error_mode;
+ $options = $this->_default_error_options;
+ }
+ $tmp = PEAR::raiseError($code, null, $mode, $options,
+ null, null, true);
+ return $tmp;
+ }
+
+ if ($userinfo === null) {
+ $userinfo = $this->last_query;
+ }
+
+ if ($nativecode) {
+ $userinfo .= ' [nativecode=' . trim($nativecode) . ']';
+ } else {
+ $userinfo .= ' [DB Error: ' . DB::errorMessage($code) . ']';
+ }
+
+ $tmp = PEAR::raiseError(null, $code, $mode, $options, $userinfo,
+ 'DB_Error', true);
+ return $tmp;
+ }
+
+ // }}}
+ // {{{ errorNative()
+
+ /**
+ * Gets the DBMS' native error code produced by the last query
+ *
+ * @return mixed the DBMS' error code. A DB_Error object on failure.
+ */
+ function errorNative()
+ {
+ return $this->raiseError(DB_ERROR_NOT_CAPABLE);
+ }
+
+ // }}}
+ // {{{ errorCode()
+
+ /**
+ * Maps native error codes to DB's portable ones
+ *
+ * Uses the <var>$errorcode_map</var> property defined in each driver.
+ *
+ * @param string|int $nativecode the error code returned by the DBMS
+ *
+ * @return int the portable DB error code. Return DB_ERROR if the
+ * current driver doesn't have a mapping for the
+ * $nativecode submitted.
+ */
+ function errorCode($nativecode)
+ {
+ if (isset($this->errorcode_map[$nativecode])) {
+ return $this->errorcode_map[$nativecode];
+ }
+ // Fall back to DB_ERROR if there was no mapping.
+ return DB_ERROR;
+ }
+
+ // }}}
+ // {{{ errorMessage()
+
+ /**
+ * Maps a DB error code to a textual message
+ *
+ * @param integer $dbcode the DB error code
+ *
+ * @return string the error message corresponding to the error code
+ * submitted. FALSE if the error code is unknown.
+ *
+ * @see DB::errorMessage()
+ */
+ function errorMessage($dbcode)
+ {
+ return DB::errorMessage($this->errorcode_map[$dbcode]);
+ }
+
+ // }}}
+ // {{{ tableInfo()
+
+ /**
+ * Returns information about a table or a result set
+ *
+ * The format of the resulting array depends on which <var>$mode</var>
+ * you select. The sample output below is based on this query:
+ * <pre>
+ * SELECT tblFoo.fldID, tblFoo.fldPhone, tblBar.fldId
+ * FROM tblFoo
+ * JOIN tblBar ON tblFoo.fldId = tblBar.fldId
+ * </pre>
+ *
+ * <ul>
+ * <li>
+ *
+ * <kbd>null</kbd> (default)
+ * <pre>
+ * [0] => Array (
+ * [table] => tblFoo
+ * [name] => fldId
+ * [type] => int
+ * [len] => 11
+ * [flags] => primary_key not_null
+ * )
+ * [1] => Array (
+ * [table] => tblFoo
+ * [name] => fldPhone
+ * [type] => string
+ * [len] => 20
+ * [flags] =>
+ * )
+ * [2] => Array (
+ * [table] => tblBar
+ * [name] => fldId
+ * [type] => int
+ * [len] => 11
+ * [flags] => primary_key not_null
+ * )
+ * </pre>
+ *
+ * </li><li>
+ *
+ * <kbd>DB_TABLEINFO_ORDER</kbd>
+ *
+ * <p>In addition to the information found in the default output,
+ * a notation of the number of columns is provided by the
+ * <samp>num_fields</samp> element while the <samp>order</samp>
+ * element provides an array with the column names as the keys and
+ * their location index number (corresponding to the keys in the
+ * the default output) as the values.</p>
+ *
+ * <p>If a result set has identical field names, the last one is
+ * used.</p>
+ *
+ * <pre>
+ * [num_fields] => 3
+ * [order] => Array (
+ * [fldId] => 2
+ * [fldTrans] => 1
+ * )
+ * </pre>
+ *
+ * </li><li>
+ *
+ * <kbd>DB_TABLEINFO_ORDERTABLE</kbd>
+ *
+ * <p>Similar to <kbd>DB_TABLEINFO_ORDER</kbd> but adds more
+ * dimensions to the array in which the table names are keys and
+ * the field names are sub-keys. This is helpful for queries that
+ * join tables which have identical field names.</p>
+ *
+ * <pre>
+ * [num_fields] => 3
+ * [ordertable] => Array (
+ * [tblFoo] => Array (
+ * [fldId] => 0
+ * [fldPhone] => 1
+ * )
+ * [tblBar] => Array (
+ * [fldId] => 2
+ * )
+ * )
+ * </pre>
+ *
+ * </li>
+ * </ul>
+ *
+ * The <samp>flags</samp> element contains a space separated list
+ * of extra information about the field. This data is inconsistent
+ * between DBMS's due to the way each DBMS works.
+ * + <samp>primary_key</samp>
+ * + <samp>unique_key</samp>
+ * + <samp>multiple_key</samp>
+ * + <samp>not_null</samp>
+ *
+ * Most DBMS's only provide the <samp>table</samp> and <samp>flags</samp>
+ * elements if <var>$result</var> is a table name. The following DBMS's
+ * provide full information from queries:
+ * + fbsql
+ * + mysql
+ *
+ * If the 'portability' option has <samp>DB_PORTABILITY_LOWERCASE</samp>
+ * turned on, the names of tables and fields will be lowercased.
+ *
+ * @param object|string $result DB_result object from a query or a
+ * string containing the name of a table.
+ * While this also accepts a query result
+ * resource identifier, this behavior is
+ * deprecated.
+ * @param int $mode either unused or one of the tableInfo modes:
+ * <kbd>DB_TABLEINFO_ORDERTABLE</kbd>,
+ * <kbd>DB_TABLEINFO_ORDER</kbd> or
+ * <kbd>DB_TABLEINFO_FULL</kbd> (which does both).
+ * These are bitwise, so the first two can be
+ * combined using <kbd>|</kbd>.
+ *
+ * @return array an associative array with the information requested.
+ * A DB_Error object on failure.
+ *
+ * @see DB_common::setOption()
+ */
+ function tableInfo($result, $mode = null)
+ {
+ /*
+ * If the DB_<driver> class has a tableInfo() method, that one
+ * overrides this one. But, if the driver doesn't have one,
+ * this method runs and tells users about that fact.
+ */
+ return $this->raiseError(DB_ERROR_NOT_CAPABLE);
+ }
+
+ // }}}
+ // {{{ getTables()
+
+ /**
+ * Lists the tables in the current database
+ *
+ * @return array the list of tables. A DB_Error object on failure.
+ *
+ * @deprecated Method deprecated some time before Release 1.2
+ */
+ function getTables()
+ {
+ return $this->getListOf('tables');
+ }
+
+ // }}}
+ // {{{ getListOf()
+
+ /**
+ * Lists internal database information
+ *
+ * @param string $type type of information being sought.
+ * Common items being sought are:
+ * tables, databases, users, views, functions
+ * Each DBMS's has its own capabilities.
+ *
+ * @return array an array listing the items sought.
+ * A DB DB_Error object on failure.
+ */
+ function getListOf($type)
+ {
+ $sql = $this->getSpecialQuery($type);
+ if ($sql === null) {
+ $this->last_query = '';
+ return $this->raiseError(DB_ERROR_UNSUPPORTED);
+ } elseif (is_int($sql) || DB::isError($sql)) {
+ // Previous error
+ return $this->raiseError($sql);
+ } elseif (is_array($sql)) {
+ // Already the result
+ return $sql;
+ }
+ // Launch this query
+ return $this->getCol($sql);
+ }
+
+ // }}}
+ // {{{ getSpecialQuery()
+
+ /**
+ * Obtains the query string needed for listing a given type of objects
+ *
+ * @param string $type the kind of objects you want to retrieve
+ *
+ * @return string the SQL query string or null if the driver doesn't
+ * support the object type requested
+ *
+ * @access protected
+ * @see DB_common::getListOf()
+ */
+ function getSpecialQuery($type)
+ {
+ return $this->raiseError(DB_ERROR_UNSUPPORTED);
+ }
+
+ // }}}
+ // {{{ nextQueryIsManip()
+
+ /**
+ * Sets (or unsets) a flag indicating that the next query will be a
+ * manipulation query, regardless of the usual DB::isManip() heuristics.
+ *
+ * @param boolean true to set the flag overriding the isManip() behaviour,
+ * false to clear it and fall back onto isManip()
+ *
+ * @return void
+ *
+ * @access public
+ */
+ function nextQueryIsManip($manip)
+ {
+ $this->_next_query_manip = $manip;
+ }
+
+ // }}}
+ // {{{ _checkManip()
+
+ /**
+ * Checks if the given query is a manipulation query. This also takes into
+ * account the _next_query_manip flag and sets the _last_query_manip flag
+ * (and resets _next_query_manip) according to the result.
+ *
+ * @param string The query to check.
+ *
+ * @return boolean true if the query is a manipulation query, false
+ * otherwise
+ *
+ * @access protected
+ */
+ function _checkManip($query)
+ {
+ if ($this->_next_query_manip || DB::isManip($query)) {
+ $this->_last_query_manip = true;
+ } else {
+ $this->_last_query_manip = false;
+ }
+ $this->_next_query_manip = false;
+ return $this->_last_query_manip;
+ $manip = $this->_next_query_manip;
+ }
+
+ // }}}
+ // {{{ _rtrimArrayValues()
+
+ /**
+ * Right-trims all strings in an array
+ *
+ * @param array $array the array to be trimmed (passed by reference)
+ *
+ * @return void
+ *
+ * @access protected
+ */
+ function _rtrimArrayValues(&$array)
+ {
+ foreach ($array as $key => $value) {
+ if (is_string($value)) {
+ $array[$key] = rtrim($value);
+ }
+ }
+ }
+
+ // }}}
+ // {{{ _convertNullArrayValuesToEmpty()
+
+ /**
+ * Converts all null values in an array to empty strings
+ *
+ * @param array $array the array to be de-nullified (passed by reference)
+ *
+ * @return void
+ *
+ * @access protected
+ */
+ function _convertNullArrayValuesToEmpty(&$array)
+ {
+ foreach ($array as $key => $value) {
+ if (is_null($value)) {
+ $array[$key] = '';
+ }
+ }
+ }
+
+ // }}}
+}
+
+/*
+ * Local variables:
+ * tab-width: 4
+ * c-basic-offset: 4
+ * End:
+ */
+
+?>
diff --git a/extlib/DB/dbase.php b/extlib/DB/dbase.php
new file mode 100644
index 000000000..67afc897d
--- /dev/null
+++ b/extlib/DB/dbase.php
@@ -0,0 +1,510 @@
+<?php
+
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * The PEAR DB driver for PHP's dbase extension
+ * for interacting with dBase databases
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt. If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category Database
+ * @package DB
+ * @author Tomas V.V. Cox <cox@idecnet.com>
+ * @author Daniel Convissor <danielc@php.net>
+ * @copyright 1997-2007 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0
+ * @version CVS: $Id: dbase.php,v 1.45 2007/09/21 13:40:41 aharvey Exp $
+ * @link http://pear.php.net/package/DB
+ */
+
+/**
+ * Obtain the DB_common class so it can be extended from
+ */
+require_once 'DB/common.php';
+
+/**
+ * The methods PEAR DB uses to interact with PHP's dbase extension
+ * for interacting with dBase databases
+ *
+ * These methods overload the ones declared in DB_common.
+ *
+ * @category Database
+ * @package DB
+ * @author Tomas V.V. Cox <cox@idecnet.com>
+ * @author Daniel Convissor <danielc@php.net>
+ * @copyright 1997-2007 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0
+ * @version Release: 1.7.14RC1
+ * @link http://pear.php.net/package/DB
+ */
+class DB_dbase extends DB_common
+{
+ // {{{ properties
+
+ /**
+ * The DB driver type (mysql, oci8, odbc, etc.)
+ * @var string
+ */
+ var $phptype = 'dbase';
+
+ /**
+ * The database syntax variant to be used (db2, access, etc.), if any
+ * @var string
+ */
+ var $dbsyntax = 'dbase';
+
+ /**
+ * The capabilities of this DB implementation
+ *
+ * The 'new_link' element contains the PHP version that first provided
+ * new_link support for this DBMS. Contains false if it's unsupported.
+ *
+ * Meaning of the 'limit' element:
+ * + 'emulate' = emulate with fetch row by number
+ * + 'alter' = alter the query
+ * + false = skip rows
+ *
+ * @var array
+ */
+ var $features = array(
+ 'limit' => false,
+ 'new_link' => false,
+ 'numrows' => true,
+ 'pconnect' => false,
+ 'prepare' => false,
+ 'ssl' => false,
+ 'transactions' => false,
+ );
+
+ /**
+ * A mapping of native error codes to DB error codes
+ * @var array
+ */
+ var $errorcode_map = array(
+ );
+
+ /**
+ * The raw database connection created by PHP
+ * @var resource
+ */
+ var $connection;
+
+ /**
+ * The DSN information for connecting to a database
+ * @var array
+ */
+ var $dsn = array();
+
+
+ /**
+ * A means of emulating result resources
+ * @var array
+ */
+ var $res_row = array();
+
+ /**
+ * The quantity of results so far
+ *
+ * For emulating result resources.
+ *
+ * @var integer
+ */
+ var $result = 0;
+
+ /**
+ * Maps dbase data type id's to human readable strings
+ *
+ * The human readable values are based on the output of PHP's
+ * dbase_get_header_info() function.
+ *
+ * @var array
+ * @since Property available since Release 1.7.0
+ */
+ var $types = array(
+ 'C' => 'character',
+ 'D' => 'date',
+ 'L' => 'boolean',
+ 'M' => 'memo',
+ 'N' => 'number',
+ );
+
+
+ // }}}
+ // {{{ constructor
+
+ /**
+ * This constructor calls <kbd>$this->DB_common()</kbd>
+ *
+ * @return void
+ */
+ function DB_dbase()
+ {
+ $this->DB_common();
+ }
+
+ // }}}
+ // {{{ connect()
+
+ /**
+ * Connect to the database and create it if it doesn't exist
+ *
+ * Don't call this method directly. Use DB::connect() instead.
+ *
+ * PEAR DB's dbase driver supports the following extra DSN options:
+ * + mode An integer specifying the read/write mode to use
+ * (0 = read only, 1 = write only, 2 = read/write).
+ * Available since PEAR DB 1.7.0.
+ * + fields An array of arrays that PHP's dbase_create() function needs
+ * to create a new database. This information is used if the
+ * dBase file specified in the "database" segment of the DSN
+ * does not exist. For more info, see the PHP manual's
+ * {@link http://php.net/dbase_create dbase_create()} page.
+ * Available since PEAR DB 1.7.0.
+ *
+ * Example of how to connect and establish a new dBase file if necessary:
+ * <code>
+ * require_once 'DB.php';
+ *
+ * $dsn = array(
+ * 'phptype' => 'dbase',
+ * 'database' => '/path/and/name/of/dbase/file',
+ * 'mode' => 2,
+ * 'fields' => array(
+ * array('a', 'N', 5, 0),
+ * array('b', 'C', 40),
+ * array('c', 'C', 255),
+ * array('d', 'C', 20),
+ * ),
+ * );
+ * $options = array(
+ * 'debug' => 2,
+ * 'portability' => DB_PORTABILITY_ALL,
+ * );
+ *
+ * $db = DB::connect($dsn, $options);
+ * if (PEAR::isError($db)) {
+ * die($db->getMessage());
+ * }
+ * </code>
+ *
+ * @param array $dsn the data source name
+ * @param bool $persistent should the connection be persistent?
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ */
+ function connect($dsn, $persistent = false)
+ {
+ if (!PEAR::loadExtension('dbase')) {
+ return $this->raiseError(DB_ERROR_EXTENSION_NOT_FOUND);
+ }
+
+ $this->dsn = $dsn;
+ if ($dsn['dbsyntax']) {
+ $this->dbsyntax = $dsn['dbsyntax'];
+ }
+
+ /*
+ * Turn track_errors on for entire script since $php_errormsg
+ * is the only way to find errors from the dbase extension.
+ */
+ @ini_set('track_errors', 1);
+ $php_errormsg = '';
+
+ if (!file_exists($dsn['database'])) {
+ $this->dsn['mode'] = 2;
+ if (empty($dsn['fields']) || !is_array($dsn['fields'])) {
+ return $this->raiseError(DB_ERROR_CONNECT_FAILED,
+ null, null, null,
+ 'the dbase file does not exist and '
+ . 'it could not be created because '
+ . 'the "fields" element of the DSN '
+ . 'is not properly set');
+ }
+ $this->connection = @dbase_create($dsn['database'],
+ $dsn['fields']);
+ if (!$this->connection) {
+ return $this->raiseError(DB_ERROR_CONNECT_FAILED,
+ null, null, null,
+ 'the dbase file does not exist and '
+ . 'the attempt to create it failed: '
+ . $php_errormsg);
+ }
+ } else {
+ if (!isset($this->dsn['mode'])) {
+ $this->dsn['mode'] = 0;
+ }
+ $this->connection = @dbase_open($dsn['database'],
+ $this->dsn['mode']);
+ if (!$this->connection) {
+ return $this->raiseError(DB_ERROR_CONNECT_FAILED,
+ null, null, null,
+ $php_errormsg);
+ }
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ disconnect()
+
+ /**
+ * Disconnects from the database server
+ *
+ * @return bool TRUE on success, FALSE on failure
+ */
+ function disconnect()
+ {
+ $ret = @dbase_close($this->connection);
+ $this->connection = null;
+ return $ret;
+ }
+
+ // }}}
+ // {{{ &query()
+
+ function &query($query = null)
+ {
+ // emulate result resources
+ $this->res_row[(int)$this->result] = 0;
+ $tmp = new DB_result($this, $this->result++);
+ return $tmp;
+ }
+
+ // }}}
+ // {{{ fetchInto()
+
+ /**
+ * Places a row from the result set into the given array
+ *
+ * Formating of the array and the data therein are configurable.
+ * See DB_result::fetchInto() for more information.
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::fetchInto() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result the query result resource
+ * @param array $arr the referenced array to put the data in
+ * @param int $fetchmode how the resulting array should be indexed
+ * @param int $rownum the row number to fetch (0 = first row)
+ *
+ * @return mixed DB_OK on success, NULL when the end of a result set is
+ * reached or on failure
+ *
+ * @see DB_result::fetchInto()
+ */
+ function fetchInto($result, &$arr, $fetchmode, $rownum = null)
+ {
+ if ($rownum === null) {
+ $rownum = $this->res_row[(int)$result]++;
+ }
+ if ($fetchmode & DB_FETCHMODE_ASSOC) {
+ $arr = @dbase_get_record_with_names($this->connection, $rownum);
+ if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE && $arr) {
+ $arr = array_change_key_case($arr, CASE_LOWER);
+ }
+ } else {
+ $arr = @dbase_get_record($this->connection, $rownum);
+ }
+ if (!$arr) {
+ return null;
+ }
+ if ($this->options['portability'] & DB_PORTABILITY_RTRIM) {
+ $this->_rtrimArrayValues($arr);
+ }
+ if ($this->options['portability'] & DB_PORTABILITY_NULL_TO_EMPTY) {
+ $this->_convertNullArrayValuesToEmpty($arr);
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ freeResult()
+
+ /**
+ * Deletes the result set and frees the memory occupied by the result set.
+ *
+ * This method is a no-op for dbase, as there aren't result resources in
+ * the same sense as most other database backends.
+ *
+ * @param resource $result PHP's query result resource
+ *
+ * @return bool TRUE on success, FALSE if $result is invalid
+ *
+ * @see DB_result::free()
+ */
+ function freeResult($result)
+ {
+ return true;
+ }
+
+ // }}}
+ // {{{ numCols()
+
+ /**
+ * Gets the number of columns in a result set
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::numCols() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result PHP's query result resource
+ *
+ * @return int the number of columns. A DB_Error object on failure.
+ *
+ * @see DB_result::numCols()
+ */
+ function numCols($foo)
+ {
+ return @dbase_numfields($this->connection);
+ }
+
+ // }}}
+ // {{{ numRows()
+
+ /**
+ * Gets the number of rows in a result set
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::numRows() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result PHP's query result resource
+ *
+ * @return int the number of rows. A DB_Error object on failure.
+ *
+ * @see DB_result::numRows()
+ */
+ function numRows($foo)
+ {
+ return @dbase_numrecords($this->connection);
+ }
+
+ // }}}
+ // {{{ quoteBoolean()
+
+ /**
+ * Formats a boolean value for use within a query in a locale-independent
+ * manner.
+ *
+ * @param boolean the boolean value to be quoted.
+ * @return string the quoted string.
+ * @see DB_common::quoteSmart()
+ * @since Method available since release 1.7.8.
+ */
+ function quoteBoolean($boolean) {
+ return $boolean ? 'T' : 'F';
+ }
+
+ // }}}
+ // {{{ tableInfo()
+
+ /**
+ * Returns information about the current database
+ *
+ * @param mixed $result THIS IS UNUSED IN DBASE. The current database
+ * is examined regardless of what is provided here.
+ * @param int $mode a valid tableInfo mode
+ *
+ * @return array an associative array with the information requested.
+ * A DB_Error object on failure.
+ *
+ * @see DB_common::tableInfo()
+ * @since Method available since Release 1.7.0
+ */
+ function tableInfo($result = null, $mode = null)
+ {
+ if (function_exists('dbase_get_header_info')) {
+ $id = @dbase_get_header_info($this->connection);
+ if (!$id && $php_errormsg) {
+ return $this->raiseError(DB_ERROR,
+ null, null, null,
+ $php_errormsg);
+ }
+ } else {
+ /*
+ * This segment for PHP 4 is loosely based on code by
+ * Hadi Rusiah <deegos@yahoo.com> in the comments on
+ * the dBase reference page in the PHP manual.
+ */
+ $db = @fopen($this->dsn['database'], 'r');
+ if (!$db) {
+ return $this->raiseError(DB_ERROR_CONNECT_FAILED,
+ null, null, null,
+ $php_errormsg);
+ }
+
+ $id = array();
+ $i = 0;
+
+ $line = fread($db, 32);
+ while (!feof($db)) {
+ $line = fread($db, 32);
+ if (substr($line, 0, 1) == chr(13)) {
+ break;
+ } else {
+ $pos = strpos(substr($line, 0, 10), chr(0));
+ $pos = ($pos == 0 ? 10 : $pos);
+ $id[$i] = array(
+ 'name' => substr($line, 0, $pos),
+ 'type' => $this->types[substr($line, 11, 1)],
+ 'length' => ord(substr($line, 16, 1)),
+ 'precision' => ord(substr($line, 17, 1)),
+ );
+ }
+ $i++;
+ }
+
+ fclose($db);
+ }
+
+ if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE) {
+ $case_func = 'strtolower';
+ } else {
+ $case_func = 'strval';
+ }
+
+ $res = array();
+ $count = count($id);
+
+ if ($mode) {
+ $res['num_fields'] = $count;
+ }
+
+ for ($i = 0; $i < $count; $i++) {
+ $res[$i] = array(
+ 'table' => $this->dsn['database'],
+ 'name' => $case_func($id[$i]['name']),
+ 'type' => $id[$i]['type'],
+ 'len' => $id[$i]['length'],
+ 'flags' => ''
+ );
+ if ($mode & DB_TABLEINFO_ORDER) {
+ $res['order'][$res[$i]['name']] = $i;
+ }
+ if ($mode & DB_TABLEINFO_ORDERTABLE) {
+ $res['ordertable'][$res[$i]['table']][$res[$i]['name']] = $i;
+ }
+ }
+
+ return $res;
+ }
+
+ // }}}
+}
+
+/*
+ * Local variables:
+ * tab-width: 4
+ * c-basic-offset: 4
+ * End:
+ */
+
+?>
diff --git a/extlib/DB/fbsql.php b/extlib/DB/fbsql.php
new file mode 100644
index 000000000..4de4078f7
--- /dev/null
+++ b/extlib/DB/fbsql.php
@@ -0,0 +1,769 @@
+<?php
+
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * The PEAR DB driver for PHP's fbsql extension
+ * for interacting with FrontBase databases
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt. If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category Database
+ * @package DB
+ * @author Frank M. Kromann <frank@frontbase.com>
+ * @author Daniel Convissor <danielc@php.net>
+ * @copyright 1997-2007 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0
+ * @version CVS: $Id: fbsql.php,v 1.88 2007/07/06 05:19:21 aharvey Exp $
+ * @link http://pear.php.net/package/DB
+ */
+
+/**
+ * Obtain the DB_common class so it can be extended from
+ */
+require_once 'DB/common.php';
+
+/**
+ * The methods PEAR DB uses to interact with PHP's fbsql extension
+ * for interacting with FrontBase databases
+ *
+ * These methods overload the ones declared in DB_common.
+ *
+ * @category Database
+ * @package DB
+ * @author Frank M. Kromann <frank@frontbase.com>
+ * @author Daniel Convissor <danielc@php.net>
+ * @copyright 1997-2007 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0
+ * @version Release: 1.7.14RC1
+ * @link http://pear.php.net/package/DB
+ * @since Class functional since Release 1.7.0
+ */
+class DB_fbsql extends DB_common
+{
+ // {{{ properties
+
+ /**
+ * The DB driver type (mysql, oci8, odbc, etc.)
+ * @var string
+ */
+ var $phptype = 'fbsql';
+
+ /**
+ * The database syntax variant to be used (db2, access, etc.), if any
+ * @var string
+ */
+ var $dbsyntax = 'fbsql';
+
+ /**
+ * The capabilities of this DB implementation
+ *
+ * The 'new_link' element contains the PHP version that first provided
+ * new_link support for this DBMS. Contains false if it's unsupported.
+ *
+ * Meaning of the 'limit' element:
+ * + 'emulate' = emulate with fetch row by number
+ * + 'alter' = alter the query
+ * + false = skip rows
+ *
+ * @var array
+ */
+ var $features = array(
+ 'limit' => 'alter',
+ 'new_link' => false,
+ 'numrows' => true,
+ 'pconnect' => true,
+ 'prepare' => false,
+ 'ssl' => false,
+ 'transactions' => true,
+ );
+
+ /**
+ * A mapping of native error codes to DB error codes
+ * @var array
+ */
+ var $errorcode_map = array(
+ 22 => DB_ERROR_SYNTAX,
+ 85 => DB_ERROR_ALREADY_EXISTS,
+ 108 => DB_ERROR_SYNTAX,
+ 116 => DB_ERROR_NOSUCHTABLE,
+ 124 => DB_ERROR_VALUE_COUNT_ON_ROW,
+ 215 => DB_ERROR_NOSUCHFIELD,
+ 217 => DB_ERROR_INVALID_NUMBER,
+ 226 => DB_ERROR_NOSUCHFIELD,
+ 231 => DB_ERROR_INVALID,
+ 239 => DB_ERROR_TRUNCATED,
+ 251 => DB_ERROR_SYNTAX,
+ 266 => DB_ERROR_NOT_FOUND,
+ 357 => DB_ERROR_CONSTRAINT_NOT_NULL,
+ 358 => DB_ERROR_CONSTRAINT,
+ 360 => DB_ERROR_CONSTRAINT,
+ 361 => DB_ERROR_CONSTRAINT,
+ );
+
+ /**
+ * The raw database connection created by PHP
+ * @var resource
+ */
+ var $connection;
+
+ /**
+ * The DSN information for connecting to a database
+ * @var array
+ */
+ var $dsn = array();
+
+
+ // }}}
+ // {{{ constructor
+
+ /**
+ * This constructor calls <kbd>$this->DB_common()</kbd>
+ *
+ * @return void
+ */
+ function DB_fbsql()
+ {
+ $this->DB_common();
+ }
+
+ // }}}
+ // {{{ connect()
+
+ /**
+ * Connect to the database server, log in and open the database
+ *
+ * Don't call this method directly. Use DB::connect() instead.
+ *
+ * @param array $dsn the data source name
+ * @param bool $persistent should the connection be persistent?
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ */
+ function connect($dsn, $persistent = false)
+ {
+ if (!PEAR::loadExtension('fbsql')) {
+ return $this->raiseError(DB_ERROR_EXTENSION_NOT_FOUND);
+ }
+
+ $this->dsn = $dsn;
+ if ($dsn['dbsyntax']) {
+ $this->dbsyntax = $dsn['dbsyntax'];
+ }
+
+ $params = array(
+ $dsn['hostspec'] ? $dsn['hostspec'] : 'localhost',
+ $dsn['username'] ? $dsn['username'] : null,
+ $dsn['password'] ? $dsn['password'] : null,
+ );
+
+ $connect_function = $persistent ? 'fbsql_pconnect' : 'fbsql_connect';
+
+ $ini = ini_get('track_errors');
+ $php_errormsg = '';
+ if ($ini) {
+ $this->connection = @call_user_func_array($connect_function,
+ $params);
+ } else {
+ @ini_set('track_errors', 1);
+ $this->connection = @call_user_func_array($connect_function,
+ $params);
+ @ini_set('track_errors', $ini);
+ }
+
+ if (!$this->connection) {
+ return $this->raiseError(DB_ERROR_CONNECT_FAILED,
+ null, null, null,
+ $php_errormsg);
+ }
+
+ if ($dsn['database']) {
+ if (!@fbsql_select_db($dsn['database'], $this->connection)) {
+ return $this->fbsqlRaiseError();
+ }
+ }
+
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ disconnect()
+
+ /**
+ * Disconnects from the database server
+ *
+ * @return bool TRUE on success, FALSE on failure
+ */
+ function disconnect()
+ {
+ $ret = @fbsql_close($this->connection);
+ $this->connection = null;
+ return $ret;
+ }
+
+ // }}}
+ // {{{ simpleQuery()
+
+ /**
+ * Sends a query to the database server
+ *
+ * @param string the SQL query string
+ *
+ * @return mixed + a PHP result resrouce for successful SELECT queries
+ * + the DB_OK constant for other successful queries
+ * + a DB_Error object on failure
+ */
+ function simpleQuery($query)
+ {
+ $this->last_query = $query;
+ $query = $this->modifyQuery($query);
+ $result = @fbsql_query("$query;", $this->connection);
+ if (!$result) {
+ return $this->fbsqlRaiseError();
+ }
+ // Determine which queries that should return data, and which
+ // should return an error code only.
+ if ($this->_checkManip($query)) {
+ return DB_OK;
+ }
+ return $result;
+ }
+
+ // }}}
+ // {{{ nextResult()
+
+ /**
+ * Move the internal fbsql result pointer to the next available result
+ *
+ * @param a valid fbsql result resource
+ *
+ * @access public
+ *
+ * @return true if a result is available otherwise return false
+ */
+ function nextResult($result)
+ {
+ return @fbsql_next_result($result);
+ }
+
+ // }}}
+ // {{{ fetchInto()
+
+ /**
+ * Places a row from the result set into the given array
+ *
+ * Formating of the array and the data therein are configurable.
+ * See DB_result::fetchInto() for more information.
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::fetchInto() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result the query result resource
+ * @param array $arr the referenced array to put the data in
+ * @param int $fetchmode how the resulting array should be indexed
+ * @param int $rownum the row number to fetch (0 = first row)
+ *
+ * @return mixed DB_OK on success, NULL when the end of a result set is
+ * reached or on failure
+ *
+ * @see DB_result::fetchInto()
+ */
+ function fetchInto($result, &$arr, $fetchmode, $rownum = null)
+ {
+ if ($rownum !== null) {
+ if (!@fbsql_data_seek($result, $rownum)) {
+ return null;
+ }
+ }
+ if ($fetchmode & DB_FETCHMODE_ASSOC) {
+ $arr = @fbsql_fetch_array($result, FBSQL_ASSOC);
+ if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE && $arr) {
+ $arr = array_change_key_case($arr, CASE_LOWER);
+ }
+ } else {
+ $arr = @fbsql_fetch_row($result);
+ }
+ if (!$arr) {
+ return null;
+ }
+ if ($this->options['portability'] & DB_PORTABILITY_RTRIM) {
+ $this->_rtrimArrayValues($arr);
+ }
+ if ($this->options['portability'] & DB_PORTABILITY_NULL_TO_EMPTY) {
+ $this->_convertNullArrayValuesToEmpty($arr);
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ freeResult()
+
+ /**
+ * Deletes the result set and frees the memory occupied by the result set
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::free() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result PHP's query result resource
+ *
+ * @return bool TRUE on success, FALSE if $result is invalid
+ *
+ * @see DB_result::free()
+ */
+ function freeResult($result)
+ {
+ return is_resource($result) ? fbsql_free_result($result) : false;
+ }
+
+ // }}}
+ // {{{ autoCommit()
+
+ /**
+ * Enables or disables automatic commits
+ *
+ * @param bool $onoff true turns it on, false turns it off
+ *
+ * @return int DB_OK on success. A DB_Error object if the driver
+ * doesn't support auto-committing transactions.
+ */
+ function autoCommit($onoff=false)
+ {
+ if ($onoff) {
+ $this->query("SET COMMIT TRUE");
+ } else {
+ $this->query("SET COMMIT FALSE");
+ }
+ }
+
+ // }}}
+ // {{{ commit()
+
+ /**
+ * Commits the current transaction
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ */
+ function commit()
+ {
+ @fbsql_commit($this->connection);
+ }
+
+ // }}}
+ // {{{ rollback()
+
+ /**
+ * Reverts the current transaction
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ */
+ function rollback()
+ {
+ @fbsql_rollback($this->connection);
+ }
+
+ // }}}
+ // {{{ numCols()
+
+ /**
+ * Gets the number of columns in a result set
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::numCols() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result PHP's query result resource
+ *
+ * @return int the number of columns. A DB_Error object on failure.
+ *
+ * @see DB_result::numCols()
+ */
+ function numCols($result)
+ {
+ $cols = @fbsql_num_fields($result);
+ if (!$cols) {
+ return $this->fbsqlRaiseError();
+ }
+ return $cols;
+ }
+
+ // }}}
+ // {{{ numRows()
+
+ /**
+ * Gets the number of rows in a result set
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::numRows() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result PHP's query result resource
+ *
+ * @return int the number of rows. A DB_Error object on failure.
+ *
+ * @see DB_result::numRows()
+ */
+ function numRows($result)
+ {
+ $rows = @fbsql_num_rows($result);
+ if ($rows === null) {
+ return $this->fbsqlRaiseError();
+ }
+ return $rows;
+ }
+
+ // }}}
+ // {{{ affectedRows()
+
+ /**
+ * Determines the number of rows affected by a data maniuplation query
+ *
+ * 0 is returned for queries that don't manipulate data.
+ *
+ * @return int the number of rows. A DB_Error object on failure.
+ */
+ function affectedRows()
+ {
+ if ($this->_last_query_manip) {
+ $result = @fbsql_affected_rows($this->connection);
+ } else {
+ $result = 0;
+ }
+ return $result;
+ }
+
+ // }}}
+ // {{{ nextId()
+
+ /**
+ * Returns the next free id in a sequence
+ *
+ * @param string $seq_name name of the sequence
+ * @param boolean $ondemand when true, the seqence is automatically
+ * created if it does not exist
+ *
+ * @return int the next id number in the sequence.
+ * A DB_Error object on failure.
+ *
+ * @see DB_common::nextID(), DB_common::getSequenceName(),
+ * DB_fbsql::createSequence(), DB_fbsql::dropSequence()
+ */
+ function nextId($seq_name, $ondemand = true)
+ {
+ $seqname = $this->getSequenceName($seq_name);
+ do {
+ $repeat = 0;
+ $this->pushErrorHandling(PEAR_ERROR_RETURN);
+ $result = $this->query('SELECT UNIQUE FROM ' . $seqname);
+ $this->popErrorHandling();
+ if ($ondemand && DB::isError($result) &&
+ $result->getCode() == DB_ERROR_NOSUCHTABLE) {
+ $repeat = 1;
+ $result = $this->createSequence($seq_name);
+ if (DB::isError($result)) {
+ return $result;
+ }
+ } else {
+ $repeat = 0;
+ }
+ } while ($repeat);
+ if (DB::isError($result)) {
+ return $this->fbsqlRaiseError();
+ }
+ $result->fetchInto($tmp, DB_FETCHMODE_ORDERED);
+ return $tmp[0];
+ }
+
+ /**
+ * Creates a new sequence
+ *
+ * @param string $seq_name name of the new sequence
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ *
+ * @see DB_common::createSequence(), DB_common::getSequenceName(),
+ * DB_fbsql::nextID(), DB_fbsql::dropSequence()
+ */
+ function createSequence($seq_name)
+ {
+ $seqname = $this->getSequenceName($seq_name);
+ $res = $this->query('CREATE TABLE ' . $seqname
+ . ' (id INTEGER NOT NULL,'
+ . ' PRIMARY KEY(id))');
+ if ($res) {
+ $res = $this->query('SET UNIQUE = 0 FOR ' . $seqname);
+ }
+ return $res;
+ }
+
+ // }}}
+ // {{{ dropSequence()
+
+ /**
+ * Deletes a sequence
+ *
+ * @param string $seq_name name of the sequence to be deleted
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ *
+ * @see DB_common::dropSequence(), DB_common::getSequenceName(),
+ * DB_fbsql::nextID(), DB_fbsql::createSequence()
+ */
+ function dropSequence($seq_name)
+ {
+ return $this->query('DROP TABLE ' . $this->getSequenceName($seq_name)
+ . ' RESTRICT');
+ }
+
+ // }}}
+ // {{{ modifyLimitQuery()
+
+ /**
+ * Adds LIMIT clauses to a query string according to current DBMS standards
+ *
+ * @param string $query the query to modify
+ * @param int $from the row to start to fetching (0 = the first row)
+ * @param int $count the numbers of rows to fetch
+ * @param mixed $params array, string or numeric data to be used in
+ * execution of the statement. Quantity of items
+ * passed must match quantity of placeholders in
+ * query: meaning 1 placeholder for non-array
+ * parameters or 1 placeholder per array element.
+ *
+ * @return string the query string with LIMIT clauses added
+ *
+ * @access protected
+ */
+ function modifyLimitQuery($query, $from, $count, $params = array())
+ {
+ if (DB::isManip($query) || $this->_next_query_manip) {
+ return preg_replace('/^([\s(])*SELECT/i',
+ "\\1SELECT TOP($count)", $query);
+ } else {
+ return preg_replace('/([\s(])*SELECT/i',
+ "\\1SELECT TOP($from, $count)", $query);
+ }
+ }
+
+ // }}}
+ // {{{ quoteBoolean()
+
+ /**
+ * Formats a boolean value for use within a query in a locale-independent
+ * manner.
+ *
+ * @param boolean the boolean value to be quoted.
+ * @return string the quoted string.
+ * @see DB_common::quoteSmart()
+ * @since Method available since release 1.7.8.
+ */
+ function quoteBoolean($boolean) {
+ return $boolean ? 'TRUE' : 'FALSE';
+ }
+
+ // }}}
+ // {{{ quoteFloat()
+
+ /**
+ * Formats a float value for use within a query in a locale-independent
+ * manner.
+ *
+ * @param float the float value to be quoted.
+ * @return string the quoted string.
+ * @see DB_common::quoteSmart()
+ * @since Method available since release 1.7.8.
+ */
+ function quoteFloat($float) {
+ return $this->escapeSimple(str_replace(',', '.', strval(floatval($float))));
+ }
+
+ // }}}
+ // {{{ fbsqlRaiseError()
+
+ /**
+ * Produces a DB_Error object regarding the current problem
+ *
+ * @param int $errno if the error is being manually raised pass a
+ * DB_ERROR* constant here. If this isn't passed
+ * the error information gathered from the DBMS.
+ *
+ * @return object the DB_Error object
+ *
+ * @see DB_common::raiseError(),
+ * DB_fbsql::errorNative(), DB_common::errorCode()
+ */
+ function fbsqlRaiseError($errno = null)
+ {
+ if ($errno === null) {
+ $errno = $this->errorCode(fbsql_errno($this->connection));
+ }
+ return $this->raiseError($errno, null, null, null,
+ @fbsql_error($this->connection));
+ }
+
+ // }}}
+ // {{{ errorNative()
+
+ /**
+ * Gets the DBMS' native error code produced by the last query
+ *
+ * @return int the DBMS' error code
+ */
+ function errorNative()
+ {
+ return @fbsql_errno($this->connection);
+ }
+
+ // }}}
+ // {{{ tableInfo()
+
+ /**
+ * Returns information about a table or a result set
+ *
+ * @param object|string $result DB_result object from a query or a
+ * string containing the name of a table.
+ * While this also accepts a query result
+ * resource identifier, this behavior is
+ * deprecated.
+ * @param int $mode a valid tableInfo mode
+ *
+ * @return array an associative array with the information requested.
+ * A DB_Error object on failure.
+ *
+ * @see DB_common::tableInfo()
+ */
+ function tableInfo($result, $mode = null)
+ {
+ if (is_string($result)) {
+ /*
+ * Probably received a table name.
+ * Create a result resource identifier.
+ */
+ $id = @fbsql_list_fields($this->dsn['database'],
+ $result, $this->connection);
+ $got_string = true;
+ } elseif (isset($result->result)) {
+ /*
+ * Probably received a result object.
+ * Extract the result resource identifier.
+ */
+ $id = $result->result;
+ $got_string = false;
+ } else {
+ /*
+ * Probably received a result resource identifier.
+ * Copy it.
+ * Deprecated. Here for compatibility only.
+ */
+ $id = $result;
+ $got_string = false;
+ }
+
+ if (!is_resource($id)) {
+ return $this->fbsqlRaiseError(DB_ERROR_NEED_MORE_DATA);
+ }
+
+ if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE) {
+ $case_func = 'strtolower';
+ } else {
+ $case_func = 'strval';
+ }
+
+ $count = @fbsql_num_fields($id);
+ $res = array();
+
+ if ($mode) {
+ $res['num_fields'] = $count;
+ }
+
+ for ($i = 0; $i < $count; $i++) {
+ $res[$i] = array(
+ 'table' => $case_func(@fbsql_field_table($id, $i)),
+ 'name' => $case_func(@fbsql_field_name($id, $i)),
+ 'type' => @fbsql_field_type($id, $i),
+ 'len' => @fbsql_field_len($id, $i),
+ 'flags' => @fbsql_field_flags($id, $i),
+ );
+ if ($mode & DB_TABLEINFO_ORDER) {
+ $res['order'][$res[$i]['name']] = $i;
+ }
+ if ($mode & DB_TABLEINFO_ORDERTABLE) {
+ $res['ordertable'][$res[$i]['table']][$res[$i]['name']] = $i;
+ }
+ }
+
+ // free the result only if we were called on a table
+ if ($got_string) {
+ @fbsql_free_result($id);
+ }
+ return $res;
+ }
+
+ // }}}
+ // {{{ getSpecialQuery()
+
+ /**
+ * Obtains the query string needed for listing a given type of objects
+ *
+ * @param string $type the kind of objects you want to retrieve
+ *
+ * @return string the SQL query string or null if the driver doesn't
+ * support the object type requested
+ *
+ * @access protected
+ * @see DB_common::getListOf()
+ */
+ function getSpecialQuery($type)
+ {
+ switch ($type) {
+ case 'tables':
+ return 'SELECT "table_name" FROM information_schema.tables'
+ . ' t0, information_schema.schemata t1'
+ . ' WHERE t0.schema_pk=t1.schema_pk AND'
+ . ' "table_type" = \'BASE TABLE\''
+ . ' AND "schema_name" = current_schema';
+ case 'views':
+ return 'SELECT "table_name" FROM information_schema.tables'
+ . ' t0, information_schema.schemata t1'
+ . ' WHERE t0.schema_pk=t1.schema_pk AND'
+ . ' "table_type" = \'VIEW\''
+ . ' AND "schema_name" = current_schema';
+ case 'users':
+ return 'SELECT "user_name" from information_schema.users';
+ case 'functions':
+ return 'SELECT "routine_name" FROM'
+ . ' information_schema.psm_routines'
+ . ' t0, information_schema.schemata t1'
+ . ' WHERE t0.schema_pk=t1.schema_pk'
+ . ' AND "routine_kind"=\'FUNCTION\''
+ . ' AND "schema_name" = current_schema';
+ case 'procedures':
+ return 'SELECT "routine_name" FROM'
+ . ' information_schema.psm_routines'
+ . ' t0, information_schema.schemata t1'
+ . ' WHERE t0.schema_pk=t1.schema_pk'
+ . ' AND "routine_kind"=\'PROCEDURE\''
+ . ' AND "schema_name" = current_schema';
+ default:
+ return null;
+ }
+ }
+
+ // }}}
+}
+
+/*
+ * Local variables:
+ * tab-width: 4
+ * c-basic-offset: 4
+ * End:
+ */
+
+?>
diff --git a/extlib/DB/ibase.php b/extlib/DB/ibase.php
new file mode 100644
index 000000000..ee19c5589
--- /dev/null
+++ b/extlib/DB/ibase.php
@@ -0,0 +1,1082 @@
+<?php
+
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * The PEAR DB driver for PHP's interbase extension
+ * for interacting with Interbase and Firebird databases
+ *
+ * While this class works with PHP 4, PHP's InterBase extension is
+ * unstable in PHP 4. Use PHP 5.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt. If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category Database
+ * @package DB
+ * @author Sterling Hughes <sterling@php.net>
+ * @author Daniel Convissor <danielc@php.net>
+ * @copyright 1997-2007 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0
+ * @version CVS: $Id: ibase.php,v 1.116 2007/09/21 13:40:41 aharvey Exp $
+ * @link http://pear.php.net/package/DB
+ */
+
+/**
+ * Obtain the DB_common class so it can be extended from
+ */
+require_once 'DB/common.php';
+
+/**
+ * The methods PEAR DB uses to interact with PHP's interbase extension
+ * for interacting with Interbase and Firebird databases
+ *
+ * These methods overload the ones declared in DB_common.
+ *
+ * While this class works with PHP 4, PHP's InterBase extension is
+ * unstable in PHP 4. Use PHP 5.
+ *
+ * NOTICE: limitQuery() only works for Firebird.
+ *
+ * @category Database
+ * @package DB
+ * @author Sterling Hughes <sterling@php.net>
+ * @author Daniel Convissor <danielc@php.net>
+ * @copyright 1997-2007 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0
+ * @version Release: 1.7.14RC1
+ * @link http://pear.php.net/package/DB
+ * @since Class became stable in Release 1.7.0
+ */
+class DB_ibase extends DB_common
+{
+ // {{{ properties
+
+ /**
+ * The DB driver type (mysql, oci8, odbc, etc.)
+ * @var string
+ */
+ var $phptype = 'ibase';
+
+ /**
+ * The database syntax variant to be used (db2, access, etc.), if any
+ * @var string
+ */
+ var $dbsyntax = 'ibase';
+
+ /**
+ * The capabilities of this DB implementation
+ *
+ * The 'new_link' element contains the PHP version that first provided
+ * new_link support for this DBMS. Contains false if it's unsupported.
+ *
+ * Meaning of the 'limit' element:
+ * + 'emulate' = emulate with fetch row by number
+ * + 'alter' = alter the query
+ * + false = skip rows
+ *
+ * NOTE: only firebird supports limit.
+ *
+ * @var array
+ */
+ var $features = array(
+ 'limit' => false,
+ 'new_link' => false,
+ 'numrows' => 'emulate',
+ 'pconnect' => true,
+ 'prepare' => true,
+ 'ssl' => false,
+ 'transactions' => true,
+ );
+
+ /**
+ * A mapping of native error codes to DB error codes
+ * @var array
+ */
+ var $errorcode_map = array(
+ -104 => DB_ERROR_SYNTAX,
+ -150 => DB_ERROR_ACCESS_VIOLATION,
+ -151 => DB_ERROR_ACCESS_VIOLATION,
+ -155 => DB_ERROR_NOSUCHTABLE,
+ -157 => DB_ERROR_NOSUCHFIELD,
+ -158 => DB_ERROR_VALUE_COUNT_ON_ROW,
+ -170 => DB_ERROR_MISMATCH,
+ -171 => DB_ERROR_MISMATCH,
+ -172 => DB_ERROR_INVALID,
+ // -204 => // Covers too many errors, need to use regex on msg
+ -205 => DB_ERROR_NOSUCHFIELD,
+ -206 => DB_ERROR_NOSUCHFIELD,
+ -208 => DB_ERROR_INVALID,
+ -219 => DB_ERROR_NOSUCHTABLE,
+ -297 => DB_ERROR_CONSTRAINT,
+ -303 => DB_ERROR_INVALID,
+ -413 => DB_ERROR_INVALID_NUMBER,
+ -530 => DB_ERROR_CONSTRAINT,
+ -551 => DB_ERROR_ACCESS_VIOLATION,
+ -552 => DB_ERROR_ACCESS_VIOLATION,
+ // -607 => // Covers too many errors, need to use regex on msg
+ -625 => DB_ERROR_CONSTRAINT_NOT_NULL,
+ -803 => DB_ERROR_CONSTRAINT,
+ -804 => DB_ERROR_VALUE_COUNT_ON_ROW,
+ // -902 => // Covers too many errors, need to use regex on msg
+ -904 => DB_ERROR_CONNECT_FAILED,
+ -922 => DB_ERROR_NOSUCHDB,
+ -923 => DB_ERROR_CONNECT_FAILED,
+ -924 => DB_ERROR_CONNECT_FAILED
+ );
+
+ /**
+ * The raw database connection created by PHP
+ * @var resource
+ */
+ var $connection;
+
+ /**
+ * The DSN information for connecting to a database
+ * @var array
+ */
+ var $dsn = array();
+
+
+ /**
+ * The number of rows affected by a data manipulation query
+ * @var integer
+ * @access private
+ */
+ var $affected = 0;
+
+ /**
+ * Should data manipulation queries be committed automatically?
+ * @var bool
+ * @access private
+ */
+ var $autocommit = true;
+
+ /**
+ * The prepared statement handle from the most recently executed statement
+ *
+ * {@internal Mainly here because the InterBase/Firebird API is only
+ * able to retrieve data from result sets if the statemnt handle is
+ * still in scope.}}
+ *
+ * @var resource
+ */
+ var $last_stmt;
+
+ /**
+ * Is the given prepared statement a data manipulation query?
+ * @var array
+ * @access private
+ */
+ var $manip_query = array();
+
+
+ // }}}
+ // {{{ constructor
+
+ /**
+ * This constructor calls <kbd>$this->DB_common()</kbd>
+ *
+ * @return void
+ */
+ function DB_ibase()
+ {
+ $this->DB_common();
+ }
+
+ // }}}
+ // {{{ connect()
+
+ /**
+ * Connect to the database server, log in and open the database
+ *
+ * Don't call this method directly. Use DB::connect() instead.
+ *
+ * PEAR DB's ibase driver supports the following extra DSN options:
+ * + buffers The number of database buffers to allocate for the
+ * server-side cache.
+ * + charset The default character set for a database.
+ * + dialect The default SQL dialect for any statement
+ * executed within a connection. Defaults to the
+ * highest one supported by client libraries.
+ * Functional only with InterBase 6 and up.
+ * + role Functional only with InterBase 5 and up.
+ *
+ * @param array $dsn the data source name
+ * @param bool $persistent should the connection be persistent?
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ */
+ function connect($dsn, $persistent = false)
+ {
+ if (!PEAR::loadExtension('interbase')) {
+ return $this->raiseError(DB_ERROR_EXTENSION_NOT_FOUND);
+ }
+
+ $this->dsn = $dsn;
+ if ($dsn['dbsyntax']) {
+ $this->dbsyntax = $dsn['dbsyntax'];
+ }
+ if ($this->dbsyntax == 'firebird') {
+ $this->features['limit'] = 'alter';
+ }
+
+ $params = array(
+ $dsn['hostspec']
+ ? ($dsn['hostspec'] . ':' . $dsn['database'])
+ : $dsn['database'],
+ $dsn['username'] ? $dsn['username'] : null,
+ $dsn['password'] ? $dsn['password'] : null,
+ isset($dsn['charset']) ? $dsn['charset'] : null,
+ isset($dsn['buffers']) ? $dsn['buffers'] : null,
+ isset($dsn['dialect']) ? $dsn['dialect'] : null,
+ isset($dsn['role']) ? $dsn['role'] : null,
+ );
+
+ $connect_function = $persistent ? 'ibase_pconnect' : 'ibase_connect';
+
+ $this->connection = @call_user_func_array($connect_function, $params);
+ if (!$this->connection) {
+ return $this->ibaseRaiseError(DB_ERROR_CONNECT_FAILED);
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ disconnect()
+
+ /**
+ * Disconnects from the database server
+ *
+ * @return bool TRUE on success, FALSE on failure
+ */
+ function disconnect()
+ {
+ $ret = @ibase_close($this->connection);
+ $this->connection = null;
+ return $ret;
+ }
+
+ // }}}
+ // {{{ simpleQuery()
+
+ /**
+ * Sends a query to the database server
+ *
+ * @param string the SQL query string
+ *
+ * @return mixed + a PHP result resrouce for successful SELECT queries
+ * + the DB_OK constant for other successful queries
+ * + a DB_Error object on failure
+ */
+ function simpleQuery($query)
+ {
+ $ismanip = $this->_checkManip($query);
+ $this->last_query = $query;
+ $query = $this->modifyQuery($query);
+ $result = @ibase_query($this->connection, $query);
+
+ if (!$result) {
+ return $this->ibaseRaiseError();
+ }
+ if ($this->autocommit && $ismanip) {
+ @ibase_commit($this->connection);
+ }
+ if ($ismanip) {
+ $this->affected = $result;
+ return DB_OK;
+ } else {
+ $this->affected = 0;
+ return $result;
+ }
+ }
+
+ // }}}
+ // {{{ modifyLimitQuery()
+
+ /**
+ * Adds LIMIT clauses to a query string according to current DBMS standards
+ *
+ * Only works with Firebird.
+ *
+ * @param string $query the query to modify
+ * @param int $from the row to start to fetching (0 = the first row)
+ * @param int $count the numbers of rows to fetch
+ * @param mixed $params array, string or numeric data to be used in
+ * execution of the statement. Quantity of items
+ * passed must match quantity of placeholders in
+ * query: meaning 1 placeholder for non-array
+ * parameters or 1 placeholder per array element.
+ *
+ * @return string the query string with LIMIT clauses added
+ *
+ * @access protected
+ */
+ function modifyLimitQuery($query, $from, $count, $params = array())
+ {
+ if ($this->dsn['dbsyntax'] == 'firebird') {
+ $query = preg_replace('/^([\s(])*SELECT/i',
+ "SELECT FIRST $count SKIP $from", $query);
+ }
+ return $query;
+ }
+
+ // }}}
+ // {{{ nextResult()
+
+ /**
+ * Move the internal ibase result pointer to the next available result
+ *
+ * @param a valid fbsql result resource
+ *
+ * @access public
+ *
+ * @return true if a result is available otherwise return false
+ */
+ function nextResult($result)
+ {
+ return false;
+ }
+
+ // }}}
+ // {{{ fetchInto()
+
+ /**
+ * Places a row from the result set into the given array
+ *
+ * Formating of the array and the data therein are configurable.
+ * See DB_result::fetchInto() for more information.
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::fetchInto() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result the query result resource
+ * @param array $arr the referenced array to put the data in
+ * @param int $fetchmode how the resulting array should be indexed
+ * @param int $rownum the row number to fetch (0 = first row)
+ *
+ * @return mixed DB_OK on success, NULL when the end of a result set is
+ * reached or on failure
+ *
+ * @see DB_result::fetchInto()
+ */
+ function fetchInto($result, &$arr, $fetchmode, $rownum = null)
+ {
+ if ($rownum !== null) {
+ return $this->ibaseRaiseError(DB_ERROR_NOT_CAPABLE);
+ }
+ if ($fetchmode & DB_FETCHMODE_ASSOC) {
+ if (function_exists('ibase_fetch_assoc')) {
+ $arr = @ibase_fetch_assoc($result);
+ } else {
+ $arr = get_object_vars(ibase_fetch_object($result));
+ }
+ if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE && $arr) {
+ $arr = array_change_key_case($arr, CASE_LOWER);
+ }
+ } else {
+ $arr = @ibase_fetch_row($result);
+ }
+ if (!$arr) {
+ return null;
+ }
+ if ($this->options['portability'] & DB_PORTABILITY_RTRIM) {
+ $this->_rtrimArrayValues($arr);
+ }
+ if ($this->options['portability'] & DB_PORTABILITY_NULL_TO_EMPTY) {
+ $this->_convertNullArrayValuesToEmpty($arr);
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ freeResult()
+
+ /**
+ * Deletes the result set and frees the memory occupied by the result set
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::free() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result PHP's query result resource
+ *
+ * @return bool TRUE on success, FALSE if $result is invalid
+ *
+ * @see DB_result::free()
+ */
+ function freeResult($result)
+ {
+ return is_resource($result) ? ibase_free_result($result) : false;
+ }
+
+ // }}}
+ // {{{ freeQuery()
+
+ function freeQuery($query)
+ {
+ return is_resource($query) ? ibase_free_query($query) : false;
+ }
+
+ // }}}
+ // {{{ affectedRows()
+
+ /**
+ * Determines the number of rows affected by a data maniuplation query
+ *
+ * 0 is returned for queries that don't manipulate data.
+ *
+ * @return int the number of rows. A DB_Error object on failure.
+ */
+ function affectedRows()
+ {
+ if (is_integer($this->affected)) {
+ return $this->affected;
+ }
+ return $this->ibaseRaiseError(DB_ERROR_NOT_CAPABLE);
+ }
+
+ // }}}
+ // {{{ numCols()
+
+ /**
+ * Gets the number of columns in a result set
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::numCols() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result PHP's query result resource
+ *
+ * @return int the number of columns. A DB_Error object on failure.
+ *
+ * @see DB_result::numCols()
+ */
+ function numCols($result)
+ {
+ $cols = @ibase_num_fields($result);
+ if (!$cols) {
+ return $this->ibaseRaiseError();
+ }
+ return $cols;
+ }
+
+ // }}}
+ // {{{ prepare()
+
+ /**
+ * Prepares a query for multiple execution with execute().
+ *
+ * prepare() requires a generic query as string like <code>
+ * INSERT INTO numbers VALUES (?, ?, ?)
+ * </code>. The <kbd>?</kbd> characters are placeholders.
+ *
+ * Three types of placeholders can be used:
+ * + <kbd>?</kbd> a quoted scalar value, i.e. strings, integers
+ * + <kbd>!</kbd> value is inserted 'as is'
+ * + <kbd>&</kbd> requires a file name. The file's contents get
+ * inserted into the query (i.e. saving binary
+ * data in a db)
+ *
+ * Use backslashes to escape placeholder characters if you don't want
+ * them to be interpreted as placeholders. Example: <code>
+ * "UPDATE foo SET col=? WHERE col='over \& under'"
+ * </code>
+ *
+ * @param string $query query to be prepared
+ * @return mixed DB statement resource on success. DB_Error on failure.
+ */
+ function prepare($query)
+ {
+ $tokens = preg_split('/((?<!\\\)[&?!])/', $query, -1,
+ PREG_SPLIT_DELIM_CAPTURE);
+ $token = 0;
+ $types = array();
+ $newquery = '';
+
+ foreach ($tokens as $key => $val) {
+ switch ($val) {
+ case '?':
+ $types[$token++] = DB_PARAM_SCALAR;
+ break;
+ case '&':
+ $types[$token++] = DB_PARAM_OPAQUE;
+ break;
+ case '!':
+ $types[$token++] = DB_PARAM_MISC;
+ break;
+ default:
+ $tokens[$key] = preg_replace('/\\\([&?!])/', "\\1", $val);
+ $newquery .= $tokens[$key] . '?';
+ }
+ }
+
+ $newquery = substr($newquery, 0, -1);
+ $this->last_query = $query;
+ $newquery = $this->modifyQuery($newquery);
+ $stmt = @ibase_prepare($this->connection, $newquery);
+
+ if ($stmt === false) {
+ $stmt = $this->ibaseRaiseError();
+ } else {
+ $this->prepare_types[(int)$stmt] = $types;
+ $this->manip_query[(int)$stmt] = DB::isManip($query);
+ }
+
+ return $stmt;
+ }
+
+ // }}}
+ // {{{ execute()
+
+ /**
+ * Executes a DB statement prepared with prepare().
+ *
+ * @param resource $stmt a DB statement resource returned from prepare()
+ * @param mixed $data array, string or numeric data to be used in
+ * execution of the statement. Quantity of items
+ * passed must match quantity of placeholders in
+ * query: meaning 1 for non-array items or the
+ * quantity of elements in the array.
+ * @return object a new DB_Result or a DB_Error when fail
+ * @see DB_ibase::prepare()
+ * @access public
+ */
+ function &execute($stmt, $data = array())
+ {
+ $data = (array)$data;
+ $this->last_parameters = $data;
+
+ $types = $this->prepare_types[(int)$stmt];
+ if (count($types) != count($data)) {
+ $tmp = $this->raiseError(DB_ERROR_MISMATCH);
+ return $tmp;
+ }
+
+ $i = 0;
+ foreach ($data as $key => $value) {
+ if ($types[$i] == DB_PARAM_MISC) {
+ /*
+ * ibase doesn't seem to have the ability to pass a
+ * parameter along unchanged, so strip off quotes from start
+ * and end, plus turn two single quotes to one single quote,
+ * in order to avoid the quotes getting escaped by
+ * ibase and ending up in the database.
+ */
+ $data[$key] = preg_replace("/^'(.*)'$/", "\\1", $data[$key]);
+ $data[$key] = str_replace("''", "'", $data[$key]);
+ } elseif ($types[$i] == DB_PARAM_OPAQUE) {
+ $fp = @fopen($data[$key], 'rb');
+ if (!$fp) {
+ $tmp = $this->raiseError(DB_ERROR_ACCESS_VIOLATION);
+ return $tmp;
+ }
+ $data[$key] = fread($fp, filesize($data[$key]));
+ fclose($fp);
+ }
+ $i++;
+ }
+
+ array_unshift($data, $stmt);
+
+ $res = call_user_func_array('ibase_execute', $data);
+ if (!$res) {
+ $tmp = $this->ibaseRaiseError();
+ return $tmp;
+ }
+ /* XXX need this?
+ if ($this->autocommit && $this->manip_query[(int)$stmt]) {
+ @ibase_commit($this->connection);
+ }*/
+ $this->last_stmt = $stmt;
+ if ($this->manip_query[(int)$stmt] || $this->_next_query_manip) {
+ $this->_last_query_manip = true;
+ $this->_next_query_manip = false;
+ $tmp = DB_OK;
+ } else {
+ $this->_last_query_manip = false;
+ $tmp = new DB_result($this, $res);
+ }
+ return $tmp;
+ }
+
+ /**
+ * Frees the internal resources associated with a prepared query
+ *
+ * @param resource $stmt the prepared statement's PHP resource
+ * @param bool $free_resource should the PHP resource be freed too?
+ * Use false if you need to get data
+ * from the result set later.
+ *
+ * @return bool TRUE on success, FALSE if $result is invalid
+ *
+ * @see DB_ibase::prepare()
+ */
+ function freePrepared($stmt, $free_resource = true)
+ {
+ if (!is_resource($stmt)) {
+ return false;
+ }
+ if ($free_resource) {
+ @ibase_free_query($stmt);
+ }
+ unset($this->prepare_tokens[(int)$stmt]);
+ unset($this->prepare_types[(int)$stmt]);
+ unset($this->manip_query[(int)$stmt]);
+ return true;
+ }
+
+ // }}}
+ // {{{ autoCommit()
+
+ /**
+ * Enables or disables automatic commits
+ *
+ * @param bool $onoff true turns it on, false turns it off
+ *
+ * @return int DB_OK on success. A DB_Error object if the driver
+ * doesn't support auto-committing transactions.
+ */
+ function autoCommit($onoff = false)
+ {
+ $this->autocommit = $onoff ? 1 : 0;
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ commit()
+
+ /**
+ * Commits the current transaction
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ */
+ function commit()
+ {
+ return @ibase_commit($this->connection);
+ }
+
+ // }}}
+ // {{{ rollback()
+
+ /**
+ * Reverts the current transaction
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ */
+ function rollback()
+ {
+ return @ibase_rollback($this->connection);
+ }
+
+ // }}}
+ // {{{ transactionInit()
+
+ function transactionInit($trans_args = 0)
+ {
+ return $trans_args
+ ? @ibase_trans($trans_args, $this->connection)
+ : @ibase_trans();
+ }
+
+ // }}}
+ // {{{ nextId()
+
+ /**
+ * Returns the next free id in a sequence
+ *
+ * @param string $seq_name name of the sequence
+ * @param boolean $ondemand when true, the seqence is automatically
+ * created if it does not exist
+ *
+ * @return int the next id number in the sequence.
+ * A DB_Error object on failure.
+ *
+ * @see DB_common::nextID(), DB_common::getSequenceName(),
+ * DB_ibase::createSequence(), DB_ibase::dropSequence()
+ */
+ function nextId($seq_name, $ondemand = true)
+ {
+ $sqn = strtoupper($this->getSequenceName($seq_name));
+ $repeat = 0;
+ do {
+ $this->pushErrorHandling(PEAR_ERROR_RETURN);
+ $result = $this->query("SELECT GEN_ID(${sqn}, 1) "
+ . 'FROM RDB$GENERATORS '
+ . "WHERE RDB\$GENERATOR_NAME='${sqn}'");
+ $this->popErrorHandling();
+ if ($ondemand && DB::isError($result)) {
+ $repeat = 1;
+ $result = $this->createSequence($seq_name);
+ if (DB::isError($result)) {
+ return $result;
+ }
+ } else {
+ $repeat = 0;
+ }
+ } while ($repeat);
+ if (DB::isError($result)) {
+ return $this->raiseError($result);
+ }
+ $arr = $result->fetchRow(DB_FETCHMODE_ORDERED);
+ $result->free();
+ return $arr[0];
+ }
+
+ // }}}
+ // {{{ createSequence()
+
+ /**
+ * Creates a new sequence
+ *
+ * @param string $seq_name name of the new sequence
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ *
+ * @see DB_common::createSequence(), DB_common::getSequenceName(),
+ * DB_ibase::nextID(), DB_ibase::dropSequence()
+ */
+ function createSequence($seq_name)
+ {
+ $sqn = strtoupper($this->getSequenceName($seq_name));
+ $this->pushErrorHandling(PEAR_ERROR_RETURN);
+ $result = $this->query("CREATE GENERATOR ${sqn}");
+ $this->popErrorHandling();
+
+ return $result;
+ }
+
+ // }}}
+ // {{{ dropSequence()
+
+ /**
+ * Deletes a sequence
+ *
+ * @param string $seq_name name of the sequence to be deleted
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ *
+ * @see DB_common::dropSequence(), DB_common::getSequenceName(),
+ * DB_ibase::nextID(), DB_ibase::createSequence()
+ */
+ function dropSequence($seq_name)
+ {
+ return $this->query('DELETE FROM RDB$GENERATORS '
+ . "WHERE RDB\$GENERATOR_NAME='"
+ . strtoupper($this->getSequenceName($seq_name))
+ . "'");
+ }
+
+ // }}}
+ // {{{ _ibaseFieldFlags()
+
+ /**
+ * Get the column's flags
+ *
+ * Supports "primary_key", "unique_key", "not_null", "default",
+ * "computed" and "blob".
+ *
+ * @param string $field_name the name of the field
+ * @param string $table_name the name of the table
+ *
+ * @return string the flags
+ *
+ * @access private
+ */
+ function _ibaseFieldFlags($field_name, $table_name)
+ {
+ $sql = 'SELECT R.RDB$CONSTRAINT_TYPE CTYPE'
+ .' FROM RDB$INDEX_SEGMENTS I'
+ .' JOIN RDB$RELATION_CONSTRAINTS R ON I.RDB$INDEX_NAME=R.RDB$INDEX_NAME'
+ .' WHERE I.RDB$FIELD_NAME=\'' . $field_name . '\''
+ .' AND UPPER(R.RDB$RELATION_NAME)=\'' . strtoupper($table_name) . '\'';
+
+ $result = @ibase_query($this->connection, $sql);
+ if (!$result) {
+ return $this->ibaseRaiseError();
+ }
+
+ $flags = '';
+ if ($obj = @ibase_fetch_object($result)) {
+ @ibase_free_result($result);
+ if (isset($obj->CTYPE) && trim($obj->CTYPE) == 'PRIMARY KEY') {
+ $flags .= 'primary_key ';
+ }
+ if (isset($obj->CTYPE) && trim($obj->CTYPE) == 'UNIQUE') {
+ $flags .= 'unique_key ';
+ }
+ }
+
+ $sql = 'SELECT R.RDB$NULL_FLAG AS NFLAG,'
+ .' R.RDB$DEFAULT_SOURCE AS DSOURCE,'
+ .' F.RDB$FIELD_TYPE AS FTYPE,'
+ .' F.RDB$COMPUTED_SOURCE AS CSOURCE'
+ .' FROM RDB$RELATION_FIELDS R '
+ .' JOIN RDB$FIELDS F ON R.RDB$FIELD_SOURCE=F.RDB$FIELD_NAME'
+ .' WHERE UPPER(R.RDB$RELATION_NAME)=\'' . strtoupper($table_name) . '\''
+ .' AND R.RDB$FIELD_NAME=\'' . $field_name . '\'';
+
+ $result = @ibase_query($this->connection, $sql);
+ if (!$result) {
+ return $this->ibaseRaiseError();
+ }
+ if ($obj = @ibase_fetch_object($result)) {
+ @ibase_free_result($result);
+ if (isset($obj->NFLAG)) {
+ $flags .= 'not_null ';
+ }
+ if (isset($obj->DSOURCE)) {
+ $flags .= 'default ';
+ }
+ if (isset($obj->CSOURCE)) {
+ $flags .= 'computed ';
+ }
+ if (isset($obj->FTYPE) && $obj->FTYPE == 261) {
+ $flags .= 'blob ';
+ }
+ }
+
+ return trim($flags);
+ }
+
+ // }}}
+ // {{{ ibaseRaiseError()
+
+ /**
+ * Produces a DB_Error object regarding the current problem
+ *
+ * @param int $errno if the error is being manually raised pass a
+ * DB_ERROR* constant here. If this isn't passed
+ * the error information gathered from the DBMS.
+ *
+ * @return object the DB_Error object
+ *
+ * @see DB_common::raiseError(),
+ * DB_ibase::errorNative(), DB_ibase::errorCode()
+ */
+ function &ibaseRaiseError($errno = null)
+ {
+ if ($errno === null) {
+ $errno = $this->errorCode($this->errorNative());
+ }
+ $tmp = $this->raiseError($errno, null, null, null, @ibase_errmsg());
+ return $tmp;
+ }
+
+ // }}}
+ // {{{ errorNative()
+
+ /**
+ * Gets the DBMS' native error code produced by the last query
+ *
+ * @return int the DBMS' error code. NULL if there is no error code.
+ *
+ * @since Method available since Release 1.7.0
+ */
+ function errorNative()
+ {
+ if (function_exists('ibase_errcode')) {
+ return @ibase_errcode();
+ }
+ if (preg_match('/^Dynamic SQL Error SQL error code = ([0-9-]+)/i',
+ @ibase_errmsg(), $m)) {
+ return (int)$m[1];
+ }
+ return null;
+ }
+
+ // }}}
+ // {{{ errorCode()
+
+ /**
+ * Maps native error codes to DB's portable ones
+ *
+ * @param int $nativecode the error code returned by the DBMS
+ *
+ * @return int the portable DB error code. Return DB_ERROR if the
+ * current driver doesn't have a mapping for the
+ * $nativecode submitted.
+ *
+ * @since Method available since Release 1.7.0
+ */
+ function errorCode($nativecode = null)
+ {
+ if (isset($this->errorcode_map[$nativecode])) {
+ return $this->errorcode_map[$nativecode];
+ }
+
+ static $error_regexps;
+ if (!isset($error_regexps)) {
+ $error_regexps = array(
+ '/generator .* is not defined/'
+ => DB_ERROR_SYNTAX, // for compat. w ibase_errcode()
+ '/table.*(not exist|not found|unknown)/i'
+ => DB_ERROR_NOSUCHTABLE,
+ '/table .* already exists/i'
+ => DB_ERROR_ALREADY_EXISTS,
+ '/unsuccessful metadata update .* failed attempt to store duplicate value/i'
+ => DB_ERROR_ALREADY_EXISTS,
+ '/unsuccessful metadata update .* not found/i'
+ => DB_ERROR_NOT_FOUND,
+ '/validation error for column .* value "\*\*\* null/i'
+ => DB_ERROR_CONSTRAINT_NOT_NULL,
+ '/violation of [\w ]+ constraint/i'
+ => DB_ERROR_CONSTRAINT,
+ '/conversion error from string/i'
+ => DB_ERROR_INVALID_NUMBER,
+ '/no permission for/i'
+ => DB_ERROR_ACCESS_VIOLATION,
+ '/arithmetic exception, numeric overflow, or string truncation/i'
+ => DB_ERROR_INVALID,
+ '/feature is not supported/i'
+ => DB_ERROR_NOT_CAPABLE,
+ );
+ }
+
+ $errormsg = @ibase_errmsg();
+ foreach ($error_regexps as $regexp => $code) {
+ if (preg_match($regexp, $errormsg)) {
+ return $code;
+ }
+ }
+ return DB_ERROR;
+ }
+
+ // }}}
+ // {{{ tableInfo()
+
+ /**
+ * Returns information about a table or a result set
+ *
+ * NOTE: only supports 'table' and 'flags' if <var>$result</var>
+ * is a table name.
+ *
+ * @param object|string $result DB_result object from a query or a
+ * string containing the name of a table.
+ * While this also accepts a query result
+ * resource identifier, this behavior is
+ * deprecated.
+ * @param int $mode a valid tableInfo mode
+ *
+ * @return array an associative array with the information requested.
+ * A DB_Error object on failure.
+ *
+ * @see DB_common::tableInfo()
+ */
+ function tableInfo($result, $mode = null)
+ {
+ if (is_string($result)) {
+ /*
+ * Probably received a table name.
+ * Create a result resource identifier.
+ */
+ $id = @ibase_query($this->connection,
+ "SELECT * FROM $result WHERE 1=0");
+ $got_string = true;
+ } elseif (isset($result->result)) {
+ /*
+ * Probably received a result object.
+ * Extract the result resource identifier.
+ */
+ $id = $result->result;
+ $got_string = false;
+ } else {
+ /*
+ * Probably received a result resource identifier.
+ * Copy it.
+ * Deprecated. Here for compatibility only.
+ */
+ $id = $result;
+ $got_string = false;
+ }
+
+ if (!is_resource($id)) {
+ return $this->ibaseRaiseError(DB_ERROR_NEED_MORE_DATA);
+ }
+
+ if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE) {
+ $case_func = 'strtolower';
+ } else {
+ $case_func = 'strval';
+ }
+
+ $count = @ibase_num_fields($id);
+ $res = array();
+
+ if ($mode) {
+ $res['num_fields'] = $count;
+ }
+
+ for ($i = 0; $i < $count; $i++) {
+ $info = @ibase_field_info($id, $i);
+ $res[$i] = array(
+ 'table' => $got_string ? $case_func($result) : '',
+ 'name' => $case_func($info['name']),
+ 'type' => $info['type'],
+ 'len' => $info['length'],
+ 'flags' => ($got_string)
+ ? $this->_ibaseFieldFlags($info['name'], $result)
+ : '',
+ );
+ if ($mode & DB_TABLEINFO_ORDER) {
+ $res['order'][$res[$i]['name']] = $i;
+ }
+ if ($mode & DB_TABLEINFO_ORDERTABLE) {
+ $res['ordertable'][$res[$i]['table']][$res[$i]['name']] = $i;
+ }
+ }
+
+ // free the result only if we were called on a table
+ if ($got_string) {
+ @ibase_free_result($id);
+ }
+ return $res;
+ }
+
+ // }}}
+ // {{{ getSpecialQuery()
+
+ /**
+ * Obtains the query string needed for listing a given type of objects
+ *
+ * @param string $type the kind of objects you want to retrieve
+ *
+ * @return string the SQL query string or null if the driver doesn't
+ * support the object type requested
+ *
+ * @access protected
+ * @see DB_common::getListOf()
+ */
+ function getSpecialQuery($type)
+ {
+ switch ($type) {
+ case 'tables':
+ return 'SELECT DISTINCT R.RDB$RELATION_NAME FROM '
+ . 'RDB$RELATION_FIELDS R WHERE R.RDB$SYSTEM_FLAG=0';
+ case 'views':
+ return 'SELECT DISTINCT RDB$VIEW_NAME from RDB$VIEW_RELATIONS';
+ case 'users':
+ return 'SELECT DISTINCT RDB$USER FROM RDB$USER_PRIVILEGES';
+ default:
+ return null;
+ }
+ }
+
+ // }}}
+
+}
+
+/*
+ * Local variables:
+ * tab-width: 4
+ * c-basic-offset: 4
+ * End:
+ */
+
+?>
diff --git a/extlib/DB/ifx.php b/extlib/DB/ifx.php
new file mode 100644
index 000000000..baa6f2867
--- /dev/null
+++ b/extlib/DB/ifx.php
@@ -0,0 +1,683 @@
+<?php
+
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * The PEAR DB driver for PHP's ifx extension
+ * for interacting with Informix databases
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt. If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category Database
+ * @package DB
+ * @author Tomas V.V.Cox <cox@idecnet.com>
+ * @author Daniel Convissor <danielc@php.net>
+ * @copyright 1997-2007 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0
+ * @version CVS: $Id: ifx.php,v 1.75 2007/07/06 05:19:21 aharvey Exp $
+ * @link http://pear.php.net/package/DB
+ */
+
+/**
+ * Obtain the DB_common class so it can be extended from
+ */
+require_once 'DB/common.php';
+
+/**
+ * The methods PEAR DB uses to interact with PHP's ifx extension
+ * for interacting with Informix databases
+ *
+ * These methods overload the ones declared in DB_common.
+ *
+ * More info on Informix errors can be found at:
+ * http://www.informix.com/answers/english/ierrors.htm
+ *
+ * TODO:
+ * - set needed env Informix vars on connect
+ * - implement native prepare/execute
+ *
+ * @category Database
+ * @package DB
+ * @author Tomas V.V.Cox <cox@idecnet.com>
+ * @author Daniel Convissor <danielc@php.net>
+ * @copyright 1997-2007 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0
+ * @version Release: 1.7.14RC1
+ * @link http://pear.php.net/package/DB
+ */
+class DB_ifx extends DB_common
+{
+ // {{{ properties
+
+ /**
+ * The DB driver type (mysql, oci8, odbc, etc.)
+ * @var string
+ */
+ var $phptype = 'ifx';
+
+ /**
+ * The database syntax variant to be used (db2, access, etc.), if any
+ * @var string
+ */
+ var $dbsyntax = 'ifx';
+
+ /**
+ * The capabilities of this DB implementation
+ *
+ * The 'new_link' element contains the PHP version that first provided
+ * new_link support for this DBMS. Contains false if it's unsupported.
+ *
+ * Meaning of the 'limit' element:
+ * + 'emulate' = emulate with fetch row by number
+ * + 'alter' = alter the query
+ * + false = skip rows
+ *
+ * @var array
+ */
+ var $features = array(
+ 'limit' => 'emulate',
+ 'new_link' => false,
+ 'numrows' => 'emulate',
+ 'pconnect' => true,
+ 'prepare' => false,
+ 'ssl' => false,
+ 'transactions' => true,
+ );
+
+ /**
+ * A mapping of native error codes to DB error codes
+ * @var array
+ */
+ var $errorcode_map = array(
+ '-201' => DB_ERROR_SYNTAX,
+ '-206' => DB_ERROR_NOSUCHTABLE,
+ '-217' => DB_ERROR_NOSUCHFIELD,
+ '-236' => DB_ERROR_VALUE_COUNT_ON_ROW,
+ '-239' => DB_ERROR_CONSTRAINT,
+ '-253' => DB_ERROR_SYNTAX,
+ '-268' => DB_ERROR_CONSTRAINT,
+ '-292' => DB_ERROR_CONSTRAINT_NOT_NULL,
+ '-310' => DB_ERROR_ALREADY_EXISTS,
+ '-316' => DB_ERROR_ALREADY_EXISTS,
+ '-319' => DB_ERROR_NOT_FOUND,
+ '-329' => DB_ERROR_NODBSELECTED,
+ '-346' => DB_ERROR_CONSTRAINT,
+ '-386' => DB_ERROR_CONSTRAINT_NOT_NULL,
+ '-391' => DB_ERROR_CONSTRAINT_NOT_NULL,
+ '-554' => DB_ERROR_SYNTAX,
+ '-691' => DB_ERROR_CONSTRAINT,
+ '-692' => DB_ERROR_CONSTRAINT,
+ '-703' => DB_ERROR_CONSTRAINT_NOT_NULL,
+ '-1202' => DB_ERROR_DIVZERO,
+ '-1204' => DB_ERROR_INVALID_DATE,
+ '-1205' => DB_ERROR_INVALID_DATE,
+ '-1206' => DB_ERROR_INVALID_DATE,
+ '-1209' => DB_ERROR_INVALID_DATE,
+ '-1210' => DB_ERROR_INVALID_DATE,
+ '-1212' => DB_ERROR_INVALID_DATE,
+ '-1213' => DB_ERROR_INVALID_NUMBER,
+ );
+
+ /**
+ * The raw database connection created by PHP
+ * @var resource
+ */
+ var $connection;
+
+ /**
+ * The DSN information for connecting to a database
+ * @var array
+ */
+ var $dsn = array();
+
+
+ /**
+ * Should data manipulation queries be committed automatically?
+ * @var bool
+ * @access private
+ */
+ var $autocommit = true;
+
+ /**
+ * The quantity of transactions begun
+ *
+ * {@internal While this is private, it can't actually be designated
+ * private in PHP 5 because it is directly accessed in the test suite.}}
+ *
+ * @var integer
+ * @access private
+ */
+ var $transaction_opcount = 0;
+
+ /**
+ * The number of rows affected by a data manipulation query
+ * @var integer
+ * @access private
+ */
+ var $affected = 0;
+
+
+ // }}}
+ // {{{ constructor
+
+ /**
+ * This constructor calls <kbd>$this->DB_common()</kbd>
+ *
+ * @return void
+ */
+ function DB_ifx()
+ {
+ $this->DB_common();
+ }
+
+ // }}}
+ // {{{ connect()
+
+ /**
+ * Connect to the database server, log in and open the database
+ *
+ * Don't call this method directly. Use DB::connect() instead.
+ *
+ * @param array $dsn the data source name
+ * @param bool $persistent should the connection be persistent?
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ */
+ function connect($dsn, $persistent = false)
+ {
+ if (!PEAR::loadExtension('informix') &&
+ !PEAR::loadExtension('Informix'))
+ {
+ return $this->raiseError(DB_ERROR_EXTENSION_NOT_FOUND);
+ }
+
+ $this->dsn = $dsn;
+ if ($dsn['dbsyntax']) {
+ $this->dbsyntax = $dsn['dbsyntax'];
+ }
+
+ $dbhost = $dsn['hostspec'] ? '@' . $dsn['hostspec'] : '';
+ $dbname = $dsn['database'] ? $dsn['database'] . $dbhost : '';
+ $user = $dsn['username'] ? $dsn['username'] : '';
+ $pw = $dsn['password'] ? $dsn['password'] : '';
+
+ $connect_function = $persistent ? 'ifx_pconnect' : 'ifx_connect';
+
+ $this->connection = @$connect_function($dbname, $user, $pw);
+ if (!is_resource($this->connection)) {
+ return $this->ifxRaiseError(DB_ERROR_CONNECT_FAILED);
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ disconnect()
+
+ /**
+ * Disconnects from the database server
+ *
+ * @return bool TRUE on success, FALSE on failure
+ */
+ function disconnect()
+ {
+ $ret = @ifx_close($this->connection);
+ $this->connection = null;
+ return $ret;
+ }
+
+ // }}}
+ // {{{ simpleQuery()
+
+ /**
+ * Sends a query to the database server
+ *
+ * @param string the SQL query string
+ *
+ * @return mixed + a PHP result resrouce for successful SELECT queries
+ * + the DB_OK constant for other successful queries
+ * + a DB_Error object on failure
+ */
+ function simpleQuery($query)
+ {
+ $ismanip = $this->_checkManip($query);
+ $this->last_query = $query;
+ $this->affected = null;
+ if (preg_match('/(SELECT|EXECUTE)/i', $query)) { //TESTME: Use !DB::isManip()?
+ // the scroll is needed for fetching absolute row numbers
+ // in a select query result
+ $result = @ifx_query($query, $this->connection, IFX_SCROLL);
+ } else {
+ if (!$this->autocommit && $ismanip) {
+ if ($this->transaction_opcount == 0) {
+ $result = @ifx_query('BEGIN WORK', $this->connection);
+ if (!$result) {
+ return $this->ifxRaiseError();
+ }
+ }
+ $this->transaction_opcount++;
+ }
+ $result = @ifx_query($query, $this->connection);
+ }
+ if (!$result) {
+ return $this->ifxRaiseError();
+ }
+ $this->affected = @ifx_affected_rows($result);
+ // Determine which queries should return data, and which
+ // should return an error code only.
+ if (preg_match('/(SELECT|EXECUTE)/i', $query)) {
+ return $result;
+ }
+ // XXX Testme: free results inside a transaction
+ // may cause to stop it and commit the work?
+
+ // Result has to be freed even with a insert or update
+ @ifx_free_result($result);
+
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ nextResult()
+
+ /**
+ * Move the internal ifx result pointer to the next available result
+ *
+ * @param a valid fbsql result resource
+ *
+ * @access public
+ *
+ * @return true if a result is available otherwise return false
+ */
+ function nextResult($result)
+ {
+ return false;
+ }
+
+ // }}}
+ // {{{ affectedRows()
+
+ /**
+ * Determines the number of rows affected by a data maniuplation query
+ *
+ * 0 is returned for queries that don't manipulate data.
+ *
+ * @return int the number of rows. A DB_Error object on failure.
+ */
+ function affectedRows()
+ {
+ if ($this->_last_query_manip) {
+ return $this->affected;
+ } else {
+ return 0;
+ }
+ }
+
+ // }}}
+ // {{{ fetchInto()
+
+ /**
+ * Places a row from the result set into the given array
+ *
+ * Formating of the array and the data therein are configurable.
+ * See DB_result::fetchInto() for more information.
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::fetchInto() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result the query result resource
+ * @param array $arr the referenced array to put the data in
+ * @param int $fetchmode how the resulting array should be indexed
+ * @param int $rownum the row number to fetch (0 = first row)
+ *
+ * @return mixed DB_OK on success, NULL when the end of a result set is
+ * reached or on failure
+ *
+ * @see DB_result::fetchInto()
+ */
+ function fetchInto($result, &$arr, $fetchmode, $rownum = null)
+ {
+ if (($rownum !== null) && ($rownum < 0)) {
+ return null;
+ }
+ if ($rownum === null) {
+ /*
+ * Even though fetch_row() should return the next row if
+ * $rownum is null, it doesn't in all cases. Bug 598.
+ */
+ $rownum = 'NEXT';
+ } else {
+ // Index starts at row 1, unlike most DBMS's starting at 0.
+ $rownum++;
+ }
+ if (!$arr = @ifx_fetch_row($result, $rownum)) {
+ return null;
+ }
+ if ($fetchmode !== DB_FETCHMODE_ASSOC) {
+ $i=0;
+ $order = array();
+ foreach ($arr as $val) {
+ $order[$i++] = $val;
+ }
+ $arr = $order;
+ } elseif ($fetchmode == DB_FETCHMODE_ASSOC &&
+ $this->options['portability'] & DB_PORTABILITY_LOWERCASE)
+ {
+ $arr = array_change_key_case($arr, CASE_LOWER);
+ }
+ if ($this->options['portability'] & DB_PORTABILITY_RTRIM) {
+ $this->_rtrimArrayValues($arr);
+ }
+ if ($this->options['portability'] & DB_PORTABILITY_NULL_TO_EMPTY) {
+ $this->_convertNullArrayValuesToEmpty($arr);
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ numCols()
+
+ /**
+ * Gets the number of columns in a result set
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::numCols() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result PHP's query result resource
+ *
+ * @return int the number of columns. A DB_Error object on failure.
+ *
+ * @see DB_result::numCols()
+ */
+ function numCols($result)
+ {
+ if (!$cols = @ifx_num_fields($result)) {
+ return $this->ifxRaiseError();
+ }
+ return $cols;
+ }
+
+ // }}}
+ // {{{ freeResult()
+
+ /**
+ * Deletes the result set and frees the memory occupied by the result set
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::free() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result PHP's query result resource
+ *
+ * @return bool TRUE on success, FALSE if $result is invalid
+ *
+ * @see DB_result::free()
+ */
+ function freeResult($result)
+ {
+ return is_resource($result) ? ifx_free_result($result) : false;
+ }
+
+ // }}}
+ // {{{ autoCommit()
+
+ /**
+ * Enables or disables automatic commits
+ *
+ * @param bool $onoff true turns it on, false turns it off
+ *
+ * @return int DB_OK on success. A DB_Error object if the driver
+ * doesn't support auto-committing transactions.
+ */
+ function autoCommit($onoff = true)
+ {
+ // XXX if $this->transaction_opcount > 0, we should probably
+ // issue a warning here.
+ $this->autocommit = $onoff ? true : false;
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ commit()
+
+ /**
+ * Commits the current transaction
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ */
+ function commit()
+ {
+ if ($this->transaction_opcount > 0) {
+ $result = @ifx_query('COMMIT WORK', $this->connection);
+ $this->transaction_opcount = 0;
+ if (!$result) {
+ return $this->ifxRaiseError();
+ }
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ rollback()
+
+ /**
+ * Reverts the current transaction
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ */
+ function rollback()
+ {
+ if ($this->transaction_opcount > 0) {
+ $result = @ifx_query('ROLLBACK WORK', $this->connection);
+ $this->transaction_opcount = 0;
+ if (!$result) {
+ return $this->ifxRaiseError();
+ }
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ ifxRaiseError()
+
+ /**
+ * Produces a DB_Error object regarding the current problem
+ *
+ * @param int $errno if the error is being manually raised pass a
+ * DB_ERROR* constant here. If this isn't passed
+ * the error information gathered from the DBMS.
+ *
+ * @return object the DB_Error object
+ *
+ * @see DB_common::raiseError(),
+ * DB_ifx::errorNative(), DB_ifx::errorCode()
+ */
+ function ifxRaiseError($errno = null)
+ {
+ if ($errno === null) {
+ $errno = $this->errorCode(ifx_error());
+ }
+ return $this->raiseError($errno, null, null, null,
+ $this->errorNative());
+ }
+
+ // }}}
+ // {{{ errorNative()
+
+ /**
+ * Gets the DBMS' native error code and message produced by the last query
+ *
+ * @return string the DBMS' error code and message
+ */
+ function errorNative()
+ {
+ return @ifx_error() . ' ' . @ifx_errormsg();
+ }
+
+ // }}}
+ // {{{ errorCode()
+
+ /**
+ * Maps native error codes to DB's portable ones.
+ *
+ * Requires that the DB implementation's constructor fills
+ * in the <var>$errorcode_map</var> property.
+ *
+ * @param string $nativecode error code returned by the database
+ * @return int a portable DB error code, or DB_ERROR if this DB
+ * implementation has no mapping for the given error code.
+ */
+ function errorCode($nativecode)
+ {
+ if (ereg('SQLCODE=(.*)]', $nativecode, $match)) {
+ $code = $match[1];
+ if (isset($this->errorcode_map[$code])) {
+ return $this->errorcode_map[$code];
+ }
+ }
+ return DB_ERROR;
+ }
+
+ // }}}
+ // {{{ tableInfo()
+
+ /**
+ * Returns information about a table or a result set
+ *
+ * NOTE: only supports 'table' if <var>$result</var> is a table name.
+ *
+ * If analyzing a query result and the result has duplicate field names,
+ * an error will be raised saying
+ * <samp>can't distinguish duplicate field names</samp>.
+ *
+ * @param object|string $result DB_result object from a query or a
+ * string containing the name of a table.
+ * While this also accepts a query result
+ * resource identifier, this behavior is
+ * deprecated.
+ * @param int $mode a valid tableInfo mode
+ *
+ * @return array an associative array with the information requested.
+ * A DB_Error object on failure.
+ *
+ * @see DB_common::tableInfo()
+ * @since Method available since Release 1.6.0
+ */
+ function tableInfo($result, $mode = null)
+ {
+ if (is_string($result)) {
+ /*
+ * Probably received a table name.
+ * Create a result resource identifier.
+ */
+ $id = @ifx_query("SELECT * FROM $result WHERE 1=0",
+ $this->connection);
+ $got_string = true;
+ } elseif (isset($result->result)) {
+ /*
+ * Probably received a result object.
+ * Extract the result resource identifier.
+ */
+ $id = $result->result;
+ $got_string = false;
+ } else {
+ /*
+ * Probably received a result resource identifier.
+ * Copy it.
+ */
+ $id = $result;
+ $got_string = false;
+ }
+
+ if (!is_resource($id)) {
+ return $this->ifxRaiseError(DB_ERROR_NEED_MORE_DATA);
+ }
+
+ $flds = @ifx_fieldproperties($id);
+ $count = @ifx_num_fields($id);
+
+ if (count($flds) != $count) {
+ return $this->raiseError("can't distinguish duplicate field names");
+ }
+
+ if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE) {
+ $case_func = 'strtolower';
+ } else {
+ $case_func = 'strval';
+ }
+
+ $i = 0;
+ $res = array();
+
+ if ($mode) {
+ $res['num_fields'] = $count;
+ }
+
+ foreach ($flds as $key => $value) {
+ $props = explode(';', $value);
+ $res[$i] = array(
+ 'table' => $got_string ? $case_func($result) : '',
+ 'name' => $case_func($key),
+ 'type' => $props[0],
+ 'len' => $props[1],
+ 'flags' => $props[4] == 'N' ? 'not_null' : '',
+ );
+ if ($mode & DB_TABLEINFO_ORDER) {
+ $res['order'][$res[$i]['name']] = $i;
+ }
+ if ($mode & DB_TABLEINFO_ORDERTABLE) {
+ $res['ordertable'][$res[$i]['table']][$res[$i]['name']] = $i;
+ }
+ $i++;
+ }
+
+ // free the result only if we were called on a table
+ if ($got_string) {
+ @ifx_free_result($id);
+ }
+ return $res;
+ }
+
+ // }}}
+ // {{{ getSpecialQuery()
+
+ /**
+ * Obtains the query string needed for listing a given type of objects
+ *
+ * @param string $type the kind of objects you want to retrieve
+ *
+ * @return string the SQL query string or null if the driver doesn't
+ * support the object type requested
+ *
+ * @access protected
+ * @see DB_common::getListOf()
+ */
+ function getSpecialQuery($type)
+ {
+ switch ($type) {
+ case 'tables':
+ return 'SELECT tabname FROM systables WHERE tabid >= 100';
+ default:
+ return null;
+ }
+ }
+
+ // }}}
+
+}
+
+/*
+ * Local variables:
+ * tab-width: 4
+ * c-basic-offset: 4
+ * End:
+ */
+
+?>
diff --git a/extlib/DB/msql.php b/extlib/DB/msql.php
new file mode 100644
index 000000000..34854f472
--- /dev/null
+++ b/extlib/DB/msql.php
@@ -0,0 +1,831 @@
+<?php
+
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * The PEAR DB driver for PHP's msql extension
+ * for interacting with Mini SQL databases
+ *
+ * PHP's mSQL extension did weird things with NULL values prior to PHP
+ * 4.3.11 and 5.0.4. Make sure your version of PHP meets or exceeds
+ * those versions.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt. If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category Database
+ * @package DB
+ * @author Daniel Convissor <danielc@php.net>
+ * @copyright 1997-2007 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0
+ * @version CVS: $Id: msql.php,v 1.64 2007/09/21 13:40:41 aharvey Exp $
+ * @link http://pear.php.net/package/DB
+ */
+
+/**
+ * Obtain the DB_common class so it can be extended from
+ */
+require_once 'DB/common.php';
+
+/**
+ * The methods PEAR DB uses to interact with PHP's msql extension
+ * for interacting with Mini SQL databases
+ *
+ * These methods overload the ones declared in DB_common.
+ *
+ * PHP's mSQL extension did weird things with NULL values prior to PHP
+ * 4.3.11 and 5.0.4. Make sure your version of PHP meets or exceeds
+ * those versions.
+ *
+ * @category Database
+ * @package DB
+ * @author Daniel Convissor <danielc@php.net>
+ * @copyright 1997-2007 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0
+ * @version Release: 1.7.14RC1
+ * @link http://pear.php.net/package/DB
+ * @since Class not functional until Release 1.7.0
+ */
+class DB_msql extends DB_common
+{
+ // {{{ properties
+
+ /**
+ * The DB driver type (mysql, oci8, odbc, etc.)
+ * @var string
+ */
+ var $phptype = 'msql';
+
+ /**
+ * The database syntax variant to be used (db2, access, etc.), if any
+ * @var string
+ */
+ var $dbsyntax = 'msql';
+
+ /**
+ * The capabilities of this DB implementation
+ *
+ * The 'new_link' element contains the PHP version that first provided
+ * new_link support for this DBMS. Contains false if it's unsupported.
+ *
+ * Meaning of the 'limit' element:
+ * + 'emulate' = emulate with fetch row by number
+ * + 'alter' = alter the query
+ * + false = skip rows
+ *
+ * @var array
+ */
+ var $features = array(
+ 'limit' => 'emulate',
+ 'new_link' => false,
+ 'numrows' => true,
+ 'pconnect' => true,
+ 'prepare' => false,
+ 'ssl' => false,
+ 'transactions' => false,
+ );
+
+ /**
+ * A mapping of native error codes to DB error codes
+ * @var array
+ */
+ var $errorcode_map = array(
+ );
+
+ /**
+ * The raw database connection created by PHP
+ * @var resource
+ */
+ var $connection;
+
+ /**
+ * The DSN information for connecting to a database
+ * @var array
+ */
+ var $dsn = array();
+
+
+ /**
+ * The query result resource created by PHP
+ *
+ * Used to make affectedRows() work. Only contains the result for
+ * data manipulation queries. Contains false for other queries.
+ *
+ * @var resource
+ * @access private
+ */
+ var $_result;
+
+
+ // }}}
+ // {{{ constructor
+
+ /**
+ * This constructor calls <kbd>$this->DB_common()</kbd>
+ *
+ * @return void
+ */
+ function DB_msql()
+ {
+ $this->DB_common();
+ }
+
+ // }}}
+ // {{{ connect()
+
+ /**
+ * Connect to the database server, log in and open the database
+ *
+ * Don't call this method directly. Use DB::connect() instead.
+ *
+ * Example of how to connect:
+ * <code>
+ * require_once 'DB.php';
+ *
+ * // $dsn = 'msql://hostname/dbname'; // use a TCP connection
+ * $dsn = 'msql:///dbname'; // use a socket
+ * $options = array(
+ * 'portability' => DB_PORTABILITY_ALL,
+ * );
+ *
+ * $db = DB::connect($dsn, $options);
+ * if (PEAR::isError($db)) {
+ * die($db->getMessage());
+ * }
+ * </code>
+ *
+ * @param array $dsn the data source name
+ * @param bool $persistent should the connection be persistent?
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ */
+ function connect($dsn, $persistent = false)
+ {
+ if (!PEAR::loadExtension('msql')) {
+ return $this->raiseError(DB_ERROR_EXTENSION_NOT_FOUND);
+ }
+
+ $this->dsn = $dsn;
+ if ($dsn['dbsyntax']) {
+ $this->dbsyntax = $dsn['dbsyntax'];
+ }
+
+ $params = array();
+ if ($dsn['hostspec']) {
+ $params[] = $dsn['port']
+ ? $dsn['hostspec'] . ',' . $dsn['port']
+ : $dsn['hostspec'];
+ }
+
+ $connect_function = $persistent ? 'msql_pconnect' : 'msql_connect';
+
+ $ini = ini_get('track_errors');
+ $php_errormsg = '';
+ if ($ini) {
+ $this->connection = @call_user_func_array($connect_function,
+ $params);
+ } else {
+ @ini_set('track_errors', 1);
+ $this->connection = @call_user_func_array($connect_function,
+ $params);
+ @ini_set('track_errors', $ini);
+ }
+
+ if (!$this->connection) {
+ if (($err = @msql_error()) != '') {
+ return $this->raiseError(DB_ERROR_CONNECT_FAILED,
+ null, null, null,
+ $err);
+ } else {
+ return $this->raiseError(DB_ERROR_CONNECT_FAILED,
+ null, null, null,
+ $php_errormsg);
+ }
+ }
+
+ if (!@msql_select_db($dsn['database'], $this->connection)) {
+ return $this->msqlRaiseError();
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ disconnect()
+
+ /**
+ * Disconnects from the database server
+ *
+ * @return bool TRUE on success, FALSE on failure
+ */
+ function disconnect()
+ {
+ $ret = @msql_close($this->connection);
+ $this->connection = null;
+ return $ret;
+ }
+
+ // }}}
+ // {{{ simpleQuery()
+
+ /**
+ * Sends a query to the database server
+ *
+ * @param string the SQL query string
+ *
+ * @return mixed + a PHP result resrouce for successful SELECT queries
+ * + the DB_OK constant for other successful queries
+ * + a DB_Error object on failure
+ */
+ function simpleQuery($query)
+ {
+ $this->last_query = $query;
+ $query = $this->modifyQuery($query);
+ $result = @msql_query($query, $this->connection);
+ if (!$result) {
+ return $this->msqlRaiseError();
+ }
+ // Determine which queries that should return data, and which
+ // should return an error code only.
+ if ($this->_checkManip($query)) {
+ $this->_result = $result;
+ return DB_OK;
+ } else {
+ $this->_result = false;
+ return $result;
+ }
+ }
+
+
+ // }}}
+ // {{{ nextResult()
+
+ /**
+ * Move the internal msql result pointer to the next available result
+ *
+ * @param a valid fbsql result resource
+ *
+ * @access public
+ *
+ * @return true if a result is available otherwise return false
+ */
+ function nextResult($result)
+ {
+ return false;
+ }
+
+ // }}}
+ // {{{ fetchInto()
+
+ /**
+ * Places a row from the result set into the given array
+ *
+ * Formating of the array and the data therein are configurable.
+ * See DB_result::fetchInto() for more information.
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::fetchInto() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * PHP's mSQL extension did weird things with NULL values prior to PHP
+ * 4.3.11 and 5.0.4. Make sure your version of PHP meets or exceeds
+ * those versions.
+ *
+ * @param resource $result the query result resource
+ * @param array $arr the referenced array to put the data in
+ * @param int $fetchmode how the resulting array should be indexed
+ * @param int $rownum the row number to fetch (0 = first row)
+ *
+ * @return mixed DB_OK on success, NULL when the end of a result set is
+ * reached or on failure
+ *
+ * @see DB_result::fetchInto()
+ */
+ function fetchInto($result, &$arr, $fetchmode, $rownum = null)
+ {
+ if ($rownum !== null) {
+ if (!@msql_data_seek($result, $rownum)) {
+ return null;
+ }
+ }
+ if ($fetchmode & DB_FETCHMODE_ASSOC) {
+ $arr = @msql_fetch_array($result, MSQL_ASSOC);
+ if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE && $arr) {
+ $arr = array_change_key_case($arr, CASE_LOWER);
+ }
+ } else {
+ $arr = @msql_fetch_row($result);
+ }
+ if (!$arr) {
+ return null;
+ }
+ if ($this->options['portability'] & DB_PORTABILITY_RTRIM) {
+ $this->_rtrimArrayValues($arr);
+ }
+ if ($this->options['portability'] & DB_PORTABILITY_NULL_TO_EMPTY) {
+ $this->_convertNullArrayValuesToEmpty($arr);
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ freeResult()
+
+ /**
+ * Deletes the result set and frees the memory occupied by the result set
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::free() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result PHP's query result resource
+ *
+ * @return bool TRUE on success, FALSE if $result is invalid
+ *
+ * @see DB_result::free()
+ */
+ function freeResult($result)
+ {
+ return is_resource($result) ? msql_free_result($result) : false;
+ }
+
+ // }}}
+ // {{{ numCols()
+
+ /**
+ * Gets the number of columns in a result set
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::numCols() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result PHP's query result resource
+ *
+ * @return int the number of columns. A DB_Error object on failure.
+ *
+ * @see DB_result::numCols()
+ */
+ function numCols($result)
+ {
+ $cols = @msql_num_fields($result);
+ if (!$cols) {
+ return $this->msqlRaiseError();
+ }
+ return $cols;
+ }
+
+ // }}}
+ // {{{ numRows()
+
+ /**
+ * Gets the number of rows in a result set
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::numRows() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result PHP's query result resource
+ *
+ * @return int the number of rows. A DB_Error object on failure.
+ *
+ * @see DB_result::numRows()
+ */
+ function numRows($result)
+ {
+ $rows = @msql_num_rows($result);
+ if ($rows === false) {
+ return $this->msqlRaiseError();
+ }
+ return $rows;
+ }
+
+ // }}}
+ // {{{ affected()
+
+ /**
+ * Determines the number of rows affected by a data maniuplation query
+ *
+ * 0 is returned for queries that don't manipulate data.
+ *
+ * @return int the number of rows. A DB_Error object on failure.
+ */
+ function affectedRows()
+ {
+ if (!$this->_result) {
+ return 0;
+ }
+ return msql_affected_rows($this->_result);
+ }
+
+ // }}}
+ // {{{ nextId()
+
+ /**
+ * Returns the next free id in a sequence
+ *
+ * @param string $seq_name name of the sequence
+ * @param boolean $ondemand when true, the seqence is automatically
+ * created if it does not exist
+ *
+ * @return int the next id number in the sequence.
+ * A DB_Error object on failure.
+ *
+ * @see DB_common::nextID(), DB_common::getSequenceName(),
+ * DB_msql::createSequence(), DB_msql::dropSequence()
+ */
+ function nextId($seq_name, $ondemand = true)
+ {
+ $seqname = $this->getSequenceName($seq_name);
+ $repeat = false;
+ do {
+ $this->pushErrorHandling(PEAR_ERROR_RETURN);
+ $result = $this->query("SELECT _seq FROM ${seqname}");
+ $this->popErrorHandling();
+ if ($ondemand && DB::isError($result) &&
+ $result->getCode() == DB_ERROR_NOSUCHTABLE) {
+ $repeat = true;
+ $this->pushErrorHandling(PEAR_ERROR_RETURN);
+ $result = $this->createSequence($seq_name);
+ $this->popErrorHandling();
+ if (DB::isError($result)) {
+ return $this->raiseError($result);
+ }
+ } else {
+ $repeat = false;
+ }
+ } while ($repeat);
+ if (DB::isError($result)) {
+ return $this->raiseError($result);
+ }
+ $arr = $result->fetchRow(DB_FETCHMODE_ORDERED);
+ $result->free();
+ return $arr[0];
+ }
+
+ // }}}
+ // {{{ createSequence()
+
+ /**
+ * Creates a new sequence
+ *
+ * Also creates a new table to associate the sequence with. Uses
+ * a separate table to ensure portability with other drivers.
+ *
+ * @param string $seq_name name of the new sequence
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ *
+ * @see DB_common::createSequence(), DB_common::getSequenceName(),
+ * DB_msql::nextID(), DB_msql::dropSequence()
+ */
+ function createSequence($seq_name)
+ {
+ $seqname = $this->getSequenceName($seq_name);
+ $res = $this->query('CREATE TABLE ' . $seqname
+ . ' (id INTEGER NOT NULL)');
+ if (DB::isError($res)) {
+ return $res;
+ }
+ $res = $this->query("CREATE SEQUENCE ON ${seqname}");
+ return $res;
+ }
+
+ // }}}
+ // {{{ dropSequence()
+
+ /**
+ * Deletes a sequence
+ *
+ * @param string $seq_name name of the sequence to be deleted
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ *
+ * @see DB_common::dropSequence(), DB_common::getSequenceName(),
+ * DB_msql::nextID(), DB_msql::createSequence()
+ */
+ function dropSequence($seq_name)
+ {
+ return $this->query('DROP TABLE ' . $this->getSequenceName($seq_name));
+ }
+
+ // }}}
+ // {{{ quoteIdentifier()
+
+ /**
+ * mSQL does not support delimited identifiers
+ *
+ * @param string $str the identifier name to be quoted
+ *
+ * @return object a DB_Error object
+ *
+ * @see DB_common::quoteIdentifier()
+ * @since Method available since Release 1.7.0
+ */
+ function quoteIdentifier($str)
+ {
+ return $this->raiseError(DB_ERROR_UNSUPPORTED);
+ }
+
+ // }}}
+ // {{{ quoteFloat()
+
+ /**
+ * Formats a float value for use within a query in a locale-independent
+ * manner.
+ *
+ * @param float the float value to be quoted.
+ * @return string the quoted string.
+ * @see DB_common::quoteSmart()
+ * @since Method available since release 1.7.8.
+ */
+ function quoteFloat($float) {
+ return $this->escapeSimple(str_replace(',', '.', strval(floatval($float))));
+ }
+
+ // }}}
+ // {{{ escapeSimple()
+
+ /**
+ * Escapes a string according to the current DBMS's standards
+ *
+ * @param string $str the string to be escaped
+ *
+ * @return string the escaped string
+ *
+ * @see DB_common::quoteSmart()
+ * @since Method available since Release 1.7.0
+ */
+ function escapeSimple($str)
+ {
+ return addslashes($str);
+ }
+
+ // }}}
+ // {{{ msqlRaiseError()
+
+ /**
+ * Produces a DB_Error object regarding the current problem
+ *
+ * @param int $errno if the error is being manually raised pass a
+ * DB_ERROR* constant here. If this isn't passed
+ * the error information gathered from the DBMS.
+ *
+ * @return object the DB_Error object
+ *
+ * @see DB_common::raiseError(),
+ * DB_msql::errorNative(), DB_msql::errorCode()
+ */
+ function msqlRaiseError($errno = null)
+ {
+ $native = $this->errorNative();
+ if ($errno === null) {
+ $errno = $this->errorCode($native);
+ }
+ return $this->raiseError($errno, null, null, null, $native);
+ }
+
+ // }}}
+ // {{{ errorNative()
+
+ /**
+ * Gets the DBMS' native error message produced by the last query
+ *
+ * @return string the DBMS' error message
+ */
+ function errorNative()
+ {
+ return @msql_error();
+ }
+
+ // }}}
+ // {{{ errorCode()
+
+ /**
+ * Determines PEAR::DB error code from the database's text error message
+ *
+ * @param string $errormsg the error message returned from the database
+ *
+ * @return integer the error number from a DB_ERROR* constant
+ */
+ function errorCode($errormsg)
+ {
+ static $error_regexps;
+
+ // PHP 5.2+ prepends the function name to $php_errormsg, so we need
+ // this hack to work around it, per bug #9599.
+ $errormsg = preg_replace('/^msql[a-z_]+\(\): /', '', $errormsg);
+
+ if (!isset($error_regexps)) {
+ $error_regexps = array(
+ '/^Access to database denied/i'
+ => DB_ERROR_ACCESS_VIOLATION,
+ '/^Bad index name/i'
+ => DB_ERROR_ALREADY_EXISTS,
+ '/^Bad order field/i'
+ => DB_ERROR_SYNTAX,
+ '/^Bad type for comparison/i'
+ => DB_ERROR_SYNTAX,
+ '/^Can\'t perform LIKE on/i'
+ => DB_ERROR_SYNTAX,
+ '/^Can\'t use TEXT fields in LIKE comparison/i'
+ => DB_ERROR_SYNTAX,
+ '/^Couldn\'t create temporary table/i'
+ => DB_ERROR_CANNOT_CREATE,
+ '/^Error creating table file/i'
+ => DB_ERROR_CANNOT_CREATE,
+ '/^Field .* cannot be null$/i'
+ => DB_ERROR_CONSTRAINT_NOT_NULL,
+ '/^Index (field|condition) .* cannot be null$/i'
+ => DB_ERROR_SYNTAX,
+ '/^Invalid date format/i'
+ => DB_ERROR_INVALID_DATE,
+ '/^Invalid time format/i'
+ => DB_ERROR_INVALID,
+ '/^Literal value for .* is wrong type$/i'
+ => DB_ERROR_INVALID_NUMBER,
+ '/^No Database Selected/i'
+ => DB_ERROR_NODBSELECTED,
+ '/^No value specified for field/i'
+ => DB_ERROR_VALUE_COUNT_ON_ROW,
+ '/^Non unique value for unique index/i'
+ => DB_ERROR_CONSTRAINT,
+ '/^Out of memory for temporary table/i'
+ => DB_ERROR_CANNOT_CREATE,
+ '/^Permission denied/i'
+ => DB_ERROR_ACCESS_VIOLATION,
+ '/^Reference to un-selected table/i'
+ => DB_ERROR_SYNTAX,
+ '/^syntax error/i'
+ => DB_ERROR_SYNTAX,
+ '/^Table .* exists$/i'
+ => DB_ERROR_ALREADY_EXISTS,
+ '/^Unknown database/i'
+ => DB_ERROR_NOSUCHDB,
+ '/^Unknown field/i'
+ => DB_ERROR_NOSUCHFIELD,
+ '/^Unknown (index|system variable)/i'
+ => DB_ERROR_NOT_FOUND,
+ '/^Unknown table/i'
+ => DB_ERROR_NOSUCHTABLE,
+ '/^Unqualified field/i'
+ => DB_ERROR_SYNTAX,
+ );
+ }
+
+ foreach ($error_regexps as $regexp => $code) {
+ if (preg_match($regexp, $errormsg)) {
+ return $code;
+ }
+ }
+ return DB_ERROR;
+ }
+
+ // }}}
+ // {{{ tableInfo()
+
+ /**
+ * Returns information about a table or a result set
+ *
+ * @param object|string $result DB_result object from a query or a
+ * string containing the name of a table.
+ * While this also accepts a query result
+ * resource identifier, this behavior is
+ * deprecated.
+ * @param int $mode a valid tableInfo mode
+ *
+ * @return array an associative array with the information requested.
+ * A DB_Error object on failure.
+ *
+ * @see DB_common::setOption()
+ */
+ function tableInfo($result, $mode = null)
+ {
+ if (is_string($result)) {
+ /*
+ * Probably received a table name.
+ * Create a result resource identifier.
+ */
+ $id = @msql_query("SELECT * FROM $result",
+ $this->connection);
+ $got_string = true;
+ } elseif (isset($result->result)) {
+ /*
+ * Probably received a result object.
+ * Extract the result resource identifier.
+ */
+ $id = $result->result;
+ $got_string = false;
+ } else {
+ /*
+ * Probably received a result resource identifier.
+ * Copy it.
+ * Deprecated. Here for compatibility only.
+ */
+ $id = $result;
+ $got_string = false;
+ }
+
+ if (!is_resource($id)) {
+ return $this->raiseError(DB_ERROR_NEED_MORE_DATA);
+ }
+
+ if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE) {
+ $case_func = 'strtolower';
+ } else {
+ $case_func = 'strval';
+ }
+
+ $count = @msql_num_fields($id);
+ $res = array();
+
+ if ($mode) {
+ $res['num_fields'] = $count;
+ }
+
+ for ($i = 0; $i < $count; $i++) {
+ $tmp = @msql_fetch_field($id);
+
+ $flags = '';
+ if ($tmp->not_null) {
+ $flags .= 'not_null ';
+ }
+ if ($tmp->unique) {
+ $flags .= 'unique_key ';
+ }
+ $flags = trim($flags);
+
+ $res[$i] = array(
+ 'table' => $case_func($tmp->table),
+ 'name' => $case_func($tmp->name),
+ 'type' => $tmp->type,
+ 'len' => msql_field_len($id, $i),
+ 'flags' => $flags,
+ );
+
+ if ($mode & DB_TABLEINFO_ORDER) {
+ $res['order'][$res[$i]['name']] = $i;
+ }
+ if ($mode & DB_TABLEINFO_ORDERTABLE) {
+ $res['ordertable'][$res[$i]['table']][$res[$i]['name']] = $i;
+ }
+ }
+
+ // free the result only if we were called on a table
+ if ($got_string) {
+ @msql_free_result($id);
+ }
+ return $res;
+ }
+
+ // }}}
+ // {{{ getSpecialQuery()
+
+ /**
+ * Obtain a list of a given type of objects
+ *
+ * @param string $type the kind of objects you want to retrieve
+ *
+ * @return array the array containing the list of objects requested
+ *
+ * @access protected
+ * @see DB_common::getListOf()
+ */
+ function getSpecialQuery($type)
+ {
+ switch ($type) {
+ case 'databases':
+ $id = @msql_list_dbs($this->connection);
+ break;
+ case 'tables':
+ $id = @msql_list_tables($this->dsn['database'],
+ $this->connection);
+ break;
+ default:
+ return null;
+ }
+ if (!$id) {
+ return $this->msqlRaiseError();
+ }
+ $out = array();
+ while ($row = @msql_fetch_row($id)) {
+ $out[] = $row[0];
+ }
+ return $out;
+ }
+
+ // }}}
+
+}
+
+/*
+ * Local variables:
+ * tab-width: 4
+ * c-basic-offset: 4
+ * End:
+ */
+
+?>
diff --git a/extlib/DB/mssql.php b/extlib/DB/mssql.php
new file mode 100644
index 000000000..511a2b686
--- /dev/null
+++ b/extlib/DB/mssql.php
@@ -0,0 +1,963 @@
+<?php
+
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * The PEAR DB driver for PHP's mssql extension
+ * for interacting with Microsoft SQL Server databases
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt. If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category Database
+ * @package DB
+ * @author Sterling Hughes <sterling@php.net>
+ * @author Daniel Convissor <danielc@php.net>
+ * @copyright 1997-2007 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0
+ * @version CVS: $Id: mssql.php,v 1.92 2007/09/21 13:40:41 aharvey Exp $
+ * @link http://pear.php.net/package/DB
+ */
+
+/**
+ * Obtain the DB_common class so it can be extended from
+ */
+require_once 'DB/common.php';
+
+/**
+ * The methods PEAR DB uses to interact with PHP's mssql extension
+ * for interacting with Microsoft SQL Server databases
+ *
+ * These methods overload the ones declared in DB_common.
+ *
+ * DB's mssql driver is only for Microsfoft SQL Server databases.
+ *
+ * If you're connecting to a Sybase database, you MUST specify "sybase"
+ * as the "phptype" in the DSN.
+ *
+ * This class only works correctly if you have compiled PHP using
+ * --with-mssql=[dir_to_FreeTDS].
+ *
+ * @category Database
+ * @package DB
+ * @author Sterling Hughes <sterling@php.net>
+ * @author Daniel Convissor <danielc@php.net>
+ * @copyright 1997-2007 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0
+ * @version Release: 1.7.14RC1
+ * @link http://pear.php.net/package/DB
+ */
+class DB_mssql extends DB_common
+{
+ // {{{ properties
+
+ /**
+ * The DB driver type (mysql, oci8, odbc, etc.)
+ * @var string
+ */
+ var $phptype = 'mssql';
+
+ /**
+ * The database syntax variant to be used (db2, access, etc.), if any
+ * @var string
+ */
+ var $dbsyntax = 'mssql';
+
+ /**
+ * The capabilities of this DB implementation
+ *
+ * The 'new_link' element contains the PHP version that first provided
+ * new_link support for this DBMS. Contains false if it's unsupported.
+ *
+ * Meaning of the 'limit' element:
+ * + 'emulate' = emulate with fetch row by number
+ * + 'alter' = alter the query
+ * + false = skip rows
+ *
+ * @var array
+ */
+ var $features = array(
+ 'limit' => 'emulate',
+ 'new_link' => false,
+ 'numrows' => true,
+ 'pconnect' => true,
+ 'prepare' => false,
+ 'ssl' => false,
+ 'transactions' => true,
+ );
+
+ /**
+ * A mapping of native error codes to DB error codes
+ * @var array
+ */
+ // XXX Add here error codes ie: 'S100E' => DB_ERROR_SYNTAX
+ var $errorcode_map = array(
+ 102 => DB_ERROR_SYNTAX,
+ 110 => DB_ERROR_VALUE_COUNT_ON_ROW,
+ 155 => DB_ERROR_NOSUCHFIELD,
+ 156 => DB_ERROR_SYNTAX,
+ 170 => DB_ERROR_SYNTAX,
+ 207 => DB_ERROR_NOSUCHFIELD,
+ 208 => DB_ERROR_NOSUCHTABLE,
+ 245 => DB_ERROR_INVALID_NUMBER,
+ 319 => DB_ERROR_SYNTAX,
+ 321 => DB_ERROR_NOSUCHFIELD,
+ 325 => DB_ERROR_SYNTAX,
+ 336 => DB_ERROR_SYNTAX,
+ 515 => DB_ERROR_CONSTRAINT_NOT_NULL,
+ 547 => DB_ERROR_CONSTRAINT,
+ 1018 => DB_ERROR_SYNTAX,
+ 1035 => DB_ERROR_SYNTAX,
+ 1913 => DB_ERROR_ALREADY_EXISTS,
+ 2209 => DB_ERROR_SYNTAX,
+ 2223 => DB_ERROR_SYNTAX,
+ 2248 => DB_ERROR_SYNTAX,
+ 2256 => DB_ERROR_SYNTAX,
+ 2257 => DB_ERROR_SYNTAX,
+ 2627 => DB_ERROR_CONSTRAINT,
+ 2714 => DB_ERROR_ALREADY_EXISTS,
+ 3607 => DB_ERROR_DIVZERO,
+ 3701 => DB_ERROR_NOSUCHTABLE,
+ 7630 => DB_ERROR_SYNTAX,
+ 8134 => DB_ERROR_DIVZERO,
+ 9303 => DB_ERROR_SYNTAX,
+ 9317 => DB_ERROR_SYNTAX,
+ 9318 => DB_ERROR_SYNTAX,
+ 9331 => DB_ERROR_SYNTAX,
+ 9332 => DB_ERROR_SYNTAX,
+ 15253 => DB_ERROR_SYNTAX,
+ );
+
+ /**
+ * The raw database connection created by PHP
+ * @var resource
+ */
+ var $connection;
+
+ /**
+ * The DSN information for connecting to a database
+ * @var array
+ */
+ var $dsn = array();
+
+
+ /**
+ * Should data manipulation queries be committed automatically?
+ * @var bool
+ * @access private
+ */
+ var $autocommit = true;
+
+ /**
+ * The quantity of transactions begun
+ *
+ * {@internal While this is private, it can't actually be designated
+ * private in PHP 5 because it is directly accessed in the test suite.}}
+ *
+ * @var integer
+ * @access private
+ */
+ var $transaction_opcount = 0;
+
+ /**
+ * The database specified in the DSN
+ *
+ * It's a fix to allow calls to different databases in the same script.
+ *
+ * @var string
+ * @access private
+ */
+ var $_db = null;
+
+
+ // }}}
+ // {{{ constructor
+
+ /**
+ * This constructor calls <kbd>$this->DB_common()</kbd>
+ *
+ * @return void
+ */
+ function DB_mssql()
+ {
+ $this->DB_common();
+ }
+
+ // }}}
+ // {{{ connect()
+
+ /**
+ * Connect to the database server, log in and open the database
+ *
+ * Don't call this method directly. Use DB::connect() instead.
+ *
+ * @param array $dsn the data source name
+ * @param bool $persistent should the connection be persistent?
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ */
+ function connect($dsn, $persistent = false)
+ {
+ if (!PEAR::loadExtension('mssql') && !PEAR::loadExtension('sybase')
+ && !PEAR::loadExtension('sybase_ct'))
+ {
+ return $this->raiseError(DB_ERROR_EXTENSION_NOT_FOUND);
+ }
+
+ $this->dsn = $dsn;
+ if ($dsn['dbsyntax']) {
+ $this->dbsyntax = $dsn['dbsyntax'];
+ }
+
+ $params = array(
+ $dsn['hostspec'] ? $dsn['hostspec'] : 'localhost',
+ $dsn['username'] ? $dsn['username'] : null,
+ $dsn['password'] ? $dsn['password'] : null,
+ );
+ if ($dsn['port']) {
+ $params[0] .= ((substr(PHP_OS, 0, 3) == 'WIN') ? ',' : ':')
+ . $dsn['port'];
+ }
+
+ $connect_function = $persistent ? 'mssql_pconnect' : 'mssql_connect';
+
+ $this->connection = @call_user_func_array($connect_function, $params);
+
+ if (!$this->connection) {
+ return $this->raiseError(DB_ERROR_CONNECT_FAILED,
+ null, null, null,
+ @mssql_get_last_message());
+ }
+ if ($dsn['database']) {
+ if (!@mssql_select_db($dsn['database'], $this->connection)) {
+ return $this->raiseError(DB_ERROR_NODBSELECTED,
+ null, null, null,
+ @mssql_get_last_message());
+ }
+ $this->_db = $dsn['database'];
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ disconnect()
+
+ /**
+ * Disconnects from the database server
+ *
+ * @return bool TRUE on success, FALSE on failure
+ */
+ function disconnect()
+ {
+ $ret = @mssql_close($this->connection);
+ $this->connection = null;
+ return $ret;
+ }
+
+ // }}}
+ // {{{ simpleQuery()
+
+ /**
+ * Sends a query to the database server
+ *
+ * @param string the SQL query string
+ *
+ * @return mixed + a PHP result resrouce for successful SELECT queries
+ * + the DB_OK constant for other successful queries
+ * + a DB_Error object on failure
+ */
+ function simpleQuery($query)
+ {
+ $ismanip = $this->_checkManip($query);
+ $this->last_query = $query;
+ if (!@mssql_select_db($this->_db, $this->connection)) {
+ return $this->mssqlRaiseError(DB_ERROR_NODBSELECTED);
+ }
+ $query = $this->modifyQuery($query);
+ if (!$this->autocommit && $ismanip) {
+ if ($this->transaction_opcount == 0) {
+ $result = @mssql_query('BEGIN TRAN', $this->connection);
+ if (!$result) {
+ return $this->mssqlRaiseError();
+ }
+ }
+ $this->transaction_opcount++;
+ }
+ $result = @mssql_query($query, $this->connection);
+ if (!$result) {
+ return $this->mssqlRaiseError();
+ }
+ // Determine which queries that should return data, and which
+ // should return an error code only.
+ return $ismanip ? DB_OK : $result;
+ }
+
+ // }}}
+ // {{{ nextResult()
+
+ /**
+ * Move the internal mssql result pointer to the next available result
+ *
+ * @param a valid fbsql result resource
+ *
+ * @access public
+ *
+ * @return true if a result is available otherwise return false
+ */
+ function nextResult($result)
+ {
+ return @mssql_next_result($result);
+ }
+
+ // }}}
+ // {{{ fetchInto()
+
+ /**
+ * Places a row from the result set into the given array
+ *
+ * Formating of the array and the data therein are configurable.
+ * See DB_result::fetchInto() for more information.
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::fetchInto() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result the query result resource
+ * @param array $arr the referenced array to put the data in
+ * @param int $fetchmode how the resulting array should be indexed
+ * @param int $rownum the row number to fetch (0 = first row)
+ *
+ * @return mixed DB_OK on success, NULL when the end of a result set is
+ * reached or on failure
+ *
+ * @see DB_result::fetchInto()
+ */
+ function fetchInto($result, &$arr, $fetchmode, $rownum = null)
+ {
+ if ($rownum !== null) {
+ if (!@mssql_data_seek($result, $rownum)) {
+ return null;
+ }
+ }
+ if ($fetchmode & DB_FETCHMODE_ASSOC) {
+ $arr = @mssql_fetch_assoc($result);
+ if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE && $arr) {
+ $arr = array_change_key_case($arr, CASE_LOWER);
+ }
+ } else {
+ $arr = @mssql_fetch_row($result);
+ }
+ if (!$arr) {
+ return null;
+ }
+ if ($this->options['portability'] & DB_PORTABILITY_RTRIM) {
+ $this->_rtrimArrayValues($arr);
+ }
+ if ($this->options['portability'] & DB_PORTABILITY_NULL_TO_EMPTY) {
+ $this->_convertNullArrayValuesToEmpty($arr);
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ freeResult()
+
+ /**
+ * Deletes the result set and frees the memory occupied by the result set
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::free() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result PHP's query result resource
+ *
+ * @return bool TRUE on success, FALSE if $result is invalid
+ *
+ * @see DB_result::free()
+ */
+ function freeResult($result)
+ {
+ return is_resource($result) ? mssql_free_result($result) : false;
+ }
+
+ // }}}
+ // {{{ numCols()
+
+ /**
+ * Gets the number of columns in a result set
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::numCols() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result PHP's query result resource
+ *
+ * @return int the number of columns. A DB_Error object on failure.
+ *
+ * @see DB_result::numCols()
+ */
+ function numCols($result)
+ {
+ $cols = @mssql_num_fields($result);
+ if (!$cols) {
+ return $this->mssqlRaiseError();
+ }
+ return $cols;
+ }
+
+ // }}}
+ // {{{ numRows()
+
+ /**
+ * Gets the number of rows in a result set
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::numRows() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result PHP's query result resource
+ *
+ * @return int the number of rows. A DB_Error object on failure.
+ *
+ * @see DB_result::numRows()
+ */
+ function numRows($result)
+ {
+ $rows = @mssql_num_rows($result);
+ if ($rows === false) {
+ return $this->mssqlRaiseError();
+ }
+ return $rows;
+ }
+
+ // }}}
+ // {{{ autoCommit()
+
+ /**
+ * Enables or disables automatic commits
+ *
+ * @param bool $onoff true turns it on, false turns it off
+ *
+ * @return int DB_OK on success. A DB_Error object if the driver
+ * doesn't support auto-committing transactions.
+ */
+ function autoCommit($onoff = false)
+ {
+ // XXX if $this->transaction_opcount > 0, we should probably
+ // issue a warning here.
+ $this->autocommit = $onoff ? true : false;
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ commit()
+
+ /**
+ * Commits the current transaction
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ */
+ function commit()
+ {
+ if ($this->transaction_opcount > 0) {
+ if (!@mssql_select_db($this->_db, $this->connection)) {
+ return $this->mssqlRaiseError(DB_ERROR_NODBSELECTED);
+ }
+ $result = @mssql_query('COMMIT TRAN', $this->connection);
+ $this->transaction_opcount = 0;
+ if (!$result) {
+ return $this->mssqlRaiseError();
+ }
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ rollback()
+
+ /**
+ * Reverts the current transaction
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ */
+ function rollback()
+ {
+ if ($this->transaction_opcount > 0) {
+ if (!@mssql_select_db($this->_db, $this->connection)) {
+ return $this->mssqlRaiseError(DB_ERROR_NODBSELECTED);
+ }
+ $result = @mssql_query('ROLLBACK TRAN', $this->connection);
+ $this->transaction_opcount = 0;
+ if (!$result) {
+ return $this->mssqlRaiseError();
+ }
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ affectedRows()
+
+ /**
+ * Determines the number of rows affected by a data maniuplation query
+ *
+ * 0 is returned for queries that don't manipulate data.
+ *
+ * @return int the number of rows. A DB_Error object on failure.
+ */
+ function affectedRows()
+ {
+ if ($this->_last_query_manip) {
+ $res = @mssql_query('select @@rowcount', $this->connection);
+ if (!$res) {
+ return $this->mssqlRaiseError();
+ }
+ $ar = @mssql_fetch_row($res);
+ if (!$ar) {
+ $result = 0;
+ } else {
+ @mssql_free_result($res);
+ $result = $ar[0];
+ }
+ } else {
+ $result = 0;
+ }
+ return $result;
+ }
+
+ // }}}
+ // {{{ nextId()
+
+ /**
+ * Returns the next free id in a sequence
+ *
+ * @param string $seq_name name of the sequence
+ * @param boolean $ondemand when true, the seqence is automatically
+ * created if it does not exist
+ *
+ * @return int the next id number in the sequence.
+ * A DB_Error object on failure.
+ *
+ * @see DB_common::nextID(), DB_common::getSequenceName(),
+ * DB_mssql::createSequence(), DB_mssql::dropSequence()
+ */
+ function nextId($seq_name, $ondemand = true)
+ {
+ $seqname = $this->getSequenceName($seq_name);
+ if (!@mssql_select_db($this->_db, $this->connection)) {
+ return $this->mssqlRaiseError(DB_ERROR_NODBSELECTED);
+ }
+ $repeat = 0;
+ do {
+ $this->pushErrorHandling(PEAR_ERROR_RETURN);
+ $result = $this->query("INSERT INTO $seqname (vapor) VALUES (0)");
+ $this->popErrorHandling();
+ if ($ondemand && DB::isError($result) &&
+ ($result->getCode() == DB_ERROR || $result->getCode() == DB_ERROR_NOSUCHTABLE))
+ {
+ $repeat = 1;
+ $result = $this->createSequence($seq_name);
+ if (DB::isError($result)) {
+ return $this->raiseError($result);
+ }
+ } elseif (!DB::isError($result)) {
+ $result = $this->query("SELECT IDENT_CURRENT('$seqname')");
+ if (DB::isError($result)) {
+ /* Fallback code for MS SQL Server 7.0, which doesn't have
+ * IDENT_CURRENT. This is *not* safe for concurrent
+ * requests, and really, if you're using it, you're in a
+ * world of hurt. Nevertheless, it's here to ensure BC. See
+ * bug #181 for the gory details.*/
+ $result = $this->query("SELECT @@IDENTITY FROM $seqname");
+ }
+ $repeat = 0;
+ } else {
+ $repeat = false;
+ }
+ } while ($repeat);
+ if (DB::isError($result)) {
+ return $this->raiseError($result);
+ }
+ $result = $result->fetchRow(DB_FETCHMODE_ORDERED);
+ return $result[0];
+ }
+
+ /**
+ * Creates a new sequence
+ *
+ * @param string $seq_name name of the new sequence
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ *
+ * @see DB_common::createSequence(), DB_common::getSequenceName(),
+ * DB_mssql::nextID(), DB_mssql::dropSequence()
+ */
+ function createSequence($seq_name)
+ {
+ return $this->query('CREATE TABLE '
+ . $this->getSequenceName($seq_name)
+ . ' ([id] [int] IDENTITY (1, 1) NOT NULL,'
+ . ' [vapor] [int] NULL)');
+ }
+
+ // }}}
+ // {{{ dropSequence()
+
+ /**
+ * Deletes a sequence
+ *
+ * @param string $seq_name name of the sequence to be deleted
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ *
+ * @see DB_common::dropSequence(), DB_common::getSequenceName(),
+ * DB_mssql::nextID(), DB_mssql::createSequence()
+ */
+ function dropSequence($seq_name)
+ {
+ return $this->query('DROP TABLE ' . $this->getSequenceName($seq_name));
+ }
+
+ // }}}
+ // {{{ quoteIdentifier()
+
+ /**
+ * Quotes a string so it can be safely used as a table or column name
+ *
+ * @param string $str identifier name to be quoted
+ *
+ * @return string quoted identifier string
+ *
+ * @see DB_common::quoteIdentifier()
+ * @since Method available since Release 1.6.0
+ */
+ function quoteIdentifier($str)
+ {
+ return '[' . str_replace(']', ']]', $str) . ']';
+ }
+
+ // }}}
+ // {{{ mssqlRaiseError()
+
+ /**
+ * Produces a DB_Error object regarding the current problem
+ *
+ * @param int $errno if the error is being manually raised pass a
+ * DB_ERROR* constant here. If this isn't passed
+ * the error information gathered from the DBMS.
+ *
+ * @return object the DB_Error object
+ *
+ * @see DB_common::raiseError(),
+ * DB_mssql::errorNative(), DB_mssql::errorCode()
+ */
+ function mssqlRaiseError($code = null)
+ {
+ $message = @mssql_get_last_message();
+ if (!$code) {
+ $code = $this->errorNative();
+ }
+ return $this->raiseError($this->errorCode($code, $message),
+ null, null, null, "$code - $message");
+ }
+
+ // }}}
+ // {{{ errorNative()
+
+ /**
+ * Gets the DBMS' native error code produced by the last query
+ *
+ * @return int the DBMS' error code
+ */
+ function errorNative()
+ {
+ $res = @mssql_query('select @@ERROR as ErrorCode', $this->connection);
+ if (!$res) {
+ return DB_ERROR;
+ }
+ $row = @mssql_fetch_row($res);
+ return $row[0];
+ }
+
+ // }}}
+ // {{{ errorCode()
+
+ /**
+ * Determines PEAR::DB error code from mssql's native codes.
+ *
+ * If <var>$nativecode</var> isn't known yet, it will be looked up.
+ *
+ * @param mixed $nativecode mssql error code, if known
+ * @return integer an error number from a DB error constant
+ * @see errorNative()
+ */
+ function errorCode($nativecode = null, $msg = '')
+ {
+ if (!$nativecode) {
+ $nativecode = $this->errorNative();
+ }
+ if (isset($this->errorcode_map[$nativecode])) {
+ if ($nativecode == 3701
+ && preg_match('/Cannot drop the index/i', $msg))
+ {
+ return DB_ERROR_NOT_FOUND;
+ }
+ return $this->errorcode_map[$nativecode];
+ } else {
+ return DB_ERROR;
+ }
+ }
+
+ // }}}
+ // {{{ tableInfo()
+
+ /**
+ * Returns information about a table or a result set
+ *
+ * NOTE: only supports 'table' and 'flags' if <var>$result</var>
+ * is a table name.
+ *
+ * @param object|string $result DB_result object from a query or a
+ * string containing the name of a table.
+ * While this also accepts a query result
+ * resource identifier, this behavior is
+ * deprecated.
+ * @param int $mode a valid tableInfo mode
+ *
+ * @return array an associative array with the information requested.
+ * A DB_Error object on failure.
+ *
+ * @see DB_common::tableInfo()
+ */
+ function tableInfo($result, $mode = null)
+ {
+ if (is_string($result)) {
+ /*
+ * Probably received a table name.
+ * Create a result resource identifier.
+ */
+ if (!@mssql_select_db($this->_db, $this->connection)) {
+ return $this->mssqlRaiseError(DB_ERROR_NODBSELECTED);
+ }
+ $id = @mssql_query("SELECT * FROM $result WHERE 1=0",
+ $this->connection);
+ $got_string = true;
+ } elseif (isset($result->result)) {
+ /*
+ * Probably received a result object.
+ * Extract the result resource identifier.
+ */
+ $id = $result->result;
+ $got_string = false;
+ } else {
+ /*
+ * Probably received a result resource identifier.
+ * Copy it.
+ * Deprecated. Here for compatibility only.
+ */
+ $id = $result;
+ $got_string = false;
+ }
+
+ if (!is_resource($id)) {
+ return $this->mssqlRaiseError(DB_ERROR_NEED_MORE_DATA);
+ }
+
+ if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE) {
+ $case_func = 'strtolower';
+ } else {
+ $case_func = 'strval';
+ }
+
+ $count = @mssql_num_fields($id);
+ $res = array();
+
+ if ($mode) {
+ $res['num_fields'] = $count;
+ }
+
+ for ($i = 0; $i < $count; $i++) {
+ if ($got_string) {
+ $flags = $this->_mssql_field_flags($result,
+ @mssql_field_name($id, $i));
+ if (DB::isError($flags)) {
+ return $flags;
+ }
+ } else {
+ $flags = '';
+ }
+
+ $res[$i] = array(
+ 'table' => $got_string ? $case_func($result) : '',
+ 'name' => $case_func(@mssql_field_name($id, $i)),
+ 'type' => @mssql_field_type($id, $i),
+ 'len' => @mssql_field_length($id, $i),
+ 'flags' => $flags,
+ );
+ if ($mode & DB_TABLEINFO_ORDER) {
+ $res['order'][$res[$i]['name']] = $i;
+ }
+ if ($mode & DB_TABLEINFO_ORDERTABLE) {
+ $res['ordertable'][$res[$i]['table']][$res[$i]['name']] = $i;
+ }
+ }
+
+ // free the result only if we were called on a table
+ if ($got_string) {
+ @mssql_free_result($id);
+ }
+ return $res;
+ }
+
+ // }}}
+ // {{{ _mssql_field_flags()
+
+ /**
+ * Get a column's flags
+ *
+ * Supports "not_null", "primary_key",
+ * "auto_increment" (mssql identity), "timestamp" (mssql timestamp),
+ * "unique_key" (mssql unique index, unique check or primary_key) and
+ * "multiple_key" (multikey index)
+ *
+ * mssql timestamp is NOT similar to the mysql timestamp so this is maybe
+ * not useful at all - is the behaviour of mysql_field_flags that primary
+ * keys are alway unique? is the interpretation of multiple_key correct?
+ *
+ * @param string $table the table name
+ * @param string $column the field name
+ *
+ * @return string the flags
+ *
+ * @access private
+ * @author Joern Barthel <j_barthel@web.de>
+ */
+ function _mssql_field_flags($table, $column)
+ {
+ static $tableName = null;
+ static $flags = array();
+
+ if ($table != $tableName) {
+
+ $flags = array();
+ $tableName = $table;
+
+ // get unique and primary keys
+ $res = $this->getAll("EXEC SP_HELPINDEX $table", DB_FETCHMODE_ASSOC);
+ if (DB::isError($res)) {
+ return $res;
+ }
+
+ foreach ($res as $val) {
+ $keys = explode(', ', $val['index_keys']);
+
+ if (sizeof($keys) > 1) {
+ foreach ($keys as $key) {
+ $this->_add_flag($flags[$key], 'multiple_key');
+ }
+ }
+
+ if (strpos($val['index_description'], 'primary key')) {
+ foreach ($keys as $key) {
+ $this->_add_flag($flags[$key], 'primary_key');
+ }
+ } elseif (strpos($val['index_description'], 'unique')) {
+ foreach ($keys as $key) {
+ $this->_add_flag($flags[$key], 'unique_key');
+ }
+ }
+ }
+
+ // get auto_increment, not_null and timestamp
+ $res = $this->getAll("EXEC SP_COLUMNS $table", DB_FETCHMODE_ASSOC);
+ if (DB::isError($res)) {
+ return $res;
+ }
+
+ foreach ($res as $val) {
+ $val = array_change_key_case($val, CASE_LOWER);
+ if ($val['nullable'] == '0') {
+ $this->_add_flag($flags[$val['column_name']], 'not_null');
+ }
+ if (strpos($val['type_name'], 'identity')) {
+ $this->_add_flag($flags[$val['column_name']], 'auto_increment');
+ }
+ if (strpos($val['type_name'], 'timestamp')) {
+ $this->_add_flag($flags[$val['column_name']], 'timestamp');
+ }
+ }
+ }
+
+ if (array_key_exists($column, $flags)) {
+ return(implode(' ', $flags[$column]));
+ }
+ return '';
+ }
+
+ // }}}
+ // {{{ _add_flag()
+
+ /**
+ * Adds a string to the flags array if the flag is not yet in there
+ * - if there is no flag present the array is created
+ *
+ * @param array &$array the reference to the flag-array
+ * @param string $value the flag value
+ *
+ * @return void
+ *
+ * @access private
+ * @author Joern Barthel <j_barthel@web.de>
+ */
+ function _add_flag(&$array, $value)
+ {
+ if (!is_array($array)) {
+ $array = array($value);
+ } elseif (!in_array($value, $array)) {
+ array_push($array, $value);
+ }
+ }
+
+ // }}}
+ // {{{ getSpecialQuery()
+
+ /**
+ * Obtains the query string needed for listing a given type of objects
+ *
+ * @param string $type the kind of objects you want to retrieve
+ *
+ * @return string the SQL query string or null if the driver doesn't
+ * support the object type requested
+ *
+ * @access protected
+ * @see DB_common::getListOf()
+ */
+ function getSpecialQuery($type)
+ {
+ switch ($type) {
+ case 'tables':
+ return "SELECT name FROM sysobjects WHERE type = 'U'"
+ . ' ORDER BY name';
+ case 'views':
+ return "SELECT name FROM sysobjects WHERE type = 'V'";
+ default:
+ return null;
+ }
+ }
+
+ // }}}
+}
+
+/*
+ * Local variables:
+ * tab-width: 4
+ * c-basic-offset: 4
+ * End:
+ */
+
+?>
diff --git a/extlib/DB/mysql.php b/extlib/DB/mysql.php
new file mode 100644
index 000000000..c67254520
--- /dev/null
+++ b/extlib/DB/mysql.php
@@ -0,0 +1,1045 @@
+<?php
+
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * The PEAR DB driver for PHP's mysql extension
+ * for interacting with MySQL databases
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt. If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category Database
+ * @package DB
+ * @author Stig Bakken <ssb@php.net>
+ * @author Daniel Convissor <danielc@php.net>
+ * @copyright 1997-2007 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0
+ * @version CVS: $Id: mysql.php,v 1.126 2007/09/21 13:32:52 aharvey Exp $
+ * @link http://pear.php.net/package/DB
+ */
+
+/**
+ * Obtain the DB_common class so it can be extended from
+ */
+require_once 'DB/common.php';
+
+/**
+ * The methods PEAR DB uses to interact with PHP's mysql extension
+ * for interacting with MySQL databases
+ *
+ * These methods overload the ones declared in DB_common.
+ *
+ * @category Database
+ * @package DB
+ * @author Stig Bakken <ssb@php.net>
+ * @author Daniel Convissor <danielc@php.net>
+ * @copyright 1997-2007 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0
+ * @version Release: 1.7.14RC1
+ * @link http://pear.php.net/package/DB
+ */
+class DB_mysql extends DB_common
+{
+ // {{{ properties
+
+ /**
+ * The DB driver type (mysql, oci8, odbc, etc.)
+ * @var string
+ */
+ var $phptype = 'mysql';
+
+ /**
+ * The database syntax variant to be used (db2, access, etc.), if any
+ * @var string
+ */
+ var $dbsyntax = 'mysql';
+
+ /**
+ * The capabilities of this DB implementation
+ *
+ * The 'new_link' element contains the PHP version that first provided
+ * new_link support for this DBMS. Contains false if it's unsupported.
+ *
+ * Meaning of the 'limit' element:
+ * + 'emulate' = emulate with fetch row by number
+ * + 'alter' = alter the query
+ * + false = skip rows
+ *
+ * @var array
+ */
+ var $features = array(
+ 'limit' => 'alter',
+ 'new_link' => '4.2.0',
+ 'numrows' => true,
+ 'pconnect' => true,
+ 'prepare' => false,
+ 'ssl' => false,
+ 'transactions' => true,
+ );
+
+ /**
+ * A mapping of native error codes to DB error codes
+ * @var array
+ */
+ var $errorcode_map = array(
+ 1004 => DB_ERROR_CANNOT_CREATE,
+ 1005 => DB_ERROR_CANNOT_CREATE,
+ 1006 => DB_ERROR_CANNOT_CREATE,
+ 1007 => DB_ERROR_ALREADY_EXISTS,
+ 1008 => DB_ERROR_CANNOT_DROP,
+ 1022 => DB_ERROR_ALREADY_EXISTS,
+ 1044 => DB_ERROR_ACCESS_VIOLATION,
+ 1046 => DB_ERROR_NODBSELECTED,
+ 1048 => DB_ERROR_CONSTRAINT,
+ 1049 => DB_ERROR_NOSUCHDB,
+ 1050 => DB_ERROR_ALREADY_EXISTS,
+ 1051 => DB_ERROR_NOSUCHTABLE,
+ 1054 => DB_ERROR_NOSUCHFIELD,
+ 1061 => DB_ERROR_ALREADY_EXISTS,
+ 1062 => DB_ERROR_ALREADY_EXISTS,
+ 1064 => DB_ERROR_SYNTAX,
+ 1091 => DB_ERROR_NOT_FOUND,
+ 1100 => DB_ERROR_NOT_LOCKED,
+ 1136 => DB_ERROR_VALUE_COUNT_ON_ROW,
+ 1142 => DB_ERROR_ACCESS_VIOLATION,
+ 1146 => DB_ERROR_NOSUCHTABLE,
+ 1216 => DB_ERROR_CONSTRAINT,
+ 1217 => DB_ERROR_CONSTRAINT,
+ 1356 => DB_ERROR_DIVZERO,
+ 1451 => DB_ERROR_CONSTRAINT,
+ 1452 => DB_ERROR_CONSTRAINT,
+ );
+
+ /**
+ * The raw database connection created by PHP
+ * @var resource
+ */
+ var $connection;
+
+ /**
+ * The DSN information for connecting to a database
+ * @var array
+ */
+ var $dsn = array();
+
+
+ /**
+ * Should data manipulation queries be committed automatically?
+ * @var bool
+ * @access private
+ */
+ var $autocommit = true;
+
+ /**
+ * The quantity of transactions begun
+ *
+ * {@internal While this is private, it can't actually be designated
+ * private in PHP 5 because it is directly accessed in the test suite.}}
+ *
+ * @var integer
+ * @access private
+ */
+ var $transaction_opcount = 0;
+
+ /**
+ * The database specified in the DSN
+ *
+ * It's a fix to allow calls to different databases in the same script.
+ *
+ * @var string
+ * @access private
+ */
+ var $_db = '';
+
+
+ // }}}
+ // {{{ constructor
+
+ /**
+ * This constructor calls <kbd>$this->DB_common()</kbd>
+ *
+ * @return void
+ */
+ function DB_mysql()
+ {
+ $this->DB_common();
+ }
+
+ // }}}
+ // {{{ connect()
+
+ /**
+ * Connect to the database server, log in and open the database
+ *
+ * Don't call this method directly. Use DB::connect() instead.
+ *
+ * PEAR DB's mysql driver supports the following extra DSN options:
+ * + new_link If set to true, causes subsequent calls to connect()
+ * to return a new connection link instead of the
+ * existing one. WARNING: this is not portable to
+ * other DBMS's. Available since PEAR DB 1.7.0.
+ * + client_flags Any combination of MYSQL_CLIENT_* constants.
+ * Only used if PHP is at version 4.3.0 or greater.
+ * Available since PEAR DB 1.7.0.
+ *
+ * @param array $dsn the data source name
+ * @param bool $persistent should the connection be persistent?
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ */
+ function connect($dsn, $persistent = false)
+ {
+ if (!PEAR::loadExtension('mysql')) {
+ return $this->raiseError(DB_ERROR_EXTENSION_NOT_FOUND);
+ }
+
+ $this->dsn = $dsn;
+ if ($dsn['dbsyntax']) {
+ $this->dbsyntax = $dsn['dbsyntax'];
+ }
+
+ $params = array();
+ if ($dsn['protocol'] && $dsn['protocol'] == 'unix') {
+ $params[0] = ':' . $dsn['socket'];
+ } else {
+ $params[0] = $dsn['hostspec'] ? $dsn['hostspec']
+ : 'localhost';
+ if ($dsn['port']) {
+ $params[0] .= ':' . $dsn['port'];
+ }
+ }
+ $params[] = $dsn['username'] ? $dsn['username'] : null;
+ $params[] = $dsn['password'] ? $dsn['password'] : null;
+
+ if (!$persistent) {
+ if (isset($dsn['new_link'])
+ && ($dsn['new_link'] == 'true' || $dsn['new_link'] === true))
+ {
+ $params[] = true;
+ } else {
+ $params[] = false;
+ }
+ }
+ if (version_compare(phpversion(), '4.3.0', '>=')) {
+ $params[] = isset($dsn['client_flags'])
+ ? $dsn['client_flags'] : null;
+ }
+
+ $connect_function = $persistent ? 'mysql_pconnect' : 'mysql_connect';
+
+ $ini = ini_get('track_errors');
+ $php_errormsg = '';
+ if ($ini) {
+ $this->connection = @call_user_func_array($connect_function,
+ $params);
+ } else {
+ @ini_set('track_errors', 1);
+ $this->connection = @call_user_func_array($connect_function,
+ $params);
+ @ini_set('track_errors', $ini);
+ }
+
+ if (!$this->connection) {
+ if (($err = @mysql_error()) != '') {
+ return $this->raiseError(DB_ERROR_CONNECT_FAILED,
+ null, null, null,
+ $err);
+ } else {
+ return $this->raiseError(DB_ERROR_CONNECT_FAILED,
+ null, null, null,
+ $php_errormsg);
+ }
+ }
+
+ if ($dsn['database']) {
+ if (!@mysql_select_db($dsn['database'], $this->connection)) {
+ return $this->mysqlRaiseError();
+ }
+ $this->_db = $dsn['database'];
+ }
+
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ disconnect()
+
+ /**
+ * Disconnects from the database server
+ *
+ * @return bool TRUE on success, FALSE on failure
+ */
+ function disconnect()
+ {
+ $ret = @mysql_close($this->connection);
+ $this->connection = null;
+ return $ret;
+ }
+
+ // }}}
+ // {{{ simpleQuery()
+
+ /**
+ * Sends a query to the database server
+ *
+ * Generally uses mysql_query(). If you want to use
+ * mysql_unbuffered_query() set the "result_buffering" option to 0 using
+ * setOptions(). This option was added in Release 1.7.0.
+ *
+ * @param string the SQL query string
+ *
+ * @return mixed + a PHP result resrouce for successful SELECT queries
+ * + the DB_OK constant for other successful queries
+ * + a DB_Error object on failure
+ */
+ function simpleQuery($query)
+ {
+ $ismanip = $this->_checkManip($query);
+ $this->last_query = $query;
+ $query = $this->modifyQuery($query);
+ if ($this->_db) {
+ if (!@mysql_select_db($this->_db, $this->connection)) {
+ return $this->mysqlRaiseError(DB_ERROR_NODBSELECTED);
+ }
+ }
+ if (!$this->autocommit && $ismanip) {
+ if ($this->transaction_opcount == 0) {
+ $result = @mysql_query('SET AUTOCOMMIT=0', $this->connection);
+ $result = @mysql_query('BEGIN', $this->connection);
+ if (!$result) {
+ return $this->mysqlRaiseError();
+ }
+ }
+ $this->transaction_opcount++;
+ }
+ if (!$this->options['result_buffering']) {
+ $result = @mysql_unbuffered_query($query, $this->connection);
+ } else {
+ $result = @mysql_query($query, $this->connection);
+ }
+ if (!$result) {
+ return $this->mysqlRaiseError();
+ }
+ if (is_resource($result)) {
+ return $result;
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ nextResult()
+
+ /**
+ * Move the internal mysql result pointer to the next available result
+ *
+ * This method has not been implemented yet.
+ *
+ * @param a valid sql result resource
+ *
+ * @return false
+ */
+ function nextResult($result)
+ {
+ return false;
+ }
+
+ // }}}
+ // {{{ fetchInto()
+
+ /**
+ * Places a row from the result set into the given array
+ *
+ * Formating of the array and the data therein are configurable.
+ * See DB_result::fetchInto() for more information.
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::fetchInto() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result the query result resource
+ * @param array $arr the referenced array to put the data in
+ * @param int $fetchmode how the resulting array should be indexed
+ * @param int $rownum the row number to fetch (0 = first row)
+ *
+ * @return mixed DB_OK on success, NULL when the end of a result set is
+ * reached or on failure
+ *
+ * @see DB_result::fetchInto()
+ */
+ function fetchInto($result, &$arr, $fetchmode, $rownum = null)
+ {
+ if ($rownum !== null) {
+ if (!@mysql_data_seek($result, $rownum)) {
+ return null;
+ }
+ }
+ if ($fetchmode & DB_FETCHMODE_ASSOC) {
+ $arr = @mysql_fetch_array($result, MYSQL_ASSOC);
+ if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE && $arr) {
+ $arr = array_change_key_case($arr, CASE_LOWER);
+ }
+ } else {
+ $arr = @mysql_fetch_row($result);
+ }
+ if (!$arr) {
+ return null;
+ }
+ if ($this->options['portability'] & DB_PORTABILITY_RTRIM) {
+ /*
+ * Even though this DBMS already trims output, we do this because
+ * a field might have intentional whitespace at the end that
+ * gets removed by DB_PORTABILITY_RTRIM under another driver.
+ */
+ $this->_rtrimArrayValues($arr);
+ }
+ if ($this->options['portability'] & DB_PORTABILITY_NULL_TO_EMPTY) {
+ $this->_convertNullArrayValuesToEmpty($arr);
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ freeResult()
+
+ /**
+ * Deletes the result set and frees the memory occupied by the result set
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::free() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result PHP's query result resource
+ *
+ * @return bool TRUE on success, FALSE if $result is invalid
+ *
+ * @see DB_result::free()
+ */
+ function freeResult($result)
+ {
+ return is_resource($result) ? mysql_free_result($result) : false;
+ }
+
+ // }}}
+ // {{{ numCols()
+
+ /**
+ * Gets the number of columns in a result set
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::numCols() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result PHP's query result resource
+ *
+ * @return int the number of columns. A DB_Error object on failure.
+ *
+ * @see DB_result::numCols()
+ */
+ function numCols($result)
+ {
+ $cols = @mysql_num_fields($result);
+ if (!$cols) {
+ return $this->mysqlRaiseError();
+ }
+ return $cols;
+ }
+
+ // }}}
+ // {{{ numRows()
+
+ /**
+ * Gets the number of rows in a result set
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::numRows() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result PHP's query result resource
+ *
+ * @return int the number of rows. A DB_Error object on failure.
+ *
+ * @see DB_result::numRows()
+ */
+ function numRows($result)
+ {
+ $rows = @mysql_num_rows($result);
+ if ($rows === null) {
+ return $this->mysqlRaiseError();
+ }
+ return $rows;
+ }
+
+ // }}}
+ // {{{ autoCommit()
+
+ /**
+ * Enables or disables automatic commits
+ *
+ * @param bool $onoff true turns it on, false turns it off
+ *
+ * @return int DB_OK on success. A DB_Error object if the driver
+ * doesn't support auto-committing transactions.
+ */
+ function autoCommit($onoff = false)
+ {
+ // XXX if $this->transaction_opcount > 0, we should probably
+ // issue a warning here.
+ $this->autocommit = $onoff ? true : false;
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ commit()
+
+ /**
+ * Commits the current transaction
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ */
+ function commit()
+ {
+ if ($this->transaction_opcount > 0) {
+ if ($this->_db) {
+ if (!@mysql_select_db($this->_db, $this->connection)) {
+ return $this->mysqlRaiseError(DB_ERROR_NODBSELECTED);
+ }
+ }
+ $result = @mysql_query('COMMIT', $this->connection);
+ $result = @mysql_query('SET AUTOCOMMIT=1', $this->connection);
+ $this->transaction_opcount = 0;
+ if (!$result) {
+ return $this->mysqlRaiseError();
+ }
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ rollback()
+
+ /**
+ * Reverts the current transaction
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ */
+ function rollback()
+ {
+ if ($this->transaction_opcount > 0) {
+ if ($this->_db) {
+ if (!@mysql_select_db($this->_db, $this->connection)) {
+ return $this->mysqlRaiseError(DB_ERROR_NODBSELECTED);
+ }
+ }
+ $result = @mysql_query('ROLLBACK', $this->connection);
+ $result = @mysql_query('SET AUTOCOMMIT=1', $this->connection);
+ $this->transaction_opcount = 0;
+ if (!$result) {
+ return $this->mysqlRaiseError();
+ }
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ affectedRows()
+
+ /**
+ * Determines the number of rows affected by a data maniuplation query
+ *
+ * 0 is returned for queries that don't manipulate data.
+ *
+ * @return int the number of rows. A DB_Error object on failure.
+ */
+ function affectedRows()
+ {
+ if ($this->_last_query_manip) {
+ return @mysql_affected_rows($this->connection);
+ } else {
+ return 0;
+ }
+ }
+
+ // }}}
+ // {{{ nextId()
+
+ /**
+ * Returns the next free id in a sequence
+ *
+ * @param string $seq_name name of the sequence
+ * @param boolean $ondemand when true, the seqence is automatically
+ * created if it does not exist
+ *
+ * @return int the next id number in the sequence.
+ * A DB_Error object on failure.
+ *
+ * @see DB_common::nextID(), DB_common::getSequenceName(),
+ * DB_mysql::createSequence(), DB_mysql::dropSequence()
+ */
+ function nextId($seq_name, $ondemand = true)
+ {
+ $seqname = $this->getSequenceName($seq_name);
+ do {
+ $repeat = 0;
+ $this->pushErrorHandling(PEAR_ERROR_RETURN);
+ $result = $this->query("UPDATE ${seqname} ".
+ 'SET id=LAST_INSERT_ID(id+1)');
+ $this->popErrorHandling();
+ if ($result === DB_OK) {
+ // COMMON CASE
+ $id = @mysql_insert_id($this->connection);
+ if ($id != 0) {
+ return $id;
+ }
+ // EMPTY SEQ TABLE
+ // Sequence table must be empty for some reason, so fill
+ // it and return 1 and obtain a user-level lock
+ $result = $this->getOne("SELECT GET_LOCK('${seqname}_lock',10)");
+ if (DB::isError($result)) {
+ return $this->raiseError($result);
+ }
+ if ($result == 0) {
+ // Failed to get the lock
+ return $this->mysqlRaiseError(DB_ERROR_NOT_LOCKED);
+ }
+
+ // add the default value
+ $result = $this->query("REPLACE INTO ${seqname} (id) VALUES (0)");
+ if (DB::isError($result)) {
+ return $this->raiseError($result);
+ }
+
+ // Release the lock
+ $result = $this->getOne('SELECT RELEASE_LOCK('
+ . "'${seqname}_lock')");
+ if (DB::isError($result)) {
+ return $this->raiseError($result);
+ }
+ // We know what the result will be, so no need to try again
+ return 1;
+
+ } elseif ($ondemand && DB::isError($result) &&
+ $result->getCode() == DB_ERROR_NOSUCHTABLE)
+ {
+ // ONDEMAND TABLE CREATION
+ $result = $this->createSequence($seq_name);
+ if (DB::isError($result)) {
+ return $this->raiseError($result);
+ } else {
+ $repeat = 1;
+ }
+
+ } elseif (DB::isError($result) &&
+ $result->getCode() == DB_ERROR_ALREADY_EXISTS)
+ {
+ // BACKWARDS COMPAT
+ // see _BCsequence() comment
+ $result = $this->_BCsequence($seqname);
+ if (DB::isError($result)) {
+ return $this->raiseError($result);
+ }
+ $repeat = 1;
+ }
+ } while ($repeat);
+
+ return $this->raiseError($result);
+ }
+
+ // }}}
+ // {{{ createSequence()
+
+ /**
+ * Creates a new sequence
+ *
+ * @param string $seq_name name of the new sequence
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ *
+ * @see DB_common::createSequence(), DB_common::getSequenceName(),
+ * DB_mysql::nextID(), DB_mysql::dropSequence()
+ */
+ function createSequence($seq_name)
+ {
+ $seqname = $this->getSequenceName($seq_name);
+ $res = $this->query('CREATE TABLE ' . $seqname
+ . ' (id INTEGER UNSIGNED AUTO_INCREMENT NOT NULL,'
+ . ' PRIMARY KEY(id))');
+ if (DB::isError($res)) {
+ return $res;
+ }
+ // insert yields value 1, nextId call will generate ID 2
+ $res = $this->query("INSERT INTO ${seqname} (id) VALUES (0)");
+ if (DB::isError($res)) {
+ return $res;
+ }
+ // so reset to zero
+ return $this->query("UPDATE ${seqname} SET id = 0");
+ }
+
+ // }}}
+ // {{{ dropSequence()
+
+ /**
+ * Deletes a sequence
+ *
+ * @param string $seq_name name of the sequence to be deleted
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ *
+ * @see DB_common::dropSequence(), DB_common::getSequenceName(),
+ * DB_mysql::nextID(), DB_mysql::createSequence()
+ */
+ function dropSequence($seq_name)
+ {
+ return $this->query('DROP TABLE ' . $this->getSequenceName($seq_name));
+ }
+
+ // }}}
+ // {{{ _BCsequence()
+
+ /**
+ * Backwards compatibility with old sequence emulation implementation
+ * (clean up the dupes)
+ *
+ * @param string $seqname the sequence name to clean up
+ *
+ * @return bool true on success. A DB_Error object on failure.
+ *
+ * @access private
+ */
+ function _BCsequence($seqname)
+ {
+ // Obtain a user-level lock... this will release any previous
+ // application locks, but unlike LOCK TABLES, it does not abort
+ // the current transaction and is much less frequently used.
+ $result = $this->getOne("SELECT GET_LOCK('${seqname}_lock',10)");
+ if (DB::isError($result)) {
+ return $result;
+ }
+ if ($result == 0) {
+ // Failed to get the lock, can't do the conversion, bail
+ // with a DB_ERROR_NOT_LOCKED error
+ return $this->mysqlRaiseError(DB_ERROR_NOT_LOCKED);
+ }
+
+ $highest_id = $this->getOne("SELECT MAX(id) FROM ${seqname}");
+ if (DB::isError($highest_id)) {
+ return $highest_id;
+ }
+ // This should kill all rows except the highest
+ // We should probably do something if $highest_id isn't
+ // numeric, but I'm at a loss as how to handle that...
+ $result = $this->query('DELETE FROM ' . $seqname
+ . " WHERE id <> $highest_id");
+ if (DB::isError($result)) {
+ return $result;
+ }
+
+ // If another thread has been waiting for this lock,
+ // it will go thru the above procedure, but will have no
+ // real effect
+ $result = $this->getOne("SELECT RELEASE_LOCK('${seqname}_lock')");
+ if (DB::isError($result)) {
+ return $result;
+ }
+ return true;
+ }
+
+ // }}}
+ // {{{ quoteIdentifier()
+
+ /**
+ * Quotes a string so it can be safely used as a table or column name
+ * (WARNING: using names that require this is a REALLY BAD IDEA)
+ *
+ * WARNING: Older versions of MySQL can't handle the backtick
+ * character (<kbd>`</kbd>) in table or column names.
+ *
+ * @param string $str identifier name to be quoted
+ *
+ * @return string quoted identifier string
+ *
+ * @see DB_common::quoteIdentifier()
+ * @since Method available since Release 1.6.0
+ */
+ function quoteIdentifier($str)
+ {
+ return '`' . str_replace('`', '``', $str) . '`';
+ }
+
+ // }}}
+ // {{{ quote()
+
+ /**
+ * @deprecated Deprecated in release 1.6.0
+ */
+ function quote($str)
+ {
+ return $this->quoteSmart($str);
+ }
+
+ // }}}
+ // {{{ escapeSimple()
+
+ /**
+ * Escapes a string according to the current DBMS's standards
+ *
+ * @param string $str the string to be escaped
+ *
+ * @return string the escaped string
+ *
+ * @see DB_common::quoteSmart()
+ * @since Method available since Release 1.6.0
+ */
+ function escapeSimple($str)
+ {
+ if (function_exists('mysql_real_escape_string')) {
+ return @mysql_real_escape_string($str, $this->connection);
+ } else {
+ return @mysql_escape_string($str);
+ }
+ }
+
+ // }}}
+ // {{{ modifyQuery()
+
+ /**
+ * Changes a query string for various DBMS specific reasons
+ *
+ * This little hack lets you know how many rows were deleted
+ * when running a "DELETE FROM table" query. Only implemented
+ * if the DB_PORTABILITY_DELETE_COUNT portability option is on.
+ *
+ * @param string $query the query string to modify
+ *
+ * @return string the modified query string
+ *
+ * @access protected
+ * @see DB_common::setOption()
+ */
+ function modifyQuery($query)
+ {
+ if ($this->options['portability'] & DB_PORTABILITY_DELETE_COUNT) {
+ // "DELETE FROM table" gives 0 affected rows in MySQL.
+ // This little hack lets you know how many rows were deleted.
+ if (preg_match('/^\s*DELETE\s+FROM\s+(\S+)\s*$/i', $query)) {
+ $query = preg_replace('/^\s*DELETE\s+FROM\s+(\S+)\s*$/',
+ 'DELETE FROM \1 WHERE 1=1', $query);
+ }
+ }
+ return $query;
+ }
+
+ // }}}
+ // {{{ modifyLimitQuery()
+
+ /**
+ * Adds LIMIT clauses to a query string according to current DBMS standards
+ *
+ * @param string $query the query to modify
+ * @param int $from the row to start to fetching (0 = the first row)
+ * @param int $count the numbers of rows to fetch
+ * @param mixed $params array, string or numeric data to be used in
+ * execution of the statement. Quantity of items
+ * passed must match quantity of placeholders in
+ * query: meaning 1 placeholder for non-array
+ * parameters or 1 placeholder per array element.
+ *
+ * @return string the query string with LIMIT clauses added
+ *
+ * @access protected
+ */
+ function modifyLimitQuery($query, $from, $count, $params = array())
+ {
+ if (DB::isManip($query) || $this->_next_query_manip) {
+ return $query . " LIMIT $count";
+ } else {
+ return $query . " LIMIT $from, $count";
+ }
+ }
+
+ // }}}
+ // {{{ mysqlRaiseError()
+
+ /**
+ * Produces a DB_Error object regarding the current problem
+ *
+ * @param int $errno if the error is being manually raised pass a
+ * DB_ERROR* constant here. If this isn't passed
+ * the error information gathered from the DBMS.
+ *
+ * @return object the DB_Error object
+ *
+ * @see DB_common::raiseError(),
+ * DB_mysql::errorNative(), DB_common::errorCode()
+ */
+ function mysqlRaiseError($errno = null)
+ {
+ if ($errno === null) {
+ if ($this->options['portability'] & DB_PORTABILITY_ERRORS) {
+ $this->errorcode_map[1022] = DB_ERROR_CONSTRAINT;
+ $this->errorcode_map[1048] = DB_ERROR_CONSTRAINT_NOT_NULL;
+ $this->errorcode_map[1062] = DB_ERROR_CONSTRAINT;
+ } else {
+ // Doing this in case mode changes during runtime.
+ $this->errorcode_map[1022] = DB_ERROR_ALREADY_EXISTS;
+ $this->errorcode_map[1048] = DB_ERROR_CONSTRAINT;
+ $this->errorcode_map[1062] = DB_ERROR_ALREADY_EXISTS;
+ }
+ $errno = $this->errorCode(mysql_errno($this->connection));
+ }
+ return $this->raiseError($errno, null, null, null,
+ @mysql_errno($this->connection) . ' ** ' .
+ @mysql_error($this->connection));
+ }
+
+ // }}}
+ // {{{ errorNative()
+
+ /**
+ * Gets the DBMS' native error code produced by the last query
+ *
+ * @return int the DBMS' error code
+ */
+ function errorNative()
+ {
+ return @mysql_errno($this->connection);
+ }
+
+ // }}}
+ // {{{ tableInfo()
+
+ /**
+ * Returns information about a table or a result set
+ *
+ * @param object|string $result DB_result object from a query or a
+ * string containing the name of a table.
+ * While this also accepts a query result
+ * resource identifier, this behavior is
+ * deprecated.
+ * @param int $mode a valid tableInfo mode
+ *
+ * @return array an associative array with the information requested.
+ * A DB_Error object on failure.
+ *
+ * @see DB_common::tableInfo()
+ */
+ function tableInfo($result, $mode = null)
+ {
+ if (is_string($result)) {
+ // Fix for bug #11580.
+ if ($this->_db) {
+ if (!@mysql_select_db($this->_db, $this->connection)) {
+ return $this->mysqlRaiseError(DB_ERROR_NODBSELECTED);
+ }
+ }
+
+ /*
+ * Probably received a table name.
+ * Create a result resource identifier.
+ */
+ $id = @mysql_query("SELECT * FROM $result LIMIT 0",
+ $this->connection);
+ $got_string = true;
+ } elseif (isset($result->result)) {
+ /*
+ * Probably received a result object.
+ * Extract the result resource identifier.
+ */
+ $id = $result->result;
+ $got_string = false;
+ } else {
+ /*
+ * Probably received a result resource identifier.
+ * Copy it.
+ * Deprecated. Here for compatibility only.
+ */
+ $id = $result;
+ $got_string = false;
+ }
+
+ if (!is_resource($id)) {
+ return $this->mysqlRaiseError(DB_ERROR_NEED_MORE_DATA);
+ }
+
+ if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE) {
+ $case_func = 'strtolower';
+ } else {
+ $case_func = 'strval';
+ }
+
+ $count = @mysql_num_fields($id);
+ $res = array();
+
+ if ($mode) {
+ $res['num_fields'] = $count;
+ }
+
+ for ($i = 0; $i < $count; $i++) {
+ $res[$i] = array(
+ 'table' => $case_func(@mysql_field_table($id, $i)),
+ 'name' => $case_func(@mysql_field_name($id, $i)),
+ 'type' => @mysql_field_type($id, $i),
+ 'len' => @mysql_field_len($id, $i),
+ 'flags' => @mysql_field_flags($id, $i),
+ );
+ if ($mode & DB_TABLEINFO_ORDER) {
+ $res['order'][$res[$i]['name']] = $i;
+ }
+ if ($mode & DB_TABLEINFO_ORDERTABLE) {
+ $res['ordertable'][$res[$i]['table']][$res[$i]['name']] = $i;
+ }
+ }
+
+ // free the result only if we were called on a table
+ if ($got_string) {
+ @mysql_free_result($id);
+ }
+ return $res;
+ }
+
+ // }}}
+ // {{{ getSpecialQuery()
+
+ /**
+ * Obtains the query string needed for listing a given type of objects
+ *
+ * @param string $type the kind of objects you want to retrieve
+ *
+ * @return string the SQL query string or null if the driver doesn't
+ * support the object type requested
+ *
+ * @access protected
+ * @see DB_common::getListOf()
+ */
+ function getSpecialQuery($type)
+ {
+ switch ($type) {
+ case 'tables':
+ return 'SHOW TABLES';
+ case 'users':
+ return 'SELECT DISTINCT User FROM mysql.user';
+ case 'databases':
+ return 'SHOW DATABASES';
+ default:
+ return null;
+ }
+ }
+
+ // }}}
+
+}
+
+/*
+ * Local variables:
+ * tab-width: 4
+ * c-basic-offset: 4
+ * End:
+ */
+
+?>
diff --git a/extlib/DB/mysqli.php b/extlib/DB/mysqli.php
new file mode 100644
index 000000000..c6941b170
--- /dev/null
+++ b/extlib/DB/mysqli.php
@@ -0,0 +1,1092 @@
+<?php
+
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * The PEAR DB driver for PHP's mysqli extension
+ * for interacting with MySQL databases
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt. If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category Database
+ * @package DB
+ * @author Daniel Convissor <danielc@php.net>
+ * @copyright 1997-2007 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0
+ * @version CVS: $Id: mysqli.php,v 1.82 2007/09/21 13:40:41 aharvey Exp $
+ * @link http://pear.php.net/package/DB
+ */
+
+/**
+ * Obtain the DB_common class so it can be extended from
+ */
+require_once 'DB/common.php';
+
+/**
+ * The methods PEAR DB uses to interact with PHP's mysqli extension
+ * for interacting with MySQL databases
+ *
+ * This is for MySQL versions 4.1 and above. Requires PHP 5.
+ *
+ * Note that persistent connections no longer exist.
+ *
+ * These methods overload the ones declared in DB_common.
+ *
+ * @category Database
+ * @package DB
+ * @author Daniel Convissor <danielc@php.net>
+ * @copyright 1997-2007 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0
+ * @version Release: 1.7.14RC1
+ * @link http://pear.php.net/package/DB
+ * @since Class functional since Release 1.6.3
+ */
+class DB_mysqli extends DB_common
+{
+ // {{{ properties
+
+ /**
+ * The DB driver type (mysql, oci8, odbc, etc.)
+ * @var string
+ */
+ var $phptype = 'mysqli';
+
+ /**
+ * The database syntax variant to be used (db2, access, etc.), if any
+ * @var string
+ */
+ var $dbsyntax = 'mysqli';
+
+ /**
+ * The capabilities of this DB implementation
+ *
+ * The 'new_link' element contains the PHP version that first provided
+ * new_link support for this DBMS. Contains false if it's unsupported.
+ *
+ * Meaning of the 'limit' element:
+ * + 'emulate' = emulate with fetch row by number
+ * + 'alter' = alter the query
+ * + false = skip rows
+ *
+ * @var array
+ */
+ var $features = array(
+ 'limit' => 'alter',
+ 'new_link' => false,
+ 'numrows' => true,
+ 'pconnect' => false,
+ 'prepare' => false,
+ 'ssl' => true,
+ 'transactions' => true,
+ );
+
+ /**
+ * A mapping of native error codes to DB error codes
+ * @var array
+ */
+ var $errorcode_map = array(
+ 1004 => DB_ERROR_CANNOT_CREATE,
+ 1005 => DB_ERROR_CANNOT_CREATE,
+ 1006 => DB_ERROR_CANNOT_CREATE,
+ 1007 => DB_ERROR_ALREADY_EXISTS,
+ 1008 => DB_ERROR_CANNOT_DROP,
+ 1022 => DB_ERROR_ALREADY_EXISTS,
+ 1044 => DB_ERROR_ACCESS_VIOLATION,
+ 1046 => DB_ERROR_NODBSELECTED,
+ 1048 => DB_ERROR_CONSTRAINT,
+ 1049 => DB_ERROR_NOSUCHDB,
+ 1050 => DB_ERROR_ALREADY_EXISTS,
+ 1051 => DB_ERROR_NOSUCHTABLE,
+ 1054 => DB_ERROR_NOSUCHFIELD,
+ 1061 => DB_ERROR_ALREADY_EXISTS,
+ 1062 => DB_ERROR_ALREADY_EXISTS,
+ 1064 => DB_ERROR_SYNTAX,
+ 1091 => DB_ERROR_NOT_FOUND,
+ 1100 => DB_ERROR_NOT_LOCKED,
+ 1136 => DB_ERROR_VALUE_COUNT_ON_ROW,
+ 1142 => DB_ERROR_ACCESS_VIOLATION,
+ 1146 => DB_ERROR_NOSUCHTABLE,
+ 1216 => DB_ERROR_CONSTRAINT,
+ 1217 => DB_ERROR_CONSTRAINT,
+ 1356 => DB_ERROR_DIVZERO,
+ 1451 => DB_ERROR_CONSTRAINT,
+ 1452 => DB_ERROR_CONSTRAINT,
+ );
+
+ /**
+ * The raw database connection created by PHP
+ * @var resource
+ */
+ var $connection;
+
+ /**
+ * The DSN information for connecting to a database
+ * @var array
+ */
+ var $dsn = array();
+
+
+ /**
+ * Should data manipulation queries be committed automatically?
+ * @var bool
+ * @access private
+ */
+ var $autocommit = true;
+
+ /**
+ * The quantity of transactions begun
+ *
+ * {@internal While this is private, it can't actually be designated
+ * private in PHP 5 because it is directly accessed in the test suite.}}
+ *
+ * @var integer
+ * @access private
+ */
+ var $transaction_opcount = 0;
+
+ /**
+ * The database specified in the DSN
+ *
+ * It's a fix to allow calls to different databases in the same script.
+ *
+ * @var string
+ * @access private
+ */
+ var $_db = '';
+
+ /**
+ * Array for converting MYSQLI_*_FLAG constants to text values
+ * @var array
+ * @access public
+ * @since Property available since Release 1.6.5
+ */
+ var $mysqli_flags = array(
+ MYSQLI_NOT_NULL_FLAG => 'not_null',
+ MYSQLI_PRI_KEY_FLAG => 'primary_key',
+ MYSQLI_UNIQUE_KEY_FLAG => 'unique_key',
+ MYSQLI_MULTIPLE_KEY_FLAG => 'multiple_key',
+ MYSQLI_BLOB_FLAG => 'blob',
+ MYSQLI_UNSIGNED_FLAG => 'unsigned',
+ MYSQLI_ZEROFILL_FLAG => 'zerofill',
+ MYSQLI_AUTO_INCREMENT_FLAG => 'auto_increment',
+ MYSQLI_TIMESTAMP_FLAG => 'timestamp',
+ MYSQLI_SET_FLAG => 'set',
+ // MYSQLI_NUM_FLAG => 'numeric', // unnecessary
+ // MYSQLI_PART_KEY_FLAG => 'multiple_key', // duplicatvie
+ MYSQLI_GROUP_FLAG => 'group_by'
+ );
+
+ /**
+ * Array for converting MYSQLI_TYPE_* constants to text values
+ * @var array
+ * @access public
+ * @since Property available since Release 1.6.5
+ */
+ var $mysqli_types = array(
+ MYSQLI_TYPE_DECIMAL => 'decimal',
+ MYSQLI_TYPE_TINY => 'tinyint',
+ MYSQLI_TYPE_SHORT => 'int',
+ MYSQLI_TYPE_LONG => 'int',
+ MYSQLI_TYPE_FLOAT => 'float',
+ MYSQLI_TYPE_DOUBLE => 'double',
+ // MYSQLI_TYPE_NULL => 'DEFAULT NULL', // let flags handle it
+ MYSQLI_TYPE_TIMESTAMP => 'timestamp',
+ MYSQLI_TYPE_LONGLONG => 'bigint',
+ MYSQLI_TYPE_INT24 => 'mediumint',
+ MYSQLI_TYPE_DATE => 'date',
+ MYSQLI_TYPE_TIME => 'time',
+ MYSQLI_TYPE_DATETIME => 'datetime',
+ MYSQLI_TYPE_YEAR => 'year',
+ MYSQLI_TYPE_NEWDATE => 'date',
+ MYSQLI_TYPE_ENUM => 'enum',
+ MYSQLI_TYPE_SET => 'set',
+ MYSQLI_TYPE_TINY_BLOB => 'tinyblob',
+ MYSQLI_TYPE_MEDIUM_BLOB => 'mediumblob',
+ MYSQLI_TYPE_LONG_BLOB => 'longblob',
+ MYSQLI_TYPE_BLOB => 'blob',
+ MYSQLI_TYPE_VAR_STRING => 'varchar',
+ MYSQLI_TYPE_STRING => 'char',
+ MYSQLI_TYPE_GEOMETRY => 'geometry',
+ /* These constants are conditionally compiled in ext/mysqli, so we'll
+ * define them by number rather than constant. */
+ 16 => 'bit',
+ 246 => 'decimal',
+ );
+
+
+ // }}}
+ // {{{ constructor
+
+ /**
+ * This constructor calls <kbd>$this->DB_common()</kbd>
+ *
+ * @return void
+ */
+ function DB_mysqli()
+ {
+ $this->DB_common();
+ }
+
+ // }}}
+ // {{{ connect()
+
+ /**
+ * Connect to the database server, log in and open the database
+ *
+ * Don't call this method directly. Use DB::connect() instead.
+ *
+ * PEAR DB's mysqli driver supports the following extra DSN options:
+ * + When the 'ssl' $option passed to DB::connect() is true:
+ * + key The path to the key file.
+ * + cert The path to the certificate file.
+ * + ca The path to the certificate authority file.
+ * + capath The path to a directory that contains trusted SSL
+ * CA certificates in pem format.
+ * + cipher The list of allowable ciphers for SSL encryption.
+ *
+ * Example of how to connect using SSL:
+ * <code>
+ * require_once 'DB.php';
+ *
+ * $dsn = array(
+ * 'phptype' => 'mysqli',
+ * 'username' => 'someuser',
+ * 'password' => 'apasswd',
+ * 'hostspec' => 'localhost',
+ * 'database' => 'thedb',
+ * 'key' => 'client-key.pem',
+ * 'cert' => 'client-cert.pem',
+ * 'ca' => 'cacert.pem',
+ * 'capath' => '/path/to/ca/dir',
+ * 'cipher' => 'AES',
+ * );
+ *
+ * $options = array(
+ * 'ssl' => true,
+ * );
+ *
+ * $db = DB::connect($dsn, $options);
+ * if (PEAR::isError($db)) {
+ * die($db->getMessage());
+ * }
+ * </code>
+ *
+ * @param array $dsn the data source name
+ * @param bool $persistent should the connection be persistent?
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ */
+ function connect($dsn, $persistent = false)
+ {
+ if (!PEAR::loadExtension('mysqli')) {
+ return $this->raiseError(DB_ERROR_EXTENSION_NOT_FOUND);
+ }
+
+ $this->dsn = $dsn;
+ if ($dsn['dbsyntax']) {
+ $this->dbsyntax = $dsn['dbsyntax'];
+ }
+
+ $ini = ini_get('track_errors');
+ @ini_set('track_errors', 1);
+ $php_errormsg = '';
+
+ if (((int) $this->getOption('ssl')) === 1) {
+ $init = mysqli_init();
+ mysqli_ssl_set(
+ $init,
+ empty($dsn['key']) ? null : $dsn['key'],
+ empty($dsn['cert']) ? null : $dsn['cert'],
+ empty($dsn['ca']) ? null : $dsn['ca'],
+ empty($dsn['capath']) ? null : $dsn['capath'],
+ empty($dsn['cipher']) ? null : $dsn['cipher']
+ );
+ if ($this->connection = @mysqli_real_connect(
+ $init,
+ $dsn['hostspec'],
+ $dsn['username'],
+ $dsn['password'],
+ $dsn['database'],
+ $dsn['port'],
+ $dsn['socket']))
+ {
+ $this->connection = $init;
+ }
+ } else {
+ $this->connection = @mysqli_connect(
+ $dsn['hostspec'],
+ $dsn['username'],
+ $dsn['password'],
+ $dsn['database'],
+ $dsn['port'],
+ $dsn['socket']
+ );
+ }
+
+ @ini_set('track_errors', $ini);
+
+ if (!$this->connection) {
+ if (($err = @mysqli_connect_error()) != '') {
+ return $this->raiseError(DB_ERROR_CONNECT_FAILED,
+ null, null, null,
+ $err);
+ } else {
+ return $this->raiseError(DB_ERROR_CONNECT_FAILED,
+ null, null, null,
+ $php_errormsg);
+ }
+ }
+
+ if ($dsn['database']) {
+ $this->_db = $dsn['database'];
+ }
+
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ disconnect()
+
+ /**
+ * Disconnects from the database server
+ *
+ * @return bool TRUE on success, FALSE on failure
+ */
+ function disconnect()
+ {
+ $ret = @mysqli_close($this->connection);
+ $this->connection = null;
+ return $ret;
+ }
+
+ // }}}
+ // {{{ simpleQuery()
+
+ /**
+ * Sends a query to the database server
+ *
+ * @param string the SQL query string
+ *
+ * @return mixed + a PHP result resrouce for successful SELECT queries
+ * + the DB_OK constant for other successful queries
+ * + a DB_Error object on failure
+ */
+ function simpleQuery($query)
+ {
+ $ismanip = $this->_checkManip($query);
+ $this->last_query = $query;
+ $query = $this->modifyQuery($query);
+ if ($this->_db) {
+ if (!@mysqli_select_db($this->connection, $this->_db)) {
+ return $this->mysqliRaiseError(DB_ERROR_NODBSELECTED);
+ }
+ }
+ if (!$this->autocommit && $ismanip) {
+ if ($this->transaction_opcount == 0) {
+ $result = @mysqli_query($this->connection, 'SET AUTOCOMMIT=0');
+ $result = @mysqli_query($this->connection, 'BEGIN');
+ if (!$result) {
+ return $this->mysqliRaiseError();
+ }
+ }
+ $this->transaction_opcount++;
+ }
+ $result = @mysqli_query($this->connection, $query);
+ if (!$result) {
+ return $this->mysqliRaiseError();
+ }
+ if (is_object($result)) {
+ return $result;
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ nextResult()
+
+ /**
+ * Move the internal mysql result pointer to the next available result.
+ *
+ * This method has not been implemented yet.
+ *
+ * @param resource $result a valid sql result resource
+ * @return false
+ * @access public
+ */
+ function nextResult($result)
+ {
+ return false;
+ }
+
+ // }}}
+ // {{{ fetchInto()
+
+ /**
+ * Places a row from the result set into the given array
+ *
+ * Formating of the array and the data therein are configurable.
+ * See DB_result::fetchInto() for more information.
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::fetchInto() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result the query result resource
+ * @param array $arr the referenced array to put the data in
+ * @param int $fetchmode how the resulting array should be indexed
+ * @param int $rownum the row number to fetch (0 = first row)
+ *
+ * @return mixed DB_OK on success, NULL when the end of a result set is
+ * reached or on failure
+ *
+ * @see DB_result::fetchInto()
+ */
+ function fetchInto($result, &$arr, $fetchmode, $rownum = null)
+ {
+ if ($rownum !== null) {
+ if (!@mysqli_data_seek($result, $rownum)) {
+ return null;
+ }
+ }
+ if ($fetchmode & DB_FETCHMODE_ASSOC) {
+ $arr = @mysqli_fetch_array($result, MYSQLI_ASSOC);
+ if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE && $arr) {
+ $arr = array_change_key_case($arr, CASE_LOWER);
+ }
+ } else {
+ $arr = @mysqli_fetch_row($result);
+ }
+ if (!$arr) {
+ return null;
+ }
+ if ($this->options['portability'] & DB_PORTABILITY_RTRIM) {
+ /*
+ * Even though this DBMS already trims output, we do this because
+ * a field might have intentional whitespace at the end that
+ * gets removed by DB_PORTABILITY_RTRIM under another driver.
+ */
+ $this->_rtrimArrayValues($arr);
+ }
+ if ($this->options['portability'] & DB_PORTABILITY_NULL_TO_EMPTY) {
+ $this->_convertNullArrayValuesToEmpty($arr);
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ freeResult()
+
+ /**
+ * Deletes the result set and frees the memory occupied by the result set
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::free() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result PHP's query result resource
+ *
+ * @return bool TRUE on success, FALSE if $result is invalid
+ *
+ * @see DB_result::free()
+ */
+ function freeResult($result)
+ {
+ return is_resource($result) ? mysqli_free_result($result) : false;
+ }
+
+ // }}}
+ // {{{ numCols()
+
+ /**
+ * Gets the number of columns in a result set
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::numCols() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result PHP's query result resource
+ *
+ * @return int the number of columns. A DB_Error object on failure.
+ *
+ * @see DB_result::numCols()
+ */
+ function numCols($result)
+ {
+ $cols = @mysqli_num_fields($result);
+ if (!$cols) {
+ return $this->mysqliRaiseError();
+ }
+ return $cols;
+ }
+
+ // }}}
+ // {{{ numRows()
+
+ /**
+ * Gets the number of rows in a result set
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::numRows() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result PHP's query result resource
+ *
+ * @return int the number of rows. A DB_Error object on failure.
+ *
+ * @see DB_result::numRows()
+ */
+ function numRows($result)
+ {
+ $rows = @mysqli_num_rows($result);
+ if ($rows === null) {
+ return $this->mysqliRaiseError();
+ }
+ return $rows;
+ }
+
+ // }}}
+ // {{{ autoCommit()
+
+ /**
+ * Enables or disables automatic commits
+ *
+ * @param bool $onoff true turns it on, false turns it off
+ *
+ * @return int DB_OK on success. A DB_Error object if the driver
+ * doesn't support auto-committing transactions.
+ */
+ function autoCommit($onoff = false)
+ {
+ // XXX if $this->transaction_opcount > 0, we should probably
+ // issue a warning here.
+ $this->autocommit = $onoff ? true : false;
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ commit()
+
+ /**
+ * Commits the current transaction
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ */
+ function commit()
+ {
+ if ($this->transaction_opcount > 0) {
+ if ($this->_db) {
+ if (!@mysqli_select_db($this->connection, $this->_db)) {
+ return $this->mysqliRaiseError(DB_ERROR_NODBSELECTED);
+ }
+ }
+ $result = @mysqli_query($this->connection, 'COMMIT');
+ $result = @mysqli_query($this->connection, 'SET AUTOCOMMIT=1');
+ $this->transaction_opcount = 0;
+ if (!$result) {
+ return $this->mysqliRaiseError();
+ }
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ rollback()
+
+ /**
+ * Reverts the current transaction
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ */
+ function rollback()
+ {
+ if ($this->transaction_opcount > 0) {
+ if ($this->_db) {
+ if (!@mysqli_select_db($this->connection, $this->_db)) {
+ return $this->mysqliRaiseError(DB_ERROR_NODBSELECTED);
+ }
+ }
+ $result = @mysqli_query($this->connection, 'ROLLBACK');
+ $result = @mysqli_query($this->connection, 'SET AUTOCOMMIT=1');
+ $this->transaction_opcount = 0;
+ if (!$result) {
+ return $this->mysqliRaiseError();
+ }
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ affectedRows()
+
+ /**
+ * Determines the number of rows affected by a data maniuplation query
+ *
+ * 0 is returned for queries that don't manipulate data.
+ *
+ * @return int the number of rows. A DB_Error object on failure.
+ */
+ function affectedRows()
+ {
+ if ($this->_last_query_manip) {
+ return @mysqli_affected_rows($this->connection);
+ } else {
+ return 0;
+ }
+ }
+
+ // }}}
+ // {{{ nextId()
+
+ /**
+ * Returns the next free id in a sequence
+ *
+ * @param string $seq_name name of the sequence
+ * @param boolean $ondemand when true, the seqence is automatically
+ * created if it does not exist
+ *
+ * @return int the next id number in the sequence.
+ * A DB_Error object on failure.
+ *
+ * @see DB_common::nextID(), DB_common::getSequenceName(),
+ * DB_mysqli::createSequence(), DB_mysqli::dropSequence()
+ */
+ function nextId($seq_name, $ondemand = true)
+ {
+ $seqname = $this->getSequenceName($seq_name);
+ do {
+ $repeat = 0;
+ $this->pushErrorHandling(PEAR_ERROR_RETURN);
+ $result = $this->query('UPDATE ' . $seqname
+ . ' SET id = LAST_INSERT_ID(id + 1)');
+ $this->popErrorHandling();
+ if ($result === DB_OK) {
+ // COMMON CASE
+ $id = @mysqli_insert_id($this->connection);
+ if ($id != 0) {
+ return $id;
+ }
+
+ // EMPTY SEQ TABLE
+ // Sequence table must be empty for some reason,
+ // so fill it and return 1
+ // Obtain a user-level lock
+ $result = $this->getOne('SELECT GET_LOCK('
+ . "'${seqname}_lock', 10)");
+ if (DB::isError($result)) {
+ return $this->raiseError($result);
+ }
+ if ($result == 0) {
+ return $this->mysqliRaiseError(DB_ERROR_NOT_LOCKED);
+ }
+
+ // add the default value
+ $result = $this->query('REPLACE INTO ' . $seqname
+ . ' (id) VALUES (0)');
+ if (DB::isError($result)) {
+ return $this->raiseError($result);
+ }
+
+ // Release the lock
+ $result = $this->getOne('SELECT RELEASE_LOCK('
+ . "'${seqname}_lock')");
+ if (DB::isError($result)) {
+ return $this->raiseError($result);
+ }
+ // We know what the result will be, so no need to try again
+ return 1;
+
+ } elseif ($ondemand && DB::isError($result) &&
+ $result->getCode() == DB_ERROR_NOSUCHTABLE)
+ {
+ // ONDEMAND TABLE CREATION
+ $result = $this->createSequence($seq_name);
+
+ // Since createSequence initializes the ID to be 1,
+ // we do not need to retrieve the ID again (or we will get 2)
+ if (DB::isError($result)) {
+ return $this->raiseError($result);
+ } else {
+ // First ID of a newly created sequence is 1
+ return 1;
+ }
+
+ } elseif (DB::isError($result) &&
+ $result->getCode() == DB_ERROR_ALREADY_EXISTS)
+ {
+ // BACKWARDS COMPAT
+ // see _BCsequence() comment
+ $result = $this->_BCsequence($seqname);
+ if (DB::isError($result)) {
+ return $this->raiseError($result);
+ }
+ $repeat = 1;
+ }
+ } while ($repeat);
+
+ return $this->raiseError($result);
+ }
+
+ /**
+ * Creates a new sequence
+ *
+ * @param string $seq_name name of the new sequence
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ *
+ * @see DB_common::createSequence(), DB_common::getSequenceName(),
+ * DB_mysqli::nextID(), DB_mysqli::dropSequence()
+ */
+ function createSequence($seq_name)
+ {
+ $seqname = $this->getSequenceName($seq_name);
+ $res = $this->query('CREATE TABLE ' . $seqname
+ . ' (id INTEGER UNSIGNED AUTO_INCREMENT NOT NULL,'
+ . ' PRIMARY KEY(id))');
+ if (DB::isError($res)) {
+ return $res;
+ }
+ // insert yields value 1, nextId call will generate ID 2
+ return $this->query("INSERT INTO ${seqname} (id) VALUES (0)");
+ }
+
+ // }}}
+ // {{{ dropSequence()
+
+ /**
+ * Deletes a sequence
+ *
+ * @param string $seq_name name of the sequence to be deleted
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ *
+ * @see DB_common::dropSequence(), DB_common::getSequenceName(),
+ * DB_mysql::nextID(), DB_mysql::createSequence()
+ */
+ function dropSequence($seq_name)
+ {
+ return $this->query('DROP TABLE ' . $this->getSequenceName($seq_name));
+ }
+
+ // }}}
+ // {{{ _BCsequence()
+
+ /**
+ * Backwards compatibility with old sequence emulation implementation
+ * (clean up the dupes)
+ *
+ * @param string $seqname the sequence name to clean up
+ *
+ * @return bool true on success. A DB_Error object on failure.
+ *
+ * @access private
+ */
+ function _BCsequence($seqname)
+ {
+ // Obtain a user-level lock... this will release any previous
+ // application locks, but unlike LOCK TABLES, it does not abort
+ // the current transaction and is much less frequently used.
+ $result = $this->getOne("SELECT GET_LOCK('${seqname}_lock',10)");
+ if (DB::isError($result)) {
+ return $result;
+ }
+ if ($result == 0) {
+ // Failed to get the lock, can't do the conversion, bail
+ // with a DB_ERROR_NOT_LOCKED error
+ return $this->mysqliRaiseError(DB_ERROR_NOT_LOCKED);
+ }
+
+ $highest_id = $this->getOne("SELECT MAX(id) FROM ${seqname}");
+ if (DB::isError($highest_id)) {
+ return $highest_id;
+ }
+
+ // This should kill all rows except the highest
+ // We should probably do something if $highest_id isn't
+ // numeric, but I'm at a loss as how to handle that...
+ $result = $this->query('DELETE FROM ' . $seqname
+ . " WHERE id <> $highest_id");
+ if (DB::isError($result)) {
+ return $result;
+ }
+
+ // If another thread has been waiting for this lock,
+ // it will go thru the above procedure, but will have no
+ // real effect
+ $result = $this->getOne("SELECT RELEASE_LOCK('${seqname}_lock')");
+ if (DB::isError($result)) {
+ return $result;
+ }
+ return true;
+ }
+
+ // }}}
+ // {{{ quoteIdentifier()
+
+ /**
+ * Quotes a string so it can be safely used as a table or column name
+ * (WARNING: using names that require this is a REALLY BAD IDEA)
+ *
+ * WARNING: Older versions of MySQL can't handle the backtick
+ * character (<kbd>`</kbd>) in table or column names.
+ *
+ * @param string $str identifier name to be quoted
+ *
+ * @return string quoted identifier string
+ *
+ * @see DB_common::quoteIdentifier()
+ * @since Method available since Release 1.6.0
+ */
+ function quoteIdentifier($str)
+ {
+ return '`' . str_replace('`', '``', $str) . '`';
+ }
+
+ // }}}
+ // {{{ escapeSimple()
+
+ /**
+ * Escapes a string according to the current DBMS's standards
+ *
+ * @param string $str the string to be escaped
+ *
+ * @return string the escaped string
+ *
+ * @see DB_common::quoteSmart()
+ * @since Method available since Release 1.6.0
+ */
+ function escapeSimple($str)
+ {
+ return @mysqli_real_escape_string($this->connection, $str);
+ }
+
+ // }}}
+ // {{{ modifyLimitQuery()
+
+ /**
+ * Adds LIMIT clauses to a query string according to current DBMS standards
+ *
+ * @param string $query the query to modify
+ * @param int $from the row to start to fetching (0 = the first row)
+ * @param int $count the numbers of rows to fetch
+ * @param mixed $params array, string or numeric data to be used in
+ * execution of the statement. Quantity of items
+ * passed must match quantity of placeholders in
+ * query: meaning 1 placeholder for non-array
+ * parameters or 1 placeholder per array element.
+ *
+ * @return string the query string with LIMIT clauses added
+ *
+ * @access protected
+ */
+ function modifyLimitQuery($query, $from, $count, $params = array())
+ {
+ if (DB::isManip($query) || $this->_next_query_manip) {
+ return $query . " LIMIT $count";
+ } else {
+ return $query . " LIMIT $from, $count";
+ }
+ }
+
+ // }}}
+ // {{{ mysqliRaiseError()
+
+ /**
+ * Produces a DB_Error object regarding the current problem
+ *
+ * @param int $errno if the error is being manually raised pass a
+ * DB_ERROR* constant here. If this isn't passed
+ * the error information gathered from the DBMS.
+ *
+ * @return object the DB_Error object
+ *
+ * @see DB_common::raiseError(),
+ * DB_mysqli::errorNative(), DB_common::errorCode()
+ */
+ function mysqliRaiseError($errno = null)
+ {
+ if ($errno === null) {
+ if ($this->options['portability'] & DB_PORTABILITY_ERRORS) {
+ $this->errorcode_map[1022] = DB_ERROR_CONSTRAINT;
+ $this->errorcode_map[1048] = DB_ERROR_CONSTRAINT_NOT_NULL;
+ $this->errorcode_map[1062] = DB_ERROR_CONSTRAINT;
+ } else {
+ // Doing this in case mode changes during runtime.
+ $this->errorcode_map[1022] = DB_ERROR_ALREADY_EXISTS;
+ $this->errorcode_map[1048] = DB_ERROR_CONSTRAINT;
+ $this->errorcode_map[1062] = DB_ERROR_ALREADY_EXISTS;
+ }
+ $errno = $this->errorCode(mysqli_errno($this->connection));
+ }
+ return $this->raiseError($errno, null, null, null,
+ @mysqli_errno($this->connection) . ' ** ' .
+ @mysqli_error($this->connection));
+ }
+
+ // }}}
+ // {{{ errorNative()
+
+ /**
+ * Gets the DBMS' native error code produced by the last query
+ *
+ * @return int the DBMS' error code
+ */
+ function errorNative()
+ {
+ return @mysqli_errno($this->connection);
+ }
+
+ // }}}
+ // {{{ tableInfo()
+
+ /**
+ * Returns information about a table or a result set
+ *
+ * @param object|string $result DB_result object from a query or a
+ * string containing the name of a table.
+ * While this also accepts a query result
+ * resource identifier, this behavior is
+ * deprecated.
+ * @param int $mode a valid tableInfo mode
+ *
+ * @return array an associative array with the information requested.
+ * A DB_Error object on failure.
+ *
+ * @see DB_common::setOption()
+ */
+ function tableInfo($result, $mode = null)
+ {
+ if (is_string($result)) {
+ // Fix for bug #11580.
+ if ($this->_db) {
+ if (!@mysqli_select_db($this->connection, $this->_db)) {
+ return $this->mysqliRaiseError(DB_ERROR_NODBSELECTED);
+ }
+ }
+
+ /*
+ * Probably received a table name.
+ * Create a result resource identifier.
+ */
+ $id = @mysqli_query($this->connection,
+ "SELECT * FROM $result LIMIT 0");
+ $got_string = true;
+ } elseif (isset($result->result)) {
+ /*
+ * Probably received a result object.
+ * Extract the result resource identifier.
+ */
+ $id = $result->result;
+ $got_string = false;
+ } else {
+ /*
+ * Probably received a result resource identifier.
+ * Copy it.
+ * Deprecated. Here for compatibility only.
+ */
+ $id = $result;
+ $got_string = false;
+ }
+
+ if (!is_a($id, 'mysqli_result')) {
+ return $this->mysqliRaiseError(DB_ERROR_NEED_MORE_DATA);
+ }
+
+ if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE) {
+ $case_func = 'strtolower';
+ } else {
+ $case_func = 'strval';
+ }
+
+ $count = @mysqli_num_fields($id);
+ $res = array();
+
+ if ($mode) {
+ $res['num_fields'] = $count;
+ }
+
+ for ($i = 0; $i < $count; $i++) {
+ $tmp = @mysqli_fetch_field($id);
+
+ $flags = '';
+ foreach ($this->mysqli_flags as $const => $means) {
+ if ($tmp->flags & $const) {
+ $flags .= $means . ' ';
+ }
+ }
+ if ($tmp->def) {
+ $flags .= 'default_' . rawurlencode($tmp->def);
+ }
+ $flags = trim($flags);
+
+ $res[$i] = array(
+ 'table' => $case_func($tmp->table),
+ 'name' => $case_func($tmp->name),
+ 'type' => isset($this->mysqli_types[$tmp->type])
+ ? $this->mysqli_types[$tmp->type]
+ : 'unknown',
+ // http://bugs.php.net/?id=36579
+ 'len' => $tmp->length,
+ 'flags' => $flags,
+ );
+
+ if ($mode & DB_TABLEINFO_ORDER) {
+ $res['order'][$res[$i]['name']] = $i;
+ }
+ if ($mode & DB_TABLEINFO_ORDERTABLE) {
+ $res['ordertable'][$res[$i]['table']][$res[$i]['name']] = $i;
+ }
+ }
+
+ // free the result only if we were called on a table
+ if ($got_string) {
+ @mysqli_free_result($id);
+ }
+ return $res;
+ }
+
+ // }}}
+ // {{{ getSpecialQuery()
+
+ /**
+ * Obtains the query string needed for listing a given type of objects
+ *
+ * @param string $type the kind of objects you want to retrieve
+ *
+ * @return string the SQL query string or null if the driver doesn't
+ * support the object type requested
+ *
+ * @access protected
+ * @see DB_common::getListOf()
+ */
+ function getSpecialQuery($type)
+ {
+ switch ($type) {
+ case 'tables':
+ return 'SHOW TABLES';
+ case 'users':
+ return 'SELECT DISTINCT User FROM mysql.user';
+ case 'databases':
+ return 'SHOW DATABASES';
+ default:
+ return null;
+ }
+ }
+
+ // }}}
+
+}
+
+/*
+ * Local variables:
+ * tab-width: 4
+ * c-basic-offset: 4
+ * End:
+ */
+
+?>
diff --git a/extlib/DB/oci8.php b/extlib/DB/oci8.php
new file mode 100644
index 000000000..d30794871
--- /dev/null
+++ b/extlib/DB/oci8.php
@@ -0,0 +1,1156 @@
+<?php
+
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * The PEAR DB driver for PHP's oci8 extension
+ * for interacting with Oracle databases
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt. If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category Database
+ * @package DB
+ * @author James L. Pine <jlp@valinux.com>
+ * @author Daniel Convissor <danielc@php.net>
+ * @copyright 1997-2007 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0
+ * @version CVS: $Id: oci8.php,v 1.116 2007/11/28 02:22:39 aharvey Exp $
+ * @link http://pear.php.net/package/DB
+ */
+
+/**
+ * Obtain the DB_common class so it can be extended from
+ */
+require_once 'DB/common.php';
+
+/**
+ * The methods PEAR DB uses to interact with PHP's oci8 extension
+ * for interacting with Oracle databases
+ *
+ * Definitely works with versions 8 and 9 of Oracle.
+ *
+ * These methods overload the ones declared in DB_common.
+ *
+ * Be aware... OCIError() only appears to return anything when given a
+ * statement, so functions return the generic DB_ERROR instead of more
+ * useful errors that have to do with feedback from the database.
+ *
+ * @category Database
+ * @package DB
+ * @author James L. Pine <jlp@valinux.com>
+ * @author Daniel Convissor <danielc@php.net>
+ * @copyright 1997-2007 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0
+ * @version Release: 1.7.14RC1
+ * @link http://pear.php.net/package/DB
+ */
+class DB_oci8 extends DB_common
+{
+ // {{{ properties
+
+ /**
+ * The DB driver type (mysql, oci8, odbc, etc.)
+ * @var string
+ */
+ var $phptype = 'oci8';
+
+ /**
+ * The database syntax variant to be used (db2, access, etc.), if any
+ * @var string
+ */
+ var $dbsyntax = 'oci8';
+
+ /**
+ * The capabilities of this DB implementation
+ *
+ * The 'new_link' element contains the PHP version that first provided
+ * new_link support for this DBMS. Contains false if it's unsupported.
+ *
+ * Meaning of the 'limit' element:
+ * + 'emulate' = emulate with fetch row by number
+ * + 'alter' = alter the query
+ * + false = skip rows
+ *
+ * @var array
+ */
+ var $features = array(
+ 'limit' => 'alter',
+ 'new_link' => '5.0.0',
+ 'numrows' => 'subquery',
+ 'pconnect' => true,
+ 'prepare' => true,
+ 'ssl' => false,
+ 'transactions' => true,
+ );
+
+ /**
+ * A mapping of native error codes to DB error codes
+ * @var array
+ */
+ var $errorcode_map = array(
+ 1 => DB_ERROR_CONSTRAINT,
+ 900 => DB_ERROR_SYNTAX,
+ 904 => DB_ERROR_NOSUCHFIELD,
+ 913 => DB_ERROR_VALUE_COUNT_ON_ROW,
+ 921 => DB_ERROR_SYNTAX,
+ 923 => DB_ERROR_SYNTAX,
+ 942 => DB_ERROR_NOSUCHTABLE,
+ 955 => DB_ERROR_ALREADY_EXISTS,
+ 1400 => DB_ERROR_CONSTRAINT_NOT_NULL,
+ 1401 => DB_ERROR_INVALID,
+ 1407 => DB_ERROR_CONSTRAINT_NOT_NULL,
+ 1418 => DB_ERROR_NOT_FOUND,
+ 1476 => DB_ERROR_DIVZERO,
+ 1722 => DB_ERROR_INVALID_NUMBER,
+ 2289 => DB_ERROR_NOSUCHTABLE,
+ 2291 => DB_ERROR_CONSTRAINT,
+ 2292 => DB_ERROR_CONSTRAINT,
+ 2449 => DB_ERROR_CONSTRAINT,
+ 12899 => DB_ERROR_INVALID,
+ );
+
+ /**
+ * The raw database connection created by PHP
+ * @var resource
+ */
+ var $connection;
+
+ /**
+ * The DSN information for connecting to a database
+ * @var array
+ */
+ var $dsn = array();
+
+
+ /**
+ * Should data manipulation queries be committed automatically?
+ * @var bool
+ * @access private
+ */
+ var $autocommit = true;
+
+ /**
+ * Stores the $data passed to execute() in the oci8 driver
+ *
+ * Gets reset to array() when simpleQuery() is run.
+ *
+ * Needed in case user wants to call numRows() after prepare/execute
+ * was used.
+ *
+ * @var array
+ * @access private
+ */
+ var $_data = array();
+
+ /**
+ * The result or statement handle from the most recently executed query
+ * @var resource
+ */
+ var $last_stmt;
+
+ /**
+ * Is the given prepared statement a data manipulation query?
+ * @var array
+ * @access private
+ */
+ var $manip_query = array();
+
+ /**
+ * Store of prepared SQL queries.
+ * @var array
+ * @access private
+ */
+ var $_prepared_queries = array();
+
+
+ // }}}
+ // {{{ constructor
+
+ /**
+ * This constructor calls <kbd>$this->DB_common()</kbd>
+ *
+ * @return void
+ */
+ function DB_oci8()
+ {
+ $this->DB_common();
+ }
+
+ // }}}
+ // {{{ connect()
+
+ /**
+ * Connect to the database server, log in and open the database
+ *
+ * Don't call this method directly. Use DB::connect() instead.
+ *
+ * If PHP is at version 5.0.0 or greater:
+ * + Generally, oci_connect() or oci_pconnect() are used.
+ * + But if the new_link DSN option is set to true, oci_new_connect()
+ * is used.
+ *
+ * When using PHP version 4.x, OCILogon() or OCIPLogon() are used.
+ *
+ * PEAR DB's oci8 driver supports the following extra DSN options:
+ * + charset The character set to be used on the connection.
+ * Only used if PHP is at version 5.0.0 or greater
+ * and the Oracle server is at 9.2 or greater.
+ * Available since PEAR DB 1.7.0.
+ * + new_link If set to true, causes subsequent calls to
+ * connect() to return a new connection link
+ * instead of the existing one. WARNING: this is
+ * not portable to other DBMS's.
+ * Available since PEAR DB 1.7.0.
+ *
+ * @param array $dsn the data source name
+ * @param bool $persistent should the connection be persistent?
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ */
+ function connect($dsn, $persistent = false)
+ {
+ if (!PEAR::loadExtension('oci8')) {
+ return $this->raiseError(DB_ERROR_EXTENSION_NOT_FOUND);
+ }
+
+ $this->dsn = $dsn;
+ if ($dsn['dbsyntax']) {
+ $this->dbsyntax = $dsn['dbsyntax'];
+ }
+
+ // Backwards compatibility with DB < 1.7.0
+ if (empty($dsn['database']) && !empty($dsn['hostspec'])) {
+ $db = $dsn['hostspec'];
+ } else {
+ $db = $dsn['database'];
+ }
+
+ if (function_exists('oci_connect')) {
+ if (isset($dsn['new_link'])
+ && ($dsn['new_link'] == 'true' || $dsn['new_link'] === true))
+ {
+ $connect_function = 'oci_new_connect';
+ } else {
+ $connect_function = $persistent ? 'oci_pconnect'
+ : 'oci_connect';
+ }
+ if (isset($this->dsn['port']) && $this->dsn['port']) {
+ $db = '//'.$db.':'.$this->dsn['port'];
+ }
+
+ $char = empty($dsn['charset']) ? null : $dsn['charset'];
+ $this->connection = @$connect_function($dsn['username'],
+ $dsn['password'],
+ $db,
+ $char);
+ $error = OCIError();
+ if (!empty($error) && $error['code'] == 12541) {
+ // Couldn't find TNS listener. Try direct connection.
+ $this->connection = @$connect_function($dsn['username'],
+ $dsn['password'],
+ null,
+ $char);
+ }
+ } else {
+ $connect_function = $persistent ? 'OCIPLogon' : 'OCILogon';
+ if ($db) {
+ $this->connection = @$connect_function($dsn['username'],
+ $dsn['password'],
+ $db);
+ } elseif ($dsn['username'] || $dsn['password']) {
+ $this->connection = @$connect_function($dsn['username'],
+ $dsn['password']);
+ }
+ }
+
+ if (!$this->connection) {
+ $error = OCIError();
+ $error = (is_array($error)) ? $error['message'] : null;
+ return $this->raiseError(DB_ERROR_CONNECT_FAILED,
+ null, null, null,
+ $error);
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ disconnect()
+
+ /**
+ * Disconnects from the database server
+ *
+ * @return bool TRUE on success, FALSE on failure
+ */
+ function disconnect()
+ {
+ if (function_exists('oci_close')) {
+ $ret = @oci_close($this->connection);
+ } else {
+ $ret = @OCILogOff($this->connection);
+ }
+ $this->connection = null;
+ return $ret;
+ }
+
+ // }}}
+ // {{{ simpleQuery()
+
+ /**
+ * Sends a query to the database server
+ *
+ * To determine how many rows of a result set get buffered using
+ * ocisetprefetch(), see the "result_buffering" option in setOptions().
+ * This option was added in Release 1.7.0.
+ *
+ * @param string the SQL query string
+ *
+ * @return mixed + a PHP result resrouce for successful SELECT queries
+ * + the DB_OK constant for other successful queries
+ * + a DB_Error object on failure
+ */
+ function simpleQuery($query)
+ {
+ $this->_data = array();
+ $this->last_parameters = array();
+ $this->last_query = $query;
+ $query = $this->modifyQuery($query);
+ $result = @OCIParse($this->connection, $query);
+ if (!$result) {
+ return $this->oci8RaiseError();
+ }
+ if ($this->autocommit) {
+ $success = @OCIExecute($result,OCI_COMMIT_ON_SUCCESS);
+ } else {
+ $success = @OCIExecute($result,OCI_DEFAULT);
+ }
+ if (!$success) {
+ return $this->oci8RaiseError($result);
+ }
+ $this->last_stmt = $result;
+ if ($this->_checkManip($query)) {
+ return DB_OK;
+ } else {
+ @ocisetprefetch($result, $this->options['result_buffering']);
+ return $result;
+ }
+ }
+
+ // }}}
+ // {{{ nextResult()
+
+ /**
+ * Move the internal oracle result pointer to the next available result
+ *
+ * @param a valid oci8 result resource
+ *
+ * @access public
+ *
+ * @return true if a result is available otherwise return false
+ */
+ function nextResult($result)
+ {
+ return false;
+ }
+
+ // }}}
+ // {{{ fetchInto()
+
+ /**
+ * Places a row from the result set into the given array
+ *
+ * Formating of the array and the data therein are configurable.
+ * See DB_result::fetchInto() for more information.
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::fetchInto() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result the query result resource
+ * @param array $arr the referenced array to put the data in
+ * @param int $fetchmode how the resulting array should be indexed
+ * @param int $rownum the row number to fetch (0 = first row)
+ *
+ * @return mixed DB_OK on success, NULL when the end of a result set is
+ * reached or on failure
+ *
+ * @see DB_result::fetchInto()
+ */
+ function fetchInto($result, &$arr, $fetchmode, $rownum = null)
+ {
+ if ($rownum !== null) {
+ return $this->raiseError(DB_ERROR_NOT_CAPABLE);
+ }
+ if ($fetchmode & DB_FETCHMODE_ASSOC) {
+ $moredata = @OCIFetchInto($result,$arr,OCI_ASSOC+OCI_RETURN_NULLS+OCI_RETURN_LOBS);
+ if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE &&
+ $moredata)
+ {
+ $arr = array_change_key_case($arr, CASE_LOWER);
+ }
+ } else {
+ $moredata = OCIFetchInto($result,$arr,OCI_RETURN_NULLS+OCI_RETURN_LOBS);
+ }
+ if (!$moredata) {
+ return null;
+ }
+ if ($this->options['portability'] & DB_PORTABILITY_RTRIM) {
+ $this->_rtrimArrayValues($arr);
+ }
+ if ($this->options['portability'] & DB_PORTABILITY_NULL_TO_EMPTY) {
+ $this->_convertNullArrayValuesToEmpty($arr);
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ freeResult()
+
+ /**
+ * Deletes the result set and frees the memory occupied by the result set
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::free() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result PHP's query result resource
+ *
+ * @return bool TRUE on success, FALSE if $result is invalid
+ *
+ * @see DB_result::free()
+ */
+ function freeResult($result)
+ {
+ return is_resource($result) ? OCIFreeStatement($result) : false;
+ }
+
+ /**
+ * Frees the internal resources associated with a prepared query
+ *
+ * @param resource $stmt the prepared statement's resource
+ * @param bool $free_resource should the PHP resource be freed too?
+ * Use false if you need to get data
+ * from the result set later.
+ *
+ * @return bool TRUE on success, FALSE if $result is invalid
+ *
+ * @see DB_oci8::prepare()
+ */
+ function freePrepared($stmt, $free_resource = true)
+ {
+ if (!is_resource($stmt)) {
+ return false;
+ }
+ if ($free_resource) {
+ @ocifreestatement($stmt);
+ }
+ if (isset($this->prepare_types[(int)$stmt])) {
+ unset($this->prepare_types[(int)$stmt]);
+ unset($this->manip_query[(int)$stmt]);
+ } else {
+ return false;
+ }
+ return true;
+ }
+
+ // }}}
+ // {{{ numRows()
+
+ /**
+ * Gets the number of rows in a result set
+ *
+ * Only works if the DB_PORTABILITY_NUMROWS portability option
+ * is turned on.
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::numRows() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result PHP's query result resource
+ *
+ * @return int the number of rows. A DB_Error object on failure.
+ *
+ * @see DB_result::numRows(), DB_common::setOption()
+ */
+ function numRows($result)
+ {
+ // emulate numRows for Oracle. yuck.
+ if ($this->options['portability'] & DB_PORTABILITY_NUMROWS &&
+ $result === $this->last_stmt)
+ {
+ $countquery = 'SELECT COUNT(*) FROM ('.$this->last_query.')';
+ $save_query = $this->last_query;
+ $save_stmt = $this->last_stmt;
+
+ $count = $this->query($countquery);
+
+ // Restore the last query and statement.
+ $this->last_query = $save_query;
+ $this->last_stmt = $save_stmt;
+
+ if (DB::isError($count) ||
+ DB::isError($row = $count->fetchRow(DB_FETCHMODE_ORDERED)))
+ {
+ return $this->raiseError(DB_ERROR_NOT_CAPABLE);
+ }
+
+ return $row[0];
+ }
+ return $this->raiseError(DB_ERROR_NOT_CAPABLE);
+ }
+
+ // }}}
+ // {{{ numCols()
+
+ /**
+ * Gets the number of columns in a result set
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::numCols() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result PHP's query result resource
+ *
+ * @return int the number of columns. A DB_Error object on failure.
+ *
+ * @see DB_result::numCols()
+ */
+ function numCols($result)
+ {
+ $cols = @OCINumCols($result);
+ if (!$cols) {
+ return $this->oci8RaiseError($result);
+ }
+ return $cols;
+ }
+
+ // }}}
+ // {{{ prepare()
+
+ /**
+ * Prepares a query for multiple execution with execute().
+ *
+ * With oci8, this is emulated.
+ *
+ * prepare() requires a generic query as string like <code>
+ * INSERT INTO numbers VALUES (?, ?, ?)
+ * </code>. The <kbd>?</kbd> characters are placeholders.
+ *
+ * Three types of placeholders can be used:
+ * + <kbd>?</kbd> a quoted scalar value, i.e. strings, integers
+ * + <kbd>!</kbd> value is inserted 'as is'
+ * + <kbd>&</kbd> requires a file name. The file's contents get
+ * inserted into the query (i.e. saving binary
+ * data in a db)
+ *
+ * Use backslashes to escape placeholder characters if you don't want
+ * them to be interpreted as placeholders. Example: <code>
+ * "UPDATE foo SET col=? WHERE col='over \& under'"
+ * </code>
+ *
+ * @param string $query the query to be prepared
+ *
+ * @return mixed DB statement resource on success. DB_Error on failure.
+ *
+ * @see DB_oci8::execute()
+ */
+ function prepare($query)
+ {
+ $tokens = preg_split('/((?<!\\\)[&?!])/', $query, -1,
+ PREG_SPLIT_DELIM_CAPTURE);
+ $binds = count($tokens) - 1;
+ $token = 0;
+ $types = array();
+ $newquery = '';
+
+ foreach ($tokens as $key => $val) {
+ switch ($val) {
+ case '?':
+ $types[$token++] = DB_PARAM_SCALAR;
+ unset($tokens[$key]);
+ break;
+ case '&':
+ $types[$token++] = DB_PARAM_OPAQUE;
+ unset($tokens[$key]);
+ break;
+ case '!':
+ $types[$token++] = DB_PARAM_MISC;
+ unset($tokens[$key]);
+ break;
+ default:
+ $tokens[$key] = preg_replace('/\\\([&?!])/', "\\1", $val);
+ if ($key != $binds) {
+ $newquery .= $tokens[$key] . ':bind' . $token;
+ } else {
+ $newquery .= $tokens[$key];
+ }
+ }
+ }
+
+ $this->last_query = $query;
+ $newquery = $this->modifyQuery($newquery);
+ if (!$stmt = @OCIParse($this->connection, $newquery)) {
+ return $this->oci8RaiseError();
+ }
+ $this->prepare_types[(int)$stmt] = $types;
+ $this->manip_query[(int)$stmt] = DB::isManip($query);
+ $this->_prepared_queries[(int)$stmt] = $newquery;
+ return $stmt;
+ }
+
+ // }}}
+ // {{{ execute()
+
+ /**
+ * Executes a DB statement prepared with prepare().
+ *
+ * To determine how many rows of a result set get buffered using
+ * ocisetprefetch(), see the "result_buffering" option in setOptions().
+ * This option was added in Release 1.7.0.
+ *
+ * @param resource $stmt a DB statement resource returned from prepare()
+ * @param mixed $data array, string or numeric data to be used in
+ * execution of the statement. Quantity of items
+ * passed must match quantity of placeholders in
+ * query: meaning 1 for non-array items or the
+ * quantity of elements in the array.
+ *
+ * @return mixed returns an oic8 result resource for successful SELECT
+ * queries, DB_OK for other successful queries.
+ * A DB error object is returned on failure.
+ *
+ * @see DB_oci8::prepare()
+ */
+ function &execute($stmt, $data = array())
+ {
+ $data = (array)$data;
+ $this->last_parameters = $data;
+ $this->last_query = $this->_prepared_queries[(int)$stmt];
+ $this->_data = $data;
+
+ $types = $this->prepare_types[(int)$stmt];
+ if (count($types) != count($data)) {
+ $tmp = $this->raiseError(DB_ERROR_MISMATCH);
+ return $tmp;
+ }
+
+ $i = 0;
+ foreach ($data as $key => $value) {
+ if ($types[$i] == DB_PARAM_MISC) {
+ /*
+ * Oracle doesn't seem to have the ability to pass a
+ * parameter along unchanged, so strip off quotes from start
+ * and end, plus turn two single quotes to one single quote,
+ * in order to avoid the quotes getting escaped by
+ * Oracle and ending up in the database.
+ */
+ $data[$key] = preg_replace("/^'(.*)'$/", "\\1", $data[$key]);
+ $data[$key] = str_replace("''", "'", $data[$key]);
+ } elseif ($types[$i] == DB_PARAM_OPAQUE) {
+ $fp = @fopen($data[$key], 'rb');
+ if (!$fp) {
+ $tmp = $this->raiseError(DB_ERROR_ACCESS_VIOLATION);
+ return $tmp;
+ }
+ $data[$key] = fread($fp, filesize($data[$key]));
+ fclose($fp);
+ } elseif ($types[$i] == DB_PARAM_SCALAR) {
+ // Floats have to be converted to a locale-neutral
+ // representation.
+ if (is_float($data[$key])) {
+ $data[$key] = $this->quoteFloat($data[$key]);
+ }
+ }
+ if (!@OCIBindByName($stmt, ':bind' . $i, $data[$key], -1)) {
+ $tmp = $this->oci8RaiseError($stmt);
+ return $tmp;
+ }
+ $this->last_query = preg_replace("/:bind$i/",$this->quoteSmart($data[$key]),$this->last_query,1);
+ $i++;
+ }
+ if ($this->autocommit) {
+ $success = @OCIExecute($stmt, OCI_COMMIT_ON_SUCCESS);
+ } else {
+ $success = @OCIExecute($stmt, OCI_DEFAULT);
+ }
+ if (!$success) {
+ $tmp = $this->oci8RaiseError($stmt);
+ return $tmp;
+ }
+ $this->last_stmt = $stmt;
+ if ($this->manip_query[(int)$stmt] || $this->_next_query_manip) {
+ $this->_last_query_manip = true;
+ $this->_next_query_manip = false;
+ $tmp = DB_OK;
+ } else {
+ $this->_last_query_manip = false;
+ @ocisetprefetch($stmt, $this->options['result_buffering']);
+ $tmp = new DB_result($this, $stmt);
+ }
+ return $tmp;
+ }
+
+ // }}}
+ // {{{ autoCommit()
+
+ /**
+ * Enables or disables automatic commits
+ *
+ * @param bool $onoff true turns it on, false turns it off
+ *
+ * @return int DB_OK on success. A DB_Error object if the driver
+ * doesn't support auto-committing transactions.
+ */
+ function autoCommit($onoff = false)
+ {
+ $this->autocommit = (bool)$onoff;;
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ commit()
+
+ /**
+ * Commits the current transaction
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ */
+ function commit()
+ {
+ $result = @OCICommit($this->connection);
+ if (!$result) {
+ return $this->oci8RaiseError();
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ rollback()
+
+ /**
+ * Reverts the current transaction
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ */
+ function rollback()
+ {
+ $result = @OCIRollback($this->connection);
+ if (!$result) {
+ return $this->oci8RaiseError();
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ affectedRows()
+
+ /**
+ * Determines the number of rows affected by a data maniuplation query
+ *
+ * 0 is returned for queries that don't manipulate data.
+ *
+ * @return int the number of rows. A DB_Error object on failure.
+ */
+ function affectedRows()
+ {
+ if ($this->last_stmt === false) {
+ return $this->oci8RaiseError();
+ }
+ $result = @OCIRowCount($this->last_stmt);
+ if ($result === false) {
+ return $this->oci8RaiseError($this->last_stmt);
+ }
+ return $result;
+ }
+
+ // }}}
+ // {{{ modifyQuery()
+
+ /**
+ * Changes a query string for various DBMS specific reasons
+ *
+ * "SELECT 2+2" must be "SELECT 2+2 FROM dual" in Oracle.
+ *
+ * @param string $query the query string to modify
+ *
+ * @return string the modified query string
+ *
+ * @access protected
+ */
+ function modifyQuery($query)
+ {
+ if (preg_match('/^\s*SELECT/i', $query) &&
+ !preg_match('/\sFROM\s/i', $query)) {
+ $query .= ' FROM dual';
+ }
+ return $query;
+ }
+
+ // }}}
+ // {{{ modifyLimitQuery()
+
+ /**
+ * Adds LIMIT clauses to a query string according to current DBMS standards
+ *
+ * @param string $query the query to modify
+ * @param int $from the row to start to fetching (0 = the first row)
+ * @param int $count the numbers of rows to fetch
+ * @param mixed $params array, string or numeric data to be used in
+ * execution of the statement. Quantity of items
+ * passed must match quantity of placeholders in
+ * query: meaning 1 placeholder for non-array
+ * parameters or 1 placeholder per array element.
+ *
+ * @return string the query string with LIMIT clauses added
+ *
+ * @access protected
+ */
+ function modifyLimitQuery($query, $from, $count, $params = array())
+ {
+ // Let Oracle return the name of the columns instead of
+ // coding a "home" SQL parser
+
+ if (count($params)) {
+ $result = $this->prepare("SELECT * FROM ($query) "
+ . 'WHERE NULL = NULL');
+ $tmp = $this->execute($result, $params);
+ } else {
+ $q_fields = "SELECT * FROM ($query) WHERE NULL = NULL";
+
+ if (!$result = @OCIParse($this->connection, $q_fields)) {
+ $this->last_query = $q_fields;
+ return $this->oci8RaiseError();
+ }
+ if (!@OCIExecute($result, OCI_DEFAULT)) {
+ $this->last_query = $q_fields;
+ return $this->oci8RaiseError($result);
+ }
+ }
+
+ $ncols = OCINumCols($result);
+ $cols = array();
+ for ( $i = 1; $i <= $ncols; $i++ ) {
+ $cols[] = '"' . OCIColumnName($result, $i) . '"';
+ }
+ $fields = implode(', ', $cols);
+ // XXX Test that (tip by John Lim)
+ //if (preg_match('/^\s*SELECT\s+/is', $query, $match)) {
+ // // Introduce the FIRST_ROWS Oracle query optimizer
+ // $query = substr($query, strlen($match[0]), strlen($query));
+ // $query = "SELECT /* +FIRST_ROWS */ " . $query;
+ //}
+
+ // Construct the query
+ // more at: http://marc.theaimsgroup.com/?l=php-db&m=99831958101212&w=2
+ // Perhaps this could be optimized with the use of Unions
+ $query = "SELECT $fields FROM".
+ " (SELECT rownum as linenum, $fields FROM".
+ " ($query)".
+ ' WHERE rownum <= '. ($from + $count) .
+ ') WHERE linenum >= ' . ++$from;
+ return $query;
+ }
+
+ // }}}
+ // {{{ nextId()
+
+ /**
+ * Returns the next free id in a sequence
+ *
+ * @param string $seq_name name of the sequence
+ * @param boolean $ondemand when true, the seqence is automatically
+ * created if it does not exist
+ *
+ * @return int the next id number in the sequence.
+ * A DB_Error object on failure.
+ *
+ * @see DB_common::nextID(), DB_common::getSequenceName(),
+ * DB_oci8::createSequence(), DB_oci8::dropSequence()
+ */
+ function nextId($seq_name, $ondemand = true)
+ {
+ $seqname = $this->getSequenceName($seq_name);
+ $repeat = 0;
+ do {
+ $this->expectError(DB_ERROR_NOSUCHTABLE);
+ $result = $this->query("SELECT ${seqname}.nextval FROM dual");
+ $this->popExpect();
+ if ($ondemand && DB::isError($result) &&
+ $result->getCode() == DB_ERROR_NOSUCHTABLE) {
+ $repeat = 1;
+ $result = $this->createSequence($seq_name);
+ if (DB::isError($result)) {
+ return $this->raiseError($result);
+ }
+ } else {
+ $repeat = 0;
+ }
+ } while ($repeat);
+ if (DB::isError($result)) {
+ return $this->raiseError($result);
+ }
+ $arr = $result->fetchRow(DB_FETCHMODE_ORDERED);
+ return $arr[0];
+ }
+
+ /**
+ * Creates a new sequence
+ *
+ * @param string $seq_name name of the new sequence
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ *
+ * @see DB_common::createSequence(), DB_common::getSequenceName(),
+ * DB_oci8::nextID(), DB_oci8::dropSequence()
+ */
+ function createSequence($seq_name)
+ {
+ return $this->query('CREATE SEQUENCE '
+ . $this->getSequenceName($seq_name));
+ }
+
+ // }}}
+ // {{{ dropSequence()
+
+ /**
+ * Deletes a sequence
+ *
+ * @param string $seq_name name of the sequence to be deleted
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ *
+ * @see DB_common::dropSequence(), DB_common::getSequenceName(),
+ * DB_oci8::nextID(), DB_oci8::createSequence()
+ */
+ function dropSequence($seq_name)
+ {
+ return $this->query('DROP SEQUENCE '
+ . $this->getSequenceName($seq_name));
+ }
+
+ // }}}
+ // {{{ oci8RaiseError()
+
+ /**
+ * Produces a DB_Error object regarding the current problem
+ *
+ * @param int $errno if the error is being manually raised pass a
+ * DB_ERROR* constant here. If this isn't passed
+ * the error information gathered from the DBMS.
+ *
+ * @return object the DB_Error object
+ *
+ * @see DB_common::raiseError(),
+ * DB_oci8::errorNative(), DB_oci8::errorCode()
+ */
+ function oci8RaiseError($errno = null)
+ {
+ if ($errno === null) {
+ $error = @OCIError($this->connection);
+ return $this->raiseError($this->errorCode($error['code']),
+ null, null, null, $error['message']);
+ } elseif (is_resource($errno)) {
+ $error = @OCIError($errno);
+ return $this->raiseError($this->errorCode($error['code']),
+ null, null, null, $error['message']);
+ }
+ return $this->raiseError($this->errorCode($errno));
+ }
+
+ // }}}
+ // {{{ errorNative()
+
+ /**
+ * Gets the DBMS' native error code produced by the last query
+ *
+ * @return int the DBMS' error code. FALSE if the code could not be
+ * determined
+ */
+ function errorNative()
+ {
+ if (is_resource($this->last_stmt)) {
+ $error = @OCIError($this->last_stmt);
+ } else {
+ $error = @OCIError($this->connection);
+ }
+ if (is_array($error)) {
+ return $error['code'];
+ }
+ return false;
+ }
+
+ // }}}
+ // {{{ tableInfo()
+
+ /**
+ * Returns information about a table or a result set
+ *
+ * NOTE: only supports 'table' and 'flags' if <var>$result</var>
+ * is a table name.
+ *
+ * NOTE: flags won't contain index information.
+ *
+ * @param object|string $result DB_result object from a query or a
+ * string containing the name of a table.
+ * While this also accepts a query result
+ * resource identifier, this behavior is
+ * deprecated.
+ * @param int $mode a valid tableInfo mode
+ *
+ * @return array an associative array with the information requested.
+ * A DB_Error object on failure.
+ *
+ * @see DB_common::tableInfo()
+ */
+ function tableInfo($result, $mode = null)
+ {
+ if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE) {
+ $case_func = 'strtolower';
+ } else {
+ $case_func = 'strval';
+ }
+
+ $res = array();
+
+ if (is_string($result)) {
+ /*
+ * Probably received a table name.
+ * Create a result resource identifier.
+ */
+ $result = strtoupper($result);
+ $q_fields = 'SELECT column_name, data_type, data_length, '
+ . 'nullable '
+ . 'FROM user_tab_columns '
+ . "WHERE table_name='$result' ORDER BY column_id";
+
+ $this->last_query = $q_fields;
+
+ if (!$stmt = @OCIParse($this->connection, $q_fields)) {
+ return $this->oci8RaiseError(DB_ERROR_NEED_MORE_DATA);
+ }
+ if (!@OCIExecute($stmt, OCI_DEFAULT)) {
+ return $this->oci8RaiseError($stmt);
+ }
+
+ $i = 0;
+ while (@OCIFetch($stmt)) {
+ $res[$i] = array(
+ 'table' => $case_func($result),
+ 'name' => $case_func(@OCIResult($stmt, 1)),
+ 'type' => @OCIResult($stmt, 2),
+ 'len' => @OCIResult($stmt, 3),
+ 'flags' => (@OCIResult($stmt, 4) == 'N') ? 'not_null' : '',
+ );
+ if ($mode & DB_TABLEINFO_ORDER) {
+ $res['order'][$res[$i]['name']] = $i;
+ }
+ if ($mode & DB_TABLEINFO_ORDERTABLE) {
+ $res['ordertable'][$res[$i]['table']][$res[$i]['name']] = $i;
+ }
+ $i++;
+ }
+
+ if ($mode) {
+ $res['num_fields'] = $i;
+ }
+ @OCIFreeStatement($stmt);
+
+ } else {
+ if (isset($result->result)) {
+ /*
+ * Probably received a result object.
+ * Extract the result resource identifier.
+ */
+ $result = $result->result;
+ }
+
+ $res = array();
+
+ if ($result === $this->last_stmt) {
+ $count = @OCINumCols($result);
+ if ($mode) {
+ $res['num_fields'] = $count;
+ }
+ for ($i = 0; $i < $count; $i++) {
+ $res[$i] = array(
+ 'table' => '',
+ 'name' => $case_func(@OCIColumnName($result, $i+1)),
+ 'type' => @OCIColumnType($result, $i+1),
+ 'len' => @OCIColumnSize($result, $i+1),
+ 'flags' => '',
+ );
+ if ($mode & DB_TABLEINFO_ORDER) {
+ $res['order'][$res[$i]['name']] = $i;
+ }
+ if ($mode & DB_TABLEINFO_ORDERTABLE) {
+ $res['ordertable'][$res[$i]['table']][$res[$i]['name']] = $i;
+ }
+ }
+ } else {
+ return $this->raiseError(DB_ERROR_NOT_CAPABLE);
+ }
+ }
+ return $res;
+ }
+
+ // }}}
+ // {{{ getSpecialQuery()
+
+ /**
+ * Obtains the query string needed for listing a given type of objects
+ *
+ * @param string $type the kind of objects you want to retrieve
+ *
+ * @return string the SQL query string or null if the driver doesn't
+ * support the object type requested
+ *
+ * @access protected
+ * @see DB_common::getListOf()
+ */
+ function getSpecialQuery($type)
+ {
+ switch ($type) {
+ case 'tables':
+ return 'SELECT table_name FROM user_tables';
+ case 'synonyms':
+ return 'SELECT synonym_name FROM user_synonyms';
+ case 'views':
+ return 'SELECT view_name FROM user_views';
+ default:
+ return null;
+ }
+ }
+
+ // }}}
+ // {{{ quoteFloat()
+
+ /**
+ * Formats a float value for use within a query in a locale-independent
+ * manner.
+ *
+ * @param float the float value to be quoted.
+ * @return string the quoted string.
+ * @see DB_common::quoteSmart()
+ * @since Method available since release 1.7.8.
+ */
+ function quoteFloat($float) {
+ return $this->escapeSimple(str_replace(',', '.', strval(floatval($float))));
+ }
+
+ // }}}
+
+}
+
+/*
+ * Local variables:
+ * tab-width: 4
+ * c-basic-offset: 4
+ * End:
+ */
+
+?>
diff --git a/extlib/DB/odbc.php b/extlib/DB/odbc.php
new file mode 100644
index 000000000..eba43659a
--- /dev/null
+++ b/extlib/DB/odbc.php
@@ -0,0 +1,883 @@
+<?php
+
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * The PEAR DB driver for PHP's odbc extension
+ * for interacting with databases via ODBC connections
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt. If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category Database
+ * @package DB
+ * @author Stig Bakken <ssb@php.net>
+ * @author Daniel Convissor <danielc@php.net>
+ * @copyright 1997-2007 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0
+ * @version CVS: $Id: odbc.php,v 1.81 2007/07/06 05:19:21 aharvey Exp $
+ * @link http://pear.php.net/package/DB
+ */
+
+/**
+ * Obtain the DB_common class so it can be extended from
+ */
+require_once 'DB/common.php';
+
+/**
+ * The methods PEAR DB uses to interact with PHP's odbc extension
+ * for interacting with databases via ODBC connections
+ *
+ * These methods overload the ones declared in DB_common.
+ *
+ * More info on ODBC errors could be found here:
+ * http://msdn.microsoft.com/library/default.asp?url=/library/en-us/trblsql/tr_err_odbc_5stz.asp
+ *
+ * @category Database
+ * @package DB
+ * @author Stig Bakken <ssb@php.net>
+ * @author Daniel Convissor <danielc@php.net>
+ * @copyright 1997-2007 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0
+ * @version Release: 1.7.14RC1
+ * @link http://pear.php.net/package/DB
+ */
+class DB_odbc extends DB_common
+{
+ // {{{ properties
+
+ /**
+ * The DB driver type (mysql, oci8, odbc, etc.)
+ * @var string
+ */
+ var $phptype = 'odbc';
+
+ /**
+ * The database syntax variant to be used (db2, access, etc.), if any
+ * @var string
+ */
+ var $dbsyntax = 'sql92';
+
+ /**
+ * The capabilities of this DB implementation
+ *
+ * The 'new_link' element contains the PHP version that first provided
+ * new_link support for this DBMS. Contains false if it's unsupported.
+ *
+ * Meaning of the 'limit' element:
+ * + 'emulate' = emulate with fetch row by number
+ * + 'alter' = alter the query
+ * + false = skip rows
+ *
+ * NOTE: The feature set of the following drivers are different than
+ * the default:
+ * + solid: 'transactions' = true
+ * + navision: 'limit' = false
+ *
+ * @var array
+ */
+ var $features = array(
+ 'limit' => 'emulate',
+ 'new_link' => false,
+ 'numrows' => true,
+ 'pconnect' => true,
+ 'prepare' => false,
+ 'ssl' => false,
+ 'transactions' => false,
+ );
+
+ /**
+ * A mapping of native error codes to DB error codes
+ * @var array
+ */
+ var $errorcode_map = array(
+ '01004' => DB_ERROR_TRUNCATED,
+ '07001' => DB_ERROR_MISMATCH,
+ '21S01' => DB_ERROR_VALUE_COUNT_ON_ROW,
+ '21S02' => DB_ERROR_MISMATCH,
+ '22001' => DB_ERROR_INVALID,
+ '22003' => DB_ERROR_INVALID_NUMBER,
+ '22005' => DB_ERROR_INVALID_NUMBER,
+ '22008' => DB_ERROR_INVALID_DATE,
+ '22012' => DB_ERROR_DIVZERO,
+ '23000' => DB_ERROR_CONSTRAINT,
+ '23502' => DB_ERROR_CONSTRAINT_NOT_NULL,
+ '23503' => DB_ERROR_CONSTRAINT,
+ '23504' => DB_ERROR_CONSTRAINT,
+ '23505' => DB_ERROR_CONSTRAINT,
+ '24000' => DB_ERROR_INVALID,
+ '34000' => DB_ERROR_INVALID,
+ '37000' => DB_ERROR_SYNTAX,
+ '42000' => DB_ERROR_SYNTAX,
+ '42601' => DB_ERROR_SYNTAX,
+ 'IM001' => DB_ERROR_UNSUPPORTED,
+ 'S0000' => DB_ERROR_NOSUCHTABLE,
+ 'S0001' => DB_ERROR_ALREADY_EXISTS,
+ 'S0002' => DB_ERROR_NOSUCHTABLE,
+ 'S0011' => DB_ERROR_ALREADY_EXISTS,
+ 'S0012' => DB_ERROR_NOT_FOUND,
+ 'S0021' => DB_ERROR_ALREADY_EXISTS,
+ 'S0022' => DB_ERROR_NOSUCHFIELD,
+ 'S1009' => DB_ERROR_INVALID,
+ 'S1090' => DB_ERROR_INVALID,
+ 'S1C00' => DB_ERROR_NOT_CAPABLE,
+ );
+
+ /**
+ * The raw database connection created by PHP
+ * @var resource
+ */
+ var $connection;
+
+ /**
+ * The DSN information for connecting to a database
+ * @var array
+ */
+ var $dsn = array();
+
+
+ /**
+ * The number of rows affected by a data manipulation query
+ * @var integer
+ * @access private
+ */
+ var $affected = 0;
+
+
+ // }}}
+ // {{{ constructor
+
+ /**
+ * This constructor calls <kbd>$this->DB_common()</kbd>
+ *
+ * @return void
+ */
+ function DB_odbc()
+ {
+ $this->DB_common();
+ }
+
+ // }}}
+ // {{{ connect()
+
+ /**
+ * Connect to the database server, log in and open the database
+ *
+ * Don't call this method directly. Use DB::connect() instead.
+ *
+ * PEAR DB's odbc driver supports the following extra DSN options:
+ * + cursor The type of cursor to be used for this connection.
+ *
+ * @param array $dsn the data source name
+ * @param bool $persistent should the connection be persistent?
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ */
+ function connect($dsn, $persistent = false)
+ {
+ if (!PEAR::loadExtension('odbc')) {
+ return $this->raiseError(DB_ERROR_EXTENSION_NOT_FOUND);
+ }
+
+ $this->dsn = $dsn;
+ if ($dsn['dbsyntax']) {
+ $this->dbsyntax = $dsn['dbsyntax'];
+ }
+ switch ($this->dbsyntax) {
+ case 'access':
+ case 'db2':
+ case 'solid':
+ $this->features['transactions'] = true;
+ break;
+ case 'navision':
+ $this->features['limit'] = false;
+ }
+
+ /*
+ * This is hear for backwards compatibility. Should have been using
+ * 'database' all along, but prior to 1.6.0RC3 'hostspec' was used.
+ */
+ if ($dsn['database']) {
+ $odbcdsn = $dsn['database'];
+ } elseif ($dsn['hostspec']) {
+ $odbcdsn = $dsn['hostspec'];
+ } else {
+ $odbcdsn = 'localhost';
+ }
+
+ $connect_function = $persistent ? 'odbc_pconnect' : 'odbc_connect';
+
+ if (empty($dsn['cursor'])) {
+ $this->connection = @$connect_function($odbcdsn, $dsn['username'],
+ $dsn['password']);
+ } else {
+ $this->connection = @$connect_function($odbcdsn, $dsn['username'],
+ $dsn['password'],
+ $dsn['cursor']);
+ }
+
+ if (!is_resource($this->connection)) {
+ return $this->raiseError(DB_ERROR_CONNECT_FAILED,
+ null, null, null,
+ $this->errorNative());
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ disconnect()
+
+ /**
+ * Disconnects from the database server
+ *
+ * @return bool TRUE on success, FALSE on failure
+ */
+ function disconnect()
+ {
+ $err = @odbc_close($this->connection);
+ $this->connection = null;
+ return $err;
+ }
+
+ // }}}
+ // {{{ simpleQuery()
+
+ /**
+ * Sends a query to the database server
+ *
+ * @param string the SQL query string
+ *
+ * @return mixed + a PHP result resrouce for successful SELECT queries
+ * + the DB_OK constant for other successful queries
+ * + a DB_Error object on failure
+ */
+ function simpleQuery($query)
+ {
+ $this->last_query = $query;
+ $query = $this->modifyQuery($query);
+ $result = @odbc_exec($this->connection, $query);
+ if (!$result) {
+ return $this->odbcRaiseError(); // XXX ERRORMSG
+ }
+ // Determine which queries that should return data, and which
+ // should return an error code only.
+ if ($this->_checkManip($query)) {
+ $this->affected = $result; // For affectedRows()
+ return DB_OK;
+ }
+ $this->affected = 0;
+ return $result;
+ }
+
+ // }}}
+ // {{{ nextResult()
+
+ /**
+ * Move the internal odbc result pointer to the next available result
+ *
+ * @param a valid fbsql result resource
+ *
+ * @access public
+ *
+ * @return true if a result is available otherwise return false
+ */
+ function nextResult($result)
+ {
+ return @odbc_next_result($result);
+ }
+
+ // }}}
+ // {{{ fetchInto()
+
+ /**
+ * Places a row from the result set into the given array
+ *
+ * Formating of the array and the data therein are configurable.
+ * See DB_result::fetchInto() for more information.
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::fetchInto() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result the query result resource
+ * @param array $arr the referenced array to put the data in
+ * @param int $fetchmode how the resulting array should be indexed
+ * @param int $rownum the row number to fetch (0 = first row)
+ *
+ * @return mixed DB_OK on success, NULL when the end of a result set is
+ * reached or on failure
+ *
+ * @see DB_result::fetchInto()
+ */
+ function fetchInto($result, &$arr, $fetchmode, $rownum = null)
+ {
+ $arr = array();
+ if ($rownum !== null) {
+ $rownum++; // ODBC first row is 1
+ if (version_compare(phpversion(), '4.2.0', 'ge')) {
+ $cols = @odbc_fetch_into($result, $arr, $rownum);
+ } else {
+ $cols = @odbc_fetch_into($result, $rownum, $arr);
+ }
+ } else {
+ $cols = @odbc_fetch_into($result, $arr);
+ }
+ if (!$cols) {
+ return null;
+ }
+ if ($fetchmode !== DB_FETCHMODE_ORDERED) {
+ for ($i = 0; $i < count($arr); $i++) {
+ $colName = @odbc_field_name($result, $i+1);
+ $a[$colName] = $arr[$i];
+ }
+ if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE) {
+ $a = array_change_key_case($a, CASE_LOWER);
+ }
+ $arr = $a;
+ }
+ if ($this->options['portability'] & DB_PORTABILITY_RTRIM) {
+ $this->_rtrimArrayValues($arr);
+ }
+ if ($this->options['portability'] & DB_PORTABILITY_NULL_TO_EMPTY) {
+ $this->_convertNullArrayValuesToEmpty($arr);
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ freeResult()
+
+ /**
+ * Deletes the result set and frees the memory occupied by the result set
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::free() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result PHP's query result resource
+ *
+ * @return bool TRUE on success, FALSE if $result is invalid
+ *
+ * @see DB_result::free()
+ */
+ function freeResult($result)
+ {
+ return is_resource($result) ? odbc_free_result($result) : false;
+ }
+
+ // }}}
+ // {{{ numCols()
+
+ /**
+ * Gets the number of columns in a result set
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::numCols() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result PHP's query result resource
+ *
+ * @return int the number of columns. A DB_Error object on failure.
+ *
+ * @see DB_result::numCols()
+ */
+ function numCols($result)
+ {
+ $cols = @odbc_num_fields($result);
+ if (!$cols) {
+ return $this->odbcRaiseError();
+ }
+ return $cols;
+ }
+
+ // }}}
+ // {{{ affectedRows()
+
+ /**
+ * Determines the number of rows affected by a data maniuplation query
+ *
+ * 0 is returned for queries that don't manipulate data.
+ *
+ * @return int the number of rows. A DB_Error object on failure.
+ */
+ function affectedRows()
+ {
+ if (empty($this->affected)) { // In case of SELECT stms
+ return 0;
+ }
+ $nrows = @odbc_num_rows($this->affected);
+ if ($nrows == -1) {
+ return $this->odbcRaiseError();
+ }
+ return $nrows;
+ }
+
+ // }}}
+ // {{{ numRows()
+
+ /**
+ * Gets the number of rows in a result set
+ *
+ * Not all ODBC drivers support this functionality. If they don't
+ * a DB_Error object for DB_ERROR_UNSUPPORTED is returned.
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::numRows() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result PHP's query result resource
+ *
+ * @return int the number of rows. A DB_Error object on failure.
+ *
+ * @see DB_result::numRows()
+ */
+ function numRows($result)
+ {
+ $nrows = @odbc_num_rows($result);
+ if ($nrows == -1) {
+ return $this->odbcRaiseError(DB_ERROR_UNSUPPORTED);
+ }
+ if ($nrows === false) {
+ return $this->odbcRaiseError();
+ }
+ return $nrows;
+ }
+
+ // }}}
+ // {{{ quoteIdentifier()
+
+ /**
+ * Quotes a string so it can be safely used as a table or column name
+ *
+ * Use 'mssql' as the dbsyntax in the DB DSN only if you've unchecked
+ * "Use ANSI quoted identifiers" when setting up the ODBC data source.
+ *
+ * @param string $str identifier name to be quoted
+ *
+ * @return string quoted identifier string
+ *
+ * @see DB_common::quoteIdentifier()
+ * @since Method available since Release 1.6.0
+ */
+ function quoteIdentifier($str)
+ {
+ switch ($this->dsn['dbsyntax']) {
+ case 'access':
+ return '[' . $str . ']';
+ case 'mssql':
+ case 'sybase':
+ return '[' . str_replace(']', ']]', $str) . ']';
+ case 'mysql':
+ case 'mysqli':
+ return '`' . $str . '`';
+ default:
+ return '"' . str_replace('"', '""', $str) . '"';
+ }
+ }
+
+ // }}}
+ // {{{ quote()
+
+ /**
+ * @deprecated Deprecated in release 1.6.0
+ * @internal
+ */
+ function quote($str)
+ {
+ return $this->quoteSmart($str);
+ }
+
+ // }}}
+ // {{{ nextId()
+
+ /**
+ * Returns the next free id in a sequence
+ *
+ * @param string $seq_name name of the sequence
+ * @param boolean $ondemand when true, the seqence is automatically
+ * created if it does not exist
+ *
+ * @return int the next id number in the sequence.
+ * A DB_Error object on failure.
+ *
+ * @see DB_common::nextID(), DB_common::getSequenceName(),
+ * DB_odbc::createSequence(), DB_odbc::dropSequence()
+ */
+ function nextId($seq_name, $ondemand = true)
+ {
+ $seqname = $this->getSequenceName($seq_name);
+ $repeat = 0;
+ do {
+ $this->pushErrorHandling(PEAR_ERROR_RETURN);
+ $result = $this->query("update ${seqname} set id = id + 1");
+ $this->popErrorHandling();
+ if ($ondemand && DB::isError($result) &&
+ $result->getCode() == DB_ERROR_NOSUCHTABLE) {
+ $repeat = 1;
+ $this->pushErrorHandling(PEAR_ERROR_RETURN);
+ $result = $this->createSequence($seq_name);
+ $this->popErrorHandling();
+ if (DB::isError($result)) {
+ return $this->raiseError($result);
+ }
+ $result = $this->query("insert into ${seqname} (id) values(0)");
+ } else {
+ $repeat = 0;
+ }
+ } while ($repeat);
+
+ if (DB::isError($result)) {
+ return $this->raiseError($result);
+ }
+
+ $result = $this->query("select id from ${seqname}");
+ if (DB::isError($result)) {
+ return $result;
+ }
+
+ $row = $result->fetchRow(DB_FETCHMODE_ORDERED);
+ if (DB::isError($row || !$row)) {
+ return $row;
+ }
+
+ return $row[0];
+ }
+
+ /**
+ * Creates a new sequence
+ *
+ * @param string $seq_name name of the new sequence
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ *
+ * @see DB_common::createSequence(), DB_common::getSequenceName(),
+ * DB_odbc::nextID(), DB_odbc::dropSequence()
+ */
+ function createSequence($seq_name)
+ {
+ return $this->query('CREATE TABLE '
+ . $this->getSequenceName($seq_name)
+ . ' (id integer NOT NULL,'
+ . ' PRIMARY KEY(id))');
+ }
+
+ // }}}
+ // {{{ dropSequence()
+
+ /**
+ * Deletes a sequence
+ *
+ * @param string $seq_name name of the sequence to be deleted
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ *
+ * @see DB_common::dropSequence(), DB_common::getSequenceName(),
+ * DB_odbc::nextID(), DB_odbc::createSequence()
+ */
+ function dropSequence($seq_name)
+ {
+ return $this->query('DROP TABLE ' . $this->getSequenceName($seq_name));
+ }
+
+ // }}}
+ // {{{ autoCommit()
+
+ /**
+ * Enables or disables automatic commits
+ *
+ * @param bool $onoff true turns it on, false turns it off
+ *
+ * @return int DB_OK on success. A DB_Error object if the driver
+ * doesn't support auto-committing transactions.
+ */
+ function autoCommit($onoff = false)
+ {
+ if (!@odbc_autocommit($this->connection, $onoff)) {
+ return $this->odbcRaiseError();
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ commit()
+
+ /**
+ * Commits the current transaction
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ */
+ function commit()
+ {
+ if (!@odbc_commit($this->connection)) {
+ return $this->odbcRaiseError();
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ rollback()
+
+ /**
+ * Reverts the current transaction
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ */
+ function rollback()
+ {
+ if (!@odbc_rollback($this->connection)) {
+ return $this->odbcRaiseError();
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ odbcRaiseError()
+
+ /**
+ * Produces a DB_Error object regarding the current problem
+ *
+ * @param int $errno if the error is being manually raised pass a
+ * DB_ERROR* constant here. If this isn't passed
+ * the error information gathered from the DBMS.
+ *
+ * @return object the DB_Error object
+ *
+ * @see DB_common::raiseError(),
+ * DB_odbc::errorNative(), DB_common::errorCode()
+ */
+ function odbcRaiseError($errno = null)
+ {
+ if ($errno === null) {
+ switch ($this->dbsyntax) {
+ case 'access':
+ if ($this->options['portability'] & DB_PORTABILITY_ERRORS) {
+ $this->errorcode_map['07001'] = DB_ERROR_NOSUCHFIELD;
+ } else {
+ // Doing this in case mode changes during runtime.
+ $this->errorcode_map['07001'] = DB_ERROR_MISMATCH;
+ }
+
+ $native_code = odbc_error($this->connection);
+
+ // S1000 is for "General Error." Let's be more specific.
+ if ($native_code == 'S1000') {
+ $errormsg = odbc_errormsg($this->connection);
+ static $error_regexps;
+ if (!isset($error_regexps)) {
+ $error_regexps = array(
+ '/includes related records.$/i' => DB_ERROR_CONSTRAINT,
+ '/cannot contain a Null value/i' => DB_ERROR_CONSTRAINT_NOT_NULL,
+ );
+ }
+ foreach ($error_regexps as $regexp => $code) {
+ if (preg_match($regexp, $errormsg)) {
+ return $this->raiseError($code,
+ null, null, null,
+ $native_code . ' ' . $errormsg);
+ }
+ }
+ $errno = DB_ERROR;
+ } else {
+ $errno = $this->errorCode($native_code);
+ }
+ break;
+ default:
+ $errno = $this->errorCode(odbc_error($this->connection));
+ }
+ }
+ return $this->raiseError($errno, null, null, null,
+ $this->errorNative());
+ }
+
+ // }}}
+ // {{{ errorNative()
+
+ /**
+ * Gets the DBMS' native error code and message produced by the last query
+ *
+ * @return string the DBMS' error code and message
+ */
+ function errorNative()
+ {
+ if (!is_resource($this->connection)) {
+ return @odbc_error() . ' ' . @odbc_errormsg();
+ }
+ return @odbc_error($this->connection) . ' ' . @odbc_errormsg($this->connection);
+ }
+
+ // }}}
+ // {{{ tableInfo()
+
+ /**
+ * Returns information about a table or a result set
+ *
+ * @param object|string $result DB_result object from a query or a
+ * string containing the name of a table.
+ * While this also accepts a query result
+ * resource identifier, this behavior is
+ * deprecated.
+ * @param int $mode a valid tableInfo mode
+ *
+ * @return array an associative array with the information requested.
+ * A DB_Error object on failure.
+ *
+ * @see DB_common::tableInfo()
+ * @since Method available since Release 1.7.0
+ */
+ function tableInfo($result, $mode = null)
+ {
+ if (is_string($result)) {
+ /*
+ * Probably received a table name.
+ * Create a result resource identifier.
+ */
+ $id = @odbc_exec($this->connection, "SELECT * FROM $result");
+ if (!$id) {
+ return $this->odbcRaiseError();
+ }
+ $got_string = true;
+ } elseif (isset($result->result)) {
+ /*
+ * Probably received a result object.
+ * Extract the result resource identifier.
+ */
+ $id = $result->result;
+ $got_string = false;
+ } else {
+ /*
+ * Probably received a result resource identifier.
+ * Copy it.
+ * Deprecated. Here for compatibility only.
+ */
+ $id = $result;
+ $got_string = false;
+ }
+
+ if (!is_resource($id)) {
+ return $this->odbcRaiseError(DB_ERROR_NEED_MORE_DATA);
+ }
+
+ if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE) {
+ $case_func = 'strtolower';
+ } else {
+ $case_func = 'strval';
+ }
+
+ $count = @odbc_num_fields($id);
+ $res = array();
+
+ if ($mode) {
+ $res['num_fields'] = $count;
+ }
+
+ for ($i = 0; $i < $count; $i++) {
+ $col = $i + 1;
+ $res[$i] = array(
+ 'table' => $got_string ? $case_func($result) : '',
+ 'name' => $case_func(@odbc_field_name($id, $col)),
+ 'type' => @odbc_field_type($id, $col),
+ 'len' => @odbc_field_len($id, $col),
+ 'flags' => '',
+ );
+ if ($mode & DB_TABLEINFO_ORDER) {
+ $res['order'][$res[$i]['name']] = $i;
+ }
+ if ($mode & DB_TABLEINFO_ORDERTABLE) {
+ $res['ordertable'][$res[$i]['table']][$res[$i]['name']] = $i;
+ }
+ }
+
+ // free the result only if we were called on a table
+ if ($got_string) {
+ @odbc_free_result($id);
+ }
+ return $res;
+ }
+
+ // }}}
+ // {{{ getSpecialQuery()
+
+ /**
+ * Obtains the query string needed for listing a given type of objects
+ *
+ * Thanks to symbol1@gmail.com and Philippe.Jausions@11abacus.com.
+ *
+ * @param string $type the kind of objects you want to retrieve
+ *
+ * @return string the list of objects requested
+ *
+ * @access protected
+ * @see DB_common::getListOf()
+ * @since Method available since Release 1.7.0
+ */
+ function getSpecialQuery($type)
+ {
+ switch ($type) {
+ case 'databases':
+ if (!function_exists('odbc_data_source')) {
+ return null;
+ }
+ $res = @odbc_data_source($this->connection, SQL_FETCH_FIRST);
+ if (is_array($res)) {
+ $out = array($res['server']);
+ while($res = @odbc_data_source($this->connection,
+ SQL_FETCH_NEXT))
+ {
+ $out[] = $res['server'];
+ }
+ return $out;
+ } else {
+ return $this->odbcRaiseError();
+ }
+ break;
+ case 'tables':
+ case 'schema.tables':
+ $keep = 'TABLE';
+ break;
+ case 'views':
+ $keep = 'VIEW';
+ break;
+ default:
+ return null;
+ }
+
+ /*
+ * Removing non-conforming items in the while loop rather than
+ * in the odbc_tables() call because some backends choke on this:
+ * odbc_tables($this->connection, '', '', '', 'TABLE')
+ */
+ $res = @odbc_tables($this->connection);
+ if (!$res) {
+ return $this->odbcRaiseError();
+ }
+ $out = array();
+ while ($row = odbc_fetch_array($res)) {
+ if ($row['TABLE_TYPE'] != $keep) {
+ continue;
+ }
+ if ($type == 'schema.tables') {
+ $out[] = $row['TABLE_SCHEM'] . '.' . $row['TABLE_NAME'];
+ } else {
+ $out[] = $row['TABLE_NAME'];
+ }
+ }
+ return $out;
+ }
+
+ // }}}
+
+}
+
+/*
+ * Local variables:
+ * tab-width: 4
+ * c-basic-offset: 4
+ * End:
+ */
+
+?>
diff --git a/extlib/DB/pgsql.php b/extlib/DB/pgsql.php
new file mode 100644
index 000000000..6030bb4c1
--- /dev/null
+++ b/extlib/DB/pgsql.php
@@ -0,0 +1,1135 @@
+<?php
+
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * The PEAR DB driver for PHP's pgsql extension
+ * for interacting with PostgreSQL databases
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt. If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category Database
+ * @package DB
+ * @author Rui Hirokawa <hirokawa@php.net>
+ * @author Stig Bakken <ssb@php.net>
+ * @author Daniel Convissor <danielc@php.net>
+ * @copyright 1997-2007 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0
+ * @version CVS: $Id: pgsql.php,v 1.139 2007/11/28 02:19:44 aharvey Exp $
+ * @link http://pear.php.net/package/DB
+ */
+
+/**
+ * Obtain the DB_common class so it can be extended from
+ */
+require_once 'DB/common.php';
+
+/**
+ * The methods PEAR DB uses to interact with PHP's pgsql extension
+ * for interacting with PostgreSQL databases
+ *
+ * These methods overload the ones declared in DB_common.
+ *
+ * @category Database
+ * @package DB
+ * @author Rui Hirokawa <hirokawa@php.net>
+ * @author Stig Bakken <ssb@php.net>
+ * @author Daniel Convissor <danielc@php.net>
+ * @copyright 1997-2007 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0
+ * @version Release: 1.7.14RC1
+ * @link http://pear.php.net/package/DB
+ */
+class DB_pgsql extends DB_common
+{
+ // {{{ properties
+
+ /**
+ * The DB driver type (mysql, oci8, odbc, etc.)
+ * @var string
+ */
+ var $phptype = 'pgsql';
+
+ /**
+ * The database syntax variant to be used (db2, access, etc.), if any
+ * @var string
+ */
+ var $dbsyntax = 'pgsql';
+
+ /**
+ * The capabilities of this DB implementation
+ *
+ * The 'new_link' element contains the PHP version that first provided
+ * new_link support for this DBMS. Contains false if it's unsupported.
+ *
+ * Meaning of the 'limit' element:
+ * + 'emulate' = emulate with fetch row by number
+ * + 'alter' = alter the query
+ * + false = skip rows
+ *
+ * @var array
+ */
+ var $features = array(
+ 'limit' => 'alter',
+ 'new_link' => '4.3.0',
+ 'numrows' => true,
+ 'pconnect' => true,
+ 'prepare' => false,
+ 'ssl' => true,
+ 'transactions' => true,
+ );
+
+ /**
+ * A mapping of native error codes to DB error codes
+ * @var array
+ */
+ var $errorcode_map = array(
+ );
+
+ /**
+ * The raw database connection created by PHP
+ * @var resource
+ */
+ var $connection;
+
+ /**
+ * The DSN information for connecting to a database
+ * @var array
+ */
+ var $dsn = array();
+
+
+ /**
+ * Should data manipulation queries be committed automatically?
+ * @var bool
+ * @access private
+ */
+ var $autocommit = true;
+
+ /**
+ * The quantity of transactions begun
+ *
+ * {@internal While this is private, it can't actually be designated
+ * private in PHP 5 because it is directly accessed in the test suite.}}
+ *
+ * @var integer
+ * @access private
+ */
+ var $transaction_opcount = 0;
+
+ /**
+ * The number of rows affected by a data manipulation query
+ * @var integer
+ */
+ var $affected = 0;
+
+ /**
+ * The current row being looked at in fetchInto()
+ * @var array
+ * @access private
+ */
+ var $row = array();
+
+ /**
+ * The number of rows in a given result set
+ * @var array
+ * @access private
+ */
+ var $_num_rows = array();
+
+
+ // }}}
+ // {{{ constructor
+
+ /**
+ * This constructor calls <kbd>$this->DB_common()</kbd>
+ *
+ * @return void
+ */
+ function DB_pgsql()
+ {
+ $this->DB_common();
+ }
+
+ // }}}
+ // {{{ connect()
+
+ /**
+ * Connect to the database server, log in and open the database
+ *
+ * Don't call this method directly. Use DB::connect() instead.
+ *
+ * PEAR DB's pgsql driver supports the following extra DSN options:
+ * + connect_timeout How many seconds to wait for a connection to
+ * be established. Available since PEAR DB 1.7.0.
+ * + new_link If set to true, causes subsequent calls to
+ * connect() to return a new connection link
+ * instead of the existing one. WARNING: this is
+ * not portable to other DBMS's. Available only
+ * if PHP is >= 4.3.0 and PEAR DB is >= 1.7.0.
+ * + options Command line options to be sent to the server.
+ * Available since PEAR DB 1.6.4.
+ * + service Specifies a service name in pg_service.conf that
+ * holds additional connection parameters.
+ * Available since PEAR DB 1.7.0.
+ * + sslmode How should SSL be used when connecting? Values:
+ * disable, allow, prefer or require.
+ * Available since PEAR DB 1.7.0.
+ * + tty This was used to specify where to send server
+ * debug output. Available since PEAR DB 1.6.4.
+ *
+ * Example of connecting to a new link via a socket:
+ * <code>
+ * require_once 'DB.php';
+ *
+ * $dsn = 'pgsql://user:pass@unix(/tmp)/dbname?new_link=true';
+ * $options = array(
+ * 'portability' => DB_PORTABILITY_ALL,
+ * );
+ *
+ * $db = DB::connect($dsn, $options);
+ * if (PEAR::isError($db)) {
+ * die($db->getMessage());
+ * }
+ * </code>
+ *
+ * @param array $dsn the data source name
+ * @param bool $persistent should the connection be persistent?
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ *
+ * @link http://www.postgresql.org/docs/current/static/libpq.html#LIBPQ-CONNECT
+ */
+ function connect($dsn, $persistent = false)
+ {
+ if (!PEAR::loadExtension('pgsql')) {
+ return $this->raiseError(DB_ERROR_EXTENSION_NOT_FOUND);
+ }
+
+ $this->dsn = $dsn;
+ if ($dsn['dbsyntax']) {
+ $this->dbsyntax = $dsn['dbsyntax'];
+ }
+
+ $protocol = $dsn['protocol'] ? $dsn['protocol'] : 'tcp';
+
+ $params = array('');
+ if ($protocol == 'tcp') {
+ if ($dsn['hostspec']) {
+ $params[0] .= 'host=' . $dsn['hostspec'];
+ }
+ if ($dsn['port']) {
+ $params[0] .= ' port=' . $dsn['port'];
+ }
+ } elseif ($protocol == 'unix') {
+ // Allow for pg socket in non-standard locations.
+ if ($dsn['socket']) {
+ $params[0] .= 'host=' . $dsn['socket'];
+ }
+ if ($dsn['port']) {
+ $params[0] .= ' port=' . $dsn['port'];
+ }
+ }
+ if ($dsn['database']) {
+ $params[0] .= ' dbname=\'' . addslashes($dsn['database']) . '\'';
+ }
+ if ($dsn['username']) {
+ $params[0] .= ' user=\'' . addslashes($dsn['username']) . '\'';
+ }
+ if ($dsn['password']) {
+ $params[0] .= ' password=\'' . addslashes($dsn['password']) . '\'';
+ }
+ if (!empty($dsn['options'])) {
+ $params[0] .= ' options=' . $dsn['options'];
+ }
+ if (!empty($dsn['tty'])) {
+ $params[0] .= ' tty=' . $dsn['tty'];
+ }
+ if (!empty($dsn['connect_timeout'])) {
+ $params[0] .= ' connect_timeout=' . $dsn['connect_timeout'];
+ }
+ if (!empty($dsn['sslmode'])) {
+ $params[0] .= ' sslmode=' . $dsn['sslmode'];
+ }
+ if (!empty($dsn['service'])) {
+ $params[0] .= ' service=' . $dsn['service'];
+ }
+
+ if (isset($dsn['new_link'])
+ && ($dsn['new_link'] == 'true' || $dsn['new_link'] === true))
+ {
+ if (version_compare(phpversion(), '4.3.0', '>=')) {
+ $params[] = PGSQL_CONNECT_FORCE_NEW;
+ }
+ }
+
+ $connect_function = $persistent ? 'pg_pconnect' : 'pg_connect';
+
+ $ini = ini_get('track_errors');
+ $php_errormsg = '';
+ if ($ini) {
+ $this->connection = @call_user_func_array($connect_function,
+ $params);
+ } else {
+ @ini_set('track_errors', 1);
+ $this->connection = @call_user_func_array($connect_function,
+ $params);
+ @ini_set('track_errors', $ini);
+ }
+
+ if (!$this->connection) {
+ return $this->raiseError(DB_ERROR_CONNECT_FAILED,
+ null, null, null,
+ $php_errormsg);
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ disconnect()
+
+ /**
+ * Disconnects from the database server
+ *
+ * @return bool TRUE on success, FALSE on failure
+ */
+ function disconnect()
+ {
+ $ret = @pg_close($this->connection);
+ $this->connection = null;
+ return $ret;
+ }
+
+ // }}}
+ // {{{ simpleQuery()
+
+ /**
+ * Sends a query to the database server
+ *
+ * @param string the SQL query string
+ *
+ * @return mixed + a PHP result resrouce for successful SELECT queries
+ * + the DB_OK constant for other successful queries
+ * + a DB_Error object on failure
+ */
+ function simpleQuery($query)
+ {
+ $ismanip = $this->_checkManip($query);
+ $this->last_query = $query;
+ $query = $this->modifyQuery($query);
+ if (!$this->autocommit && $ismanip) {
+ if ($this->transaction_opcount == 0) {
+ $result = @pg_exec($this->connection, 'begin;');
+ if (!$result) {
+ return $this->pgsqlRaiseError();
+ }
+ }
+ $this->transaction_opcount++;
+ }
+ $result = @pg_exec($this->connection, $query);
+ if (!$result) {
+ return $this->pgsqlRaiseError();
+ }
+
+ /*
+ * Determine whether queries produce affected rows, result or nothing.
+ *
+ * This logic was introduced in version 1.1 of the file by ssb,
+ * though the regex has been modified slightly since then.
+ *
+ * PostgreSQL commands:
+ * ABORT, ALTER, BEGIN, CLOSE, CLUSTER, COMMIT, COPY,
+ * CREATE, DECLARE, DELETE, DROP TABLE, EXPLAIN, FETCH,
+ * GRANT, INSERT, LISTEN, LOAD, LOCK, MOVE, NOTIFY, RESET,
+ * REVOKE, ROLLBACK, SELECT, SELECT INTO, SET, SHOW,
+ * UNLISTEN, UPDATE, VACUUM
+ */
+ if ($ismanip) {
+ $this->affected = @pg_affected_rows($result);
+ return DB_OK;
+ } elseif (preg_match('/^\s*\(*\s*(SELECT|EXPLAIN|FETCH|SHOW)\s/si',
+ $query))
+ {
+ $this->row[(int)$result] = 0; // reset the row counter.
+ $numrows = $this->numRows($result);
+ if (is_object($numrows)) {
+ return $numrows;
+ }
+ $this->_num_rows[(int)$result] = $numrows;
+ $this->affected = 0;
+ return $result;
+ } else {
+ $this->affected = 0;
+ return DB_OK;
+ }
+ }
+
+ // }}}
+ // {{{ nextResult()
+
+ /**
+ * Move the internal pgsql result pointer to the next available result
+ *
+ * @param a valid fbsql result resource
+ *
+ * @access public
+ *
+ * @return true if a result is available otherwise return false
+ */
+ function nextResult($result)
+ {
+ return false;
+ }
+
+ // }}}
+ // {{{ fetchInto()
+
+ /**
+ * Places a row from the result set into the given array
+ *
+ * Formating of the array and the data therein are configurable.
+ * See DB_result::fetchInto() for more information.
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::fetchInto() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result the query result resource
+ * @param array $arr the referenced array to put the data in
+ * @param int $fetchmode how the resulting array should be indexed
+ * @param int $rownum the row number to fetch (0 = first row)
+ *
+ * @return mixed DB_OK on success, NULL when the end of a result set is
+ * reached or on failure
+ *
+ * @see DB_result::fetchInto()
+ */
+ function fetchInto($result, &$arr, $fetchmode, $rownum = null)
+ {
+ $result_int = (int)$result;
+ $rownum = ($rownum !== null) ? $rownum : $this->row[$result_int];
+ if ($rownum >= $this->_num_rows[$result_int]) {
+ return null;
+ }
+ if ($fetchmode & DB_FETCHMODE_ASSOC) {
+ $arr = @pg_fetch_array($result, $rownum, PGSQL_ASSOC);
+ if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE && $arr) {
+ $arr = array_change_key_case($arr, CASE_LOWER);
+ }
+ } else {
+ $arr = @pg_fetch_row($result, $rownum);
+ }
+ if (!$arr) {
+ return null;
+ }
+ if ($this->options['portability'] & DB_PORTABILITY_RTRIM) {
+ $this->_rtrimArrayValues($arr);
+ }
+ if ($this->options['portability'] & DB_PORTABILITY_NULL_TO_EMPTY) {
+ $this->_convertNullArrayValuesToEmpty($arr);
+ }
+ $this->row[$result_int] = ++$rownum;
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ freeResult()
+
+ /**
+ * Deletes the result set and frees the memory occupied by the result set
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::free() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result PHP's query result resource
+ *
+ * @return bool TRUE on success, FALSE if $result is invalid
+ *
+ * @see DB_result::free()
+ */
+ function freeResult($result)
+ {
+ if (is_resource($result)) {
+ unset($this->row[(int)$result]);
+ unset($this->_num_rows[(int)$result]);
+ $this->affected = 0;
+ return @pg_freeresult($result);
+ }
+ return false;
+ }
+
+ // }}}
+ // {{{ quote()
+
+ /**
+ * @deprecated Deprecated in release 1.6.0
+ * @internal
+ */
+ function quote($str)
+ {
+ return $this->quoteSmart($str);
+ }
+
+ // }}}
+ // {{{ quoteBoolean()
+
+ /**
+ * Formats a boolean value for use within a query in a locale-independent
+ * manner.
+ *
+ * @param boolean the boolean value to be quoted.
+ * @return string the quoted string.
+ * @see DB_common::quoteSmart()
+ * @since Method available since release 1.7.8.
+ */
+ function quoteBoolean($boolean) {
+ return $boolean ? 'TRUE' : 'FALSE';
+ }
+
+ // }}}
+ // {{{ escapeSimple()
+
+ /**
+ * Escapes a string according to the current DBMS's standards
+ *
+ * {@internal PostgreSQL treats a backslash as an escape character,
+ * so they are escaped as well.
+ *
+ * @param string $str the string to be escaped
+ *
+ * @return string the escaped string
+ *
+ * @see DB_common::quoteSmart()
+ * @since Method available since Release 1.6.0
+ */
+ function escapeSimple($str)
+ {
+ if (function_exists('pg_escape_string')) {
+ /* This fixes an undocumented BC break in PHP 5.2.0 which changed
+ * the prototype of pg_escape_string. I'm not thrilled about having
+ * to sniff the PHP version, quite frankly, but it's the only way
+ * to deal with the problem. Revision 1.331.2.13.2.10 on
+ * php-src/ext/pgsql/pgsql.c (PHP_5_2 branch) is to blame, for the
+ * record. */
+ if (version_compare(PHP_VERSION, '5.2.0', '>=')) {
+ return pg_escape_string($this->connection, $str);
+ } else {
+ return pg_escape_string($str);
+ }
+ } else {
+ return str_replace("'", "''", str_replace('\\', '\\\\', $str));
+ }
+ }
+
+ // }}}
+ // {{{ numCols()
+
+ /**
+ * Gets the number of columns in a result set
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::numCols() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result PHP's query result resource
+ *
+ * @return int the number of columns. A DB_Error object on failure.
+ *
+ * @see DB_result::numCols()
+ */
+ function numCols($result)
+ {
+ $cols = @pg_numfields($result);
+ if (!$cols) {
+ return $this->pgsqlRaiseError();
+ }
+ return $cols;
+ }
+
+ // }}}
+ // {{{ numRows()
+
+ /**
+ * Gets the number of rows in a result set
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::numRows() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result PHP's query result resource
+ *
+ * @return int the number of rows. A DB_Error object on failure.
+ *
+ * @see DB_result::numRows()
+ */
+ function numRows($result)
+ {
+ $rows = @pg_numrows($result);
+ if ($rows === null) {
+ return $this->pgsqlRaiseError();
+ }
+ return $rows;
+ }
+
+ // }}}
+ // {{{ autoCommit()
+
+ /**
+ * Enables or disables automatic commits
+ *
+ * @param bool $onoff true turns it on, false turns it off
+ *
+ * @return int DB_OK on success. A DB_Error object if the driver
+ * doesn't support auto-committing transactions.
+ */
+ function autoCommit($onoff = false)
+ {
+ // XXX if $this->transaction_opcount > 0, we should probably
+ // issue a warning here.
+ $this->autocommit = $onoff ? true : false;
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ commit()
+
+ /**
+ * Commits the current transaction
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ */
+ function commit()
+ {
+ if ($this->transaction_opcount > 0) {
+ // (disabled) hack to shut up error messages from libpq.a
+ //@fclose(@fopen("php://stderr", "w"));
+ $result = @pg_exec($this->connection, 'end;');
+ $this->transaction_opcount = 0;
+ if (!$result) {
+ return $this->pgsqlRaiseError();
+ }
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ rollback()
+
+ /**
+ * Reverts the current transaction
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ */
+ function rollback()
+ {
+ if ($this->transaction_opcount > 0) {
+ $result = @pg_exec($this->connection, 'abort;');
+ $this->transaction_opcount = 0;
+ if (!$result) {
+ return $this->pgsqlRaiseError();
+ }
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ affectedRows()
+
+ /**
+ * Determines the number of rows affected by a data maniuplation query
+ *
+ * 0 is returned for queries that don't manipulate data.
+ *
+ * @return int the number of rows. A DB_Error object on failure.
+ */
+ function affectedRows()
+ {
+ return $this->affected;
+ }
+
+ // }}}
+ // {{{ nextId()
+
+ /**
+ * Returns the next free id in a sequence
+ *
+ * @param string $seq_name name of the sequence
+ * @param boolean $ondemand when true, the seqence is automatically
+ * created if it does not exist
+ *
+ * @return int the next id number in the sequence.
+ * A DB_Error object on failure.
+ *
+ * @see DB_common::nextID(), DB_common::getSequenceName(),
+ * DB_pgsql::createSequence(), DB_pgsql::dropSequence()
+ */
+ function nextId($seq_name, $ondemand = true)
+ {
+ $seqname = $this->getSequenceName($seq_name);
+ $repeat = false;
+ do {
+ $this->pushErrorHandling(PEAR_ERROR_RETURN);
+ $result = $this->query("SELECT NEXTVAL('${seqname}')");
+ $this->popErrorHandling();
+ if ($ondemand && DB::isError($result) &&
+ $result->getCode() == DB_ERROR_NOSUCHTABLE) {
+ $repeat = true;
+ $this->pushErrorHandling(PEAR_ERROR_RETURN);
+ $result = $this->createSequence($seq_name);
+ $this->popErrorHandling();
+ if (DB::isError($result)) {
+ return $this->raiseError($result);
+ }
+ } else {
+ $repeat = false;
+ }
+ } while ($repeat);
+ if (DB::isError($result)) {
+ return $this->raiseError($result);
+ }
+ $arr = $result->fetchRow(DB_FETCHMODE_ORDERED);
+ $result->free();
+ return $arr[0];
+ }
+
+ // }}}
+ // {{{ createSequence()
+
+ /**
+ * Creates a new sequence
+ *
+ * @param string $seq_name name of the new sequence
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ *
+ * @see DB_common::createSequence(), DB_common::getSequenceName(),
+ * DB_pgsql::nextID(), DB_pgsql::dropSequence()
+ */
+ function createSequence($seq_name)
+ {
+ $seqname = $this->getSequenceName($seq_name);
+ $result = $this->query("CREATE SEQUENCE ${seqname}");
+ return $result;
+ }
+
+ // }}}
+ // {{{ dropSequence()
+
+ /**
+ * Deletes a sequence
+ *
+ * @param string $seq_name name of the sequence to be deleted
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ *
+ * @see DB_common::dropSequence(), DB_common::getSequenceName(),
+ * DB_pgsql::nextID(), DB_pgsql::createSequence()
+ */
+ function dropSequence($seq_name)
+ {
+ return $this->query('DROP SEQUENCE '
+ . $this->getSequenceName($seq_name));
+ }
+
+ // }}}
+ // {{{ modifyLimitQuery()
+
+ /**
+ * Adds LIMIT clauses to a query string according to current DBMS standards
+ *
+ * @param string $query the query to modify
+ * @param int $from the row to start to fetching (0 = the first row)
+ * @param int $count the numbers of rows to fetch
+ * @param mixed $params array, string or numeric data to be used in
+ * execution of the statement. Quantity of items
+ * passed must match quantity of placeholders in
+ * query: meaning 1 placeholder for non-array
+ * parameters or 1 placeholder per array element.
+ *
+ * @return string the query string with LIMIT clauses added
+ *
+ * @access protected
+ */
+ function modifyLimitQuery($query, $from, $count, $params = array())
+ {
+ return "$query LIMIT $count OFFSET $from";
+ }
+
+ // }}}
+ // {{{ pgsqlRaiseError()
+
+ /**
+ * Produces a DB_Error object regarding the current problem
+ *
+ * @param int $errno if the error is being manually raised pass a
+ * DB_ERROR* constant here. If this isn't passed
+ * the error information gathered from the DBMS.
+ *
+ * @return object the DB_Error object
+ *
+ * @see DB_common::raiseError(),
+ * DB_pgsql::errorNative(), DB_pgsql::errorCode()
+ */
+ function pgsqlRaiseError($errno = null)
+ {
+ $native = $this->errorNative();
+ if (!$native) {
+ $native = 'Database connection has been lost.';
+ $errno = DB_ERROR_CONNECT_FAILED;
+ }
+ if ($errno === null) {
+ $errno = $this->errorCode($native);
+ }
+ return $this->raiseError($errno, null, null, null, $native);
+ }
+
+ // }}}
+ // {{{ errorNative()
+
+ /**
+ * Gets the DBMS' native error message produced by the last query
+ *
+ * {@internal Error messages are used instead of error codes
+ * in order to support older versions of PostgreSQL.}}
+ *
+ * @return string the DBMS' error message
+ */
+ function errorNative()
+ {
+ return @pg_errormessage($this->connection);
+ }
+
+ // }}}
+ // {{{ errorCode()
+
+ /**
+ * Determines PEAR::DB error code from the database's text error message.
+ *
+ * @param string $errormsg error message returned from the database
+ * @return integer an error number from a DB error constant
+ */
+ function errorCode($errormsg)
+ {
+ static $error_regexps;
+ if (!isset($error_regexps)) {
+ $error_regexps = array(
+ '/column .* (of relation .*)?does not exist/i'
+ => DB_ERROR_NOSUCHFIELD,
+ '/(relation|sequence|table).*does not exist|class .* not found/i'
+ => DB_ERROR_NOSUCHTABLE,
+ '/index .* does not exist/'
+ => DB_ERROR_NOT_FOUND,
+ '/relation .* already exists/i'
+ => DB_ERROR_ALREADY_EXISTS,
+ '/(divide|division) by zero$/i'
+ => DB_ERROR_DIVZERO,
+ '/pg_atoi: error in .*: can\'t parse /i'
+ => DB_ERROR_INVALID_NUMBER,
+ '/invalid input syntax for( type)? (integer|numeric)/i'
+ => DB_ERROR_INVALID_NUMBER,
+ '/value .* is out of range for type \w*int/i'
+ => DB_ERROR_INVALID_NUMBER,
+ '/integer out of range/i'
+ => DB_ERROR_INVALID_NUMBER,
+ '/value too long for type character/i'
+ => DB_ERROR_INVALID,
+ '/attribute .* not found|relation .* does not have attribute/i'
+ => DB_ERROR_NOSUCHFIELD,
+ '/column .* specified in USING clause does not exist in (left|right) table/i'
+ => DB_ERROR_NOSUCHFIELD,
+ '/parser: parse error at or near/i'
+ => DB_ERROR_SYNTAX,
+ '/syntax error at/'
+ => DB_ERROR_SYNTAX,
+ '/column reference .* is ambiguous/i'
+ => DB_ERROR_SYNTAX,
+ '/permission denied/'
+ => DB_ERROR_ACCESS_VIOLATION,
+ '/violates not-null constraint/'
+ => DB_ERROR_CONSTRAINT_NOT_NULL,
+ '/violates [\w ]+ constraint/'
+ => DB_ERROR_CONSTRAINT,
+ '/referential integrity violation/'
+ => DB_ERROR_CONSTRAINT,
+ '/more expressions than target columns/i'
+ => DB_ERROR_VALUE_COUNT_ON_ROW,
+ );
+ }
+ foreach ($error_regexps as $regexp => $code) {
+ if (preg_match($regexp, $errormsg)) {
+ return $code;
+ }
+ }
+ // Fall back to DB_ERROR if there was no mapping.
+ return DB_ERROR;
+ }
+
+ // }}}
+ // {{{ tableInfo()
+
+ /**
+ * Returns information about a table or a result set
+ *
+ * NOTE: only supports 'table' and 'flags' if <var>$result</var>
+ * is a table name.
+ *
+ * @param object|string $result DB_result object from a query or a
+ * string containing the name of a table.
+ * While this also accepts a query result
+ * resource identifier, this behavior is
+ * deprecated.
+ * @param int $mode a valid tableInfo mode
+ *
+ * @return array an associative array with the information requested.
+ * A DB_Error object on failure.
+ *
+ * @see DB_common::tableInfo()
+ */
+ function tableInfo($result, $mode = null)
+ {
+ if (is_string($result)) {
+ /*
+ * Probably received a table name.
+ * Create a result resource identifier.
+ */
+ $id = @pg_exec($this->connection, "SELECT * FROM $result LIMIT 0");
+ $got_string = true;
+ } elseif (isset($result->result)) {
+ /*
+ * Probably received a result object.
+ * Extract the result resource identifier.
+ */
+ $id = $result->result;
+ $got_string = false;
+ } else {
+ /*
+ * Probably received a result resource identifier.
+ * Copy it.
+ * Deprecated. Here for compatibility only.
+ */
+ $id = $result;
+ $got_string = false;
+ }
+
+ if (!is_resource($id)) {
+ return $this->pgsqlRaiseError(DB_ERROR_NEED_MORE_DATA);
+ }
+
+ if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE) {
+ $case_func = 'strtolower';
+ } else {
+ $case_func = 'strval';
+ }
+
+ $count = @pg_numfields($id);
+ $res = array();
+
+ if ($mode) {
+ $res['num_fields'] = $count;
+ }
+
+ for ($i = 0; $i < $count; $i++) {
+ $res[$i] = array(
+ 'table' => $got_string ? $case_func($result) : '',
+ 'name' => $case_func(@pg_fieldname($id, $i)),
+ 'type' => @pg_fieldtype($id, $i),
+ 'len' => @pg_fieldsize($id, $i),
+ 'flags' => $got_string
+ ? $this->_pgFieldFlags($id, $i, $result)
+ : '',
+ );
+ if ($mode & DB_TABLEINFO_ORDER) {
+ $res['order'][$res[$i]['name']] = $i;
+ }
+ if ($mode & DB_TABLEINFO_ORDERTABLE) {
+ $res['ordertable'][$res[$i]['table']][$res[$i]['name']] = $i;
+ }
+ }
+
+ // free the result only if we were called on a table
+ if ($got_string) {
+ @pg_freeresult($id);
+ }
+ return $res;
+ }
+
+ // }}}
+ // {{{ _pgFieldFlags()
+
+ /**
+ * Get a column's flags
+ *
+ * Supports "not_null", "default_value", "primary_key", "unique_key"
+ * and "multiple_key". The default value is passed through
+ * rawurlencode() in case there are spaces in it.
+ *
+ * @param int $resource the PostgreSQL result identifier
+ * @param int $num_field the field number
+ *
+ * @return string the flags
+ *
+ * @access private
+ */
+ function _pgFieldFlags($resource, $num_field, $table_name)
+ {
+ $field_name = @pg_fieldname($resource, $num_field);
+
+ // Check if there's a schema in $table_name and update things
+ // accordingly.
+ $from = 'pg_attribute f, pg_class tab, pg_type typ';
+ if (strpos($table_name, '.') !== false) {
+ $from .= ', pg_namespace nsp';
+ list($schema, $table) = explode('.', $table_name);
+ $tableWhere = "tab.relname = '$table' AND tab.relnamespace = nsp.oid AND nsp.nspname = '$schema'";
+ } else {
+ $tableWhere = "tab.relname = '$table_name'";
+ }
+
+ $result = @pg_exec($this->connection, "SELECT f.attnotnull, f.atthasdef
+ FROM $from
+ WHERE tab.relname = typ.typname
+ AND typ.typrelid = f.attrelid
+ AND f.attname = '$field_name'
+ AND $tableWhere");
+ if (@pg_numrows($result) > 0) {
+ $row = @pg_fetch_row($result, 0);
+ $flags = ($row[0] == 't') ? 'not_null ' : '';
+
+ if ($row[1] == 't') {
+ $result = @pg_exec($this->connection, "SELECT a.adsrc
+ FROM $from, pg_attrdef a
+ WHERE tab.relname = typ.typname AND typ.typrelid = f.attrelid
+ AND f.attrelid = a.adrelid AND f.attname = '$field_name'
+ AND $tableWhere AND f.attnum = a.adnum");
+ $row = @pg_fetch_row($result, 0);
+ $num = preg_replace("/'(.*)'::\w+/", "\\1", $row[0]);
+ $flags .= 'default_' . rawurlencode($num) . ' ';
+ }
+ } else {
+ $flags = '';
+ }
+ $result = @pg_exec($this->connection, "SELECT i.indisunique, i.indisprimary, i.indkey
+ FROM $from, pg_index i
+ WHERE tab.relname = typ.typname
+ AND typ.typrelid = f.attrelid
+ AND f.attrelid = i.indrelid
+ AND f.attname = '$field_name'
+ AND $tableWhere");
+ $count = @pg_numrows($result);
+
+ for ($i = 0; $i < $count ; $i++) {
+ $row = @pg_fetch_row($result, $i);
+ $keys = explode(' ', $row[2]);
+
+ if (in_array($num_field + 1, $keys)) {
+ $flags .= ($row[0] == 't' && $row[1] == 'f') ? 'unique_key ' : '';
+ $flags .= ($row[1] == 't') ? 'primary_key ' : '';
+ if (count($keys) > 1)
+ $flags .= 'multiple_key ';
+ }
+ }
+
+ return trim($flags);
+ }
+
+ // }}}
+ // {{{ getSpecialQuery()
+
+ /**
+ * Obtains the query string needed for listing a given type of objects
+ *
+ * @param string $type the kind of objects you want to retrieve
+ *
+ * @return string the SQL query string or null if the driver doesn't
+ * support the object type requested
+ *
+ * @access protected
+ * @see DB_common::getListOf()
+ */
+ function getSpecialQuery($type)
+ {
+ switch ($type) {
+ case 'tables':
+ return 'SELECT c.relname AS "Name"'
+ . ' FROM pg_class c, pg_user u'
+ . ' WHERE c.relowner = u.usesysid'
+ . " AND c.relkind = 'r'"
+ . ' AND NOT EXISTS'
+ . ' (SELECT 1 FROM pg_views'
+ . ' WHERE viewname = c.relname)'
+ . " AND c.relname !~ '^(pg_|sql_)'"
+ . ' UNION'
+ . ' SELECT c.relname AS "Name"'
+ . ' FROM pg_class c'
+ . " WHERE c.relkind = 'r'"
+ . ' AND NOT EXISTS'
+ . ' (SELECT 1 FROM pg_views'
+ . ' WHERE viewname = c.relname)'
+ . ' AND NOT EXISTS'
+ . ' (SELECT 1 FROM pg_user'
+ . ' WHERE usesysid = c.relowner)'
+ . " AND c.relname !~ '^pg_'";
+ case 'schema.tables':
+ return "SELECT schemaname || '.' || tablename"
+ . ' AS "Name"'
+ . ' FROM pg_catalog.pg_tables'
+ . ' WHERE schemaname NOT IN'
+ . " ('pg_catalog', 'information_schema', 'pg_toast')";
+ case 'schema.views':
+ return "SELECT schemaname || '.' || viewname from pg_views WHERE schemaname"
+ . " NOT IN ('information_schema', 'pg_catalog')";
+ case 'views':
+ // Table cols: viewname | viewowner | definition
+ return 'SELECT viewname from pg_views WHERE schemaname'
+ . " NOT IN ('information_schema', 'pg_catalog')";
+ case 'users':
+ // cols: usename |usesysid|usecreatedb|usetrace|usesuper|usecatupd|passwd |valuntil
+ return 'SELECT usename FROM pg_user';
+ case 'databases':
+ return 'SELECT datname FROM pg_database';
+ case 'functions':
+ case 'procedures':
+ return 'SELECT proname FROM pg_proc WHERE proowner <> 1';
+ default:
+ return null;
+ }
+ }
+
+ // }}}
+ // {{{ _checkManip()
+
+ /**
+ * Checks if the given query is a manipulation query. This also takes into
+ * account the _next_query_manip flag and sets the _last_query_manip flag
+ * (and resets _next_query_manip) according to the result.
+ *
+ * @param string The query to check.
+ *
+ * @return boolean true if the query is a manipulation query, false
+ * otherwise
+ *
+ * @access protected
+ */
+ function _checkManip($query)
+ {
+ return (preg_match('/^\s*(SAVEPOINT|RELEASE)\s+/i', $query)
+ || parent::_checkManip($query));
+ }
+
+}
+
+/*
+ * Local variables:
+ * tab-width: 4
+ * c-basic-offset: 4
+ * End:
+ */
+
+?>
diff --git a/extlib/DB/sqlite.php b/extlib/DB/sqlite.php
new file mode 100644
index 000000000..5c4b396e5
--- /dev/null
+++ b/extlib/DB/sqlite.php
@@ -0,0 +1,960 @@
+<?php
+
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * The PEAR DB driver for PHP's sqlite extension
+ * for interacting with SQLite databases
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt. If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category Database
+ * @package DB
+ * @author Urs Gehrig <urs@circle.ch>
+ * @author Mika Tuupola <tuupola@appelsiini.net>
+ * @author Daniel Convissor <danielc@php.net>
+ * @copyright 1997-2007 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0 3.0
+ * @version CVS: $Id: sqlite.php,v 1.118 2007/11/26 22:57:18 aharvey Exp $
+ * @link http://pear.php.net/package/DB
+ */
+
+/**
+ * Obtain the DB_common class so it can be extended from
+ */
+require_once 'DB/common.php';
+
+/**
+ * The methods PEAR DB uses to interact with PHP's sqlite extension
+ * for interacting with SQLite databases
+ *
+ * These methods overload the ones declared in DB_common.
+ *
+ * NOTICE: This driver needs PHP's track_errors ini setting to be on.
+ * It is automatically turned on when connecting to the database.
+ * Make sure your scripts don't turn it off.
+ *
+ * @category Database
+ * @package DB
+ * @author Urs Gehrig <urs@circle.ch>
+ * @author Mika Tuupola <tuupola@appelsiini.net>
+ * @author Daniel Convissor <danielc@php.net>
+ * @copyright 1997-2007 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0 3.0
+ * @version Release: 1.7.14RC1
+ * @link http://pear.php.net/package/DB
+ */
+class DB_sqlite extends DB_common
+{
+ // {{{ properties
+
+ /**
+ * The DB driver type (mysql, oci8, odbc, etc.)
+ * @var string
+ */
+ var $phptype = 'sqlite';
+
+ /**
+ * The database syntax variant to be used (db2, access, etc.), if any
+ * @var string
+ */
+ var $dbsyntax = 'sqlite';
+
+ /**
+ * The capabilities of this DB implementation
+ *
+ * The 'new_link' element contains the PHP version that first provided
+ * new_link support for this DBMS. Contains false if it's unsupported.
+ *
+ * Meaning of the 'limit' element:
+ * + 'emulate' = emulate with fetch row by number
+ * + 'alter' = alter the query
+ * + false = skip rows
+ *
+ * @var array
+ */
+ var $features = array(
+ 'limit' => 'alter',
+ 'new_link' => false,
+ 'numrows' => true,
+ 'pconnect' => true,
+ 'prepare' => false,
+ 'ssl' => false,
+ 'transactions' => false,
+ );
+
+ /**
+ * A mapping of native error codes to DB error codes
+ *
+ * {@internal Error codes according to sqlite_exec. See the online
+ * manual at http://sqlite.org/c_interface.html for info.
+ * This error handling based on sqlite_exec is not yet implemented.}}
+ *
+ * @var array
+ */
+ var $errorcode_map = array(
+ );
+
+ /**
+ * The raw database connection created by PHP
+ * @var resource
+ */
+ var $connection;
+
+ /**
+ * The DSN information for connecting to a database
+ * @var array
+ */
+ var $dsn = array();
+
+
+ /**
+ * SQLite data types
+ *
+ * @link http://www.sqlite.org/datatypes.html
+ *
+ * @var array
+ */
+ var $keywords = array (
+ 'BLOB' => '',
+ 'BOOLEAN' => '',
+ 'CHARACTER' => '',
+ 'CLOB' => '',
+ 'FLOAT' => '',
+ 'INTEGER' => '',
+ 'KEY' => '',
+ 'NATIONAL' => '',
+ 'NUMERIC' => '',
+ 'NVARCHAR' => '',
+ 'PRIMARY' => '',
+ 'TEXT' => '',
+ 'TIMESTAMP' => '',
+ 'UNIQUE' => '',
+ 'VARCHAR' => '',
+ 'VARYING' => '',
+ );
+
+ /**
+ * The most recent error message from $php_errormsg
+ * @var string
+ * @access private
+ */
+ var $_lasterror = '';
+
+
+ // }}}
+ // {{{ constructor
+
+ /**
+ * This constructor calls <kbd>$this->DB_common()</kbd>
+ *
+ * @return void
+ */
+ function DB_sqlite()
+ {
+ $this->DB_common();
+ }
+
+ // }}}
+ // {{{ connect()
+
+ /**
+ * Connect to the database server, log in and open the database
+ *
+ * Don't call this method directly. Use DB::connect() instead.
+ *
+ * PEAR DB's sqlite driver supports the following extra DSN options:
+ * + mode The permissions for the database file, in four digit
+ * chmod octal format (eg "0600").
+ *
+ * Example of connecting to a database in read-only mode:
+ * <code>
+ * require_once 'DB.php';
+ *
+ * $dsn = 'sqlite:///path/and/name/of/db/file?mode=0400';
+ * $options = array(
+ * 'portability' => DB_PORTABILITY_ALL,
+ * );
+ *
+ * $db = DB::connect($dsn, $options);
+ * if (PEAR::isError($db)) {
+ * die($db->getMessage());
+ * }
+ * </code>
+ *
+ * @param array $dsn the data source name
+ * @param bool $persistent should the connection be persistent?
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ */
+ function connect($dsn, $persistent = false)
+ {
+ if (!PEAR::loadExtension('sqlite')) {
+ return $this->raiseError(DB_ERROR_EXTENSION_NOT_FOUND);
+ }
+
+ $this->dsn = $dsn;
+ if ($dsn['dbsyntax']) {
+ $this->dbsyntax = $dsn['dbsyntax'];
+ }
+
+ if (!$dsn['database']) {
+ return $this->sqliteRaiseError(DB_ERROR_ACCESS_VIOLATION);
+ }
+
+ if ($dsn['database'] !== ':memory:') {
+ if (!file_exists($dsn['database'])) {
+ if (!touch($dsn['database'])) {
+ return $this->sqliteRaiseError(DB_ERROR_NOT_FOUND);
+ }
+ if (!isset($dsn['mode']) ||
+ !is_numeric($dsn['mode']))
+ {
+ $mode = 0644;
+ } else {
+ $mode = octdec($dsn['mode']);
+ }
+ if (!chmod($dsn['database'], $mode)) {
+ return $this->sqliteRaiseError(DB_ERROR_NOT_FOUND);
+ }
+ if (!file_exists($dsn['database'])) {
+ return $this->sqliteRaiseError(DB_ERROR_NOT_FOUND);
+ }
+ }
+ if (!is_file($dsn['database'])) {
+ return $this->sqliteRaiseError(DB_ERROR_INVALID);
+ }
+ if (!is_readable($dsn['database'])) {
+ return $this->sqliteRaiseError(DB_ERROR_ACCESS_VIOLATION);
+ }
+ }
+
+ $connect_function = $persistent ? 'sqlite_popen' : 'sqlite_open';
+
+ // track_errors must remain on for simpleQuery()
+ @ini_set('track_errors', 1);
+ $php_errormsg = '';
+
+ if (!$this->connection = @$connect_function($dsn['database'])) {
+ return $this->raiseError(DB_ERROR_NODBSELECTED,
+ null, null, null,
+ $php_errormsg);
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ disconnect()
+
+ /**
+ * Disconnects from the database server
+ *
+ * @return bool TRUE on success, FALSE on failure
+ */
+ function disconnect()
+ {
+ $ret = @sqlite_close($this->connection);
+ $this->connection = null;
+ return $ret;
+ }
+
+ // }}}
+ // {{{ simpleQuery()
+
+ /**
+ * Sends a query to the database server
+ *
+ * NOTICE: This method needs PHP's track_errors ini setting to be on.
+ * It is automatically turned on when connecting to the database.
+ * Make sure your scripts don't turn it off.
+ *
+ * @param string the SQL query string
+ *
+ * @return mixed + a PHP result resrouce for successful SELECT queries
+ * + the DB_OK constant for other successful queries
+ * + a DB_Error object on failure
+ */
+ function simpleQuery($query)
+ {
+ $ismanip = $this->_checkManip($query);
+ $this->last_query = $query;
+ $query = $this->modifyQuery($query);
+
+ $php_errormsg = '';
+
+ $result = @sqlite_query($query, $this->connection);
+ $this->_lasterror = $php_errormsg ? $php_errormsg : '';
+
+ $this->result = $result;
+ if (!$this->result) {
+ return $this->sqliteRaiseError(null);
+ }
+
+ // sqlite_query() seems to allways return a resource
+ // so cant use that. Using $ismanip instead
+ if (!$ismanip) {
+ $numRows = $this->numRows($result);
+ if (is_object($numRows)) {
+ // we've got PEAR_Error
+ return $numRows;
+ }
+ return $result;
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ nextResult()
+
+ /**
+ * Move the internal sqlite result pointer to the next available result
+ *
+ * @param resource $result the valid sqlite result resource
+ *
+ * @return bool true if a result is available otherwise return false
+ */
+ function nextResult($result)
+ {
+ return false;
+ }
+
+ // }}}
+ // {{{ fetchInto()
+
+ /**
+ * Places a row from the result set into the given array
+ *
+ * Formating of the array and the data therein are configurable.
+ * See DB_result::fetchInto() for more information.
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::fetchInto() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result the query result resource
+ * @param array $arr the referenced array to put the data in
+ * @param int $fetchmode how the resulting array should be indexed
+ * @param int $rownum the row number to fetch (0 = first row)
+ *
+ * @return mixed DB_OK on success, NULL when the end of a result set is
+ * reached or on failure
+ *
+ * @see DB_result::fetchInto()
+ */
+ function fetchInto($result, &$arr, $fetchmode, $rownum = null)
+ {
+ if ($rownum !== null) {
+ if (!@sqlite_seek($this->result, $rownum)) {
+ return null;
+ }
+ }
+ if ($fetchmode & DB_FETCHMODE_ASSOC) {
+ $arr = @sqlite_fetch_array($result, SQLITE_ASSOC);
+ if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE && $arr) {
+ $arr = array_change_key_case($arr, CASE_LOWER);
+ }
+
+ /* Remove extraneous " characters from the fields in the result.
+ * Fixes bug #11716. */
+ if (is_array($arr) && count($arr) > 0) {
+ $strippedArr = array();
+ foreach ($arr as $field => $value) {
+ $strippedArr[trim($field, '"')] = $value;
+ }
+ $arr = $strippedArr;
+ }
+ } else {
+ $arr = @sqlite_fetch_array($result, SQLITE_NUM);
+ }
+ if (!$arr) {
+ return null;
+ }
+ if ($this->options['portability'] & DB_PORTABILITY_RTRIM) {
+ /*
+ * Even though this DBMS already trims output, we do this because
+ * a field might have intentional whitespace at the end that
+ * gets removed by DB_PORTABILITY_RTRIM under another driver.
+ */
+ $this->_rtrimArrayValues($arr);
+ }
+ if ($this->options['portability'] & DB_PORTABILITY_NULL_TO_EMPTY) {
+ $this->_convertNullArrayValuesToEmpty($arr);
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ freeResult()
+
+ /**
+ * Deletes the result set and frees the memory occupied by the result set
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::free() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result PHP's query result resource
+ *
+ * @return bool TRUE on success, FALSE if $result is invalid
+ *
+ * @see DB_result::free()
+ */
+ function freeResult(&$result)
+ {
+ // XXX No native free?
+ if (!is_resource($result)) {
+ return false;
+ }
+ $result = null;
+ return true;
+ }
+
+ // }}}
+ // {{{ numCols()
+
+ /**
+ * Gets the number of columns in a result set
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::numCols() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result PHP's query result resource
+ *
+ * @return int the number of columns. A DB_Error object on failure.
+ *
+ * @see DB_result::numCols()
+ */
+ function numCols($result)
+ {
+ $cols = @sqlite_num_fields($result);
+ if (!$cols) {
+ return $this->sqliteRaiseError();
+ }
+ return $cols;
+ }
+
+ // }}}
+ // {{{ numRows()
+
+ /**
+ * Gets the number of rows in a result set
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::numRows() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result PHP's query result resource
+ *
+ * @return int the number of rows. A DB_Error object on failure.
+ *
+ * @see DB_result::numRows()
+ */
+ function numRows($result)
+ {
+ $rows = @sqlite_num_rows($result);
+ if ($rows === null) {
+ return $this->sqliteRaiseError();
+ }
+ return $rows;
+ }
+
+ // }}}
+ // {{{ affected()
+
+ /**
+ * Determines the number of rows affected by a data maniuplation query
+ *
+ * 0 is returned for queries that don't manipulate data.
+ *
+ * @return int the number of rows. A DB_Error object on failure.
+ */
+ function affectedRows()
+ {
+ return @sqlite_changes($this->connection);
+ }
+
+ // }}}
+ // {{{ dropSequence()
+
+ /**
+ * Deletes a sequence
+ *
+ * @param string $seq_name name of the sequence to be deleted
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ *
+ * @see DB_common::dropSequence(), DB_common::getSequenceName(),
+ * DB_sqlite::nextID(), DB_sqlite::createSequence()
+ */
+ function dropSequence($seq_name)
+ {
+ return $this->query('DROP TABLE ' . $this->getSequenceName($seq_name));
+ }
+
+ /**
+ * Creates a new sequence
+ *
+ * @param string $seq_name name of the new sequence
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ *
+ * @see DB_common::createSequence(), DB_common::getSequenceName(),
+ * DB_sqlite::nextID(), DB_sqlite::dropSequence()
+ */
+ function createSequence($seq_name)
+ {
+ $seqname = $this->getSequenceName($seq_name);
+ $query = 'CREATE TABLE ' . $seqname .
+ ' (id INTEGER UNSIGNED PRIMARY KEY) ';
+ $result = $this->query($query);
+ if (DB::isError($result)) {
+ return($result);
+ }
+ $query = "CREATE TRIGGER ${seqname}_cleanup AFTER INSERT ON $seqname
+ BEGIN
+ DELETE FROM $seqname WHERE id<LAST_INSERT_ROWID();
+ END ";
+ $result = $this->query($query);
+ if (DB::isError($result)) {
+ return($result);
+ }
+ }
+
+ // }}}
+ // {{{ nextId()
+
+ /**
+ * Returns the next free id in a sequence
+ *
+ * @param string $seq_name name of the sequence
+ * @param boolean $ondemand when true, the seqence is automatically
+ * created if it does not exist
+ *
+ * @return int the next id number in the sequence.
+ * A DB_Error object on failure.
+ *
+ * @see DB_common::nextID(), DB_common::getSequenceName(),
+ * DB_sqlite::createSequence(), DB_sqlite::dropSequence()
+ */
+ function nextId($seq_name, $ondemand = true)
+ {
+ $seqname = $this->getSequenceName($seq_name);
+
+ do {
+ $repeat = 0;
+ $this->pushErrorHandling(PEAR_ERROR_RETURN);
+ $result = $this->query("INSERT INTO $seqname (id) VALUES (NULL)");
+ $this->popErrorHandling();
+ if ($result === DB_OK) {
+ $id = @sqlite_last_insert_rowid($this->connection);
+ if ($id != 0) {
+ return $id;
+ }
+ } elseif ($ondemand && DB::isError($result) &&
+ $result->getCode() == DB_ERROR_NOSUCHTABLE)
+ {
+ $result = $this->createSequence($seq_name);
+ if (DB::isError($result)) {
+ return $this->raiseError($result);
+ } else {
+ $repeat = 1;
+ }
+ }
+ } while ($repeat);
+
+ return $this->raiseError($result);
+ }
+
+ // }}}
+ // {{{ getDbFileStats()
+
+ /**
+ * Get the file stats for the current database
+ *
+ * Possible arguments are dev, ino, mode, nlink, uid, gid, rdev, size,
+ * atime, mtime, ctime, blksize, blocks or a numeric key between
+ * 0 and 12.
+ *
+ * @param string $arg the array key for stats()
+ *
+ * @return mixed an array on an unspecified key, integer on a passed
+ * arg and false at a stats error
+ */
+ function getDbFileStats($arg = '')
+ {
+ $stats = stat($this->dsn['database']);
+ if ($stats == false) {
+ return false;
+ }
+ if (is_array($stats)) {
+ if (is_numeric($arg)) {
+ if (((int)$arg <= 12) & ((int)$arg >= 0)) {
+ return false;
+ }
+ return $stats[$arg ];
+ }
+ if (array_key_exists(trim($arg), $stats)) {
+ return $stats[$arg ];
+ }
+ }
+ return $stats;
+ }
+
+ // }}}
+ // {{{ escapeSimple()
+
+ /**
+ * Escapes a string according to the current DBMS's standards
+ *
+ * In SQLite, this makes things safe for inserts/updates, but may
+ * cause problems when performing text comparisons against columns
+ * containing binary data. See the
+ * {@link http://php.net/sqlite_escape_string PHP manual} for more info.
+ *
+ * @param string $str the string to be escaped
+ *
+ * @return string the escaped string
+ *
+ * @since Method available since Release 1.6.1
+ * @see DB_common::escapeSimple()
+ */
+ function escapeSimple($str)
+ {
+ return @sqlite_escape_string($str);
+ }
+
+ // }}}
+ // {{{ modifyLimitQuery()
+
+ /**
+ * Adds LIMIT clauses to a query string according to current DBMS standards
+ *
+ * @param string $query the query to modify
+ * @param int $from the row to start to fetching (0 = the first row)
+ * @param int $count the numbers of rows to fetch
+ * @param mixed $params array, string or numeric data to be used in
+ * execution of the statement. Quantity of items
+ * passed must match quantity of placeholders in
+ * query: meaning 1 placeholder for non-array
+ * parameters or 1 placeholder per array element.
+ *
+ * @return string the query string with LIMIT clauses added
+ *
+ * @access protected
+ */
+ function modifyLimitQuery($query, $from, $count, $params = array())
+ {
+ return "$query LIMIT $count OFFSET $from";
+ }
+
+ // }}}
+ // {{{ modifyQuery()
+
+ /**
+ * Changes a query string for various DBMS specific reasons
+ *
+ * This little hack lets you know how many rows were deleted
+ * when running a "DELETE FROM table" query. Only implemented
+ * if the DB_PORTABILITY_DELETE_COUNT portability option is on.
+ *
+ * @param string $query the query string to modify
+ *
+ * @return string the modified query string
+ *
+ * @access protected
+ * @see DB_common::setOption()
+ */
+ function modifyQuery($query)
+ {
+ if ($this->options['portability'] & DB_PORTABILITY_DELETE_COUNT) {
+ if (preg_match('/^\s*DELETE\s+FROM\s+(\S+)\s*$/i', $query)) {
+ $query = preg_replace('/^\s*DELETE\s+FROM\s+(\S+)\s*$/',
+ 'DELETE FROM \1 WHERE 1=1', $query);
+ }
+ }
+ return $query;
+ }
+
+ // }}}
+ // {{{ sqliteRaiseError()
+
+ /**
+ * Produces a DB_Error object regarding the current problem
+ *
+ * @param int $errno if the error is being manually raised pass a
+ * DB_ERROR* constant here. If this isn't passed
+ * the error information gathered from the DBMS.
+ *
+ * @return object the DB_Error object
+ *
+ * @see DB_common::raiseError(),
+ * DB_sqlite::errorNative(), DB_sqlite::errorCode()
+ */
+ function sqliteRaiseError($errno = null)
+ {
+ $native = $this->errorNative();
+ if ($errno === null) {
+ $errno = $this->errorCode($native);
+ }
+
+ $errorcode = @sqlite_last_error($this->connection);
+ $userinfo = "$errorcode ** $this->last_query";
+
+ return $this->raiseError($errno, null, null, $userinfo, $native);
+ }
+
+ // }}}
+ // {{{ errorNative()
+
+ /**
+ * Gets the DBMS' native error message produced by the last query
+ *
+ * {@internal This is used to retrieve more meaningfull error messages
+ * because sqlite_last_error() does not provide adequate info.}}
+ *
+ * @return string the DBMS' error message
+ */
+ function errorNative()
+ {
+ return $this->_lasterror;
+ }
+
+ // }}}
+ // {{{ errorCode()
+
+ /**
+ * Determines PEAR::DB error code from the database's text error message
+ *
+ * @param string $errormsg the error message returned from the database
+ *
+ * @return integer the DB error number
+ */
+ function errorCode($errormsg)
+ {
+ static $error_regexps;
+
+ // PHP 5.2+ prepends the function name to $php_errormsg, so we need
+ // this hack to work around it, per bug #9599.
+ $errormsg = preg_replace('/^sqlite[a-z_]+\(\): /', '', $errormsg);
+
+ if (!isset($error_regexps)) {
+ $error_regexps = array(
+ '/^no such table:/' => DB_ERROR_NOSUCHTABLE,
+ '/^no such index:/' => DB_ERROR_NOT_FOUND,
+ '/^(table|index) .* already exists$/' => DB_ERROR_ALREADY_EXISTS,
+ '/PRIMARY KEY must be unique/i' => DB_ERROR_CONSTRAINT,
+ '/is not unique/' => DB_ERROR_CONSTRAINT,
+ '/columns .* are not unique/i' => DB_ERROR_CONSTRAINT,
+ '/uniqueness constraint failed/' => DB_ERROR_CONSTRAINT,
+ '/may not be NULL/' => DB_ERROR_CONSTRAINT_NOT_NULL,
+ '/^no such column:/' => DB_ERROR_NOSUCHFIELD,
+ '/no column named/' => DB_ERROR_NOSUCHFIELD,
+ '/column not present in both tables/i' => DB_ERROR_NOSUCHFIELD,
+ '/^near ".*": syntax error$/' => DB_ERROR_SYNTAX,
+ '/[0-9]+ values for [0-9]+ columns/i' => DB_ERROR_VALUE_COUNT_ON_ROW,
+ );
+ }
+ foreach ($error_regexps as $regexp => $code) {
+ if (preg_match($regexp, $errormsg)) {
+ return $code;
+ }
+ }
+ // Fall back to DB_ERROR if there was no mapping.
+ return DB_ERROR;
+ }
+
+ // }}}
+ // {{{ tableInfo()
+
+ /**
+ * Returns information about a table
+ *
+ * @param string $result a string containing the name of a table
+ * @param int $mode a valid tableInfo mode
+ *
+ * @return array an associative array with the information requested.
+ * A DB_Error object on failure.
+ *
+ * @see DB_common::tableInfo()
+ * @since Method available since Release 1.7.0
+ */
+ function tableInfo($result, $mode = null)
+ {
+ if (is_string($result)) {
+ /*
+ * Probably received a table name.
+ * Create a result resource identifier.
+ */
+ $id = @sqlite_array_query($this->connection,
+ "PRAGMA table_info('$result');",
+ SQLITE_ASSOC);
+ $got_string = true;
+ } else {
+ $this->last_query = '';
+ return $this->raiseError(DB_ERROR_NOT_CAPABLE, null, null, null,
+ 'This DBMS can not obtain tableInfo' .
+ ' from result sets');
+ }
+
+ if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE) {
+ $case_func = 'strtolower';
+ } else {
+ $case_func = 'strval';
+ }
+
+ $count = count($id);
+ $res = array();
+
+ if ($mode) {
+ $res['num_fields'] = $count;
+ }
+
+ for ($i = 0; $i < $count; $i++) {
+ if (strpos($id[$i]['type'], '(') !== false) {
+ $bits = explode('(', $id[$i]['type']);
+ $type = $bits[0];
+ $len = rtrim($bits[1],')');
+ } else {
+ $type = $id[$i]['type'];
+ $len = 0;
+ }
+
+ $flags = '';
+ if ($id[$i]['pk']) {
+ $flags .= 'primary_key ';
+ }
+ if ($id[$i]['notnull']) {
+ $flags .= 'not_null ';
+ }
+ if ($id[$i]['dflt_value'] !== null) {
+ $flags .= 'default_' . rawurlencode($id[$i]['dflt_value']);
+ }
+ $flags = trim($flags);
+
+ $res[$i] = array(
+ 'table' => $case_func($result),
+ 'name' => $case_func($id[$i]['name']),
+ 'type' => $type,
+ 'len' => $len,
+ 'flags' => $flags,
+ );
+
+ if ($mode & DB_TABLEINFO_ORDER) {
+ $res['order'][$res[$i]['name']] = $i;
+ }
+ if ($mode & DB_TABLEINFO_ORDERTABLE) {
+ $res['ordertable'][$res[$i]['table']][$res[$i]['name']] = $i;
+ }
+ }
+
+ return $res;
+ }
+
+ // }}}
+ // {{{ getSpecialQuery()
+
+ /**
+ * Obtains the query string needed for listing a given type of objects
+ *
+ * @param string $type the kind of objects you want to retrieve
+ * @param array $args SQLITE DRIVER ONLY: a private array of arguments
+ * used by the getSpecialQuery(). Do not use
+ * this directly.
+ *
+ * @return string the SQL query string or null if the driver doesn't
+ * support the object type requested
+ *
+ * @access protected
+ * @see DB_common::getListOf()
+ */
+ function getSpecialQuery($type, $args = array())
+ {
+ if (!is_array($args)) {
+ return $this->raiseError('no key specified', null, null, null,
+ 'Argument has to be an array.');
+ }
+
+ switch ($type) {
+ case 'master':
+ return 'SELECT * FROM sqlite_master;';
+ case 'tables':
+ return "SELECT name FROM sqlite_master WHERE type='table' "
+ . 'UNION ALL SELECT name FROM sqlite_temp_master '
+ . "WHERE type='table' ORDER BY name;";
+ case 'schema':
+ return 'SELECT sql FROM (SELECT * FROM sqlite_master '
+ . 'UNION ALL SELECT * FROM sqlite_temp_master) '
+ . "WHERE type!='meta' "
+ . 'ORDER BY tbl_name, type DESC, name;';
+ case 'schemax':
+ case 'schema_x':
+ /*
+ * Use like:
+ * $res = $db->query($db->getSpecialQuery('schema_x',
+ * array('table' => 'table3')));
+ */
+ return 'SELECT sql FROM (SELECT * FROM sqlite_master '
+ . 'UNION ALL SELECT * FROM sqlite_temp_master) '
+ . "WHERE tbl_name LIKE '{$args['table']}' "
+ . "AND type!='meta' "
+ . 'ORDER BY type DESC, name;';
+ case 'alter':
+ /*
+ * SQLite does not support ALTER TABLE; this is a helper query
+ * to handle this. 'table' represents the table name, 'rows'
+ * the news rows to create, 'save' the row(s) to keep _with_
+ * the data.
+ *
+ * Use like:
+ * $args = array(
+ * 'table' => $table,
+ * 'rows' => "id INTEGER PRIMARY KEY, firstname TEXT, surname TEXT, datetime TEXT",
+ * 'save' => "NULL, titel, content, datetime"
+ * );
+ * $res = $db->query( $db->getSpecialQuery('alter', $args));
+ */
+ $rows = strtr($args['rows'], $this->keywords);
+
+ $q = array(
+ 'BEGIN TRANSACTION',
+ "CREATE TEMPORARY TABLE {$args['table']}_backup ({$args['rows']})",
+ "INSERT INTO {$args['table']}_backup SELECT {$args['save']} FROM {$args['table']}",
+ "DROP TABLE {$args['table']}",
+ "CREATE TABLE {$args['table']} ({$args['rows']})",
+ "INSERT INTO {$args['table']} SELECT {$rows} FROM {$args['table']}_backup",
+ "DROP TABLE {$args['table']}_backup",
+ 'COMMIT',
+ );
+
+ /*
+ * This is a dirty hack, since the above query will not get
+ * executed with a single query call so here the query method
+ * will be called directly and return a select instead.
+ */
+ foreach ($q as $query) {
+ $this->query($query);
+ }
+ return "SELECT * FROM {$args['table']};";
+ default:
+ return null;
+ }
+ }
+
+ // }}}
+}
+
+/*
+ * Local variables:
+ * tab-width: 4
+ * c-basic-offset: 4
+ * End:
+ */
+
+?>
diff --git a/extlib/DB/storage.php b/extlib/DB/storage.php
new file mode 100644
index 000000000..ffa2d9447
--- /dev/null
+++ b/extlib/DB/storage.php
@@ -0,0 +1,506 @@
+<?php
+
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * Provides an object interface to a table row
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt. If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category Database
+ * @package DB
+ * @author Stig Bakken <stig@php.net>
+ * @copyright 1997-2007 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0
+ * @version CVS: $Id: storage.php,v 1.24 2007/08/12 05:27:25 aharvey Exp $
+ * @link http://pear.php.net/package/DB
+ */
+
+/**
+ * Obtain the DB class so it can be extended from
+ */
+require_once 'DB.php';
+
+/**
+ * Provides an object interface to a table row
+ *
+ * It lets you add, delete and change rows using objects rather than SQL
+ * statements.
+ *
+ * @category Database
+ * @package DB
+ * @author Stig Bakken <stig@php.net>
+ * @copyright 1997-2007 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0
+ * @version Release: 1.7.14RC1
+ * @link http://pear.php.net/package/DB
+ */
+class DB_storage extends PEAR
+{
+ // {{{ properties
+
+ /** the name of the table (or view, if the backend database supports
+ updates in views) we hold data from */
+ var $_table = null;
+
+ /** which column(s) in the table contains primary keys, can be a
+ string for single-column primary keys, or an array of strings
+ for multiple-column primary keys */
+ var $_keycolumn = null;
+
+ /** DB connection handle used for all transactions */
+ var $_dbh = null;
+
+ /** an assoc with the names of database fields stored as properties
+ in this object */
+ var $_properties = array();
+
+ /** an assoc with the names of the properties in this object that
+ have been changed since they were fetched from the database */
+ var $_changes = array();
+
+ /** flag that decides if data in this object can be changed.
+ objects that don't have their table's key column in their
+ property lists will be flagged as read-only. */
+ var $_readonly = false;
+
+ /** function or method that implements a validator for fields that
+ are set, this validator function returns true if the field is
+ valid, false if not */
+ var $_validator = null;
+
+ // }}}
+ // {{{ constructor
+
+ /**
+ * Constructor
+ *
+ * @param $table string the name of the database table
+ *
+ * @param $keycolumn mixed string with name of key column, or array of
+ * strings if the table has a primary key of more than one column
+ *
+ * @param $dbh object database connection object
+ *
+ * @param $validator mixed function or method used to validate
+ * each new value, called with three parameters: the name of the
+ * field/column that is changing, a reference to the new value and
+ * a reference to this object
+ *
+ */
+ function DB_storage($table, $keycolumn, &$dbh, $validator = null)
+ {
+ $this->PEAR('DB_Error');
+ $this->_table = $table;
+ $this->_keycolumn = $keycolumn;
+ $this->_dbh = $dbh;
+ $this->_readonly = false;
+ $this->_validator = $validator;
+ }
+
+ // }}}
+ // {{{ _makeWhere()
+
+ /**
+ * Utility method to build a "WHERE" clause to locate ourselves in
+ * the table.
+ *
+ * XXX future improvement: use rowids?
+ *
+ * @access private
+ */
+ function _makeWhere($keyval = null)
+ {
+ if (is_array($this->_keycolumn)) {
+ if ($keyval === null) {
+ for ($i = 0; $i < sizeof($this->_keycolumn); $i++) {
+ $keyval[] = $this->{$this->_keycolumn[$i]};
+ }
+ }
+ $whereclause = '';
+ for ($i = 0; $i < sizeof($this->_keycolumn); $i++) {
+ if ($i > 0) {
+ $whereclause .= ' AND ';
+ }
+ $whereclause .= $this->_keycolumn[$i];
+ if (is_null($keyval[$i])) {
+ // there's not much point in having a NULL key,
+ // but we support it anyway
+ $whereclause .= ' IS NULL';
+ } else {
+ $whereclause .= ' = ' . $this->_dbh->quote($keyval[$i]);
+ }
+ }
+ } else {
+ if ($keyval === null) {
+ $keyval = @$this->{$this->_keycolumn};
+ }
+ $whereclause = $this->_keycolumn;
+ if (is_null($keyval)) {
+ // there's not much point in having a NULL key,
+ // but we support it anyway
+ $whereclause .= ' IS NULL';
+ } else {
+ $whereclause .= ' = ' . $this->_dbh->quote($keyval);
+ }
+ }
+ return $whereclause;
+ }
+
+ // }}}
+ // {{{ setup()
+
+ /**
+ * Method used to initialize a DB_storage object from the
+ * configured table.
+ *
+ * @param $keyval mixed the key[s] of the row to fetch (string or array)
+ *
+ * @return int DB_OK on success, a DB error if not
+ */
+ function setup($keyval)
+ {
+ $whereclause = $this->_makeWhere($keyval);
+ $query = 'SELECT * FROM ' . $this->_table . ' WHERE ' . $whereclause;
+ $sth = $this->_dbh->query($query);
+ if (DB::isError($sth)) {
+ return $sth;
+ }
+ $row = $sth->fetchRow(DB_FETCHMODE_ASSOC);
+ if (DB::isError($row)) {
+ return $row;
+ }
+ if (!$row) {
+ return $this->raiseError(null, DB_ERROR_NOT_FOUND, null, null,
+ $query, null, true);
+ }
+ foreach ($row as $key => $value) {
+ $this->_properties[$key] = true;
+ $this->$key = $value;
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ insert()
+
+ /**
+ * Create a new (empty) row in the configured table for this
+ * object.
+ */
+ function insert($newpk)
+ {
+ if (is_array($this->_keycolumn)) {
+ $primarykey = $this->_keycolumn;
+ } else {
+ $primarykey = array($this->_keycolumn);
+ }
+ settype($newpk, "array");
+ for ($i = 0; $i < sizeof($primarykey); $i++) {
+ $pkvals[] = $this->_dbh->quote($newpk[$i]);
+ }
+
+ $sth = $this->_dbh->query("INSERT INTO $this->_table (" .
+ implode(",", $primarykey) . ") VALUES(" .
+ implode(",", $pkvals) . ")");
+ if (DB::isError($sth)) {
+ return $sth;
+ }
+ if (sizeof($newpk) == 1) {
+ $newpk = $newpk[0];
+ }
+ $this->setup($newpk);
+ }
+
+ // }}}
+ // {{{ toString()
+
+ /**
+ * Output a simple description of this DB_storage object.
+ * @return string object description
+ */
+ function toString()
+ {
+ $info = strtolower(get_class($this));
+ $info .= " (table=";
+ $info .= $this->_table;
+ $info .= ", keycolumn=";
+ if (is_array($this->_keycolumn)) {
+ $info .= "(" . implode(",", $this->_keycolumn) . ")";
+ } else {
+ $info .= $this->_keycolumn;
+ }
+ $info .= ", dbh=";
+ if (is_object($this->_dbh)) {
+ $info .= $this->_dbh->toString();
+ } else {
+ $info .= "null";
+ }
+ $info .= ")";
+ if (sizeof($this->_properties)) {
+ $info .= " [loaded, key=";
+ $keyname = $this->_keycolumn;
+ if (is_array($keyname)) {
+ $info .= "(";
+ for ($i = 0; $i < sizeof($keyname); $i++) {
+ if ($i > 0) {
+ $info .= ",";
+ }
+ $info .= $this->$keyname[$i];
+ }
+ $info .= ")";
+ } else {
+ $info .= $this->$keyname;
+ }
+ $info .= "]";
+ }
+ if (sizeof($this->_changes)) {
+ $info .= " [modified]";
+ }
+ return $info;
+ }
+
+ // }}}
+ // {{{ dump()
+
+ /**
+ * Dump the contents of this object to "standard output".
+ */
+ function dump()
+ {
+ foreach ($this->_properties as $prop => $foo) {
+ print "$prop = ";
+ print htmlentities($this->$prop);
+ print "<br />\n";
+ }
+ }
+
+ // }}}
+ // {{{ &create()
+
+ /**
+ * Static method used to create new DB storage objects.
+ * @param $data assoc. array where the keys are the names
+ * of properties/columns
+ * @return object a new instance of DB_storage or a subclass of it
+ */
+ function &create($table, &$data)
+ {
+ $classname = strtolower(get_class($this));
+ $obj = new $classname($table);
+ foreach ($data as $name => $value) {
+ $obj->_properties[$name] = true;
+ $obj->$name = &$value;
+ }
+ return $obj;
+ }
+
+ // }}}
+ // {{{ loadFromQuery()
+
+ /**
+ * Loads data into this object from the given query. If this
+ * object already contains table data, changes will be saved and
+ * the object re-initialized first.
+ *
+ * @param $query SQL query
+ *
+ * @param $params parameter list in case you want to use
+ * prepare/execute mode
+ *
+ * @return int DB_OK on success, DB_WARNING_READ_ONLY if the
+ * returned object is read-only (because the object's specified
+ * key column was not found among the columns returned by $query),
+ * or another DB error code in case of errors.
+ */
+// XXX commented out for now
+/*
+ function loadFromQuery($query, $params = null)
+ {
+ if (sizeof($this->_properties)) {
+ if (sizeof($this->_changes)) {
+ $this->store();
+ $this->_changes = array();
+ }
+ $this->_properties = array();
+ }
+ $rowdata = $this->_dbh->getRow($query, DB_FETCHMODE_ASSOC, $params);
+ if (DB::isError($rowdata)) {
+ return $rowdata;
+ }
+ reset($rowdata);
+ $found_keycolumn = false;
+ while (list($key, $value) = each($rowdata)) {
+ if ($key == $this->_keycolumn) {
+ $found_keycolumn = true;
+ }
+ $this->_properties[$key] = true;
+ $this->$key = &$value;
+ unset($value); // have to unset, or all properties will
+ // refer to the same value
+ }
+ if (!$found_keycolumn) {
+ $this->_readonly = true;
+ return DB_WARNING_READ_ONLY;
+ }
+ return DB_OK;
+ }
+ */
+
+ // }}}
+ // {{{ set()
+
+ /**
+ * Modify an attriute value.
+ */
+ function set($property, $newvalue)
+ {
+ // only change if $property is known and object is not
+ // read-only
+ if ($this->_readonly) {
+ return $this->raiseError(null, DB_WARNING_READ_ONLY, null,
+ null, null, null, true);
+ }
+ if (@isset($this->_properties[$property])) {
+ if (empty($this->_validator)) {
+ $valid = true;
+ } else {
+ $valid = @call_user_func($this->_validator,
+ $this->_table,
+ $property,
+ $newvalue,
+ $this->$property,
+ $this);
+ }
+ if ($valid) {
+ $this->$property = $newvalue;
+ if (empty($this->_changes[$property])) {
+ $this->_changes[$property] = 0;
+ } else {
+ $this->_changes[$property]++;
+ }
+ } else {
+ return $this->raiseError(null, DB_ERROR_INVALID, null,
+ null, "invalid field: $property",
+ null, true);
+ }
+ return true;
+ }
+ return $this->raiseError(null, DB_ERROR_NOSUCHFIELD, null,
+ null, "unknown field: $property",
+ null, true);
+ }
+
+ // }}}
+ // {{{ &get()
+
+ /**
+ * Fetch an attribute value.
+ *
+ * @param string attribute name
+ *
+ * @return attribute contents, or null if the attribute name is
+ * unknown
+ */
+ function &get($property)
+ {
+ // only return if $property is known
+ if (isset($this->_properties[$property])) {
+ return $this->$property;
+ }
+ $tmp = null;
+ return $tmp;
+ }
+
+ // }}}
+ // {{{ _DB_storage()
+
+ /**
+ * Destructor, calls DB_storage::store() if there are changes
+ * that are to be kept.
+ */
+ function _DB_storage()
+ {
+ if (sizeof($this->_changes)) {
+ $this->store();
+ }
+ $this->_properties = array();
+ $this->_changes = array();
+ $this->_table = null;
+ }
+
+ // }}}
+ // {{{ store()
+
+ /**
+ * Stores changes to this object in the database.
+ *
+ * @return DB_OK or a DB error
+ */
+ function store()
+ {
+ $params = array();
+ $vars = array();
+ foreach ($this->_changes as $name => $foo) {
+ $params[] = &$this->$name;
+ $vars[] = $name . ' = ?';
+ }
+ if ($vars) {
+ $query = 'UPDATE ' . $this->_table . ' SET ' .
+ implode(', ', $vars) . ' WHERE ' .
+ $this->_makeWhere();
+ $stmt = $this->_dbh->prepare($query);
+ $res = $this->_dbh->execute($stmt, $params);
+ if (DB::isError($res)) {
+ return $res;
+ }
+ $this->_changes = array();
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ remove()
+
+ /**
+ * Remove the row represented by this object from the database.
+ *
+ * @return mixed DB_OK or a DB error
+ */
+ function remove()
+ {
+ if ($this->_readonly) {
+ return $this->raiseError(null, DB_WARNING_READ_ONLY, null,
+ null, null, null, true);
+ }
+ $query = 'DELETE FROM ' . $this->_table .' WHERE '.
+ $this->_makeWhere();
+ $res = $this->_dbh->query($query);
+ if (DB::isError($res)) {
+ return $res;
+ }
+ foreach ($this->_properties as $prop => $foo) {
+ unset($this->$prop);
+ }
+ $this->_properties = array();
+ $this->_changes = array();
+ return DB_OK;
+ }
+
+ // }}}
+}
+
+/*
+ * Local variables:
+ * tab-width: 4
+ * c-basic-offset: 4
+ * End:
+ */
+
+?>
diff --git a/extlib/DB/sybase.php b/extlib/DB/sybase.php
new file mode 100644
index 000000000..3befbf6ea
--- /dev/null
+++ b/extlib/DB/sybase.php
@@ -0,0 +1,942 @@
+<?php
+
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * The PEAR DB driver for PHP's sybase extension
+ * for interacting with Sybase databases
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt. If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category Database
+ * @package DB
+ * @author Sterling Hughes <sterling@php.net>
+ * @author Antônio Carlos Venâncio Júnior <floripa@php.net>
+ * @author Daniel Convissor <danielc@php.net>
+ * @copyright 1997-2007 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0
+ * @version CVS: $Id: sybase.php,v 1.87 2007/09/21 13:40:42 aharvey Exp $
+ * @link http://pear.php.net/package/DB
+ */
+
+/**
+ * Obtain the DB_common class so it can be extended from
+ */
+require_once 'DB/common.php';
+
+/**
+ * The methods PEAR DB uses to interact with PHP's sybase extension
+ * for interacting with Sybase databases
+ *
+ * These methods overload the ones declared in DB_common.
+ *
+ * WARNING: This driver may fail with multiple connections under the
+ * same user/pass/host and different databases.
+ *
+ * @category Database
+ * @package DB
+ * @author Sterling Hughes <sterling@php.net>
+ * @author Antônio Carlos Venâncio Júnior <floripa@php.net>
+ * @author Daniel Convissor <danielc@php.net>
+ * @copyright 1997-2007 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0
+ * @version Release: 1.7.14RC1
+ * @link http://pear.php.net/package/DB
+ */
+class DB_sybase extends DB_common
+{
+ // {{{ properties
+
+ /**
+ * The DB driver type (mysql, oci8, odbc, etc.)
+ * @var string
+ */
+ var $phptype = 'sybase';
+
+ /**
+ * The database syntax variant to be used (db2, access, etc.), if any
+ * @var string
+ */
+ var $dbsyntax = 'sybase';
+
+ /**
+ * The capabilities of this DB implementation
+ *
+ * The 'new_link' element contains the PHP version that first provided
+ * new_link support for this DBMS. Contains false if it's unsupported.
+ *
+ * Meaning of the 'limit' element:
+ * + 'emulate' = emulate with fetch row by number
+ * + 'alter' = alter the query
+ * + false = skip rows
+ *
+ * @var array
+ */
+ var $features = array(
+ 'limit' => 'emulate',
+ 'new_link' => false,
+ 'numrows' => true,
+ 'pconnect' => true,
+ 'prepare' => false,
+ 'ssl' => false,
+ 'transactions' => true,
+ );
+
+ /**
+ * A mapping of native error codes to DB error codes
+ * @var array
+ */
+ var $errorcode_map = array(
+ );
+
+ /**
+ * The raw database connection created by PHP
+ * @var resource
+ */
+ var $connection;
+
+ /**
+ * The DSN information for connecting to a database
+ * @var array
+ */
+ var $dsn = array();
+
+
+ /**
+ * Should data manipulation queries be committed automatically?
+ * @var bool
+ * @access private
+ */
+ var $autocommit = true;
+
+ /**
+ * The quantity of transactions begun
+ *
+ * {@internal While this is private, it can't actually be designated
+ * private in PHP 5 because it is directly accessed in the test suite.}}
+ *
+ * @var integer
+ * @access private
+ */
+ var $transaction_opcount = 0;
+
+ /**
+ * The database specified in the DSN
+ *
+ * It's a fix to allow calls to different databases in the same script.
+ *
+ * @var string
+ * @access private
+ */
+ var $_db = '';
+
+
+ // }}}
+ // {{{ constructor
+
+ /**
+ * This constructor calls <kbd>$this->DB_common()</kbd>
+ *
+ * @return void
+ */
+ function DB_sybase()
+ {
+ $this->DB_common();
+ }
+
+ // }}}
+ // {{{ connect()
+
+ /**
+ * Connect to the database server, log in and open the database
+ *
+ * Don't call this method directly. Use DB::connect() instead.
+ *
+ * PEAR DB's sybase driver supports the following extra DSN options:
+ * + appname The application name to use on this connection.
+ * Available since PEAR DB 1.7.0.
+ * + charset The character set to use on this connection.
+ * Available since PEAR DB 1.7.0.
+ *
+ * @param array $dsn the data source name
+ * @param bool $persistent should the connection be persistent?
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ */
+ function connect($dsn, $persistent = false)
+ {
+ if (!PEAR::loadExtension('sybase') &&
+ !PEAR::loadExtension('sybase_ct'))
+ {
+ return $this->raiseError(DB_ERROR_EXTENSION_NOT_FOUND);
+ }
+
+ $this->dsn = $dsn;
+ if ($dsn['dbsyntax']) {
+ $this->dbsyntax = $dsn['dbsyntax'];
+ }
+
+ $dsn['hostspec'] = $dsn['hostspec'] ? $dsn['hostspec'] : 'localhost';
+ $dsn['password'] = !empty($dsn['password']) ? $dsn['password'] : false;
+ $dsn['charset'] = isset($dsn['charset']) ? $dsn['charset'] : false;
+ $dsn['appname'] = isset($dsn['appname']) ? $dsn['appname'] : false;
+
+ $connect_function = $persistent ? 'sybase_pconnect' : 'sybase_connect';
+
+ if ($dsn['username']) {
+ $this->connection = @$connect_function($dsn['hostspec'],
+ $dsn['username'],
+ $dsn['password'],
+ $dsn['charset'],
+ $dsn['appname']);
+ } else {
+ return $this->raiseError(DB_ERROR_CONNECT_FAILED,
+ null, null, null,
+ 'The DSN did not contain a username.');
+ }
+
+ if (!$this->connection) {
+ return $this->raiseError(DB_ERROR_CONNECT_FAILED,
+ null, null, null,
+ @sybase_get_last_message());
+ }
+
+ if ($dsn['database']) {
+ if (!@sybase_select_db($dsn['database'], $this->connection)) {
+ return $this->raiseError(DB_ERROR_NODBSELECTED,
+ null, null, null,
+ @sybase_get_last_message());
+ }
+ $this->_db = $dsn['database'];
+ }
+
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ disconnect()
+
+ /**
+ * Disconnects from the database server
+ *
+ * @return bool TRUE on success, FALSE on failure
+ */
+ function disconnect()
+ {
+ $ret = @sybase_close($this->connection);
+ $this->connection = null;
+ return $ret;
+ }
+
+ // }}}
+ // {{{ simpleQuery()
+
+ /**
+ * Sends a query to the database server
+ *
+ * @param string the SQL query string
+ *
+ * @return mixed + a PHP result resrouce for successful SELECT queries
+ * + the DB_OK constant for other successful queries
+ * + a DB_Error object on failure
+ */
+ function simpleQuery($query)
+ {
+ $ismanip = $this->_checkManip($query);
+ $this->last_query = $query;
+ if ($this->_db && !@sybase_select_db($this->_db, $this->connection)) {
+ return $this->sybaseRaiseError(DB_ERROR_NODBSELECTED);
+ }
+ $query = $this->modifyQuery($query);
+ if (!$this->autocommit && $ismanip) {
+ if ($this->transaction_opcount == 0) {
+ $result = @sybase_query('BEGIN TRANSACTION', $this->connection);
+ if (!$result) {
+ return $this->sybaseRaiseError();
+ }
+ }
+ $this->transaction_opcount++;
+ }
+ $result = @sybase_query($query, $this->connection);
+ if (!$result) {
+ return $this->sybaseRaiseError();
+ }
+ if (is_resource($result)) {
+ return $result;
+ }
+ // Determine which queries that should return data, and which
+ // should return an error code only.
+ return $ismanip ? DB_OK : $result;
+ }
+
+ // }}}
+ // {{{ nextResult()
+
+ /**
+ * Move the internal sybase result pointer to the next available result
+ *
+ * @param a valid sybase result resource
+ *
+ * @access public
+ *
+ * @return true if a result is available otherwise return false
+ */
+ function nextResult($result)
+ {
+ return false;
+ }
+
+ // }}}
+ // {{{ fetchInto()
+
+ /**
+ * Places a row from the result set into the given array
+ *
+ * Formating of the array and the data therein are configurable.
+ * See DB_result::fetchInto() for more information.
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::fetchInto() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result the query result resource
+ * @param array $arr the referenced array to put the data in
+ * @param int $fetchmode how the resulting array should be indexed
+ * @param int $rownum the row number to fetch (0 = first row)
+ *
+ * @return mixed DB_OK on success, NULL when the end of a result set is
+ * reached or on failure
+ *
+ * @see DB_result::fetchInto()
+ */
+ function fetchInto($result, &$arr, $fetchmode, $rownum = null)
+ {
+ if ($rownum !== null) {
+ if (!@sybase_data_seek($result, $rownum)) {
+ return null;
+ }
+ }
+ if ($fetchmode & DB_FETCHMODE_ASSOC) {
+ if (function_exists('sybase_fetch_assoc')) {
+ $arr = @sybase_fetch_assoc($result);
+ } else {
+ if ($arr = @sybase_fetch_array($result)) {
+ foreach ($arr as $key => $value) {
+ if (is_int($key)) {
+ unset($arr[$key]);
+ }
+ }
+ }
+ }
+ if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE && $arr) {
+ $arr = array_change_key_case($arr, CASE_LOWER);
+ }
+ } else {
+ $arr = @sybase_fetch_row($result);
+ }
+ if (!$arr) {
+ return null;
+ }
+ if ($this->options['portability'] & DB_PORTABILITY_RTRIM) {
+ $this->_rtrimArrayValues($arr);
+ }
+ if ($this->options['portability'] & DB_PORTABILITY_NULL_TO_EMPTY) {
+ $this->_convertNullArrayValuesToEmpty($arr);
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ freeResult()
+
+ /**
+ * Deletes the result set and frees the memory occupied by the result set
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::free() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result PHP's query result resource
+ *
+ * @return bool TRUE on success, FALSE if $result is invalid
+ *
+ * @see DB_result::free()
+ */
+ function freeResult($result)
+ {
+ return is_resource($result) ? sybase_free_result($result) : false;
+ }
+
+ // }}}
+ // {{{ numCols()
+
+ /**
+ * Gets the number of columns in a result set
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::numCols() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result PHP's query result resource
+ *
+ * @return int the number of columns. A DB_Error object on failure.
+ *
+ * @see DB_result::numCols()
+ */
+ function numCols($result)
+ {
+ $cols = @sybase_num_fields($result);
+ if (!$cols) {
+ return $this->sybaseRaiseError();
+ }
+ return $cols;
+ }
+
+ // }}}
+ // {{{ numRows()
+
+ /**
+ * Gets the number of rows in a result set
+ *
+ * This method is not meant to be called directly. Use
+ * DB_result::numRows() instead. It can't be declared "protected"
+ * because DB_result is a separate object.
+ *
+ * @param resource $result PHP's query result resource
+ *
+ * @return int the number of rows. A DB_Error object on failure.
+ *
+ * @see DB_result::numRows()
+ */
+ function numRows($result)
+ {
+ $rows = @sybase_num_rows($result);
+ if ($rows === false) {
+ return $this->sybaseRaiseError();
+ }
+ return $rows;
+ }
+
+ // }}}
+ // {{{ affectedRows()
+
+ /**
+ * Determines the number of rows affected by a data maniuplation query
+ *
+ * 0 is returned for queries that don't manipulate data.
+ *
+ * @return int the number of rows. A DB_Error object on failure.
+ */
+ function affectedRows()
+ {
+ if ($this->_last_query_manip) {
+ $result = @sybase_affected_rows($this->connection);
+ } else {
+ $result = 0;
+ }
+ return $result;
+ }
+
+ // }}}
+ // {{{ nextId()
+
+ /**
+ * Returns the next free id in a sequence
+ *
+ * @param string $seq_name name of the sequence
+ * @param boolean $ondemand when true, the seqence is automatically
+ * created if it does not exist
+ *
+ * @return int the next id number in the sequence.
+ * A DB_Error object on failure.
+ *
+ * @see DB_common::nextID(), DB_common::getSequenceName(),
+ * DB_sybase::createSequence(), DB_sybase::dropSequence()
+ */
+ function nextId($seq_name, $ondemand = true)
+ {
+ $seqname = $this->getSequenceName($seq_name);
+ if ($this->_db && !@sybase_select_db($this->_db, $this->connection)) {
+ return $this->sybaseRaiseError(DB_ERROR_NODBSELECTED);
+ }
+ $repeat = 0;
+ do {
+ $this->pushErrorHandling(PEAR_ERROR_RETURN);
+ $result = $this->query("INSERT INTO $seqname (vapor) VALUES (0)");
+ $this->popErrorHandling();
+ if ($ondemand && DB::isError($result) &&
+ ($result->getCode() == DB_ERROR || $result->getCode() == DB_ERROR_NOSUCHTABLE))
+ {
+ $repeat = 1;
+ $result = $this->createSequence($seq_name);
+ if (DB::isError($result)) {
+ return $this->raiseError($result);
+ }
+ } elseif (!DB::isError($result)) {
+ $result = $this->query("SELECT @@IDENTITY FROM $seqname");
+ $repeat = 0;
+ } else {
+ $repeat = false;
+ }
+ } while ($repeat);
+ if (DB::isError($result)) {
+ return $this->raiseError($result);
+ }
+ $result = $result->fetchRow(DB_FETCHMODE_ORDERED);
+ return $result[0];
+ }
+
+ /**
+ * Creates a new sequence
+ *
+ * @param string $seq_name name of the new sequence
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ *
+ * @see DB_common::createSequence(), DB_common::getSequenceName(),
+ * DB_sybase::nextID(), DB_sybase::dropSequence()
+ */
+ function createSequence($seq_name)
+ {
+ return $this->query('CREATE TABLE '
+ . $this->getSequenceName($seq_name)
+ . ' (id numeric(10, 0) IDENTITY NOT NULL,'
+ . ' vapor int NULL)');
+ }
+
+ // }}}
+ // {{{ dropSequence()
+
+ /**
+ * Deletes a sequence
+ *
+ * @param string $seq_name name of the sequence to be deleted
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ *
+ * @see DB_common::dropSequence(), DB_common::getSequenceName(),
+ * DB_sybase::nextID(), DB_sybase::createSequence()
+ */
+ function dropSequence($seq_name)
+ {
+ return $this->query('DROP TABLE ' . $this->getSequenceName($seq_name));
+ }
+
+ // }}}
+ // {{{ quoteFloat()
+
+ /**
+ * Formats a float value for use within a query in a locale-independent
+ * manner.
+ *
+ * @param float the float value to be quoted.
+ * @return string the quoted string.
+ * @see DB_common::quoteSmart()
+ * @since Method available since release 1.7.8.
+ */
+ function quoteFloat($float) {
+ return $this->escapeSimple(str_replace(',', '.', strval(floatval($float))));
+ }
+
+ // }}}
+ // {{{ autoCommit()
+
+ /**
+ * Enables or disables automatic commits
+ *
+ * @param bool $onoff true turns it on, false turns it off
+ *
+ * @return int DB_OK on success. A DB_Error object if the driver
+ * doesn't support auto-committing transactions.
+ */
+ function autoCommit($onoff = false)
+ {
+ // XXX if $this->transaction_opcount > 0, we should probably
+ // issue a warning here.
+ $this->autocommit = $onoff ? true : false;
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ commit()
+
+ /**
+ * Commits the current transaction
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ */
+ function commit()
+ {
+ if ($this->transaction_opcount > 0) {
+ if ($this->_db && !@sybase_select_db($this->_db, $this->connection)) {
+ return $this->sybaseRaiseError(DB_ERROR_NODBSELECTED);
+ }
+ $result = @sybase_query('COMMIT', $this->connection);
+ $this->transaction_opcount = 0;
+ if (!$result) {
+ return $this->sybaseRaiseError();
+ }
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ rollback()
+
+ /**
+ * Reverts the current transaction
+ *
+ * @return int DB_OK on success. A DB_Error object on failure.
+ */
+ function rollback()
+ {
+ if ($this->transaction_opcount > 0) {
+ if ($this->_db && !@sybase_select_db($this->_db, $this->connection)) {
+ return $this->sybaseRaiseError(DB_ERROR_NODBSELECTED);
+ }
+ $result = @sybase_query('ROLLBACK', $this->connection);
+ $this->transaction_opcount = 0;
+ if (!$result) {
+ return $this->sybaseRaiseError();
+ }
+ }
+ return DB_OK;
+ }
+
+ // }}}
+ // {{{ sybaseRaiseError()
+
+ /**
+ * Produces a DB_Error object regarding the current problem
+ *
+ * @param int $errno if the error is being manually raised pass a
+ * DB_ERROR* constant here. If this isn't passed
+ * the error information gathered from the DBMS.
+ *
+ * @return object the DB_Error object
+ *
+ * @see DB_common::raiseError(),
+ * DB_sybase::errorNative(), DB_sybase::errorCode()
+ */
+ function sybaseRaiseError($errno = null)
+ {
+ $native = $this->errorNative();
+ if ($errno === null) {
+ $errno = $this->errorCode($native);
+ }
+ return $this->raiseError($errno, null, null, null, $native);
+ }
+
+ // }}}
+ // {{{ errorNative()
+
+ /**
+ * Gets the DBMS' native error message produced by the last query
+ *
+ * @return string the DBMS' error message
+ */
+ function errorNative()
+ {
+ return @sybase_get_last_message();
+ }
+
+ // }}}
+ // {{{ errorCode()
+
+ /**
+ * Determines PEAR::DB error code from the database's text error message.
+ *
+ * @param string $errormsg error message returned from the database
+ * @return integer an error number from a DB error constant
+ */
+ function errorCode($errormsg)
+ {
+ static $error_regexps;
+
+ // PHP 5.2+ prepends the function name to $php_errormsg, so we need
+ // this hack to work around it, per bug #9599.
+ $errormsg = preg_replace('/^sybase[a-z_]+\(\): /', '', $errormsg);
+
+ if (!isset($error_regexps)) {
+ $error_regexps = array(
+ '/Incorrect syntax near/'
+ => DB_ERROR_SYNTAX,
+ '/^Unclosed quote before the character string [\"\'].*[\"\']\./'
+ => DB_ERROR_SYNTAX,
+ '/Implicit conversion (from datatype|of NUMERIC value)/i'
+ => DB_ERROR_INVALID_NUMBER,
+ '/Cannot drop the table [\"\'].+[\"\'], because it doesn\'t exist in the system catalogs\./'
+ => DB_ERROR_NOSUCHTABLE,
+ '/Only the owner of object [\"\'].+[\"\'] or a user with System Administrator \(SA\) role can run this command\./'
+ => DB_ERROR_ACCESS_VIOLATION,
+ '/^.+ permission denied on object .+, database .+, owner .+/'
+ => DB_ERROR_ACCESS_VIOLATION,
+ '/^.* permission denied, database .+, owner .+/'
+ => DB_ERROR_ACCESS_VIOLATION,
+ '/[^.*] not found\./'
+ => DB_ERROR_NOSUCHTABLE,
+ '/There is already an object named/'
+ => DB_ERROR_ALREADY_EXISTS,
+ '/Invalid column name/'
+ => DB_ERROR_NOSUCHFIELD,
+ '/does not allow null values/'
+ => DB_ERROR_CONSTRAINT_NOT_NULL,
+ '/Command has been aborted/'
+ => DB_ERROR_CONSTRAINT,
+ '/^Cannot drop the index .* because it doesn\'t exist/i'
+ => DB_ERROR_NOT_FOUND,
+ '/^There is already an index/i'
+ => DB_ERROR_ALREADY_EXISTS,
+ '/^There are fewer columns in the INSERT statement than values specified/i'
+ => DB_ERROR_VALUE_COUNT_ON_ROW,
+ '/Divide by zero/i'
+ => DB_ERROR_DIVZERO,
+ );
+ }
+
+ foreach ($error_regexps as $regexp => $code) {
+ if (preg_match($regexp, $errormsg)) {
+ return $code;
+ }
+ }
+ return DB_ERROR;
+ }
+
+ // }}}
+ // {{{ tableInfo()
+
+ /**
+ * Returns information about a table or a result set
+ *
+ * NOTE: only supports 'table' and 'flags' if <var>$result</var>
+ * is a table name.
+ *
+ * @param object|string $result DB_result object from a query or a
+ * string containing the name of a table.
+ * While this also accepts a query result
+ * resource identifier, this behavior is
+ * deprecated.
+ * @param int $mode a valid tableInfo mode
+ *
+ * @return array an associative array with the information requested.
+ * A DB_Error object on failure.
+ *
+ * @see DB_common::tableInfo()
+ * @since Method available since Release 1.6.0
+ */
+ function tableInfo($result, $mode = null)
+ {
+ if (is_string($result)) {
+ /*
+ * Probably received a table name.
+ * Create a result resource identifier.
+ */
+ if ($this->_db && !@sybase_select_db($this->_db, $this->connection)) {
+ return $this->sybaseRaiseError(DB_ERROR_NODBSELECTED);
+ }
+ $id = @sybase_query("SELECT * FROM $result WHERE 1=0",
+ $this->connection);
+ $got_string = true;
+ } elseif (isset($result->result)) {
+ /*
+ * Probably received a result object.
+ * Extract the result resource identifier.
+ */
+ $id = $result->result;
+ $got_string = false;
+ } else {
+ /*
+ * Probably received a result resource identifier.
+ * Copy it.
+ * Deprecated. Here for compatibility only.
+ */
+ $id = $result;
+ $got_string = false;
+ }
+
+ if (!is_resource($id)) {
+ return $this->sybaseRaiseError(DB_ERROR_NEED_MORE_DATA);
+ }
+
+ if ($this->options['portability'] & DB_PORTABILITY_LOWERCASE) {
+ $case_func = 'strtolower';
+ } else {
+ $case_func = 'strval';
+ }
+
+ $count = @sybase_num_fields($id);
+ $res = array();
+
+ if ($mode) {
+ $res['num_fields'] = $count;
+ }
+
+ for ($i = 0; $i < $count; $i++) {
+ $f = @sybase_fetch_field($id, $i);
+ // column_source is often blank
+ $res[$i] = array(
+ 'table' => $got_string
+ ? $case_func($result)
+ : $case_func($f->column_source),
+ 'name' => $case_func($f->name),
+ 'type' => $f->type,
+ 'len' => $f->max_length,
+ 'flags' => '',
+ );
+ if ($res[$i]['table']) {
+ $res[$i]['flags'] = $this->_sybase_field_flags(
+ $res[$i]['table'], $res[$i]['name']);
+ }
+ if ($mode & DB_TABLEINFO_ORDER) {
+ $res['order'][$res[$i]['name']] = $i;
+ }
+ if ($mode & DB_TABLEINFO_ORDERTABLE) {
+ $res['ordertable'][$res[$i]['table']][$res[$i]['name']] = $i;
+ }
+ }
+
+ // free the result only if we were called on a table
+ if ($got_string) {
+ @sybase_free_result($id);
+ }
+ return $res;
+ }
+
+ // }}}
+ // {{{ _sybase_field_flags()
+
+ /**
+ * Get the flags for a field
+ *
+ * Currently supports:
+ * + <samp>unique_key</samp> (unique index, unique check or primary_key)
+ * + <samp>multiple_key</samp> (multi-key index)
+ *
+ * @param string $table the table name
+ * @param string $column the field name
+ *
+ * @return string space delimited string of flags. Empty string if none.
+ *
+ * @access private
+ */
+ function _sybase_field_flags($table, $column)
+ {
+ static $tableName = null;
+ static $flags = array();
+
+ if ($table != $tableName) {
+ $flags = array();
+ $tableName = $table;
+
+ /* We're running sp_helpindex directly because it doesn't exist in
+ * older versions of ASE -- unfortunately, we can't just use
+ * DB::isError() because the user may be using callback error
+ * handling. */
+ $res = @sybase_query("sp_helpindex $table", $this->connection);
+
+ if ($res === false || $res === true) {
+ // Fake a valid response for BC reasons.
+ return '';
+ }
+
+ while (($val = sybase_fetch_assoc($res)) !== false) {
+ if (!isset($val['index_keys'])) {
+ /* No useful information returned. Break and be done with
+ * it, which preserves the pre-1.7.9 behaviour. */
+ break;
+ }
+
+ $keys = explode(', ', trim($val['index_keys']));
+
+ if (sizeof($keys) > 1) {
+ foreach ($keys as $key) {
+ $this->_add_flag($flags[$key], 'multiple_key');
+ }
+ }
+
+ if (strpos($val['index_description'], 'unique')) {
+ foreach ($keys as $key) {
+ $this->_add_flag($flags[$key], 'unique_key');
+ }
+ }
+ }
+
+ sybase_free_result($res);
+
+ }
+
+ if (array_key_exists($column, $flags)) {
+ return(implode(' ', $flags[$column]));
+ }
+
+ return '';
+ }
+
+ // }}}
+ // {{{ _add_flag()
+
+ /**
+ * Adds a string to the flags array if the flag is not yet in there
+ * - if there is no flag present the array is created
+ *
+ * @param array $array reference of flags array to add a value to
+ * @param mixed $value value to add to the flag array
+ *
+ * @return void
+ *
+ * @access private
+ */
+ function _add_flag(&$array, $value)
+ {
+ if (!is_array($array)) {
+ $array = array($value);
+ } elseif (!in_array($value, $array)) {
+ array_push($array, $value);
+ }
+ }
+
+ // }}}
+ // {{{ getSpecialQuery()
+
+ /**
+ * Obtains the query string needed for listing a given type of objects
+ *
+ * @param string $type the kind of objects you want to retrieve
+ *
+ * @return string the SQL query string or null if the driver doesn't
+ * support the object type requested
+ *
+ * @access protected
+ * @see DB_common::getListOf()
+ */
+ function getSpecialQuery($type)
+ {
+ switch ($type) {
+ case 'tables':
+ return "SELECT name FROM sysobjects WHERE type = 'U'"
+ . ' ORDER BY name';
+ case 'views':
+ return "SELECT name FROM sysobjects WHERE type = 'V'";
+ default:
+ return null;
+ }
+ }
+
+ // }}}
+
+}
+
+/*
+ * Local variables:
+ * tab-width: 4
+ * c-basic-offset: 4
+ * End:
+ */
+
+?>
diff --git a/extlib/Mail.php b/extlib/Mail.php
new file mode 100644
index 000000000..3a0c1a9cb
--- /dev/null
+++ b/extlib/Mail.php
@@ -0,0 +1,238 @@
+<?php
+//
+// +----------------------------------------------------------------------+
+// | PHP Version 4 |
+// +----------------------------------------------------------------------+
+// | Copyright (c) 1997-2003 The PHP Group |
+// +----------------------------------------------------------------------+
+// | This source file is subject to version 2.02 of the PHP license, |
+// | that is bundled with this package in the file LICENSE, and is |
+// | available at through the world-wide-web at |
+// | http://www.php.net/license/2_02.txt. |
+// | If you did not receive a copy of the PHP license and are unable to |
+// | obtain it through the world-wide-web, please send a note to |
+// | license@php.net so we can mail you a copy immediately. |
+// +----------------------------------------------------------------------+
+// | Author: Chuck Hagenbuch <chuck@horde.org> |
+// +----------------------------------------------------------------------+
+//
+// $Id: Mail.php,v 1.17 2006/09/15 03:41:18 jon Exp $
+
+require_once 'PEAR.php';
+
+/**
+ * PEAR's Mail:: interface. Defines the interface for implementing
+ * mailers under the PEAR hierarchy, and provides supporting functions
+ * useful in multiple mailer backends.
+ *
+ * @access public
+ * @version $Revision: 1.17 $
+ * @package Mail
+ */
+class Mail
+{
+ /**
+ * Line terminator used for separating header lines.
+ * @var string
+ */
+ var $sep = "\r\n";
+
+ /**
+ * Provides an interface for generating Mail:: objects of various
+ * types
+ *
+ * @param string $driver The kind of Mail:: object to instantiate.
+ * @param array $params The parameters to pass to the Mail:: object.
+ * @return object Mail a instance of the driver class or if fails a PEAR Error
+ * @access public
+ */
+ function &factory($driver, $params = array())
+ {
+ $driver = strtolower($driver);
+ @include_once 'Mail/' . $driver . '.php';
+ $class = 'Mail_' . $driver;
+ if (class_exists($class)) {
+ $mailer = new $class($params);
+ return $mailer;
+ } else {
+ return PEAR::raiseError('Unable to find class for driver ' . $driver);
+ }
+ }
+
+ /**
+ * Implements Mail::send() function using php's built-in mail()
+ * command.
+ *
+ * @param mixed $recipients Either a comma-seperated list of recipients
+ * (RFC822 compliant), or an array of recipients,
+ * each RFC822 valid. This may contain recipients not
+ * specified in the headers, for Bcc:, resending
+ * messages, etc.
+ *
+ * @param array $headers The array of headers to send with the mail, in an
+ * associative array, where the array key is the
+ * header name (ie, 'Subject'), and the array value
+ * is the header value (ie, 'test'). The header
+ * produced from those values would be 'Subject:
+ * test'.
+ *
+ * @param string $body The full text of the message body, including any
+ * Mime parts, etc.
+ *
+ * @return mixed Returns true on success, or a PEAR_Error
+ * containing a descriptive error message on
+ * failure.
+ * @access public
+ * @deprecated use Mail_mail::send instead
+ */
+ function send($recipients, $headers, $body)
+ {
+ $this->_sanitizeHeaders($headers);
+
+ // if we're passed an array of recipients, implode it.
+ if (is_array($recipients)) {
+ $recipients = implode(', ', $recipients);
+ }
+
+ // get the Subject out of the headers array so that we can
+ // pass it as a seperate argument to mail().
+ $subject = '';
+ if (isset($headers['Subject'])) {
+ $subject = $headers['Subject'];
+ unset($headers['Subject']);
+ }
+
+ // flatten the headers out.
+ list(,$text_headers) = Mail::prepareHeaders($headers);
+
+ return mail($recipients, $subject, $body, $text_headers);
+
+ }
+
+ /**
+ * Sanitize an array of mail headers by removing any additional header
+ * strings present in a legitimate header's value. The goal of this
+ * filter is to prevent mail injection attacks.
+ *
+ * @param array $headers The associative array of headers to sanitize.
+ *
+ * @access private
+ */
+ function _sanitizeHeaders(&$headers)
+ {
+ foreach ($headers as $key => $value) {
+ $headers[$key] =
+ preg_replace('=((<CR>|<LF>|0x0A/%0A|0x0D/%0D|\\n|\\r)\S).*=i',
+ null, $value);
+ }
+ }
+
+ /**
+ * Take an array of mail headers and return a string containing
+ * text usable in sending a message.
+ *
+ * @param array $headers The array of headers to prepare, in an associative
+ * array, where the array key is the header name (ie,
+ * 'Subject'), and the array value is the header
+ * value (ie, 'test'). The header produced from those
+ * values would be 'Subject: test'.
+ *
+ * @return mixed Returns false if it encounters a bad address,
+ * otherwise returns an array containing two
+ * elements: Any From: address found in the headers,
+ * and the plain text version of the headers.
+ * @access private
+ */
+ function prepareHeaders($headers)
+ {
+ $lines = array();
+ $from = null;
+
+ foreach ($headers as $key => $value) {
+ if (strcasecmp($key, 'From') === 0) {
+ include_once 'Mail/RFC822.php';
+ $parser = &new Mail_RFC822();
+ $addresses = $parser->parseAddressList($value, 'localhost', false);
+ if (PEAR::isError($addresses)) {
+ return $addresses;
+ }
+
+ $from = $addresses[0]->mailbox . '@' . $addresses[0]->host;
+
+ // Reject envelope From: addresses with spaces.
+ if (strstr($from, ' ')) {
+ return false;
+ }
+
+ $lines[] = $key . ': ' . $value;
+ } elseif (strcasecmp($key, 'Received') === 0) {
+ $received = array();
+ if (is_array($value)) {
+ foreach ($value as $line) {
+ $received[] = $key . ': ' . $line;
+ }
+ }
+ else {
+ $received[] = $key . ': ' . $value;
+ }
+ // Put Received: headers at the top. Spam detectors often
+ // flag messages with Received: headers after the Subject:
+ // as spam.
+ $lines = array_merge($received, $lines);
+ } else {
+ // If $value is an array (i.e., a list of addresses), convert
+ // it to a comma-delimited string of its elements (addresses).
+ if (is_array($value)) {
+ $value = implode(', ', $value);
+ }
+ $lines[] = $key . ': ' . $value;
+ }
+ }
+
+ return array($from, join($this->sep, $lines));
+ }
+
+ /**
+ * Take a set of recipients and parse them, returning an array of
+ * bare addresses (forward paths) that can be passed to sendmail
+ * or an smtp server with the rcpt to: command.
+ *
+ * @param mixed Either a comma-seperated list of recipients
+ * (RFC822 compliant), or an array of recipients,
+ * each RFC822 valid.
+ *
+ * @return mixed An array of forward paths (bare addresses) or a PEAR_Error
+ * object if the address list could not be parsed.
+ * @access private
+ */
+ function parseRecipients($recipients)
+ {
+ include_once 'Mail/RFC822.php';
+
+ // if we're passed an array, assume addresses are valid and
+ // implode them before parsing.
+ if (is_array($recipients)) {
+ $recipients = implode(', ', $recipients);
+ }
+
+ // Parse recipients, leaving out all personal info. This is
+ // for smtp recipients, etc. All relevant personal information
+ // should already be in the headers.
+ $addresses = Mail_RFC822::parseAddressList($recipients, 'localhost', false);
+
+ // If parseAddressList() returned a PEAR_Error object, just return it.
+ if (PEAR::isError($addresses)) {
+ return $addresses;
+ }
+
+ $recipients = array();
+ if (is_array($addresses)) {
+ foreach ($addresses as $ob) {
+ $recipients[] = $ob->mailbox . '@' . $ob->host;
+ }
+ }
+
+ return $recipients;
+ }
+
+}
diff --git a/extlib/Mail/RFC822.php b/extlib/Mail/RFC822.php
new file mode 100644
index 000000000..8714df2e2
--- /dev/null
+++ b/extlib/Mail/RFC822.php
@@ -0,0 +1,940 @@
+<?php
+// +-----------------------------------------------------------------------+
+// | Copyright (c) 2001-2002, Richard Heyes |
+// | All rights reserved. |
+// | |
+// | Redistribution and use in source and binary forms, with or without |
+// | modification, are permitted provided that the following conditions |
+// | are met: |
+// | |
+// | o Redistributions of source code must retain the above copyright |
+// | notice, this list of conditions and the following disclaimer. |
+// | o Redistributions in binary form must reproduce the above copyright |
+// | notice, this list of conditions and the following disclaimer in the |
+// | documentation and/or other materials provided with the distribution.|
+// | o The names of the authors may not be used to endorse or promote |
+// | products derived from this software without specific prior written |
+// | permission. |
+// | |
+// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
+// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
+// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
+// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
+// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
+// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
+// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
+// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
+// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
+// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
+// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
+// | |
+// +-----------------------------------------------------------------------+
+// | Authors: Richard Heyes <richard@phpguru.org> |
+// | Chuck Hagenbuch <chuck@horde.org> |
+// +-----------------------------------------------------------------------+
+
+/**
+ * RFC 822 Email address list validation Utility
+ *
+ * What is it?
+ *
+ * This class will take an address string, and parse it into it's consituent
+ * parts, be that either addresses, groups, or combinations. Nested groups
+ * are not supported. The structure it returns is pretty straight forward,
+ * and is similar to that provided by the imap_rfc822_parse_adrlist(). Use
+ * print_r() to view the structure.
+ *
+ * How do I use it?
+ *
+ * $address_string = 'My Group: "Richard" <richard@localhost> (A comment), ted@example.com (Ted Bloggs), Barney;';
+ * $structure = Mail_RFC822::parseAddressList($address_string, 'example.com', true)
+ * print_r($structure);
+ *
+ * @author Richard Heyes <richard@phpguru.org>
+ * @author Chuck Hagenbuch <chuck@horde.org>
+ * @version $Revision: 1.24 $
+ * @license BSD
+ * @package Mail
+ */
+class Mail_RFC822 {
+
+ /**
+ * The address being parsed by the RFC822 object.
+ * @var string $address
+ */
+ var $address = '';
+
+ /**
+ * The default domain to use for unqualified addresses.
+ * @var string $default_domain
+ */
+ var $default_domain = 'localhost';
+
+ /**
+ * Should we return a nested array showing groups, or flatten everything?
+ * @var boolean $nestGroups
+ */
+ var $nestGroups = true;
+
+ /**
+ * Whether or not to validate atoms for non-ascii characters.
+ * @var boolean $validate
+ */
+ var $validate = true;
+
+ /**
+ * The array of raw addresses built up as we parse.
+ * @var array $addresses
+ */
+ var $addresses = array();
+
+ /**
+ * The final array of parsed address information that we build up.
+ * @var array $structure
+ */
+ var $structure = array();
+
+ /**
+ * The current error message, if any.
+ * @var string $error
+ */
+ var $error = null;
+
+ /**
+ * An internal counter/pointer.
+ * @var integer $index
+ */
+ var $index = null;
+
+ /**
+ * The number of groups that have been found in the address list.
+ * @var integer $num_groups
+ * @access public
+ */
+ var $num_groups = 0;
+
+ /**
+ * A variable so that we can tell whether or not we're inside a
+ * Mail_RFC822 object.
+ * @var boolean $mailRFC822
+ */
+ var $mailRFC822 = true;
+
+ /**
+ * A limit after which processing stops
+ * @var int $limit
+ */
+ var $limit = null;
+
+ /**
+ * Sets up the object. The address must either be set here or when
+ * calling parseAddressList(). One or the other.
+ *
+ * @access public
+ * @param string $address The address(es) to validate.
+ * @param string $default_domain Default domain/host etc. If not supplied, will be set to localhost.
+ * @param boolean $nest_groups Whether to return the structure with groups nested for easier viewing.
+ * @param boolean $validate Whether to validate atoms. Turn this off if you need to run addresses through before encoding the personal names, for instance.
+ *
+ * @return object Mail_RFC822 A new Mail_RFC822 object.
+ */
+ function Mail_RFC822($address = null, $default_domain = null, $nest_groups = null, $validate = null, $limit = null)
+ {
+ if (isset($address)) $this->address = $address;
+ if (isset($default_domain)) $this->default_domain = $default_domain;
+ if (isset($nest_groups)) $this->nestGroups = $nest_groups;
+ if (isset($validate)) $this->validate = $validate;
+ if (isset($limit)) $this->limit = $limit;
+ }
+
+ /**
+ * Starts the whole process. The address must either be set here
+ * or when creating the object. One or the other.
+ *
+ * @access public
+ * @param string $address The address(es) to validate.
+ * @param string $default_domain Default domain/host etc.
+ * @param boolean $nest_groups Whether to return the structure with groups nested for easier viewing.
+ * @param boolean $validate Whether to validate atoms. Turn this off if you need to run addresses through before encoding the personal names, for instance.
+ *
+ * @return array A structured array of addresses.
+ */
+ function parseAddressList($address = null, $default_domain = null, $nest_groups = null, $validate = null, $limit = null)
+ {
+ if (!isset($this) || !isset($this->mailRFC822)) {
+ $obj = new Mail_RFC822($address, $default_domain, $nest_groups, $validate, $limit);
+ return $obj->parseAddressList();
+ }
+
+ if (isset($address)) $this->address = $address;
+ if (isset($default_domain)) $this->default_domain = $default_domain;
+ if (isset($nest_groups)) $this->nestGroups = $nest_groups;
+ if (isset($validate)) $this->validate = $validate;
+ if (isset($limit)) $this->limit = $limit;
+
+ $this->structure = array();
+ $this->addresses = array();
+ $this->error = null;
+ $this->index = null;
+
+ // Unfold any long lines in $this->address.
+ $this->address = preg_replace('/\r?\n/', "\r\n", $this->address);
+ $this->address = preg_replace('/\r\n(\t| )+/', ' ', $this->address);
+
+ while ($this->address = $this->_splitAddresses($this->address));
+
+ if ($this->address === false || isset($this->error)) {
+ require_once 'PEAR.php';
+ return PEAR::raiseError($this->error);
+ }
+
+ // Validate each address individually. If we encounter an invalid
+ // address, stop iterating and return an error immediately.
+ foreach ($this->addresses as $address) {
+ $valid = $this->_validateAddress($address);
+
+ if ($valid === false || isset($this->error)) {
+ require_once 'PEAR.php';
+ return PEAR::raiseError($this->error);
+ }
+
+ if (!$this->nestGroups) {
+ $this->structure = array_merge($this->structure, $valid);
+ } else {
+ $this->structure[] = $valid;
+ }
+ }
+
+ return $this->structure;
+ }
+
+ /**
+ * Splits an address into separate addresses.
+ *
+ * @access private
+ * @param string $address The addresses to split.
+ * @return boolean Success or failure.
+ */
+ function _splitAddresses($address)
+ {
+ if (!empty($this->limit) && count($this->addresses) == $this->limit) {
+ return '';
+ }
+
+ if ($this->_isGroup($address) && !isset($this->error)) {
+ $split_char = ';';
+ $is_group = true;
+ } elseif (!isset($this->error)) {
+ $split_char = ',';
+ $is_group = false;
+ } elseif (isset($this->error)) {
+ return false;
+ }
+
+ // Split the string based on the above ten or so lines.
+ $parts = explode($split_char, $address);
+ $string = $this->_splitCheck($parts, $split_char);
+
+ // If a group...
+ if ($is_group) {
+ // If $string does not contain a colon outside of
+ // brackets/quotes etc then something's fubar.
+
+ // First check there's a colon at all:
+ if (strpos($string, ':') === false) {
+ $this->error = 'Invalid address: ' . $string;
+ return false;
+ }
+
+ // Now check it's outside of brackets/quotes:
+ if (!$this->_splitCheck(explode(':', $string), ':')) {
+ return false;
+ }
+
+ // We must have a group at this point, so increase the counter:
+ $this->num_groups++;
+ }
+
+ // $string now contains the first full address/group.
+ // Add to the addresses array.
+ $this->addresses[] = array(
+ 'address' => trim($string),
+ 'group' => $is_group
+ );
+
+ // Remove the now stored address from the initial line, the +1
+ // is to account for the explode character.
+ $address = trim(substr($address, strlen($string) + 1));
+
+ // If the next char is a comma and this was a group, then
+ // there are more addresses, otherwise, if there are any more
+ // chars, then there is another address.
+ if ($is_group && substr($address, 0, 1) == ','){
+ $address = trim(substr($address, 1));
+ return $address;
+
+ } elseif (strlen($address) > 0) {
+ return $address;
+
+ } else {
+ return '';
+ }
+
+ // If you got here then something's off
+ return false;
+ }
+
+ /**
+ * Checks for a group at the start of the string.
+ *
+ * @access private
+ * @param string $address The address to check.
+ * @return boolean Whether or not there is a group at the start of the string.
+ */
+ function _isGroup($address)
+ {
+ // First comma not in quotes, angles or escaped:
+ $parts = explode(',', $address);
+ $string = $this->_splitCheck($parts, ',');
+
+ // Now we have the first address, we can reliably check for a
+ // group by searching for a colon that's not escaped or in
+ // quotes or angle brackets.
+ if (count($parts = explode(':', $string)) > 1) {
+ $string2 = $this->_splitCheck($parts, ':');
+ return ($string2 !== $string);
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * A common function that will check an exploded string.
+ *
+ * @access private
+ * @param array $parts The exloded string.
+ * @param string $char The char that was exploded on.
+ * @return mixed False if the string contains unclosed quotes/brackets, or the string on success.
+ */
+ function _splitCheck($parts, $char)
+ {
+ $string = $parts[0];
+
+ for ($i = 0; $i < count($parts); $i++) {
+ if ($this->_hasUnclosedQuotes($string)
+ || $this->_hasUnclosedBrackets($string, '<>')
+ || $this->_hasUnclosedBrackets($string, '[]')
+ || $this->_hasUnclosedBrackets($string, '()')
+ || substr($string, -1) == '\\') {
+ if (isset($parts[$i + 1])) {
+ $string = $string . $char . $parts[$i + 1];
+ } else {
+ $this->error = 'Invalid address spec. Unclosed bracket or quotes';
+ return false;
+ }
+ } else {
+ $this->index = $i;
+ break;
+ }
+ }
+
+ return $string;
+ }
+
+ /**
+ * Checks if a string has unclosed quotes or not.
+ *
+ * @access private
+ * @param string $string The string to check.
+ * @return boolean True if there are unclosed quotes inside the string,
+ * false otherwise.
+ */
+ function _hasUnclosedQuotes($string)
+ {
+ $string = trim($string);
+ $iMax = strlen($string);
+ $in_quote = false;
+ $i = $slashes = 0;
+
+ for (; $i < $iMax; ++$i) {
+ switch ($string[$i]) {
+ case '\\':
+ ++$slashes;
+ break;
+
+ case '"':
+ if ($slashes % 2 == 0) {
+ $in_quote = !$in_quote;
+ }
+ // Fall through to default action below.
+
+ default:
+ $slashes = 0;
+ break;
+ }
+ }
+
+ return $in_quote;
+ }
+
+ /**
+ * Checks if a string has an unclosed brackets or not. IMPORTANT:
+ * This function handles both angle brackets and square brackets;
+ *
+ * @access private
+ * @param string $string The string to check.
+ * @param string $chars The characters to check for.
+ * @return boolean True if there are unclosed brackets inside the string, false otherwise.
+ */
+ function _hasUnclosedBrackets($string, $chars)
+ {
+ $num_angle_start = substr_count($string, $chars[0]);
+ $num_angle_end = substr_count($string, $chars[1]);
+
+ $this->_hasUnclosedBracketsSub($string, $num_angle_start, $chars[0]);
+ $this->_hasUnclosedBracketsSub($string, $num_angle_end, $chars[1]);
+
+ if ($num_angle_start < $num_angle_end) {
+ $this->error = 'Invalid address spec. Unmatched quote or bracket (' . $chars . ')';
+ return false;
+ } else {
+ return ($num_angle_start > $num_angle_end);
+ }
+ }
+
+ /**
+ * Sub function that is used only by hasUnclosedBrackets().
+ *
+ * @access private
+ * @param string $string The string to check.
+ * @param integer &$num The number of occurences.
+ * @param string $char The character to count.
+ * @return integer The number of occurences of $char in $string, adjusted for backslashes.
+ */
+ function _hasUnclosedBracketsSub($string, &$num, $char)
+ {
+ $parts = explode($char, $string);
+ for ($i = 0; $i < count($parts); $i++){
+ if (substr($parts[$i], -1) == '\\' || $this->_hasUnclosedQuotes($parts[$i]))
+ $num--;
+ if (isset($parts[$i + 1]))
+ $parts[$i + 1] = $parts[$i] . $char . $parts[$i + 1];
+ }
+
+ return $num;
+ }
+
+ /**
+ * Function to begin checking the address.
+ *
+ * @access private
+ * @param string $address The address to validate.
+ * @return mixed False on failure, or a structured array of address information on success.
+ */
+ function _validateAddress($address)
+ {
+ $is_group = false;
+ $addresses = array();
+
+ if ($address['group']) {
+ $is_group = true;
+
+ // Get the group part of the name
+ $parts = explode(':', $address['address']);
+ $groupname = $this->_splitCheck($parts, ':');
+ $structure = array();
+
+ // And validate the group part of the name.
+ if (!$this->_validatePhrase($groupname)){
+ $this->error = 'Group name did not validate.';
+ return false;
+ } else {
+ // Don't include groups if we are not nesting
+ // them. This avoids returning invalid addresses.
+ if ($this->nestGroups) {
+ $structure = new stdClass;
+ $structure->groupname = $groupname;
+ }
+ }
+
+ $address['address'] = ltrim(substr($address['address'], strlen($groupname . ':')));
+ }
+
+ // If a group then split on comma and put into an array.
+ // Otherwise, Just put the whole address in an array.
+ if ($is_group) {
+ while (strlen($address['address']) > 0) {
+ $parts = explode(',', $address['address']);
+ $addresses[] = $this->_splitCheck($parts, ',');
+ $address['address'] = trim(substr($address['address'], strlen(end($addresses) . ',')));
+ }
+ } else {
+ $addresses[] = $address['address'];
+ }
+
+ // Check that $addresses is set, if address like this:
+ // Groupname:;
+ // Then errors were appearing.
+ if (!count($addresses)){
+ $this->error = 'Empty group.';
+ return false;
+ }
+
+ // Trim the whitespace from all of the address strings.
+ array_map('trim', $addresses);
+
+ // Validate each mailbox.
+ // Format could be one of: name <geezer@domain.com>
+ // geezer@domain.com
+ // geezer
+ // ... or any other format valid by RFC 822.
+ for ($i = 0; $i < count($addresses); $i++) {
+ if (!$this->validateMailbox($addresses[$i])) {
+ if (empty($this->error)) {
+ $this->error = 'Validation failed for: ' . $addresses[$i];
+ }
+ return false;
+ }
+ }
+
+ // Nested format
+ if ($this->nestGroups) {
+ if ($is_group) {
+ $structure->addresses = $addresses;
+ } else {
+ $structure = $addresses[0];
+ }
+
+ // Flat format
+ } else {
+ if ($is_group) {
+ $structure = array_merge($structure, $addresses);
+ } else {
+ $structure = $addresses;
+ }
+ }
+
+ return $structure;
+ }
+
+ /**
+ * Function to validate a phrase.
+ *
+ * @access private
+ * @param string $phrase The phrase to check.
+ * @return boolean Success or failure.
+ */
+ function _validatePhrase($phrase)
+ {
+ // Splits on one or more Tab or space.
+ $parts = preg_split('/[ \\x09]+/', $phrase, -1, PREG_SPLIT_NO_EMPTY);
+
+ $phrase_parts = array();
+ while (count($parts) > 0){
+ $phrase_parts[] = $this->_splitCheck($parts, ' ');
+ for ($i = 0; $i < $this->index + 1; $i++)
+ array_shift($parts);
+ }
+
+ foreach ($phrase_parts as $part) {
+ // If quoted string:
+ if (substr($part, 0, 1) == '"') {
+ if (!$this->_validateQuotedString($part)) {
+ return false;
+ }
+ continue;
+ }
+
+ // Otherwise it's an atom:
+ if (!$this->_validateAtom($part)) return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Function to validate an atom which from rfc822 is:
+ * atom = 1*<any CHAR except specials, SPACE and CTLs>
+ *
+ * If validation ($this->validate) has been turned off, then
+ * validateAtom() doesn't actually check anything. This is so that you
+ * can split a list of addresses up before encoding personal names
+ * (umlauts, etc.), for example.
+ *
+ * @access private
+ * @param string $atom The string to check.
+ * @return boolean Success or failure.
+ */
+ function _validateAtom($atom)
+ {
+ if (!$this->validate) {
+ // Validation has been turned off; assume the atom is okay.
+ return true;
+ }
+
+ // Check for any char from ASCII 0 - ASCII 127
+ if (!preg_match('/^[\\x00-\\x7E]+$/i', $atom, $matches)) {
+ return false;
+ }
+
+ // Check for specials:
+ if (preg_match('/[][()<>@,;\\:". ]/', $atom)) {
+ return false;
+ }
+
+ // Check for control characters (ASCII 0-31):
+ if (preg_match('/[\\x00-\\x1F]+/', $atom)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Function to validate quoted string, which is:
+ * quoted-string = <"> *(qtext/quoted-pair) <">
+ *
+ * @access private
+ * @param string $qstring The string to check
+ * @return boolean Success or failure.
+ */
+ function _validateQuotedString($qstring)
+ {
+ // Leading and trailing "
+ $qstring = substr($qstring, 1, -1);
+
+ // Perform check, removing quoted characters first.
+ return !preg_match('/[\x0D\\\\"]/', preg_replace('/\\\\./', '', $qstring));
+ }
+
+ /**
+ * Function to validate a mailbox, which is:
+ * mailbox = addr-spec ; simple address
+ * / phrase route-addr ; name and route-addr
+ *
+ * @access public
+ * @param string &$mailbox The string to check.
+ * @return boolean Success or failure.
+ */
+ function validateMailbox(&$mailbox)
+ {
+ // A couple of defaults.
+ $phrase = '';
+ $comment = '';
+ $comments = array();
+
+ // Catch any RFC822 comments and store them separately.
+ $_mailbox = $mailbox;
+ while (strlen(trim($_mailbox)) > 0) {
+ $parts = explode('(', $_mailbox);
+ $before_comment = $this->_splitCheck($parts, '(');
+ if ($before_comment != $_mailbox) {
+ // First char should be a (.
+ $comment = substr(str_replace($before_comment, '', $_mailbox), 1);
+ $parts = explode(')', $comment);
+ $comment = $this->_splitCheck($parts, ')');
+ $comments[] = $comment;
+
+ // +1 is for the trailing )
+ $_mailbox = substr($_mailbox, strpos($_mailbox, $comment)+strlen($comment)+1);
+ } else {
+ break;
+ }
+ }
+
+ foreach ($comments as $comment) {
+ $mailbox = str_replace("($comment)", '', $mailbox);
+ }
+
+ $mailbox = trim($mailbox);
+
+ // Check for name + route-addr
+ if (substr($mailbox, -1) == '>' && substr($mailbox, 0, 1) != '<') {
+ $parts = explode('<', $mailbox);
+ $name = $this->_splitCheck($parts, '<');
+
+ $phrase = trim($name);
+ $route_addr = trim(substr($mailbox, strlen($name.'<'), -1));
+
+ if ($this->_validatePhrase($phrase) === false || ($route_addr = $this->_validateRouteAddr($route_addr)) === false) {
+ return false;
+ }
+
+ // Only got addr-spec
+ } else {
+ // First snip angle brackets if present.
+ if (substr($mailbox, 0, 1) == '<' && substr($mailbox, -1) == '>') {
+ $addr_spec = substr($mailbox, 1, -1);
+ } else {
+ $addr_spec = $mailbox;
+ }
+
+ if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) {
+ return false;
+ }
+ }
+
+ // Construct the object that will be returned.
+ $mbox = new stdClass();
+
+ // Add the phrase (even if empty) and comments
+ $mbox->personal = $phrase;
+ $mbox->comment = isset($comments) ? $comments : array();
+
+ if (isset($route_addr)) {
+ $mbox->mailbox = $route_addr['local_part'];
+ $mbox->host = $route_addr['domain'];
+ $route_addr['adl'] !== '' ? $mbox->adl = $route_addr['adl'] : '';
+ } else {
+ $mbox->mailbox = $addr_spec['local_part'];
+ $mbox->host = $addr_spec['domain'];
+ }
+
+ $mailbox = $mbox;
+ return true;
+ }
+
+ /**
+ * This function validates a route-addr which is:
+ * route-addr = "<" [route] addr-spec ">"
+ *
+ * Angle brackets have already been removed at the point of
+ * getting to this function.
+ *
+ * @access private
+ * @param string $route_addr The string to check.
+ * @return mixed False on failure, or an array containing validated address/route information on success.
+ */
+ function _validateRouteAddr($route_addr)
+ {
+ // Check for colon.
+ if (strpos($route_addr, ':') !== false) {
+ $parts = explode(':', $route_addr);
+ $route = $this->_splitCheck($parts, ':');
+ } else {
+ $route = $route_addr;
+ }
+
+ // If $route is same as $route_addr then the colon was in
+ // quotes or brackets or, of course, non existent.
+ if ($route === $route_addr){
+ unset($route);
+ $addr_spec = $route_addr;
+ if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) {
+ return false;
+ }
+ } else {
+ // Validate route part.
+ if (($route = $this->_validateRoute($route)) === false) {
+ return false;
+ }
+
+ $addr_spec = substr($route_addr, strlen($route . ':'));
+
+ // Validate addr-spec part.
+ if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) {
+ return false;
+ }
+ }
+
+ if (isset($route)) {
+ $return['adl'] = $route;
+ } else {
+ $return['adl'] = '';
+ }
+
+ $return = array_merge($return, $addr_spec);
+ return $return;
+ }
+
+ /**
+ * Function to validate a route, which is:
+ * route = 1#("@" domain) ":"
+ *
+ * @access private
+ * @param string $route The string to check.
+ * @return mixed False on failure, or the validated $route on success.
+ */
+ function _validateRoute($route)
+ {
+ // Split on comma.
+ $domains = explode(',', trim($route));
+
+ foreach ($domains as $domain) {
+ $domain = str_replace('@', '', trim($domain));
+ if (!$this->_validateDomain($domain)) return false;
+ }
+
+ return $route;
+ }
+
+ /**
+ * Function to validate a domain, though this is not quite what
+ * you expect of a strict internet domain.
+ *
+ * domain = sub-domain *("." sub-domain)
+ *
+ * @access private
+ * @param string $domain The string to check.
+ * @return mixed False on failure, or the validated domain on success.
+ */
+ function _validateDomain($domain)
+ {
+ // Note the different use of $subdomains and $sub_domains
+ $subdomains = explode('.', $domain);
+
+ while (count($subdomains) > 0) {
+ $sub_domains[] = $this->_splitCheck($subdomains, '.');
+ for ($i = 0; $i < $this->index + 1; $i++)
+ array_shift($subdomains);
+ }
+
+ foreach ($sub_domains as $sub_domain) {
+ if (!$this->_validateSubdomain(trim($sub_domain)))
+ return false;
+ }
+
+ // Managed to get here, so return input.
+ return $domain;
+ }
+
+ /**
+ * Function to validate a subdomain:
+ * subdomain = domain-ref / domain-literal
+ *
+ * @access private
+ * @param string $subdomain The string to check.
+ * @return boolean Success or failure.
+ */
+ function _validateSubdomain($subdomain)
+ {
+ if (preg_match('|^\[(.*)]$|', $subdomain, $arr)){
+ if (!$this->_validateDliteral($arr[1])) return false;
+ } else {
+ if (!$this->_validateAtom($subdomain)) return false;
+ }
+
+ // Got here, so return successful.
+ return true;
+ }
+
+ /**
+ * Function to validate a domain literal:
+ * domain-literal = "[" *(dtext / quoted-pair) "]"
+ *
+ * @access private
+ * @param string $dliteral The string to check.
+ * @return boolean Success or failure.
+ */
+ function _validateDliteral($dliteral)
+ {
+ return !preg_match('/(.)[][\x0D\\\\]/', $dliteral, $matches) && $matches[1] != '\\';
+ }
+
+ /**
+ * Function to validate an addr-spec.
+ *
+ * addr-spec = local-part "@" domain
+ *
+ * @access private
+ * @param string $addr_spec The string to check.
+ * @return mixed False on failure, or the validated addr-spec on success.
+ */
+ function _validateAddrSpec($addr_spec)
+ {
+ $addr_spec = trim($addr_spec);
+
+ // Split on @ sign if there is one.
+ if (strpos($addr_spec, '@') !== false) {
+ $parts = explode('@', $addr_spec);
+ $local_part = $this->_splitCheck($parts, '@');
+ $domain = substr($addr_spec, strlen($local_part . '@'));
+
+ // No @ sign so assume the default domain.
+ } else {
+ $local_part = $addr_spec;
+ $domain = $this->default_domain;
+ }
+
+ if (($local_part = $this->_validateLocalPart($local_part)) === false) return false;
+ if (($domain = $this->_validateDomain($domain)) === false) return false;
+
+ // Got here so return successful.
+ return array('local_part' => $local_part, 'domain' => $domain);
+ }
+
+ /**
+ * Function to validate the local part of an address:
+ * local-part = word *("." word)
+ *
+ * @access private
+ * @param string $local_part
+ * @return mixed False on failure, or the validated local part on success.
+ */
+ function _validateLocalPart($local_part)
+ {
+ $parts = explode('.', $local_part);
+ $words = array();
+
+ // Split the local_part into words.
+ while (count($parts) > 0){
+ $words[] = $this->_splitCheck($parts, '.');
+ for ($i = 0; $i < $this->index + 1; $i++) {
+ array_shift($parts);
+ }
+ }
+
+ // Validate each word.
+ foreach ($words as $word) {
+ // If this word contains an unquoted space, it is invalid. (6.2.4)
+ if (strpos($word, ' ') && $word[0] !== '"')
+ {
+ return false;
+ }
+
+ if ($this->_validatePhrase(trim($word)) === false) return false;
+ }
+
+ // Managed to get here, so return the input.
+ return $local_part;
+ }
+
+ /**
+ * Returns an approximate count of how many addresses are in the
+ * given string. This is APPROXIMATE as it only splits based on a
+ * comma which has no preceding backslash. Could be useful as
+ * large amounts of addresses will end up producing *large*
+ * structures when used with parseAddressList().
+ *
+ * @param string $data Addresses to count
+ * @return int Approximate count
+ */
+ function approximateCount($data)
+ {
+ return count(preg_split('/(?<!\\\\),/', $data));
+ }
+
+ /**
+ * This is a email validating function separate to the rest of the
+ * class. It simply validates whether an email is of the common
+ * internet form: <user>@<domain>. This can be sufficient for most
+ * people. Optional stricter mode can be utilised which restricts
+ * mailbox characters allowed to alphanumeric, full stop, hyphen
+ * and underscore.
+ *
+ * @param string $data Address to check
+ * @param boolean $strict Optional stricter mode
+ * @return mixed False if it fails, an indexed array
+ * username/domain if it matches
+ */
+ function isValidInetAddress($data, $strict = false)
+ {
+ $regex = $strict ? '/^([.0-9a-z_+-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,})$/i' : '/^([*+!.&#$|\'\\%\/0-9a-z^_`{}=?~:-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,})$/i';
+ if (preg_match($regex, trim($data), $matches)) {
+ return array($matches[1], $matches[2]);
+ } else {
+ return false;
+ }
+ }
+
+}
diff --git a/extlib/Mail/mail.php b/extlib/Mail/mail.php
new file mode 100644
index 000000000..b13d69565
--- /dev/null
+++ b/extlib/Mail/mail.php
@@ -0,0 +1,143 @@
+<?php
+//
+// +----------------------------------------------------------------------+
+// | PHP Version 4 |
+// +----------------------------------------------------------------------+
+// | Copyright (c) 1997-2003 The PHP Group |
+// +----------------------------------------------------------------------+
+// | This source file is subject to version 2.02 of the PHP license, |
+// | that is bundled with this package in the file LICENSE, and is |
+// | available at through the world-wide-web at |
+// | http://www.php.net/license/2_02.txt. |
+// | If you did not receive a copy of the PHP license and are unable to |
+// | obtain it through the world-wide-web, please send a note to |
+// | license@php.net so we can mail you a copy immediately. |
+// +----------------------------------------------------------------------+
+// | Author: Chuck Hagenbuch <chuck@horde.org> |
+// +----------------------------------------------------------------------+
+//
+// $Id: mail.php,v 1.20 2007/10/06 17:00:00 chagenbu Exp $
+
+/**
+ * internal PHP-mail() implementation of the PEAR Mail:: interface.
+ * @package Mail
+ * @version $Revision: 1.20 $
+ */
+class Mail_mail extends Mail {
+
+ /**
+ * Any arguments to pass to the mail() function.
+ * @var string
+ */
+ var $_params = '';
+
+ /**
+ * Constructor.
+ *
+ * Instantiates a new Mail_mail:: object based on the parameters
+ * passed in.
+ *
+ * @param array $params Extra arguments for the mail() function.
+ */
+ function Mail_mail($params = null)
+ {
+ // The other mail implementations accept parameters as arrays.
+ // In the interest of being consistent, explode an array into
+ // a string of parameter arguments.
+ if (is_array($params)) {
+ $this->_params = join(' ', $params);
+ } else {
+ $this->_params = $params;
+ }
+
+ /* Because the mail() function may pass headers as command
+ * line arguments, we can't guarantee the use of the standard
+ * "\r\n" separator. Instead, we use the system's native line
+ * separator. */
+ if (defined('PHP_EOL')) {
+ $this->sep = PHP_EOL;
+ } else {
+ $this->sep = (strpos(PHP_OS, 'WIN') === false) ? "\n" : "\r\n";
+ }
+ }
+
+ /**
+ * Implements Mail_mail::send() function using php's built-in mail()
+ * command.
+ *
+ * @param mixed $recipients Either a comma-seperated list of recipients
+ * (RFC822 compliant), or an array of recipients,
+ * each RFC822 valid. This may contain recipients not
+ * specified in the headers, for Bcc:, resending
+ * messages, etc.
+ *
+ * @param array $headers The array of headers to send with the mail, in an
+ * associative array, where the array key is the
+ * header name (ie, 'Subject'), and the array value
+ * is the header value (ie, 'test'). The header
+ * produced from those values would be 'Subject:
+ * test'.
+ *
+ * @param string $body The full text of the message body, including any
+ * Mime parts, etc.
+ *
+ * @return mixed Returns true on success, or a PEAR_Error
+ * containing a descriptive error message on
+ * failure.
+ *
+ * @access public
+ */
+ function send($recipients, $headers, $body)
+ {
+ if (!is_array($headers)) {
+ return PEAR::raiseError('$headers must be an array');
+ }
+
+ $result = $this->_sanitizeHeaders($headers);
+ if (is_a($result, 'PEAR_Error')) {
+ return $result;
+ }
+
+ // If we're passed an array of recipients, implode it.
+ if (is_array($recipients)) {
+ $recipients = implode(', ', $recipients);
+ }
+
+ // Get the Subject out of the headers array so that we can
+ // pass it as a seperate argument to mail().
+ $subject = '';
+ if (isset($headers['Subject'])) {
+ $subject = $headers['Subject'];
+ unset($headers['Subject']);
+ }
+
+ // Also remove the To: header. The mail() function will add its own
+ // To: header based on the contents of $recipients.
+ unset($headers['To']);
+
+ // Flatten the headers out.
+ $headerElements = $this->prepareHeaders($headers);
+ if (is_a($headerElements, 'PEAR_Error')) {
+ return $headerElements;
+ }
+ list(, $text_headers) = $headerElements;
+
+ // We only use mail()'s optional fifth parameter if the additional
+ // parameters have been provided and we're not running in safe mode.
+ if (empty($this->_params) || ini_get('safe_mode')) {
+ $result = mail($recipients, $subject, $body, $text_headers);
+ } else {
+ $result = mail($recipients, $subject, $body, $text_headers,
+ $this->_params);
+ }
+
+ // If the mail() function returned failure, we need to create a
+ // PEAR_Error object and return it instead of the boolean result.
+ if ($result === false) {
+ $result = PEAR::raiseError('mail() returned failure');
+ }
+
+ return $result;
+ }
+
+}
diff --git a/extlib/Mail/mock.php b/extlib/Mail/mock.php
new file mode 100644
index 000000000..971dae6a0
--- /dev/null
+++ b/extlib/Mail/mock.php
@@ -0,0 +1,119 @@
+<?php
+//
+// +----------------------------------------------------------------------+
+// | PHP Version 4 |
+// +----------------------------------------------------------------------+
+// | Copyright (c) 1997-2003 The PHP Group |
+// +----------------------------------------------------------------------+
+// | This source file is subject to version 2.02 of the PHP license, |
+// | that is bundled with this package in the file LICENSE, and is |
+// | available at through the world-wide-web at |
+// | http://www.php.net/license/2_02.txt. |
+// | If you did not receive a copy of the PHP license and are unable to |
+// | obtain it through the world-wide-web, please send a note to |
+// | license@php.net so we can mail you a copy immediately. |
+// +----------------------------------------------------------------------+
+// | Author: Chuck Hagenbuch <chuck@horde.org> |
+// +----------------------------------------------------------------------+
+//
+// $Id: mock.php,v 1.1 2007/12/08 17:57:54 chagenbu Exp $
+//
+
+/**
+ * Mock implementation of the PEAR Mail:: interface for testing.
+ * @access public
+ * @package Mail
+ * @version $Revision: 1.1 $
+ */
+class Mail_mock extends Mail {
+
+ /**
+ * Array of messages that have been sent with the mock.
+ *
+ * @var array
+ * @access public
+ */
+ var $sentMessages = array();
+
+ /**
+ * Callback before sending mail.
+ *
+ * @var callback
+ */
+ var $_preSendCallback;
+
+ /**
+ * Callback after sending mai.
+ *
+ * @var callback
+ */
+ var $_postSendCallback;
+
+ /**
+ * Constructor.
+ *
+ * Instantiates a new Mail_mock:: object based on the parameters
+ * passed in. It looks for the following parameters, both optional:
+ * preSendCallback Called before an email would be sent.
+ * postSendCallback Called after an email would have been sent.
+ *
+ * @param array Hash containing any parameters.
+ * @access public
+ */
+ function Mail_mock($params)
+ {
+ if (isset($params['preSendCallback']) &&
+ is_callable($params['preSendCallback'])) {
+ $this->_preSendCallback = $params['preSendCallback'];
+ }
+
+ if (isset($params['postSendCallback']) &&
+ is_callable($params['postSendCallback'])) {
+ $this->_postSendCallback = $params['postSendCallback'];
+ }
+ }
+
+ /**
+ * Implements Mail_mock::send() function. Silently discards all
+ * mail.
+ *
+ * @param mixed $recipients Either a comma-seperated list of recipients
+ * (RFC822 compliant), or an array of recipients,
+ * each RFC822 valid. This may contain recipients not
+ * specified in the headers, for Bcc:, resending
+ * messages, etc.
+ *
+ * @param array $headers The array of headers to send with the mail, in an
+ * associative array, where the array key is the
+ * header name (ie, 'Subject'), and the array value
+ * is the header value (ie, 'test'). The header
+ * produced from those values would be 'Subject:
+ * test'.
+ *
+ * @param string $body The full text of the message body, including any
+ * Mime parts, etc.
+ *
+ * @return mixed Returns true on success, or a PEAR_Error
+ * containing a descriptive error message on
+ * failure.
+ * @access public
+ */
+ function send($recipients, $headers, $body)
+ {
+ if ($this->_preSendCallback) {
+ call_user_func_array($this->_preSendCallback,
+ array(&$this, $recipients, $headers, $body));
+ }
+
+ $entry = array('recipients' => $recipients, 'headers' => $headers, 'body' => $body);
+ $this->sentMessages[] = $entry;
+
+ if ($this->_postSendCallback) {
+ call_user_func_array($this->_postSendCallback,
+ array(&$this, $recipients, $headers, $body));
+ }
+
+ return true;
+ }
+
+}
diff --git a/extlib/Mail/null.php b/extlib/Mail/null.php
new file mode 100644
index 000000000..982bfa45b
--- /dev/null
+++ b/extlib/Mail/null.php
@@ -0,0 +1,60 @@
+<?php
+//
+// +----------------------------------------------------------------------+
+// | PHP Version 4 |
+// +----------------------------------------------------------------------+
+// | Copyright (c) 1997-2003 The PHP Group |
+// +----------------------------------------------------------------------+
+// | This source file is subject to version 2.02 of the PHP license, |
+// | that is bundled with this package in the file LICENSE, and is |
+// | available at through the world-wide-web at |
+// | http://www.php.net/license/2_02.txt. |
+// | If you did not receive a copy of the PHP license and are unable to |
+// | obtain it through the world-wide-web, please send a note to |
+// | license@php.net so we can mail you a copy immediately. |
+// +----------------------------------------------------------------------+
+// | Author: Phil Kernick <philk@rotfl.com.au> |
+// +----------------------------------------------------------------------+
+//
+// $Id: null.php,v 1.2 2004/04/06 05:19:03 jon Exp $
+//
+
+/**
+ * Null implementation of the PEAR Mail:: interface.
+ * @access public
+ * @package Mail
+ * @version $Revision: 1.2 $
+ */
+class Mail_null extends Mail {
+
+ /**
+ * Implements Mail_null::send() function. Silently discards all
+ * mail.
+ *
+ * @param mixed $recipients Either a comma-seperated list of recipients
+ * (RFC822 compliant), or an array of recipients,
+ * each RFC822 valid. This may contain recipients not
+ * specified in the headers, for Bcc:, resending
+ * messages, etc.
+ *
+ * @param array $headers The array of headers to send with the mail, in an
+ * associative array, where the array key is the
+ * header name (ie, 'Subject'), and the array value
+ * is the header value (ie, 'test'). The header
+ * produced from those values would be 'Subject:
+ * test'.
+ *
+ * @param string $body The full text of the message body, including any
+ * Mime parts, etc.
+ *
+ * @return mixed Returns true on success, or a PEAR_Error
+ * containing a descriptive error message on
+ * failure.
+ * @access public
+ */
+ function send($recipients, $headers, $body)
+ {
+ return true;
+ }
+
+}
diff --git a/extlib/Mail/sendmail.php b/extlib/Mail/sendmail.php
new file mode 100644
index 000000000..cd248e61d
--- /dev/null
+++ b/extlib/Mail/sendmail.php
@@ -0,0 +1,170 @@
+<?php
+//
+// +----------------------------------------------------------------------+
+// | PHP Version 4 |
+// +----------------------------------------------------------------------+
+// | Copyright (c) 1997-2003 The PHP Group |
+// +----------------------------------------------------------------------+
+// | This source file is subject to version 2.02 of the PHP license, |
+// | that is bundled with this package in the file LICENSE, and is |
+// | available at through the world-wide-web at |
+// | http://www.php.net/license/2_02.txt. |
+// | If you did not receive a copy of the PHP license and are unable to |
+// | obtain it through the world-wide-web, please send a note to |
+// | license@php.net so we can mail you a copy immediately. |
+// +----------------------------------------------------------------------+
+// | Author: Chuck Hagenbuch <chuck@horde.org> |
+// +----------------------------------------------------------------------+
+
+/**
+ * Sendmail implementation of the PEAR Mail:: interface.
+ * @access public
+ * @package Mail
+ * @version $Revision: 1.19 $
+ */
+class Mail_sendmail extends Mail {
+
+ /**
+ * The location of the sendmail or sendmail wrapper binary on the
+ * filesystem.
+ * @var string
+ */
+ var $sendmail_path = '/usr/sbin/sendmail';
+
+ /**
+ * Any extra command-line parameters to pass to the sendmail or
+ * sendmail wrapper binary.
+ * @var string
+ */
+ var $sendmail_args = '-i';
+
+ /**
+ * Constructor.
+ *
+ * Instantiates a new Mail_sendmail:: object based on the parameters
+ * passed in. It looks for the following parameters:
+ * sendmail_path The location of the sendmail binary on the
+ * filesystem. Defaults to '/usr/sbin/sendmail'.
+ *
+ * sendmail_args Any extra parameters to pass to the sendmail
+ * or sendmail wrapper binary.
+ *
+ * If a parameter is present in the $params array, it replaces the
+ * default.
+ *
+ * @param array $params Hash containing any parameters different from the
+ * defaults.
+ * @access public
+ */
+ function Mail_sendmail($params)
+ {
+ if (isset($params['sendmail_path'])) {
+ $this->sendmail_path = $params['sendmail_path'];
+ }
+ if (isset($params['sendmail_args'])) {
+ $this->sendmail_args = $params['sendmail_args'];
+ }
+
+ /*
+ * Because we need to pass message headers to the sendmail program on
+ * the commandline, we can't guarantee the use of the standard "\r\n"
+ * separator. Instead, we use the system's native line separator.
+ */
+ if (defined('PHP_EOL')) {
+ $this->sep = PHP_EOL;
+ } else {
+ $this->sep = (strpos(PHP_OS, 'WIN') === false) ? "\n" : "\r\n";
+ }
+ }
+
+ /**
+ * Implements Mail::send() function using the sendmail
+ * command-line binary.
+ *
+ * @param mixed $recipients Either a comma-seperated list of recipients
+ * (RFC822 compliant), or an array of recipients,
+ * each RFC822 valid. This may contain recipients not
+ * specified in the headers, for Bcc:, resending
+ * messages, etc.
+ *
+ * @param array $headers The array of headers to send with the mail, in an
+ * associative array, where the array key is the
+ * header name (ie, 'Subject'), and the array value
+ * is the header value (ie, 'test'). The header
+ * produced from those values would be 'Subject:
+ * test'.
+ *
+ * @param string $body The full text of the message body, including any
+ * Mime parts, etc.
+ *
+ * @return mixed Returns true on success, or a PEAR_Error
+ * containing a descriptive error message on
+ * failure.
+ * @access public
+ */
+ function send($recipients, $headers, $body)
+ {
+ if (!is_array($headers)) {
+ return PEAR::raiseError('$headers must be an array');
+ }
+
+ $result = $this->_sanitizeHeaders($headers);
+ if (is_a($result, 'PEAR_Error')) {
+ return $result;
+ }
+
+ $recipients = $this->parseRecipients($recipients);
+ if (is_a($recipients, 'PEAR_Error')) {
+ return $recipients;
+ }
+ $recipients = escapeShellCmd(implode(' ', $recipients));
+
+ $headerElements = $this->prepareHeaders($headers);
+ if (is_a($headerElements, 'PEAR_Error')) {
+ return $headerElements;
+ }
+ list($from, $text_headers) = $headerElements;
+
+ /* Since few MTAs are going to allow this header to be forged
+ * unless it's in the MAIL FROM: exchange, we'll use
+ * Return-Path instead of From: if it's set. */
+ if (!empty($headers['Return-Path'])) {
+ $from = $headers['Return-Path'];
+ }
+
+ if (!isset($from)) {
+ return PEAR::raiseError('No from address given.');
+ } elseif (strpos($from, ' ') !== false ||
+ strpos($from, ';') !== false ||
+ strpos($from, '&') !== false ||
+ strpos($from, '`') !== false) {
+ return PEAR::raiseError('From address specified with dangerous characters.');
+ }
+
+ $from = escapeShellCmd($from);
+ $mail = @popen($this->sendmail_path . (!empty($this->sendmail_args) ? ' ' . $this->sendmail_args : '') . " -f$from -- $recipients", 'w');
+ if (!$mail) {
+ return PEAR::raiseError('Failed to open sendmail [' . $this->sendmail_path . '] for execution.');
+ }
+
+ // Write the headers following by two newlines: one to end the headers
+ // section and a second to separate the headers block from the body.
+ fputs($mail, $text_headers . $this->sep . $this->sep);
+
+ fputs($mail, $body);
+ $result = pclose($mail);
+ if (version_compare(phpversion(), '4.2.3') == -1) {
+ // With older php versions, we need to shift the pclose
+ // result to get the exit code.
+ $result = $result >> 8 & 0xFF;
+ }
+
+ if ($result != 0) {
+ return PEAR::raiseError('sendmail returned error code ' . $result,
+ $result);
+ }
+
+ return true;
+ }
+
+}
diff --git a/extlib/Mail/smtp.php b/extlib/Mail/smtp.php
new file mode 100644
index 000000000..baf3a962b
--- /dev/null
+++ b/extlib/Mail/smtp.php
@@ -0,0 +1,407 @@
+<?php
+//
+// +----------------------------------------------------------------------+
+// | PHP Version 4 |
+// +----------------------------------------------------------------------+
+// | Copyright (c) 1997-2003 The PHP Group |
+// +----------------------------------------------------------------------+
+// | This source file is subject to version 2.02 of the PHP license, |
+// | that is bundled with this package in the file LICENSE, and is |
+// | available at through the world-wide-web at |
+// | http://www.php.net/license/2_02.txt. |
+// | If you did not receive a copy of the PHP license and are unable to |
+// | obtain it through the world-wide-web, please send a note to |
+// | license@php.net so we can mail you a copy immediately. |
+// +----------------------------------------------------------------------+
+// | Authors: Chuck Hagenbuch <chuck@horde.org> |
+// | Jon Parise <jon@php.net> |
+// +----------------------------------------------------------------------+
+
+/** Error: Failed to create a Net_SMTP object */
+define('PEAR_MAIL_SMTP_ERROR_CREATE', 10000);
+
+/** Error: Failed to connect to SMTP server */
+define('PEAR_MAIL_SMTP_ERROR_CONNECT', 10001);
+
+/** Error: SMTP authentication failure */
+define('PEAR_MAIL_SMTP_ERROR_AUTH', 10002);
+
+/** Error: No From: address has been provided */
+define('PEAR_MAIL_SMTP_ERROR_FROM', 10003);
+
+/** Error: Failed to set sender */
+define('PEAR_MAIL_SMTP_ERROR_SENDER', 10004);
+
+/** Error: Failed to add recipient */
+define('PEAR_MAIL_SMTP_ERROR_RECIPIENT', 10005);
+
+/** Error: Failed to send data */
+define('PEAR_MAIL_SMTP_ERROR_DATA', 10006);
+
+/**
+ * SMTP implementation of the PEAR Mail interface. Requires the Net_SMTP class.
+ * @access public
+ * @package Mail
+ * @version $Revision: 1.33 $
+ */
+class Mail_smtp extends Mail {
+
+ /**
+ * SMTP connection object.
+ *
+ * @var object
+ * @access private
+ */
+ var $_smtp = null;
+
+ /**
+ * The list of service extension parameters to pass to the Net_SMTP
+ * mailFrom() command.
+ * @var array
+ */
+ var $_extparams = array();
+
+ /**
+ * The SMTP host to connect to.
+ * @var string
+ */
+ var $host = 'localhost';
+
+ /**
+ * The port the SMTP server is on.
+ * @var integer
+ */
+ var $port = 25;
+
+ /**
+ * Should SMTP authentication be used?
+ *
+ * This value may be set to true, false or the name of a specific
+ * authentication method.
+ *
+ * If the value is set to true, the Net_SMTP package will attempt to use
+ * the best authentication method advertised by the remote SMTP server.
+ *
+ * @var mixed
+ */
+ var $auth = false;
+
+ /**
+ * The username to use if the SMTP server requires authentication.
+ * @var string
+ */
+ var $username = '';
+
+ /**
+ * The password to use if the SMTP server requires authentication.
+ * @var string
+ */
+ var $password = '';
+
+ /**
+ * Hostname or domain that will be sent to the remote SMTP server in the
+ * HELO / EHLO message.
+ *
+ * @var string
+ */
+ var $localhost = 'localhost';
+
+ /**
+ * SMTP connection timeout value. NULL indicates no timeout.
+ *
+ * @var integer
+ */
+ var $timeout = null;
+
+ /**
+ * Turn on Net_SMTP debugging?
+ *
+ * @var boolean $debug
+ */
+ var $debug = false;
+
+ /**
+ * Indicates whether or not the SMTP connection should persist over
+ * multiple calls to the send() method.
+ *
+ * @var boolean
+ */
+ var $persist = false;
+
+ /**
+ * Use SMTP command pipelining (specified in RFC 2920) if the SMTP server
+ * supports it. This speeds up delivery over high-latency connections. By
+ * default, use the default value supplied by Net_SMTP.
+ * @var bool
+ */
+ var $pipelining;
+
+ /**
+ * Constructor.
+ *
+ * Instantiates a new Mail_smtp:: object based on the parameters
+ * passed in. It looks for the following parameters:
+ * host The server to connect to. Defaults to localhost.
+ * port The port to connect to. Defaults to 25.
+ * auth SMTP authentication. Defaults to none.
+ * username The username to use for SMTP auth. No default.
+ * password The password to use for SMTP auth. No default.
+ * localhost The local hostname / domain. Defaults to localhost.
+ * timeout The SMTP connection timeout. Defaults to none.
+ * verp Whether to use VERP or not. Defaults to false.
+ * DEPRECATED as of 1.2.0 (use setMailParams()).
+ * debug Activate SMTP debug mode? Defaults to false.
+ * persist Should the SMTP connection persist?
+ * pipelining Use SMTP command pipelining
+ *
+ * If a parameter is present in the $params array, it replaces the
+ * default.
+ *
+ * @param array Hash containing any parameters different from the
+ * defaults.
+ * @access public
+ */
+ function Mail_smtp($params)
+ {
+ if (isset($params['host'])) $this->host = $params['host'];
+ if (isset($params['port'])) $this->port = $params['port'];
+ if (isset($params['auth'])) $this->auth = $params['auth'];
+ if (isset($params['username'])) $this->username = $params['username'];
+ if (isset($params['password'])) $this->password = $params['password'];
+ if (isset($params['localhost'])) $this->localhost = $params['localhost'];
+ if (isset($params['timeout'])) $this->timeout = $params['timeout'];
+ if (isset($params['debug'])) $this->debug = (bool)$params['debug'];
+ if (isset($params['persist'])) $this->persist = (bool)$params['persist'];
+ if (isset($params['pipelining'])) $this->pipelining = (bool)$params['pipelining'];
+
+ // Deprecated options
+ if (isset($params['verp'])) {
+ $this->addServiceExtensionParameter('XVERP', is_bool($params['verp']) ? null : $params['verp']);
+ }
+
+ register_shutdown_function(array(&$this, '_Mail_smtp'));
+ }
+
+ /**
+ * Destructor implementation to ensure that we disconnect from any
+ * potentially-alive persistent SMTP connections.
+ */
+ function _Mail_smtp()
+ {
+ $this->disconnect();
+ }
+
+ /**
+ * Implements Mail::send() function using SMTP.
+ *
+ * @param mixed $recipients Either a comma-seperated list of recipients
+ * (RFC822 compliant), or an array of recipients,
+ * each RFC822 valid. This may contain recipients not
+ * specified in the headers, for Bcc:, resending
+ * messages, etc.
+ *
+ * @param array $headers The array of headers to send with the mail, in an
+ * associative array, where the array key is the
+ * header name (e.g., 'Subject'), and the array value
+ * is the header value (e.g., 'test'). The header
+ * produced from those values would be 'Subject:
+ * test'.
+ *
+ * @param string $body The full text of the message body, including any
+ * MIME parts, etc.
+ *
+ * @return mixed Returns true on success, or a PEAR_Error
+ * containing a descriptive error message on
+ * failure.
+ * @access public
+ */
+ function send($recipients, $headers, $body)
+ {
+ /* If we don't already have an SMTP object, create one. */
+ $result = &$this->getSMTPObject();
+ if (PEAR::isError($result)) {
+ return $result;
+ }
+
+ if (!is_array($headers)) {
+ return PEAR::raiseError('$headers must be an array');
+ }
+
+ $this->_sanitizeHeaders($headers);
+
+ $headerElements = $this->prepareHeaders($headers);
+ if (is_a($headerElements, 'PEAR_Error')) {
+ $this->_smtp->rset();
+ return $headerElements;
+ }
+ list($from, $textHeaders) = $headerElements;
+
+ /* Since few MTAs are going to allow this header to be forged
+ * unless it's in the MAIL FROM: exchange, we'll use
+ * Return-Path instead of From: if it's set. */
+ if (!empty($headers['Return-Path'])) {
+ $from = $headers['Return-Path'];
+ }
+
+ if (!isset($from)) {
+ $this->_smtp->rset();
+ return PEAR::raiseError('No From: address has been provided',
+ PEAR_MAIL_SMTP_ERROR_FROM);
+ }
+
+ $params = null;
+ if (!empty($this->_extparams)) {
+ foreach ($this->_extparams as $key => $val) {
+ $params .= ' ' . $key . (is_null($val) ? '' : '=' . $val);
+ }
+ }
+ if (PEAR::isError($res = $this->_smtp->mailFrom($from, ltrim($params)))) {
+ $error = $this->_error("Failed to set sender: $from", $res);
+ $this->_smtp->rset();
+ return PEAR::raiseError($error, PEAR_MAIL_SMTP_ERROR_SENDER);
+ }
+
+ $recipients = $this->parseRecipients($recipients);
+ if (is_a($recipients, 'PEAR_Error')) {
+ $this->_smtp->rset();
+ return $recipients;
+ }
+
+ foreach ($recipients as $recipient) {
+ $res = $this->_smtp->rcptTo($recipient);
+ if (is_a($res, 'PEAR_Error')) {
+ $error = $this->_error("Failed to add recipient: $recipient", $res);
+ $this->_smtp->rset();
+ return PEAR::raiseError($error, PEAR_MAIL_SMTP_ERROR_RECIPIENT);
+ }
+ }
+
+ /* Send the message's headers and the body as SMTP data. */
+ $res = $this->_smtp->data($textHeaders . "\r\n\r\n" . $body);
+ if (is_a($res, 'PEAR_Error')) {
+ $error = $this->_error('Failed to send data', $res);
+ $this->_smtp->rset();
+ return PEAR::raiseError($error, PEAR_MAIL_SMTP_ERROR_DATA);
+ }
+
+ /* If persistent connections are disabled, destroy our SMTP object. */
+ if ($this->persist === false) {
+ $this->disconnect();
+ }
+
+ return true;
+ }
+
+ /**
+ * Connect to the SMTP server by instantiating a Net_SMTP object.
+ *
+ * @return mixed Returns a reference to the Net_SMTP object on success, or
+ * a PEAR_Error containing a descriptive error message on
+ * failure.
+ *
+ * @since 1.2.0
+ * @access public
+ */
+ function &getSMTPObject()
+ {
+ if (is_object($this->_smtp) !== false) {
+ return $this->_smtp;
+ }
+
+ include_once 'Net/SMTP.php';
+ $this->_smtp = &new Net_SMTP($this->host,
+ $this->port,
+ $this->localhost);
+
+ /* If we still don't have an SMTP object at this point, fail. */
+ if (is_object($this->_smtp) === false) {
+ return PEAR::raiseError('Failed to create a Net_SMTP object',
+ PEAR_MAIL_SMTP_ERROR_CREATE);
+ }
+
+ /* Configure the SMTP connection. */
+ if ($this->debug) {
+ $this->_smtp->setDebug(true);
+ }
+
+ /* Attempt to connect to the configured SMTP server. */
+ if (PEAR::isError($res = $this->_smtp->connect($this->timeout))) {
+ $error = $this->_error('Failed to connect to ' .
+ $this->host . ':' . $this->port,
+ $res);
+ return PEAR::raiseError($error, PEAR_MAIL_SMTP_ERROR_CONNECT);
+ }
+
+ /* Attempt to authenticate if authentication has been enabled. */
+ if ($this->auth) {
+ $method = is_string($this->auth) ? $this->auth : '';
+
+ if (PEAR::isError($res = $this->_smtp->auth($this->username,
+ $this->password,
+ $method))) {
+ $error = $this->_error("$method authentication failure",
+ $res);
+ $this->_smtp->rset();
+ return PEAR::raiseError($error, PEAR_MAIL_SMTP_ERROR_AUTH);
+ }
+ }
+
+ return $this->_smtp;
+ }
+
+ /**
+ * Add parameter associated with a SMTP service extension.
+ *
+ * @param string Extension keyword.
+ * @param string Any value the keyword needs.
+ *
+ * @since 1.2.0
+ * @access public
+ */
+ function addServiceExtensionParameter($keyword, $value = null)
+ {
+ $this->_extparams[$keyword] = $value;
+ }
+
+ /**
+ * Disconnect and destroy the current SMTP connection.
+ *
+ * @return boolean True if the SMTP connection no longer exists.
+ *
+ * @since 1.1.9
+ * @access public
+ */
+ function disconnect()
+ {
+ /* If we have an SMTP object, disconnect and destroy it. */
+ if (is_object($this->_smtp) && $this->_smtp->disconnect()) {
+ $this->_smtp = null;
+ }
+
+ /* We are disconnected if we no longer have an SMTP object. */
+ return ($this->_smtp === null);
+ }
+
+ /**
+ * Build a standardized string describing the current SMTP error.
+ *
+ * @param string $text Custom string describing the error context.
+ * @param object $error Reference to the current PEAR_Error object.
+ *
+ * @return string A string describing the current SMTP error.
+ *
+ * @since 1.1.7
+ * @access private
+ */
+ function _error($text, &$error)
+ {
+ /* Split the SMTP response into a code and a response string. */
+ list($code, $response) = $this->_smtp->getResponse();
+
+ /* Build our standardized error string. */
+ return $text
+ . ' [SMTP: ' . $error->getMessage()
+ . " (code: $code, response: $response)]";
+ }
+
+}
diff --git a/extlib/Mail/smtpmx.php b/extlib/Mail/smtpmx.php
new file mode 100644
index 000000000..9d2dccfb1
--- /dev/null
+++ b/extlib/Mail/smtpmx.php
@@ -0,0 +1,478 @@
+<?PHP
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * SMTP MX
+ *
+ * SMTP MX implementation of the PEAR Mail interface. Requires the Net_SMTP class.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt. If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category Mail
+ * @package Mail_smtpmx
+ * @author gERD Schaufelberger <gerd@php-tools.net>
+ * @copyright 1997-2005 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0
+ * @version CVS: $Id: smtpmx.php,v 1.2 2007/10/06 17:00:00 chagenbu Exp $
+ * @see Mail
+ */
+
+require_once 'Net/SMTP.php';
+
+/**
+ * SMTP MX implementation of the PEAR Mail interface. Requires the Net_SMTP class.
+ *
+ *
+ * @access public
+ * @author gERD Schaufelberger <gerd@php-tools.net>
+ * @package Mail
+ * @version $Revision: 1.2 $
+ */
+class Mail_smtpmx extends Mail {
+
+ /**
+ * SMTP connection object.
+ *
+ * @var object
+ * @access private
+ */
+ var $_smtp = null;
+
+ /**
+ * The port the SMTP server is on.
+ * @var integer
+ * @see getservicebyname()
+ */
+ var $port = 25;
+
+ /**
+ * Hostname or domain that will be sent to the remote SMTP server in the
+ * HELO / EHLO message.
+ *
+ * @var string
+ * @see posix_uname()
+ */
+ var $mailname = 'localhost';
+
+ /**
+ * SMTP connection timeout value. NULL indicates no timeout.
+ *
+ * @var integer
+ */
+ var $timeout = 10;
+
+ /**
+ * use either PEAR:Net_DNS or getmxrr
+ *
+ * @var boolean
+ */
+ var $withNetDns = true;
+
+ /**
+ * PEAR:Net_DNS_Resolver
+ *
+ * @var object
+ */
+ var $resolver;
+
+ /**
+ * Whether to use VERP or not. If not a boolean, the string value
+ * will be used as the VERP separators.
+ *
+ * @var mixed boolean or string
+ */
+ var $verp = false;
+
+ /**
+ * Whether to use VRFY or not.
+ *
+ * @var boolean $vrfy
+ */
+ var $vrfy = false;
+
+ /**
+ * Switch to test mode - don't send emails for real
+ *
+ * @var boolean $debug
+ */
+ var $test = false;
+
+ /**
+ * Turn on Net_SMTP debugging?
+ *
+ * @var boolean $peardebug
+ */
+ var $debug = false;
+
+ /**
+ * internal error codes
+ *
+ * translate internal error identifier to PEAR-Error codes and human
+ * readable messages.
+ *
+ * @var boolean $debug
+ * @todo as I need unique error-codes to identify what exactly went wrond
+ * I did not use intergers as it should be. Instead I added a "namespace"
+ * for each code. This avoids conflicts with error codes from different
+ * classes. How can I use unique error codes and stay conform with PEAR?
+ */
+ var $errorCode = array(
+ 'not_connected' => array(
+ 'code' => 1,
+ 'msg' => 'Could not connect to any mail server ({HOST}) at port {PORT} to send mail to {RCPT}.'
+ ),
+ 'failed_vrfy_rcpt' => array(
+ 'code' => 2,
+ 'msg' => 'Recipient "{RCPT}" could not be veryfied.'
+ ),
+ 'failed_set_from' => array(
+ 'code' => 3,
+ 'msg' => 'Failed to set sender: {FROM}.'
+ ),
+ 'failed_set_rcpt' => array(
+ 'code' => 4,
+ 'msg' => 'Failed to set recipient: {RCPT}.'
+ ),
+ 'failed_send_data' => array(
+ 'code' => 5,
+ 'msg' => 'Failed to send mail to: {RCPT}.'
+ ),
+ 'no_from' => array(
+ 'code' => 5,
+ 'msg' => 'No from address has be provided.'
+ ),
+ 'send_data' => array(
+ 'code' => 7,
+ 'msg' => 'Failed to create Net_SMTP object.'
+ ),
+ 'no_mx' => array(
+ 'code' => 8,
+ 'msg' => 'No MX-record for {RCPT} found.'
+ ),
+ 'no_resolver' => array(
+ 'code' => 9,
+ 'msg' => 'Could not start resolver! Install PEAR:Net_DNS or switch off "netdns"'
+ ),
+ 'failed_rset' => array(
+ 'code' => 10,
+ 'msg' => 'RSET command failed, SMTP-connection corrupt.'
+ ),
+ );
+
+ /**
+ * Constructor.
+ *
+ * Instantiates a new Mail_smtp:: object based on the parameters
+ * passed in. It looks for the following parameters:
+ * mailname The name of the local mail system (a valid hostname which matches the reverse lookup)
+ * port smtp-port - the default comes from getservicebyname() and should work fine
+ * timeout The SMTP connection timeout. Defaults to 30 seconds.
+ * vrfy Whether to use VRFY or not. Defaults to false.
+ * verp Whether to use VERP or not. Defaults to false.
+ * test Activate test mode? Defaults to false.
+ * debug Activate SMTP and Net_DNS debug mode? Defaults to false.
+ * netdns whether to use PEAR:Net_DNS or the PHP build in function getmxrr, default is true
+ *
+ * If a parameter is present in the $params array, it replaces the
+ * default.
+ *
+ * @access public
+ * @param array Hash containing any parameters different from the
+ * defaults.
+ * @see _Mail_smtpmx()
+ */
+ function __construct($params)
+ {
+ if (isset($params['mailname'])) {
+ $this->mailname = $params['mailname'];
+ } else {
+ // try to find a valid mailname
+ if (function_exists('posix_uname')) {
+ $uname = posix_uname();
+ $this->mailname = $uname['nodename'];
+ }
+ }
+
+ // port number
+ if (isset($params['port'])) {
+ $this->_port = $params['port'];
+ } else {
+ $this->_port = getservbyname('smtp', 'tcp');
+ }
+
+ if (isset($params['timeout'])) $this->timeout = $params['timeout'];
+ if (isset($params['verp'])) $this->verp = $params['verp'];
+ if (isset($params['test'])) $this->test = $params['test'];
+ if (isset($params['peardebug'])) $this->test = $params['peardebug'];
+ if (isset($params['netdns'])) $this->withNetDns = $params['netdns'];
+ }
+
+ /**
+ * Constructor wrapper for PHP4
+ *
+ * @access public
+ * @param array Hash containing any parameters different from the defaults
+ * @see __construct()
+ */
+ function Mail_smtpmx($params)
+ {
+ $this->__construct($params);
+ register_shutdown_function(array(&$this, '__destruct'));
+ }
+
+ /**
+ * Destructor implementation to ensure that we disconnect from any
+ * potentially-alive persistent SMTP connections.
+ */
+ function __destruct()
+ {
+ if (is_object($this->_smtp)) {
+ $this->_smtp->disconnect();
+ $this->_smtp = null;
+ }
+ }
+
+ /**
+ * Implements Mail::send() function using SMTP direct delivery
+ *
+ * @access public
+ * @param mixed $recipients in RFC822 style or array
+ * @param array $headers The array of headers to send with the mail.
+ * @param string $body The full text of the message body,
+ * @return mixed Returns true on success, or a PEAR_Error
+ */
+ function send($recipients, $headers, $body)
+ {
+ if (!is_array($headers)) {
+ return PEAR::raiseError('$headers must be an array');
+ }
+
+ $result = $this->_sanitizeHeaders($headers);
+ if (is_a($result, 'PEAR_Error')) {
+ return $result;
+ }
+
+ // Prepare headers
+ $headerElements = $this->prepareHeaders($headers);
+ if (is_a($headerElements, 'PEAR_Error')) {
+ return $headerElements;
+ }
+ list($from, $textHeaders) = $headerElements;
+
+ // use 'Return-Path' if possible
+ if (!empty($headers['Return-Path'])) {
+ $from = $headers['Return-Path'];
+ }
+ if (!isset($from)) {
+ return $this->_raiseError('no_from');
+ }
+
+ // Prepare recipients
+ $recipients = $this->parseRecipients($recipients);
+ if (is_a($recipients, 'PEAR_Error')) {
+ return $recipients;
+ }
+
+ foreach ($recipients as $rcpt) {
+ list($user, $host) = explode('@', $rcpt);
+
+ $mx = $this->_getMx($host);
+ if (is_a($mx, 'PEAR_Error')) {
+ return $mx;
+ }
+
+ if (empty($mx)) {
+ $info = array('rcpt' => $rcpt);
+ return $this->_raiseError('no_mx', $info);
+ }
+
+ $connected = false;
+ foreach ($mx as $mserver => $mpriority) {
+ $this->_smtp = new Net_SMTP($mserver, $this->port, $this->mailname);
+
+ // configure the SMTP connection.
+ if ($this->debug) {
+ $this->_smtp->setDebug(true);
+ }
+
+ // attempt to connect to the configured SMTP server.
+ $res = $this->_smtp->connect($this->timeout);
+ if (is_a($res, 'PEAR_Error')) {
+ $this->_smtp = null;
+ continue;
+ }
+
+ // connection established
+ if ($res) {
+ $connected = true;
+ break;
+ }
+ }
+
+ if (!$connected) {
+ $info = array(
+ 'host' => implode(', ', array_keys($mx)),
+ 'port' => $this->port,
+ 'rcpt' => $rcpt,
+ );
+ return $this->_raiseError('not_connected', $info);
+ }
+
+ // Verify recipient
+ if ($this->vrfy) {
+ $res = $this->_smtp->vrfy($rcpt);
+ if (is_a($res, 'PEAR_Error')) {
+ $info = array('rcpt' => $rcpt);
+ return $this->_raiseError('failed_vrfy_rcpt', $info);
+ }
+ }
+
+ // mail from:
+ $args['verp'] = $this->verp;
+ $res = $this->_smtp->mailFrom($from, $args);
+ if (is_a($res, 'PEAR_Error')) {
+ $info = array('from' => $from);
+ return $this->_raiseError('failed_set_from', $info);
+ }
+
+ // rcpt to:
+ $res = $this->_smtp->rcptTo($rcpt);
+ if (is_a($res, 'PEAR_Error')) {
+ $info = array('rcpt' => $rcpt);
+ return $this->_raiseError('failed_set_rcpt', $info);
+ }
+
+ // Don't send anything in test mode
+ if ($this->test) {
+ $result = $this->_smtp->rset();
+ $res = $this->_smtp->rset();
+ if (is_a($res, 'PEAR_Error')) {
+ return $this->_raiseError('failed_rset');
+ }
+
+ $this->_smtp->disconnect();
+ $this->_smtp = null;
+ return true;
+ }
+
+ // Send data
+ $res = $this->_smtp->data("$textHeaders\r\n$body");
+ if (is_a($res, 'PEAR_Error')) {
+ $info = array('rcpt' => $rcpt);
+ return $this->_raiseError('failed_send_data', $info);
+ }
+
+ $this->_smtp->disconnect();
+ $this->_smtp = null;
+ }
+
+ return true;
+ }
+
+ /**
+ * Recieve mx rexords for a spciefied host
+ *
+ * The MX records
+ *
+ * @access private
+ * @param string $host mail host
+ * @return mixed sorted
+ */
+ function _getMx($host)
+ {
+ $mx = array();
+
+ if ($this->withNetDns) {
+ $res = $this->_loadNetDns();
+ if (is_a($res, 'PEAR_Error')) {
+ return $res;
+ }
+
+ $response = $this->resolver->query($host, 'MX');
+ if (!$response) {
+ return false;
+ }
+
+ foreach ($response->answer as $rr) {
+ if ($rr->type == 'MX') {
+ $mx[$rr->exchange] = $rr->preference;
+ }
+ }
+ } else {
+ $mxHost = array();
+ $mxWeight = array();
+
+ if (!getmxrr($host, $mxHost, $mxWeight)) {
+ return false;
+ }
+ for ($i = 0; $i < count($mxHost); ++$i) {
+ $mx[$mxHost[$i]] = $mxWeight[$i];
+ }
+ }
+
+ asort($mx);
+ return $mx;
+ }
+
+ /**
+ * initialize PEAR:Net_DNS_Resolver
+ *
+ * @access private
+ * @return boolean true on success
+ */
+ function _loadNetDns()
+ {
+ if (is_object($this->resolver)) {
+ return true;
+ }
+
+ if (!include_once 'Net/DNS.php') {
+ return $this->_raiseError('no_resolver');
+ }
+
+ $this->resolver = new Net_DNS_Resolver();
+ if ($this->debug) {
+ $this->resolver->test = 1;
+ }
+
+ return true;
+ }
+
+ /**
+ * raise standardized error
+ *
+ * include additional information in error message
+ *
+ * @access private
+ * @param string $id maps error ids to codes and message
+ * @param array $info optional information in associative array
+ * @see _errorCode
+ */
+ function _raiseError($id, $info = array())
+ {
+ $code = $this->errorCode[$id]['code'];
+ $msg = $this->errorCode[$id]['msg'];
+
+ // include info to messages
+ if (!empty($info)) {
+ $search = array();
+ $replace = array();
+
+ foreach ($info as $key => $value) {
+ array_push($search, '{' . strtoupper($key) . '}');
+ array_push($replace, $value);
+ }
+
+ $msg = str_replace($search, $replace, $msg);
+ }
+
+ return PEAR::raiseError($msg, $code);
+ }
+
+}
diff --git a/extlib/Net/SMTP.php b/extlib/Net/SMTP.php
new file mode 100644
index 000000000..d632258d6
--- /dev/null
+++ b/extlib/Net/SMTP.php
@@ -0,0 +1,1082 @@
+<?php
+/* vim: set expandtab softtabstop=4 tabstop=4 shiftwidth=4: */
+// +----------------------------------------------------------------------+
+// | PHP Version 4 |
+// +----------------------------------------------------------------------+
+// | Copyright (c) 1997-2003 The PHP Group |
+// +----------------------------------------------------------------------+
+// | This source file is subject to version 2.02 of the PHP license, |
+// | that is bundled with this package in the file LICENSE, and is |
+// | available at through the world-wide-web at |
+// | http://www.php.net/license/2_02.txt. |
+// | If you did not receive a copy of the PHP license and are unable to |
+// | obtain it through the world-wide-web, please send a note to |
+// | license@php.net so we can mail you a copy immediately. |
+// +----------------------------------------------------------------------+
+// | Authors: Chuck Hagenbuch <chuck@horde.org> |
+// | Jon Parise <jon@php.net> |
+// | Damian Alejandro Fernandez Sosa <damlists@cnba.uba.ar> |
+// +----------------------------------------------------------------------+
+//
+// $Id: SMTP.php,v 1.63 2008/06/10 05:39:12 jon Exp $
+
+require_once 'PEAR.php';
+require_once 'Net/Socket.php';
+
+/**
+ * Provides an implementation of the SMTP protocol using PEAR's
+ * Net_Socket:: class.
+ *
+ * @package Net_SMTP
+ * @author Chuck Hagenbuch <chuck@horde.org>
+ * @author Jon Parise <jon@php.net>
+ * @author Damian Alejandro Fernandez Sosa <damlists@cnba.uba.ar>
+ *
+ * @example basic.php A basic implementation of the Net_SMTP package.
+ */
+class Net_SMTP
+{
+ /**
+ * The server to connect to.
+ * @var string
+ * @access public
+ */
+ var $host = 'localhost';
+
+ /**
+ * The port to connect to.
+ * @var int
+ * @access public
+ */
+ var $port = 25;
+
+ /**
+ * The value to give when sending EHLO or HELO.
+ * @var string
+ * @access public
+ */
+ var $localhost = 'localhost';
+
+ /**
+ * List of supported authentication methods, in preferential order.
+ * @var array
+ * @access public
+ */
+ var $auth_methods = array('DIGEST-MD5', 'CRAM-MD5', 'LOGIN', 'PLAIN');
+
+ /**
+ * Use SMTP command pipelining (specified in RFC 2920) if the SMTP
+ * server supports it.
+ *
+ * When pipeling is enabled, rcptTo(), mailFrom(), sendFrom(),
+ * somlFrom() and samlFrom() do not wait for a response from the
+ * SMTP server but return immediately.
+ *
+ * @var bool
+ * @access public
+ */
+ var $pipelining = false;
+
+ /**
+ * Number of pipelined commands.
+ * @var int
+ * @access private
+ */
+ var $_pipelined_commands = 0;
+
+ /**
+ * Should debugging output be enabled?
+ * @var boolean
+ * @access private
+ */
+ var $_debug = false;
+
+ /**
+ * The socket resource being used to connect to the SMTP server.
+ * @var resource
+ * @access private
+ */
+ var $_socket = null;
+
+ /**
+ * The most recent server response code.
+ * @var int
+ * @access private
+ */
+ var $_code = -1;
+
+ /**
+ * The most recent server response arguments.
+ * @var array
+ * @access private
+ */
+ var $_arguments = array();
+
+ /**
+ * Stores detected features of the SMTP server.
+ * @var array
+ * @access private
+ */
+ var $_esmtp = array();
+
+ /**
+ * Instantiates a new Net_SMTP object, overriding any defaults
+ * with parameters that are passed in.
+ *
+ * If you have SSL support in PHP, you can connect to a server
+ * over SSL using an 'ssl://' prefix:
+ *
+ * // 465 is a common smtps port.
+ * $smtp = new Net_SMTP('ssl://mail.host.com', 465);
+ * $smtp->connect();
+ *
+ * @param string $host The server to connect to.
+ * @param integer $port The port to connect to.
+ * @param string $localhost The value to give when sending EHLO or HELO.
+ * @param boolean $pipeling Use SMTP command pipelining
+ *
+ * @access public
+ * @since 1.0
+ */
+ function Net_SMTP($host = null, $port = null, $localhost = null, $pipelining = false)
+ {
+ if (isset($host)) {
+ $this->host = $host;
+ }
+ if (isset($port)) {
+ $this->port = $port;
+ }
+ if (isset($localhost)) {
+ $this->localhost = $localhost;
+ }
+ $this->pipelining = $pipelining;
+
+ $this->_socket = new Net_Socket();
+
+ /* Include the Auth_SASL package. If the package is not
+ * available, we disable the authentication methods that
+ * depend upon it. */
+ if ((@include_once 'Auth/SASL.php') === false) {
+ $pos = array_search('DIGEST-MD5', $this->auth_methods);
+ unset($this->auth_methods[$pos]);
+ $pos = array_search('CRAM-MD5', $this->auth_methods);
+ unset($this->auth_methods[$pos]);
+ }
+ }
+
+ /**
+ * Set the value of the debugging flag.
+ *
+ * @param boolean $debug New value for the debugging flag.
+ *
+ * @access public
+ * @since 1.1.0
+ */
+ function setDebug($debug)
+ {
+ $this->_debug = $debug;
+ }
+
+ /**
+ * Send the given string of data to the server.
+ *
+ * @param string $data The string of data to send.
+ *
+ * @return mixed True on success or a PEAR_Error object on failure.
+ *
+ * @access private
+ * @since 1.1.0
+ */
+ function _send($data)
+ {
+ if ($this->_debug) {
+ echo "DEBUG: Send: $data\n";
+ }
+
+ if (PEAR::isError($error = $this->_socket->write($data))) {
+ return PEAR::raiseError('Failed to write to socket: ' .
+ $error->getMessage());
+ }
+
+ return true;
+ }
+
+ /**
+ * Send a command to the server with an optional string of
+ * arguments. A carriage return / linefeed (CRLF) sequence will
+ * be appended to each command string before it is sent to the
+ * SMTP server - an error will be thrown if the command string
+ * already contains any newline characters. Use _send() for
+ * commands that must contain newlines.
+ *
+ * @param string $command The SMTP command to send to the server.
+ * @param string $args A string of optional arguments to append
+ * to the command.
+ *
+ * @return mixed The result of the _send() call.
+ *
+ * @access private
+ * @since 1.1.0
+ */
+ function _put($command, $args = '')
+ {
+ if (!empty($args)) {
+ $command .= ' ' . $args;
+ }
+
+ if (strcspn($command, "\r\n") !== strlen($command)) {
+ return PEAR::raiseError('Commands cannot contain newlines');
+ }
+
+ return $this->_send($command . "\r\n");
+ }
+
+ /**
+ * Read a reply from the SMTP server. The reply consists of a response
+ * code and a response message.
+ *
+ * @param mixed $valid The set of valid response codes. These
+ * may be specified as an array of integer
+ * values or as a single integer value.
+ * @param bool $later Do not parse the response now, but wait
+ * until the last command in the pipelined
+ * command group
+ *
+ * @return mixed True if the server returned a valid response code or
+ * a PEAR_Error object is an error condition is reached.
+ *
+ * @access private
+ * @since 1.1.0
+ *
+ * @see getResponse
+ */
+ function _parseResponse($valid, $later = false)
+ {
+ $this->_code = -1;
+ $this->_arguments = array();
+
+ if ($later) {
+ $this->_pipelined_commands++;
+ return true;
+ }
+
+ for ($i = 0; $i <= $this->_pipelined_commands; $i++) {
+ while ($line = $this->_socket->readLine()) {
+ if ($this->_debug) {
+ echo "DEBUG: Recv: $line\n";
+ }
+
+ /* If we receive an empty line, the connection has been closed. */
+ if (empty($line)) {
+ $this->disconnect();
+ return PEAR::raiseError('Connection was unexpectedly closed');
+ }
+
+ /* Read the code and store the rest in the arguments array. */
+ $code = substr($line, 0, 3);
+ $this->_arguments[] = trim(substr($line, 4));
+
+ /* Check the syntax of the response code. */
+ if (is_numeric($code)) {
+ $this->_code = (int)$code;
+ } else {
+ $this->_code = -1;
+ break;
+ }
+
+ /* If this is not a multiline response, we're done. */
+ if (substr($line, 3, 1) != '-') {
+ break;
+ }
+ }
+ }
+
+ $this->_pipelined_commands = 0;
+
+ /* Compare the server's response code with the valid code/codes. */
+ if (is_int($valid) && ($this->_code === $valid)) {
+ return true;
+ } elseif (is_array($valid) && in_array($this->_code, $valid, true)) {
+ return true;
+ }
+
+ return PEAR::raiseError('Invalid response code received from server',
+ $this->_code);
+ }
+
+ /**
+ * Return a 2-tuple containing the last response from the SMTP server.
+ *
+ * @return array A two-element array: the first element contains the
+ * response code as an integer and the second element
+ * contains the response's arguments as a string.
+ *
+ * @access public
+ * @since 1.1.0
+ */
+ function getResponse()
+ {
+ return array($this->_code, join("\n", $this->_arguments));
+ }
+
+ /**
+ * Attempt to connect to the SMTP server.
+ *
+ * @param int $timeout The timeout value (in seconds) for the
+ * socket connection.
+ * @param bool $persistent Should a persistent socket connection
+ * be used?
+ *
+ * @return mixed Returns a PEAR_Error with an error message on any
+ * kind of failure, or true on success.
+ * @access public
+ * @since 1.0
+ */
+ function connect($timeout = null, $persistent = false)
+ {
+ $result = $this->_socket->connect($this->host, $this->port,
+ $persistent, $timeout);
+ if (PEAR::isError($result)) {
+ return PEAR::raiseError('Failed to connect socket: ' .
+ $result->getMessage());
+ }
+
+ if (PEAR::isError($error = $this->_parseResponse(220))) {
+ return $error;
+ }
+ if (PEAR::isError($error = $this->_negotiate())) {
+ return $error;
+ }
+
+ return true;
+ }
+
+ /**
+ * Attempt to disconnect from the SMTP server.
+ *
+ * @return mixed Returns a PEAR_Error with an error message on any
+ * kind of failure, or true on success.
+ * @access public
+ * @since 1.0
+ */
+ function disconnect()
+ {
+ if (PEAR::isError($error = $this->_put('QUIT'))) {
+ return $error;
+ }
+ if (PEAR::isError($error = $this->_parseResponse(221))) {
+ return $error;
+ }
+ if (PEAR::isError($error = $this->_socket->disconnect())) {
+ return PEAR::raiseError('Failed to disconnect socket: ' .
+ $error->getMessage());
+ }
+
+ return true;
+ }
+
+ /**
+ * Attempt to send the EHLO command and obtain a list of ESMTP
+ * extensions available, and failing that just send HELO.
+ *
+ * @return mixed Returns a PEAR_Error with an error message on any
+ * kind of failure, or true on success.
+ *
+ * @access private
+ * @since 1.1.0
+ */
+ function _negotiate()
+ {
+ if (PEAR::isError($error = $this->_put('EHLO', $this->localhost))) {
+ return $error;
+ }
+
+ if (PEAR::isError($this->_parseResponse(250))) {
+ /* If we receive a 503 response, we're already authenticated. */
+ if ($this->_code === 503) {
+ return true;
+ }
+
+ /* If the EHLO failed, try the simpler HELO command. */
+ if (PEAR::isError($error = $this->_put('HELO', $this->localhost))) {
+ return $error;
+ }
+ if (PEAR::isError($this->_parseResponse(250))) {
+ return PEAR::raiseError('HELO was not accepted: ', $this->_code);
+ }
+
+ return true;
+ }
+
+ foreach ($this->_arguments as $argument) {
+ $verb = strtok($argument, ' ');
+ $arguments = substr($argument, strlen($verb) + 1,
+ strlen($argument) - strlen($verb) - 1);
+ $this->_esmtp[$verb] = $arguments;
+ }
+
+ if (!isset($this->_esmtp['PIPELINING'])) {
+ $this->pipelining = false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns the name of the best authentication method that the server
+ * has advertised.
+ *
+ * @return mixed Returns a string containing the name of the best
+ * supported authentication method or a PEAR_Error object
+ * if a failure condition is encountered.
+ * @access private
+ * @since 1.1.0
+ */
+ function _getBestAuthMethod()
+ {
+ $available_methods = explode(' ', $this->_esmtp['AUTH']);
+
+ foreach ($this->auth_methods as $method) {
+ if (in_array($method, $available_methods)) {
+ return $method;
+ }
+ }
+
+ return PEAR::raiseError('No supported authentication methods');
+ }
+
+ /**
+ * Attempt to do SMTP authentication.
+ *
+ * @param string The userid to authenticate as.
+ * @param string The password to authenticate with.
+ * @param string The requested authentication method. If none is
+ * specified, the best supported method will be used.
+ *
+ * @return mixed Returns a PEAR_Error with an error message on any
+ * kind of failure, or true on success.
+ * @access public
+ * @since 1.0
+ */
+ function auth($uid, $pwd , $method = '')
+ {
+ if (empty($this->_esmtp['AUTH'])) {
+ if (version_compare(PHP_VERSION, '5.1.0', '>=')) {
+ if (!isset($this->_esmtp['STARTTLS'])) {
+ return PEAR::raiseError('SMTP server does not support authentication');
+ }
+ if (PEAR::isError($result = $this->_put('STARTTLS'))) {
+ return $result;
+ }
+ if (PEAR::isError($result = $this->_parseResponse(220))) {
+ return $result;
+ }
+ if (PEAR::isError($result = $this->_socket->enableCrypto(true, STREAM_CRYPTO_METHOD_TLS_CLIENT))) {
+ return $result;
+ } elseif ($result !== true) {
+ return PEAR::raiseError('STARTTLS failed');
+ }
+
+ /* Send EHLO again to recieve the AUTH string from the
+ * SMTP server. */
+ $this->_negotiate();
+ if (empty($this->_esmtp['AUTH'])) {
+ return PEAR::raiseError('SMTP server does not support authentication');
+ }
+ } else {
+ return PEAR::raiseError('SMTP server does not support authentication');
+ }
+ }
+
+ /* If no method has been specified, get the name of the best
+ * supported method advertised by the SMTP server. */
+ if (empty($method)) {
+ if (PEAR::isError($method = $this->_getBestAuthMethod())) {
+ /* Return the PEAR_Error object from _getBestAuthMethod(). */
+ return $method;
+ }
+ } else {
+ $method = strtoupper($method);
+ if (!in_array($method, $this->auth_methods)) {
+ return PEAR::raiseError("$method is not a supported authentication method");
+ }
+ }
+
+ switch ($method) {
+ case 'DIGEST-MD5':
+ $result = $this->_authDigest_MD5($uid, $pwd);
+ break;
+
+ case 'CRAM-MD5':
+ $result = $this->_authCRAM_MD5($uid, $pwd);
+ break;
+
+ case 'LOGIN':
+ $result = $this->_authLogin($uid, $pwd);
+ break;
+
+ case 'PLAIN':
+ $result = $this->_authPlain($uid, $pwd);
+ break;
+
+ default:
+ $result = PEAR::raiseError("$method is not a supported authentication method");
+ break;
+ }
+
+ /* If an error was encountered, return the PEAR_Error object. */
+ if (PEAR::isError($result)) {
+ return $result;
+ }
+
+ return true;
+ }
+
+ /**
+ * Authenticates the user using the DIGEST-MD5 method.
+ *
+ * @param string The userid to authenticate as.
+ * @param string The password to authenticate with.
+ *
+ * @return mixed Returns a PEAR_Error with an error message on any
+ * kind of failure, or true on success.
+ * @access private
+ * @since 1.1.0
+ */
+ function _authDigest_MD5($uid, $pwd)
+ {
+ if (PEAR::isError($error = $this->_put('AUTH', 'DIGEST-MD5'))) {
+ return $error;
+ }
+ /* 334: Continue authentication request */
+ if (PEAR::isError($error = $this->_parseResponse(334))) {
+ /* 503: Error: already authenticated */
+ if ($this->_code === 503) {
+ return true;
+ }
+ return $error;
+ }
+
+ $challenge = base64_decode($this->_arguments[0]);
+ $digest = &Auth_SASL::factory('digestmd5');
+ $auth_str = base64_encode($digest->getResponse($uid, $pwd, $challenge,
+ $this->host, "smtp"));
+
+ if (PEAR::isError($error = $this->_put($auth_str))) {
+ return $error;
+ }
+ /* 334: Continue authentication request */
+ if (PEAR::isError($error = $this->_parseResponse(334))) {
+ return $error;
+ }
+
+ /* We don't use the protocol's third step because SMTP doesn't
+ * allow subsequent authentication, so we just silently ignore
+ * it. */
+ if (PEAR::isError($error = $this->_put(''))) {
+ return $error;
+ }
+ /* 235: Authentication successful */
+ if (PEAR::isError($error = $this->_parseResponse(235))) {
+ return $error;
+ }
+ }
+
+ /**
+ * Authenticates the user using the CRAM-MD5 method.
+ *
+ * @param string The userid to authenticate as.
+ * @param string The password to authenticate with.
+ *
+ * @return mixed Returns a PEAR_Error with an error message on any
+ * kind of failure, or true on success.
+ * @access private
+ * @since 1.1.0
+ */
+ function _authCRAM_MD5($uid, $pwd)
+ {
+ if (PEAR::isError($error = $this->_put('AUTH', 'CRAM-MD5'))) {
+ return $error;
+ }
+ /* 334: Continue authentication request */
+ if (PEAR::isError($error = $this->_parseResponse(334))) {
+ /* 503: Error: already authenticated */
+ if ($this->_code === 503) {
+ return true;
+ }
+ return $error;
+ }
+
+ $challenge = base64_decode($this->_arguments[0]);
+ $cram = &Auth_SASL::factory('crammd5');
+ $auth_str = base64_encode($cram->getResponse($uid, $pwd, $challenge));
+
+ if (PEAR::isError($error = $this->_put($auth_str))) {
+ return $error;
+ }
+
+ /* 235: Authentication successful */
+ if (PEAR::isError($error = $this->_parseResponse(235))) {
+ return $error;
+ }
+ }
+
+ /**
+ * Authenticates the user using the LOGIN method.
+ *
+ * @param string The userid to authenticate as.
+ * @param string The password to authenticate with.
+ *
+ * @return mixed Returns a PEAR_Error with an error message on any
+ * kind of failure, or true on success.
+ * @access private
+ * @since 1.1.0
+ */
+ function _authLogin($uid, $pwd)
+ {
+ if (PEAR::isError($error = $this->_put('AUTH', 'LOGIN'))) {
+ return $error;
+ }
+ /* 334: Continue authentication request */
+ if (PEAR::isError($error = $this->_parseResponse(334))) {
+ /* 503: Error: already authenticated */
+ if ($this->_code === 503) {
+ return true;
+ }
+ return $error;
+ }
+
+ if (PEAR::isError($error = $this->_put(base64_encode($uid)))) {
+ return $error;
+ }
+ /* 334: Continue authentication request */
+ if (PEAR::isError($error = $this->_parseResponse(334))) {
+ return $error;
+ }
+
+ if (PEAR::isError($error = $this->_put(base64_encode($pwd)))) {
+ return $error;
+ }
+
+ /* 235: Authentication successful */
+ if (PEAR::isError($error = $this->_parseResponse(235))) {
+ return $error;
+ }
+
+ return true;
+ }
+
+ /**
+ * Authenticates the user using the PLAIN method.
+ *
+ * @param string The userid to authenticate as.
+ * @param string The password to authenticate with.
+ *
+ * @return mixed Returns a PEAR_Error with an error message on any
+ * kind of failure, or true on success.
+ * @access private
+ * @since 1.1.0
+ */
+ function _authPlain($uid, $pwd)
+ {
+ if (PEAR::isError($error = $this->_put('AUTH', 'PLAIN'))) {
+ return $error;
+ }
+ /* 334: Continue authentication request */
+ if (PEAR::isError($error = $this->_parseResponse(334))) {
+ /* 503: Error: already authenticated */
+ if ($this->_code === 503) {
+ return true;
+ }
+ return $error;
+ }
+
+ $auth_str = base64_encode(chr(0) . $uid . chr(0) . $pwd);
+
+ if (PEAR::isError($error = $this->_put($auth_str))) {
+ return $error;
+ }
+
+ /* 235: Authentication successful */
+ if (PEAR::isError($error = $this->_parseResponse(235))) {
+ return $error;
+ }
+
+ return true;
+ }
+
+ /**
+ * Send the HELO command.
+ *
+ * @param string The domain name to say we are.
+ *
+ * @return mixed Returns a PEAR_Error with an error message on any
+ * kind of failure, or true on success.
+ * @access public
+ * @since 1.0
+ */
+ function helo($domain)
+ {
+ if (PEAR::isError($error = $this->_put('HELO', $domain))) {
+ return $error;
+ }
+ if (PEAR::isError($error = $this->_parseResponse(250))) {
+ return $error;
+ }
+
+ return true;
+ }
+
+ /**
+ * Return the list of SMTP service extensions advertised by the server.
+ *
+ * @return array The list of SMTP service extensions.
+ * @access public
+ * @since 1.3
+ */
+ function getServiceExtensions()
+ {
+ return $this->_esmtp;
+ }
+
+ /**
+ * Send the MAIL FROM: command.
+ *
+ * @param string $sender The sender (reverse path) to set.
+ * @param string $params String containing additional MAIL parameters,
+ * such as the NOTIFY flags defined by RFC 1891
+ * or the VERP protocol.
+ *
+ * If $params is an array, only the 'verp' option
+ * is supported. If 'verp' is true, the XVERP
+ * parameter is appended to the MAIL command. If
+ * the 'verp' value is a string, the full
+ * XVERP=value parameter is appended.
+ *
+ * @return mixed Returns a PEAR_Error with an error message on any
+ * kind of failure, or true on success.
+ * @access public
+ * @since 1.0
+ */
+ function mailFrom($sender, $params = null)
+ {
+ $args = "FROM:<$sender>";
+
+ /* Support the deprecated array form of $params. */
+ if (is_array($params) && isset($params['verp'])) {
+ /* XVERP */
+ if ($params['verp'] === true) {
+ $args .= ' XVERP';
+
+ /* XVERP=something */
+ } elseif (trim($params['verp'])) {
+ $args .= ' XVERP=' . $params['verp'];
+ }
+ } elseif (is_string($params)) {
+ $args .= ' ' . $params;
+ }
+
+ if (PEAR::isError($error = $this->_put('MAIL', $args))) {
+ return $error;
+ }
+ if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) {
+ return $error;
+ }
+
+ return true;
+ }
+
+ /**
+ * Send the RCPT TO: command.
+ *
+ * @param string $recipient The recipient (forward path) to add.
+ * @param string $params String containing additional RCPT parameters,
+ * such as the NOTIFY flags defined by RFC 1891.
+ *
+ * @return mixed Returns a PEAR_Error with an error message on any
+ * kind of failure, or true on success.
+ *
+ * @access public
+ * @since 1.0
+ */
+ function rcptTo($recipient, $params = null)
+ {
+ $args = "TO:<$recipient>";
+ if (is_string($params)) {
+ $args .= ' ' . $params;
+ }
+
+ if (PEAR::isError($error = $this->_put('RCPT', $args))) {
+ return $error;
+ }
+ if (PEAR::isError($error = $this->_parseResponse(array(250, 251), $this->pipelining))) {
+ return $error;
+ }
+
+ return true;
+ }
+
+ /**
+ * Quote the data so that it meets SMTP standards.
+ *
+ * This is provided as a separate public function to facilitate
+ * easier overloading for the cases where it is desirable to
+ * customize the quoting behavior.
+ *
+ * @param string $data The message text to quote. The string must be passed
+ * by reference, and the text will be modified in place.
+ *
+ * @access public
+ * @since 1.2
+ */
+ function quotedata(&$data)
+ {
+ /* Change Unix (\n) and Mac (\r) linefeeds into
+ * Internet-standard CRLF (\r\n) linefeeds. */
+ $data = preg_replace(array('/(?<!\r)\n/','/\r(?!\n)/'), "\r\n", $data);
+
+ /* Because a single leading period (.) signifies an end to the
+ * data, legitimate leading periods need to be "doubled"
+ * (e.g. '..'). */
+ $data = str_replace("\n.", "\n..", $data);
+ }
+
+ /**
+ * Send the DATA command.
+ *
+ * @param string $data The message body to send.
+ *
+ * @return mixed Returns a PEAR_Error with an error message on any
+ * kind of failure, or true on success.
+ * @access public
+ * @since 1.0
+ */
+ function data($data)
+ {
+ /* RFC 1870, section 3, subsection 3 states "a value of zero
+ * indicates that no fixed maximum message size is in force".
+ * Furthermore, it says that if "the parameter is omitted no
+ * information is conveyed about the server's fixed maximum
+ * message size". */
+ if (isset($this->_esmtp['SIZE']) && ($this->_esmtp['SIZE'] > 0)) {
+ if (strlen($data) >= $this->_esmtp['SIZE']) {
+ $this->disconnect();
+ return PEAR::raiseError('Message size excedes the server limit');
+ }
+ }
+
+ /* Quote the data based on the SMTP standards. */
+ $this->quotedata($data);
+
+ if (PEAR::isError($error = $this->_put('DATA'))) {
+ return $error;
+ }
+ if (PEAR::isError($error = $this->_parseResponse(354))) {
+ return $error;
+ }
+
+ if (PEAR::isError($result = $this->_send($data . "\r\n.\r\n"))) {
+ return $result;
+ }
+ if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) {
+ return $error;
+ }
+
+ return true;
+ }
+
+ /**
+ * Send the SEND FROM: command.
+ *
+ * @param string The reverse path to send.
+ *
+ * @return mixed Returns a PEAR_Error with an error message on any
+ * kind of failure, or true on success.
+ * @access public
+ * @since 1.2.6
+ */
+ function sendFrom($path)
+ {
+ if (PEAR::isError($error = $this->_put('SEND', "FROM:<$path>"))) {
+ return $error;
+ }
+ if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) {
+ return $error;
+ }
+
+ return true;
+ }
+
+ /**
+ * Backwards-compatibility wrapper for sendFrom().
+ *
+ * @param string The reverse path to send.
+ *
+ * @return mixed Returns a PEAR_Error with an error message on any
+ * kind of failure, or true on success.
+ *
+ * @access public
+ * @since 1.0
+ * @deprecated 1.2.6
+ */
+ function send_from($path)
+ {
+ return sendFrom($path);
+ }
+
+ /**
+ * Send the SOML FROM: command.
+ *
+ * @param string The reverse path to send.
+ *
+ * @return mixed Returns a PEAR_Error with an error message on any
+ * kind of failure, or true on success.
+ * @access public
+ * @since 1.2.6
+ */
+ function somlFrom($path)
+ {
+ if (PEAR::isError($error = $this->_put('SOML', "FROM:<$path>"))) {
+ return $error;
+ }
+ if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) {
+ return $error;
+ }
+
+ return true;
+ }
+
+ /**
+ * Backwards-compatibility wrapper for somlFrom().
+ *
+ * @param string The reverse path to send.
+ *
+ * @return mixed Returns a PEAR_Error with an error message on any
+ * kind of failure, or true on success.
+ *
+ * @access public
+ * @since 1.0
+ * @deprecated 1.2.6
+ */
+ function soml_from($path)
+ {
+ return somlFrom($path);
+ }
+
+ /**
+ * Send the SAML FROM: command.
+ *
+ * @param string The reverse path to send.
+ *
+ * @return mixed Returns a PEAR_Error with an error message on any
+ * kind of failure, or true on success.
+ * @access public
+ * @since 1.2.6
+ */
+ function samlFrom($path)
+ {
+ if (PEAR::isError($error = $this->_put('SAML', "FROM:<$path>"))) {
+ return $error;
+ }
+ if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) {
+ return $error;
+ }
+
+ return true;
+ }
+
+ /**
+ * Backwards-compatibility wrapper for samlFrom().
+ *
+ * @param string The reverse path to send.
+ *
+ * @return mixed Returns a PEAR_Error with an error message on any
+ * kind of failure, or true on success.
+ *
+ * @access public
+ * @since 1.0
+ * @deprecated 1.2.6
+ */
+ function saml_from($path)
+ {
+ return samlFrom($path);
+ }
+
+ /**
+ * Send the RSET command.
+ *
+ * @return mixed Returns a PEAR_Error with an error message on any
+ * kind of failure, or true on success.
+ * @access public
+ * @since 1.0
+ */
+ function rset()
+ {
+ if (PEAR::isError($error = $this->_put('RSET'))) {
+ return $error;
+ }
+ if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) {
+ return $error;
+ }
+
+ return true;
+ }
+
+ /**
+ * Send the VRFY command.
+ *
+ * @param string The string to verify
+ *
+ * @return mixed Returns a PEAR_Error with an error message on any
+ * kind of failure, or true on success.
+ * @access public
+ * @since 1.0
+ */
+ function vrfy($string)
+ {
+ /* Note: 251 is also a valid response code */
+ if (PEAR::isError($error = $this->_put('VRFY', $string))) {
+ return $error;
+ }
+ if (PEAR::isError($error = $this->_parseResponse(array(250, 252)))) {
+ return $error;
+ }
+
+ return true;
+ }
+
+ /**
+ * Send the NOOP command.
+ *
+ * @return mixed Returns a PEAR_Error with an error message on any
+ * kind of failure, or true on success.
+ * @access public
+ * @since 1.0
+ */
+ function noop()
+ {
+ if (PEAR::isError($error = $this->_put('NOOP'))) {
+ return $error;
+ }
+ if (PEAR::isError($error = $this->_parseResponse(250))) {
+ return $error;
+ }
+
+ return true;
+ }
+
+ /**
+ * Backwards-compatibility method. identifySender()'s functionality is
+ * now handled internally.
+ *
+ * @return boolean This method always return true.
+ *
+ * @access public
+ * @since 1.0
+ */
+ function identifySender()
+ {
+ return true;
+ }
+
+}
diff --git a/extlib/Net/Socket.php b/extlib/Net/Socket.php
new file mode 100644
index 000000000..73bb4dd11
--- /dev/null
+++ b/extlib/Net/Socket.php
@@ -0,0 +1,592 @@
+<?php
+//
+// +----------------------------------------------------------------------+
+// | PHP Version 4 |
+// +----------------------------------------------------------------------+
+// | Copyright (c) 1997-2003 The PHP Group |
+// +----------------------------------------------------------------------+
+// | This source file is subject to version 2.0 of the PHP license, |
+// | that is bundled with this package in the file LICENSE, and is |
+// | available at through the world-wide-web at |
+// | http://www.php.net/license/2_02.txt. |
+// | If you did not receive a copy of the PHP license and are unable to |
+// | obtain it through the world-wide-web, please send a note to |
+// | license@php.net so we can mail you a copy immediately. |
+// +----------------------------------------------------------------------+
+// | Authors: Stig Bakken <ssb@php.net> |
+// | Chuck Hagenbuch <chuck@horde.org> |
+// +----------------------------------------------------------------------+
+//
+// $Id: Socket.php,v 1.38 2008/02/15 18:24:17 chagenbu Exp $
+
+require_once 'PEAR.php';
+
+define('NET_SOCKET_READ', 1);
+define('NET_SOCKET_WRITE', 2);
+define('NET_SOCKET_ERROR', 4);
+
+/**
+ * Generalized Socket class.
+ *
+ * @version 1.1
+ * @author Stig Bakken <ssb@php.net>
+ * @author Chuck Hagenbuch <chuck@horde.org>
+ */
+class Net_Socket extends PEAR {
+
+ /**
+ * Socket file pointer.
+ * @var resource $fp
+ */
+ var $fp = null;
+
+ /**
+ * Whether the socket is blocking. Defaults to true.
+ * @var boolean $blocking
+ */
+ var $blocking = true;
+
+ /**
+ * Whether the socket is persistent. Defaults to false.
+ * @var boolean $persistent
+ */
+ var $persistent = false;
+
+ /**
+ * The IP address to connect to.
+ * @var string $addr
+ */
+ var $addr = '';
+
+ /**
+ * The port number to connect to.
+ * @var integer $port
+ */
+ var $port = 0;
+
+ /**
+ * Number of seconds to wait on socket connections before assuming
+ * there's no more data. Defaults to no timeout.
+ * @var integer $timeout
+ */
+ var $timeout = false;
+
+ /**
+ * Number of bytes to read at a time in readLine() and
+ * readAll(). Defaults to 2048.
+ * @var integer $lineLength
+ */
+ var $lineLength = 2048;
+
+ /**
+ * Connect to the specified port. If called when the socket is
+ * already connected, it disconnects and connects again.
+ *
+ * @param string $addr IP address or host name.
+ * @param integer $port TCP port number.
+ * @param boolean $persistent (optional) Whether the connection is
+ * persistent (kept open between requests
+ * by the web server).
+ * @param integer $timeout (optional) How long to wait for data.
+ * @param array $options See options for stream_context_create.
+ *
+ * @access public
+ *
+ * @return boolean | PEAR_Error True on success or a PEAR_Error on failure.
+ */
+ function connect($addr, $port = 0, $persistent = null, $timeout = null, $options = null)
+ {
+ if (is_resource($this->fp)) {
+ @fclose($this->fp);
+ $this->fp = null;
+ }
+
+ if (!$addr) {
+ return $this->raiseError('$addr cannot be empty');
+ } elseif (strspn($addr, '.0123456789') == strlen($addr) ||
+ strstr($addr, '/') !== false) {
+ $this->addr = $addr;
+ } else {
+ $this->addr = @gethostbyname($addr);
+ }
+
+ $this->port = $port % 65536;
+
+ if ($persistent !== null) {
+ $this->persistent = $persistent;
+ }
+
+ if ($timeout !== null) {
+ $this->timeout = $timeout;
+ }
+
+ $openfunc = $this->persistent ? 'pfsockopen' : 'fsockopen';
+ $errno = 0;
+ $errstr = '';
+ $old_track_errors = @ini_set('track_errors', 1);
+ if ($options && function_exists('stream_context_create')) {
+ if ($this->timeout) {
+ $timeout = $this->timeout;
+ } else {
+ $timeout = 0;
+ }
+ $context = stream_context_create($options);
+
+ // Since PHP 5 fsockopen doesn't allow context specification
+ if (function_exists('stream_socket_client')) {
+ $flags = $this->persistent ? STREAM_CLIENT_PERSISTENT : STREAM_CLIENT_CONNECT;
+ $addr = $this->addr . ':' . $this->port;
+ $fp = stream_socket_client($addr, $errno, $errstr, $timeout, $flags, $context);
+ } else {
+ $fp = @$openfunc($this->addr, $this->port, $errno, $errstr, $timeout, $context);
+ }
+ } else {
+ if ($this->timeout) {
+ $fp = @$openfunc($this->addr, $this->port, $errno, $errstr, $this->timeout);
+ } else {
+ $fp = @$openfunc($this->addr, $this->port, $errno, $errstr);
+ }
+ }
+
+ if (!$fp) {
+ if ($errno == 0 && isset($php_errormsg)) {
+ $errstr = $php_errormsg;
+ }
+ @ini_set('track_errors', $old_track_errors);
+ return $this->raiseError($errstr, $errno);
+ }
+
+ @ini_set('track_errors', $old_track_errors);
+ $this->fp = $fp;
+
+ return $this->setBlocking($this->blocking);
+ }
+
+ /**
+ * Disconnects from the peer, closes the socket.
+ *
+ * @access public
+ * @return mixed true on success or a PEAR_Error instance otherwise
+ */
+ function disconnect()
+ {
+ if (!is_resource($this->fp)) {
+ return $this->raiseError('not connected');
+ }
+
+ @fclose($this->fp);
+ $this->fp = null;
+ return true;
+ }
+
+ /**
+ * Find out if the socket is in blocking mode.
+ *
+ * @access public
+ * @return boolean The current blocking mode.
+ */
+ function isBlocking()
+ {
+ return $this->blocking;
+ }
+
+ /**
+ * Sets whether the socket connection should be blocking or
+ * not. A read call to a non-blocking socket will return immediately
+ * if there is no data available, whereas it will block until there
+ * is data for blocking sockets.
+ *
+ * @param boolean $mode True for blocking sockets, false for nonblocking.
+ * @access public
+ * @return mixed true on success or a PEAR_Error instance otherwise
+ */
+ function setBlocking($mode)
+ {
+ if (!is_resource($this->fp)) {
+ return $this->raiseError('not connected');
+ }
+
+ $this->blocking = $mode;
+ socket_set_blocking($this->fp, $this->blocking);
+ return true;
+ }
+
+ /**
+ * Sets the timeout value on socket descriptor,
+ * expressed in the sum of seconds and microseconds
+ *
+ * @param integer $seconds Seconds.
+ * @param integer $microseconds Microseconds.
+ * @access public
+ * @return mixed true on success or a PEAR_Error instance otherwise
+ */
+ function setTimeout($seconds, $microseconds)
+ {
+ if (!is_resource($this->fp)) {
+ return $this->raiseError('not connected');
+ }
+
+ return socket_set_timeout($this->fp, $seconds, $microseconds);
+ }
+
+ /**
+ * Sets the file buffering size on the stream.
+ * See php's stream_set_write_buffer for more information.
+ *
+ * @param integer $size Write buffer size.
+ * @access public
+ * @return mixed on success or an PEAR_Error object otherwise
+ */
+ function setWriteBuffer($size)
+ {
+ if (!is_resource($this->fp)) {
+ return $this->raiseError('not connected');
+ }
+
+ $returned = stream_set_write_buffer($this->fp, $size);
+ if ($returned == 0) {
+ return true;
+ }
+ return $this->raiseError('Cannot set write buffer.');
+ }
+
+ /**
+ * Returns information about an existing socket resource.
+ * Currently returns four entries in the result array:
+ *
+ * <p>
+ * timed_out (bool) - The socket timed out waiting for data<br>
+ * blocked (bool) - The socket was blocked<br>
+ * eof (bool) - Indicates EOF event<br>
+ * unread_bytes (int) - Number of bytes left in the socket buffer<br>
+ * </p>
+ *
+ * @access public
+ * @return mixed Array containing information about existing socket resource or a PEAR_Error instance otherwise
+ */
+ function getStatus()
+ {
+ if (!is_resource($this->fp)) {
+ return $this->raiseError('not connected');
+ }
+
+ return socket_get_status($this->fp);
+ }
+
+ /**
+ * Get a specified line of data
+ *
+ * @access public
+ * @return $size bytes of data from the socket, or a PEAR_Error if
+ * not connected.
+ */
+ function gets($size)
+ {
+ if (!is_resource($this->fp)) {
+ return $this->raiseError('not connected');
+ }
+
+ return @fgets($this->fp, $size);
+ }
+
+ /**
+ * Read a specified amount of data. This is guaranteed to return,
+ * and has the added benefit of getting everything in one fread()
+ * chunk; if you know the size of the data you're getting
+ * beforehand, this is definitely the way to go.
+ *
+ * @param integer $size The number of bytes to read from the socket.
+ * @access public
+ * @return $size bytes of data from the socket, or a PEAR_Error if
+ * not connected.
+ */
+ function read($size)
+ {
+ if (!is_resource($this->fp)) {
+ return $this->raiseError('not connected');
+ }
+
+ return @fread($this->fp, $size);
+ }
+
+ /**
+ * Write a specified amount of data.
+ *
+ * @param string $data Data to write.
+ * @param integer $blocksize Amount of data to write at once.
+ * NULL means all at once.
+ *
+ * @access public
+ * @return mixed If the socket is not connected, returns an instance of PEAR_Error
+ * If the write succeeds, returns the number of bytes written
+ * If the write fails, returns false.
+ */
+ function write($data, $blocksize = null)
+ {
+ if (!is_resource($this->fp)) {
+ return $this->raiseError('not connected');
+ }
+
+ if (is_null($blocksize) && !OS_WINDOWS) {
+ return @fwrite($this->fp, $data);
+ } else {
+ if (is_null($blocksize)) {
+ $blocksize = 1024;
+ }
+
+ $pos = 0;
+ $size = strlen($data);
+ while ($pos < $size) {
+ $written = @fwrite($this->fp, substr($data, $pos, $blocksize));
+ if ($written === false) {
+ return false;
+ }
+ $pos += $written;
+ }
+
+ return $pos;
+ }
+ }
+
+ /**
+ * Write a line of data to the socket, followed by a trailing "\r\n".
+ *
+ * @access public
+ * @return mixed fputs result, or an error
+ */
+ function writeLine($data)
+ {
+ if (!is_resource($this->fp)) {
+ return $this->raiseError('not connected');
+ }
+
+ return fwrite($this->fp, $data . "\r\n");
+ }
+
+ /**
+ * Tests for end-of-file on a socket descriptor.
+ *
+ * Also returns true if the socket is disconnected.
+ *
+ * @access public
+ * @return bool
+ */
+ function eof()
+ {
+ return (!is_resource($this->fp) || feof($this->fp));
+ }
+
+ /**
+ * Reads a byte of data
+ *
+ * @access public
+ * @return 1 byte of data from the socket, or a PEAR_Error if
+ * not connected.
+ */
+ function readByte()
+ {
+ if (!is_resource($this->fp)) {
+ return $this->raiseError('not connected');
+ }
+
+ return ord(@fread($this->fp, 1));
+ }
+
+ /**
+ * Reads a word of data
+ *
+ * @access public
+ * @return 1 word of data from the socket, or a PEAR_Error if
+ * not connected.
+ */
+ function readWord()
+ {
+ if (!is_resource($this->fp)) {
+ return $this->raiseError('not connected');
+ }
+
+ $buf = @fread($this->fp, 2);
+ return (ord($buf[0]) + (ord($buf[1]) << 8));
+ }
+
+ /**
+ * Reads an int of data
+ *
+ * @access public
+ * @return integer 1 int of data from the socket, or a PEAR_Error if
+ * not connected.
+ */
+ function readInt()
+ {
+ if (!is_resource($this->fp)) {
+ return $this->raiseError('not connected');
+ }
+
+ $buf = @fread($this->fp, 4);
+ return (ord($buf[0]) + (ord($buf[1]) << 8) +
+ (ord($buf[2]) << 16) + (ord($buf[3]) << 24));
+ }
+
+ /**
+ * Reads a zero-terminated string of data
+ *
+ * @access public
+ * @return string, or a PEAR_Error if
+ * not connected.
+ */
+ function readString()
+ {
+ if (!is_resource($this->fp)) {
+ return $this->raiseError('not connected');
+ }
+
+ $string = '';
+ while (($char = @fread($this->fp, 1)) != "\x00") {
+ $string .= $char;
+ }
+ return $string;
+ }
+
+ /**
+ * Reads an IP Address and returns it in a dot formatted string
+ *
+ * @access public
+ * @return Dot formatted string, or a PEAR_Error if
+ * not connected.
+ */
+ function readIPAddress()
+ {
+ if (!is_resource($this->fp)) {
+ return $this->raiseError('not connected');
+ }
+
+ $buf = @fread($this->fp, 4);
+ return sprintf('%d.%d.%d.%d', ord($buf[0]), ord($buf[1]),
+ ord($buf[2]), ord($buf[3]));
+ }
+
+ /**
+ * Read until either the end of the socket or a newline, whichever
+ * comes first. Strips the trailing newline from the returned data.
+ *
+ * @access public
+ * @return All available data up to a newline, without that
+ * newline, or until the end of the socket, or a PEAR_Error if
+ * not connected.
+ */
+ function readLine()
+ {
+ if (!is_resource($this->fp)) {
+ return $this->raiseError('not connected');
+ }
+
+ $line = '';
+ $timeout = time() + $this->timeout;
+ while (!feof($this->fp) && (!$this->timeout || time() < $timeout)) {
+ $line .= @fgets($this->fp, $this->lineLength);
+ if (substr($line, -1) == "\n") {
+ return rtrim($line, "\r\n");
+ }
+ }
+ return $line;
+ }
+
+ /**
+ * Read until the socket closes, or until there is no more data in
+ * the inner PHP buffer. If the inner buffer is empty, in blocking
+ * mode we wait for at least 1 byte of data. Therefore, in
+ * blocking mode, if there is no data at all to be read, this
+ * function will never exit (unless the socket is closed on the
+ * remote end).
+ *
+ * @access public
+ *
+ * @return string All data until the socket closes, or a PEAR_Error if
+ * not connected.
+ */
+ function readAll()
+ {
+ if (!is_resource($this->fp)) {
+ return $this->raiseError('not connected');
+ }
+
+ $data = '';
+ while (!feof($this->fp)) {
+ $data .= @fread($this->fp, $this->lineLength);
+ }
+ return $data;
+ }
+
+ /**
+ * Runs the equivalent of the select() system call on the socket
+ * with a timeout specified by tv_sec and tv_usec.
+ *
+ * @param integer $state Which of read/write/error to check for.
+ * @param integer $tv_sec Number of seconds for timeout.
+ * @param integer $tv_usec Number of microseconds for timeout.
+ *
+ * @access public
+ * @return False if select fails, integer describing which of read/write/error
+ * are ready, or PEAR_Error if not connected.
+ */
+ function select($state, $tv_sec, $tv_usec = 0)
+ {
+ if (!is_resource($this->fp)) {
+ return $this->raiseError('not connected');
+ }
+
+ $read = null;
+ $write = null;
+ $except = null;
+ if ($state & NET_SOCKET_READ) {
+ $read[] = $this->fp;
+ }
+ if ($state & NET_SOCKET_WRITE) {
+ $write[] = $this->fp;
+ }
+ if ($state & NET_SOCKET_ERROR) {
+ $except[] = $this->fp;
+ }
+ if (false === ($sr = stream_select($read, $write, $except, $tv_sec, $tv_usec))) {
+ return false;
+ }
+
+ $result = 0;
+ if (count($read)) {
+ $result |= NET_SOCKET_READ;
+ }
+ if (count($write)) {
+ $result |= NET_SOCKET_WRITE;
+ }
+ if (count($except)) {
+ $result |= NET_SOCKET_ERROR;
+ }
+ return $result;
+ }
+
+ /**
+ * Turns encryption on/off on a connected socket.
+ *
+ * @param bool $enabled Set this parameter to true to enable encryption
+ * and false to disable encryption.
+ * @param integer $type Type of encryption. See
+ * http://se.php.net/manual/en/function.stream-socket-enable-crypto.php for values.
+ *
+ * @access public
+ * @return false on error, true on success and 0 if there isn't enough data and the
+ * user should try again (non-blocking sockets only). A PEAR_Error object
+ * is returned if the socket is not connected
+ */
+ function enableCrypto($enabled, $type)
+ {
+ if (version_compare(phpversion(), "5.1.0", ">=")) {
+ if (!is_resource($this->fp)) {
+ return $this->raiseError('not connected');
+ }
+ return @stream_socket_enable_crypto($this->fp, $enabled, $type);
+ } else {
+ return $this->raiseError('Net_Socket::enableCrypto() requires php version >= 5.1.0');
+ }
+ }
+
+}
diff --git a/extlib/OAuth.php b/extlib/OAuth.php
new file mode 100644
index 000000000..6dc6b3f35
--- /dev/null
+++ b/extlib/OAuth.php
@@ -0,0 +1,755 @@
+<?php
+// vim: foldmethod=marker
+
+/* Generic exception class
+ */
+class OAuthException extends Exception {/*{{{*/
+ // pass
+}/*}}}*/
+
+class OAuthConsumer {/*{{{*/
+ public $key;
+ public $secret;
+
+ function __construct($key, $secret, $callback_url=NULL) {/*{{{*/
+ $this->key = $key;
+ $this->secret = $secret;
+ $this->callback_url = $callback_url;
+ }/*}}}*/
+}/*}}}*/
+
+class OAuthToken {/*{{{*/
+ // access tokens and request tokens
+ public $key;
+ public $secret;
+
+ /**
+ * key = the token
+ * secret = the token secret
+ */
+ function __construct($key, $secret) {/*{{{*/
+ $this->key = $key;
+ $this->secret = $secret;
+ }/*}}}*/
+
+ /**
+ * generates the basic string serialization of a token that a server
+ * would respond to request_token and access_token calls with
+ */
+ function to_string() {/*{{{*/
+ return "oauth_token=" . OAuthUtil::urlencodeRFC3986($this->key) .
+ "&oauth_token_secret=" . OAuthUtil::urlencodeRFC3986($this->secret);
+ }/*}}}*/
+
+ function __toString() {/*{{{*/
+ return $this->to_string();
+ }/*}}}*/
+}/*}}}*/
+
+class OAuthSignatureMethod {/*{{{*/
+ public function check_signature(&$request, $consumer, $token, $signature) {
+ $built = $this->build_signature($request, $consumer, $token);
+ return $built == $signature;
+ }
+}/*}}}*/
+
+class OAuthSignatureMethod_HMAC_SHA1 extends OAuthSignatureMethod {/*{{{*/
+ function get_name() {/*{{{*/
+ return "HMAC-SHA1";
+ }/*}}}*/
+
+ public function build_signature($request, $consumer, $token) {/*{{{*/
+ $base_string = $request->get_signature_base_string();
+ $request->base_string = $base_string;
+
+ $key_parts = array(
+ $consumer->secret,
+ ($token) ? $token->secret : ""
+ );
+
+ $key_parts = array_map(array('OAuthUtil','urlencodeRFC3986'), $key_parts);
+ $key = implode('&', $key_parts);
+
+ return base64_encode( hash_hmac('sha1', $base_string, $key, true));
+ }/*}}}*/
+}/*}}}*/
+
+class OAuthSignatureMethod_PLAINTEXT extends OAuthSignatureMethod {/*{{{*/
+ public function get_name() {/*{{{*/
+ return "PLAINTEXT";
+ }/*}}}*/
+
+ public function build_signature($request, $consumer, $token) {/*{{{*/
+ $sig = array(
+ OAuthUtil::urlencodeRFC3986($consumer->secret)
+ );
+
+ if ($token) {
+ array_push($sig, OAuthUtil::urlencodeRFC3986($token->secret));
+ } else {
+ array_push($sig, '');
+ }
+
+ $raw = implode("&", $sig);
+ // for debug purposes
+ $request->base_string = $raw;
+
+ return OAuthUtil::urlencodeRFC3986($raw);
+ }/*}}}*/
+}/*}}}*/
+
+class OAuthSignatureMethod_RSA_SHA1 extends OAuthSignatureMethod {/*{{{*/
+ public function get_name() {/*{{{*/
+ return "RSA-SHA1";
+ }/*}}}*/
+
+ protected function fetch_public_cert(&$request) {/*{{{*/
+ // not implemented yet, ideas are:
+ // (1) do a lookup in a table of trusted certs keyed off of consumer
+ // (2) fetch via http using a url provided by the requester
+ // (3) some sort of specific discovery code based on request
+ //
+ // either way should return a string representation of the certificate
+ throw Exception("fetch_public_cert not implemented");
+ }/*}}}*/
+
+ protected function fetch_private_cert(&$request) {/*{{{*/
+ // not implemented yet, ideas are:
+ // (1) do a lookup in a table of trusted certs keyed off of consumer
+ //
+ // either way should return a string representation of the certificate
+ throw Exception("fetch_private_cert not implemented");
+ }/*}}}*/
+
+ public function build_signature(&$request, $consumer, $token) {/*{{{*/
+ $base_string = $request->get_signature_base_string();
+ $request->base_string = $base_string;
+
+ // Fetch the private key cert based on the request
+ $cert = $this->fetch_private_cert($request);
+
+ // Pull the private key ID from the certificate
+ $privatekeyid = openssl_get_privatekey($cert);
+
+ // Sign using the key
+ $ok = openssl_sign($base_string, $signature, $privatekeyid);
+
+ // Release the key resource
+ openssl_free_key($privatekeyid);
+
+ return base64_encode($signature);
+ } /*}}}*/
+
+ public function check_signature(&$request, $consumer, $token, $signature) {/*{{{*/
+ $decoded_sig = base64_decode($signature);
+
+ $base_string = $request->get_signature_base_string();
+
+ // Fetch the public key cert based on the request
+ $cert = $this->fetch_public_cert($request);
+
+ // Pull the public key ID from the certificate
+ $publickeyid = openssl_get_publickey($cert);
+
+ // Check the computed signature against the one passed in the query
+ $ok = openssl_verify($base_string, $decoded_sig, $publickeyid);
+
+ // Release the key resource
+ openssl_free_key($publickeyid);
+
+ return $ok == 1;
+ } /*}}}*/
+}/*}}}*/
+
+class OAuthRequest {/*{{{*/
+ private $parameters;
+ private $http_method;
+ private $http_url;
+ // for debug purposes
+ public $base_string;
+ public static $version = '1.0';
+
+ function __construct($http_method, $http_url, $parameters=NULL) {/*{{{*/
+ @$parameters or $parameters = array();
+ $this->parameters = $parameters;
+ $this->http_method = $http_method;
+ $this->http_url = $http_url;
+ }/*}}}*/
+
+
+ /**
+ * attempt to build up a request from what was passed to the server
+ */
+ public static function from_request($http_method=NULL, $http_url=NULL, $parameters=NULL) {/*{{{*/
+ $scheme = (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] != "on") ? 'http' : 'https';
+ @$http_url or $http_url = $scheme . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
+ @$http_method or $http_method = $_SERVER['REQUEST_METHOD'];
+
+ $request_headers = OAuthRequest::get_headers();
+
+ // let the library user override things however they'd like, if they know
+ // which parameters to use then go for it, for example XMLRPC might want to
+ // do this
+ if ($parameters) {
+ $req = new OAuthRequest($http_method, $http_url, $parameters);
+ }
+ // next check for the auth header, we need to do some extra stuff
+ // if that is the case, namely suck in the parameters from GET or POST
+ // so that we can include them in the signature
+ else if (@substr($request_headers['Authorization'], 0, 5) == "OAuth") {
+ $header_parameters = OAuthRequest::split_header($request_headers['Authorization']);
+ if ($http_method == "GET") {
+ $req_parameters = $_GET;
+ }
+ else if ($http_method == "POST") {
+ $req_parameters = $_POST;
+ }
+ $parameters = array_merge($header_parameters, $req_parameters);
+ $req = new OAuthRequest($http_method, $http_url, $parameters);
+ }
+ else if ($http_method == "GET") {
+ $req = new OAuthRequest($http_method, $http_url, $_GET);
+ }
+ else if ($http_method == "POST") {
+ $req = new OAuthRequest($http_method, $http_url, $_POST);
+ }
+ return $req;
+ }/*}}}*/
+
+ /**
+ * pretty much a helper function to set up the request
+ */
+ public static function from_consumer_and_token($consumer, $token, $http_method, $http_url, $parameters=NULL) {/*{{{*/
+ @$parameters or $parameters = array();
+ $defaults = array("oauth_version" => OAuthRequest::$version,
+ "oauth_nonce" => OAuthRequest::generate_nonce(),
+ "oauth_timestamp" => OAuthRequest::generate_timestamp(),
+ "oauth_consumer_key" => $consumer->key);
+ $parameters = array_merge($defaults, $parameters);
+
+ if ($token) {
+ $parameters['oauth_token'] = $token->key;
+ }
+ return new OAuthRequest($http_method, $http_url, $parameters);
+ }/*}}}*/
+
+ public function set_parameter($name, $value) {/*{{{*/
+ $this->parameters[$name] = $value;
+ }/*}}}*/
+
+ public function get_parameter($name) {/*{{{*/
+ return $this->parameters[$name];
+ }/*}}}*/
+
+ public function get_parameters() {/*{{{*/
+ return $this->parameters;
+ }/*}}}*/
+
+ /**
+ * Returns the normalized parameters of the request
+ *
+ * This will be all (except oauth_signature) parameters,
+ * sorted first by key, and if duplicate keys, then by
+ * value.
+ *
+ * The returned string will be all the key=value pairs
+ * concated by &.
+ *
+ * @return string
+ */
+ public function get_signable_parameters() {/*{{{*/
+ // Grab all parameters
+ $params = $this->parameters;
+
+ // Remove oauth_signature if present
+ if (isset($params['oauth_signature'])) {
+ unset($params['oauth_signature']);
+ }
+
+ // Urlencode both keys and values
+ $keys = array_map(array('OAuthUtil', 'urlencodeRFC3986'), array_keys($params));
+ $values = array_map(array('OAuthUtil', 'urlencodeRFC3986'), array_values($params));
+ $params = array_combine($keys, $values);
+
+ // Sort by keys (natsort)
+ uksort($params, 'strnatcmp');
+
+ // Generate key=value pairs
+ $pairs = array();
+ foreach ($params as $key=>$value ) {
+ if (is_array($value)) {
+ // If the value is an array, it's because there are multiple
+ // with the same key, sort them, then add all the pairs
+ natsort($value);
+ foreach ($value as $v2) {
+ $pairs[] = $key . '=' . $v2;
+ }
+ } else {
+ $pairs[] = $key . '=' . $value;
+ }
+ }
+
+ // Return the pairs, concated with &
+ return implode('&', $pairs);
+ }/*}}}*/
+
+ /**
+ * Returns the base string of this request
+ *
+ * The base string defined as the method, the url
+ * and the parameters (normalized), each urlencoded
+ * and the concated with &.
+ */
+ public function get_signature_base_string() {/*{{{*/
+ $parts = array(
+ $this->get_normalized_http_method(),
+ $this->get_normalized_http_url(),
+ $this->get_signable_parameters()
+ );
+
+ $parts = array_map(array('OAuthUtil', 'urlencodeRFC3986'), $parts);
+
+ return implode('&', $parts);
+ }/*}}}*/
+
+ /**
+ * just uppercases the http method
+ */
+ public function get_normalized_http_method() {/*{{{*/
+ return strtoupper($this->http_method);
+ }/*}}}*/
+
+ /**
+ * parses the url and rebuilds it to be
+ * scheme://host/path
+ */
+ public function get_normalized_http_url() {/*{{{*/
+ $parts = parse_url($this->http_url);
+
+ $port = @$parts['port'];
+ $scheme = $parts['scheme'];
+ $host = $parts['host'];
+ $path = @$parts['path'];
+
+ $port or $port = ($scheme == 'https') ? '443' : '80';
+
+ if (($scheme == 'https' && $port != '443')
+ || ($scheme == 'http' && $port != '80')) {
+ $host = "$host:$port";
+ }
+ return "$scheme://$host$path";
+ }/*}}}*/
+
+ /**
+ * builds a url usable for a GET request
+ */
+ public function to_url() {/*{{{*/
+ $out = $this->get_normalized_http_url() . "?";
+ $out .= $this->to_postdata();
+ return $out;
+ }/*}}}*/
+
+ /**
+ * builds the data one would send in a POST request
+ */
+ public function to_postdata() {/*{{{*/
+ $total = array();
+ foreach ($this->parameters as $k => $v) {
+ $total[] = OAuthUtil::urlencodeRFC3986($k) . "=" . OAuthUtil::urlencodeRFC3986($v);
+ }
+ $out = implode("&", $total);
+ return $out;
+ }/*}}}*/
+
+ /**
+ * builds the Authorization: header
+ */
+ public function to_header($realm="") {/*{{{*/
+ $out ='"Authorization: OAuth realm="' . $realm . '",';
+ $total = array();
+ foreach ($this->parameters as $k => $v) {
+ if (substr($k, 0, 5) != "oauth") continue;
+ $out .= ',' . OAuthUtil::urlencodeRFC3986($k) . '="' . OAuthUtil::urlencodeRFC3986($v) . '"';
+ }
+ return $out;
+ }/*}}}*/
+
+ public function __toString() {/*{{{*/
+ return $this->to_url();
+ }/*}}}*/
+
+
+ public function sign_request($signature_method, $consumer, $token) {/*{{{*/
+ $this->set_parameter("oauth_signature_method", $signature_method->get_name());
+ $signature = $this->build_signature($signature_method, $consumer, $token);
+ $this->set_parameter("oauth_signature", $signature);
+ }/*}}}*/
+
+ public function build_signature($signature_method, $consumer, $token) {/*{{{*/
+ $signature = $signature_method->build_signature($this, $consumer, $token);
+ return $signature;
+ }/*}}}*/
+
+ /**
+ * util function: current timestamp
+ */
+ private static function generate_timestamp() {/*{{{*/
+ return time();
+ }/*}}}*/
+
+ /**
+ * util function: current nonce
+ */
+ private static function generate_nonce() {/*{{{*/
+ $mt = microtime();
+ $rand = mt_rand();
+
+ return md5($mt . $rand); // md5s look nicer than numbers
+ }/*}}}*/
+
+ /**
+ * util function for turning the Authorization: header into
+ * parameters, has to do some unescaping
+ */
+ private static function split_header($header) {/*{{{*/
+ // remove 'OAuth ' at the start of a header
+ $header = substr($header, 6);
+
+ // error cases: commas in parameter values?
+ $parts = explode(",", $header);
+ $out = array();
+ foreach ($parts as $param) {
+ $param = ltrim($param);
+ // skip the "realm" param, nobody ever uses it anyway
+ if (substr($param, 0, 5) != "oauth") continue;
+
+ $param_parts = explode("=", $param);
+
+ // rawurldecode() used because urldecode() will turn a "+" in the
+ // value into a space
+ $out[$param_parts[0]] = rawurldecode(substr($param_parts[1], 1, -1));
+ }
+ return $out;
+ }/*}}}*/
+
+ /**
+ * helper to try to sort out headers for people who aren't running apache
+ */
+ private static function get_headers() {/*{{{*/
+ if (function_exists('apache_request_headers')) {
+ // we need this to get the actual Authorization: header
+ // because apache tends to tell us it doesn't exist
+ return apache_request_headers();
+ }
+ // otherwise we don't have apache and are just going to have to hope
+ // that $_SERVER actually contains what we need
+ $out = array();
+ foreach ($_SERVER as $key => $value) {
+ if (substr($key, 0, 5) == "HTTP_") {
+ // this is chaos, basically it is just there to capitalize the first
+ // letter of every word that is not an initial HTTP and strip HTTP
+ // code from przemek
+ $key = str_replace(" ", "-", ucwords(strtolower(str_replace("_", " ", substr($key, 5)))));
+ $out[$key] = $value;
+ }
+ }
+ return $out;
+ }/*}}}*/
+}/*}}}*/
+
+class OAuthServer {/*{{{*/
+ protected $timestamp_threshold = 300; // in seconds, five minutes
+ protected $version = 1.0; // hi blaine
+ protected $signature_methods = array();
+
+ protected $data_store;
+
+ function __construct($data_store) {/*{{{*/
+ $this->data_store = $data_store;
+ }/*}}}*/
+
+ public function add_signature_method($signature_method) {/*{{{*/
+ $this->signature_methods[$signature_method->get_name()] =
+ $signature_method;
+ }/*}}}*/
+
+ // high level functions
+
+ /**
+ * process a request_token request
+ * returns the request token on success
+ */
+ public function fetch_request_token(&$request) {/*{{{*/
+ $this->get_version($request);
+
+ $consumer = $this->get_consumer($request);
+
+ // no token required for the initial token request
+ $token = NULL;
+
+ $this->check_signature($request, $consumer, $token);
+
+ $new_token = $this->data_store->new_request_token($consumer);
+
+ return $new_token;
+ }/*}}}*/
+
+ /**
+ * process an access_token request
+ * returns the access token on success
+ */
+ public function fetch_access_token(&$request) {/*{{{*/
+ $this->get_version($request);
+
+ $consumer = $this->get_consumer($request);
+
+ // requires authorized request token
+ $token = $this->get_token($request, $consumer, "request");
+
+ $this->check_signature($request, $consumer, $token);
+
+ $new_token = $this->data_store->new_access_token($token, $consumer);
+
+ return $new_token;
+ }/*}}}*/
+
+ /**
+ * verify an api call, checks all the parameters
+ */
+ public function verify_request(&$request) {/*{{{*/
+ $this->get_version($request);
+ $consumer = $this->get_consumer($request);
+ $token = $this->get_token($request, $consumer, "access");
+ $this->check_signature($request, $consumer, $token);
+ return array($consumer, $token);
+ }/*}}}*/
+
+ // Internals from here
+ /**
+ * version 1
+ */
+ private function get_version(&$request) {/*{{{*/
+ $version = $request->get_parameter("oauth_version");
+ if (!$version) {
+ $version = 1.0;
+ }
+ if ($version && $version != $this->version) {
+ throw new OAuthException("OAuth version '$version' not supported");
+ }
+ return $version;
+ }/*}}}*/
+
+ /**
+ * figure out the signature with some defaults
+ */
+ private function get_signature_method(&$request) {/*{{{*/
+ $signature_method =
+ @$request->get_parameter("oauth_signature_method");
+ if (!$signature_method) {
+ $signature_method = "PLAINTEXT";
+ }
+ if (!in_array($signature_method,
+ array_keys($this->signature_methods))) {
+ throw new OAuthException(
+ "Signature method '$signature_method' not supported try one of the following: " . implode(", ", array_keys($this->signature_methods))
+ );
+ }
+ return $this->signature_methods[$signature_method];
+ }/*}}}*/
+
+ /**
+ * try to find the consumer for the provided request's consumer key
+ */
+ private function get_consumer(&$request) {/*{{{*/
+ $consumer_key = @$request->get_parameter("oauth_consumer_key");
+ if (!$consumer_key) {
+ throw new OAuthException("Invalid consumer key");
+ }
+
+ $consumer = $this->data_store->lookup_consumer($consumer_key);
+ if (!$consumer) {
+ throw new OAuthException("Invalid consumer");
+ }
+
+ return $consumer;
+ }/*}}}*/
+
+ /**
+ * try to find the token for the provided request's token key
+ */
+ private function get_token(&$request, $consumer, $token_type="access") {/*{{{*/
+ $token_field = @$request->get_parameter('oauth_token');
+ $token = $this->data_store->lookup_token(
+ $consumer, $token_type, $token_field
+ );
+ if (!$token) {
+ throw new OAuthException("Invalid $token_type token: $token_field");
+ }
+ return $token;
+ }/*}}}*/
+
+ /**
+ * all-in-one function to check the signature on a request
+ * should guess the signature method appropriately
+ */
+ private function check_signature(&$request, $consumer, $token) {/*{{{*/
+ // this should probably be in a different method
+ $timestamp = @$request->get_parameter('oauth_timestamp');
+ $nonce = @$request->get_parameter('oauth_nonce');
+
+ $this->check_timestamp($timestamp);
+ $this->check_nonce($consumer, $token, $nonce, $timestamp);
+
+ $signature_method = $this->get_signature_method($request);
+
+ $signature = $request->get_parameter('oauth_signature');
+ $valid_sig = $signature_method->check_signature(
+ $request,
+ $consumer,
+ $token,
+ $signature
+ );
+
+ if (!$valid_sig) {
+ throw new OAuthException("Invalid signature");
+ }
+ }/*}}}*/
+
+ /**
+ * check that the timestamp is new enough
+ */
+ private function check_timestamp($timestamp) {/*{{{*/
+ // verify that timestamp is recentish
+ $now = time();
+ if ($now - $timestamp > $this->timestamp_threshold) {
+ throw new OAuthException("Expired timestamp, yours $timestamp, ours $now");
+ }
+ }/*}}}*/
+
+ /**
+ * check that the nonce is not repeated
+ */
+ private function check_nonce($consumer, $token, $nonce, $timestamp) {/*{{{*/
+ // verify that the nonce is uniqueish
+ $found = $this->data_store->lookup_nonce($consumer, $token, $nonce, $timestamp);
+ if ($found) {
+ throw new OAuthException("Nonce already used: $nonce");
+ }
+ }/*}}}*/
+
+
+
+}/*}}}*/
+
+class OAuthDataStore {/*{{{*/
+ function lookup_consumer($consumer_key) {/*{{{*/
+ // implement me
+ }/*}}}*/
+
+ function lookup_token($consumer, $token_type, $token) {/*{{{*/
+ // implement me
+ }/*}}}*/
+
+ function lookup_nonce($consumer, $token, $nonce, $timestamp) {/*{{{*/
+ // implement me
+ }/*}}}*/
+
+ function fetch_request_token($consumer) {/*{{{*/
+ // return a new token attached to this consumer
+ }/*}}}*/
+
+ function fetch_access_token($token, $consumer) {/*{{{*/
+ // return a new access token attached to this consumer
+ // for the user associated with this token if the request token
+ // is authorized
+ // should also invalidate the request token
+ }/*}}}*/
+
+}/*}}}*/
+
+
+/* A very naive dbm-based oauth storage
+ */
+class SimpleOAuthDataStore extends OAuthDataStore {/*{{{*/
+ private $dbh;
+
+ function __construct($path = "oauth.gdbm") {/*{{{*/
+ $this->dbh = dba_popen($path, 'c', 'gdbm');
+ }/*}}}*/
+
+ function __destruct() {/*{{{*/
+ dba_close($this->dbh);
+ }/*}}}*/
+
+ function lookup_consumer($consumer_key) {/*{{{*/
+ $rv = dba_fetch("consumer_$consumer_key", $this->dbh);
+ if ($rv === FALSE) {
+ return NULL;
+ }
+ $obj = unserialize($rv);
+ if (!($obj instanceof OAuthConsumer)) {
+ return NULL;
+ }
+ return $obj;
+ }/*}}}*/
+
+ function lookup_token($consumer, $token_type, $token) {/*{{{*/
+ $rv = dba_fetch("${token_type}_${token}", $this->dbh);
+ if ($rv === FALSE) {
+ return NULL;
+ }
+ $obj = unserialize($rv);
+ if (!($obj instanceof OAuthToken)) {
+ return NULL;
+ }
+ return $obj;
+ }/*}}}*/
+
+ function lookup_nonce($consumer, $token, $nonce, $timestamp) {/*{{{*/
+ if (dba_exists("nonce_$nonce", $this->dbh)) {
+ return TRUE;
+ } else {
+ dba_insert("nonce_$nonce", "1", $this->dbh);
+ return FALSE;
+ }
+ }/*}}}*/
+
+ function new_token($consumer, $type="request") {/*{{{*/
+ $key = md5(time());
+ $secret = time() + time();
+ $token = new OAuthToken($key, md5(md5($secret)));
+ if (!dba_insert("${type}_$key", serialize($token), $this->dbh)) {
+ throw new OAuthException("doooom!");
+ }
+ return $token;
+ }/*}}}*/
+
+ function new_request_token($consumer) {/*{{{*/
+ return $this->new_token($consumer, "request");
+ }/*}}}*/
+
+ function new_access_token($token, $consumer) {/*{{{*/
+
+ $token = $this->new_token($consumer, 'access');
+ dba_delete("request_" . $token->key, $this->dbh);
+ return $token;
+ }/*}}}*/
+}/*}}}*/
+
+class OAuthUtil {/*{{{*/
+ public static function urlencodeRFC3986($string) {/*{{{*/
+ return str_replace('+', ' ',
+ str_replace('%7E', '~', rawurlencode($string)));
+
+ }/*}}}*/
+
+
+ // This decode function isn't taking into consideration the above
+ // modifications to the encoding process. However, this method doesn't
+ // seem to be used anywhere so leaving it as is.
+ public static function urldecodeRFC3986($string) {/*{{{*/
+ return rawurldecode($string);
+ }/*}}}*/
+}/*}}}*/
+
+?>
diff --git a/extlib/OAuth_LICENSE.txt b/extlib/OAuth_LICENSE.txt
new file mode 100644
index 000000000..89f059169
--- /dev/null
+++ b/extlib/OAuth_LICENSE.txt
@@ -0,0 +1,22 @@
+The MIT License
+
+Copyright (c) 2007 Andy Smith
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
diff --git a/extlib/PEAR.php b/extlib/PEAR.php
new file mode 100644
index 000000000..4c24c6006
--- /dev/null
+++ b/extlib/PEAR.php
@@ -0,0 +1,1118 @@
+<?php
+/**
+ * PEAR, the PHP Extension and Application Repository
+ *
+ * PEAR class and PEAR_Error class
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt. If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category pear
+ * @package PEAR
+ * @author Sterling Hughes <sterling@php.net>
+ * @author Stig Bakken <ssb@php.net>
+ * @author Tomas V.V.Cox <cox@idecnet.com>
+ * @author Greg Beaver <cellog@php.net>
+ * @copyright 1997-2008 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0
+ * @version CVS: $Id: PEAR.php,v 1.104 2008/01/03 20:26:34 cellog Exp $
+ * @link http://pear.php.net/package/PEAR
+ * @since File available since Release 0.1
+ */
+
+/**#@+
+ * ERROR constants
+ */
+define('PEAR_ERROR_RETURN', 1);
+define('PEAR_ERROR_PRINT', 2);
+define('PEAR_ERROR_TRIGGER', 4);
+define('PEAR_ERROR_DIE', 8);
+define('PEAR_ERROR_CALLBACK', 16);
+/**
+ * WARNING: obsolete
+ * @deprecated
+ */
+define('PEAR_ERROR_EXCEPTION', 32);
+/**#@-*/
+define('PEAR_ZE2', (function_exists('version_compare') &&
+ version_compare(zend_version(), "2-dev", "ge")));
+
+if (substr(PHP_OS, 0, 3) == 'WIN') {
+ define('OS_WINDOWS', true);
+ define('OS_UNIX', false);
+ define('PEAR_OS', 'Windows');
+} else {
+ define('OS_WINDOWS', false);
+ define('OS_UNIX', true);
+ define('PEAR_OS', 'Unix'); // blatant assumption
+}
+
+// instant backwards compatibility
+if (!defined('PATH_SEPARATOR')) {
+ if (OS_WINDOWS) {
+ define('PATH_SEPARATOR', ';');
+ } else {
+ define('PATH_SEPARATOR', ':');
+ }
+}
+
+$GLOBALS['_PEAR_default_error_mode'] = PEAR_ERROR_RETURN;
+$GLOBALS['_PEAR_default_error_options'] = E_USER_NOTICE;
+$GLOBALS['_PEAR_destructor_object_list'] = array();
+$GLOBALS['_PEAR_shutdown_funcs'] = array();
+$GLOBALS['_PEAR_error_handler_stack'] = array();
+
+@ini_set('track_errors', true);
+
+/**
+ * Base class for other PEAR classes. Provides rudimentary
+ * emulation of destructors.
+ *
+ * If you want a destructor in your class, inherit PEAR and make a
+ * destructor method called _yourclassname (same name as the
+ * constructor, but with a "_" prefix). Also, in your constructor you
+ * have to call the PEAR constructor: $this->PEAR();.
+ * The destructor method will be called without parameters. Note that
+ * at in some SAPI implementations (such as Apache), any output during
+ * the request shutdown (in which destructors are called) seems to be
+ * discarded. If you need to get any debug information from your
+ * destructor, use error_log(), syslog() or something similar.
+ *
+ * IMPORTANT! To use the emulated destructors you need to create the
+ * objects by reference: $obj =& new PEAR_child;
+ *
+ * @category pear
+ * @package PEAR
+ * @author Stig Bakken <ssb@php.net>
+ * @author Tomas V.V. Cox <cox@idecnet.com>
+ * @author Greg Beaver <cellog@php.net>
+ * @copyright 1997-2006 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0
+ * @version Release: 1.7.2
+ * @link http://pear.php.net/package/PEAR
+ * @see PEAR_Error
+ * @since Class available since PHP 4.0.2
+ * @link http://pear.php.net/manual/en/core.pear.php#core.pear.pear
+ */
+class PEAR
+{
+ // {{{ properties
+
+ /**
+ * Whether to enable internal debug messages.
+ *
+ * @var bool
+ * @access private
+ */
+ var $_debug = false;
+
+ /**
+ * Default error mode for this object.
+ *
+ * @var int
+ * @access private
+ */
+ var $_default_error_mode = null;
+
+ /**
+ * Default error options used for this object when error mode
+ * is PEAR_ERROR_TRIGGER.
+ *
+ * @var int
+ * @access private
+ */
+ var $_default_error_options = null;
+
+ /**
+ * Default error handler (callback) for this object, if error mode is
+ * PEAR_ERROR_CALLBACK.
+ *
+ * @var string
+ * @access private
+ */
+ var $_default_error_handler = '';
+
+ /**
+ * Which class to use for error objects.
+ *
+ * @var string
+ * @access private
+ */
+ var $_error_class = 'PEAR_Error';
+
+ /**
+ * An array of expected errors.
+ *
+ * @var array
+ * @access private
+ */
+ var $_expected_errors = array();
+
+ // }}}
+
+ // {{{ constructor
+
+ /**
+ * Constructor. Registers this object in
+ * $_PEAR_destructor_object_list for destructor emulation if a
+ * destructor object exists.
+ *
+ * @param string $error_class (optional) which class to use for
+ * error objects, defaults to PEAR_Error.
+ * @access public
+ * @return void
+ */
+ function PEAR($error_class = null)
+ {
+ $classname = strtolower(get_class($this));
+ if ($this->_debug) {
+ print "PEAR constructor called, class=$classname\n";
+ }
+ if ($error_class !== null) {
+ $this->_error_class = $error_class;
+ }
+ while ($classname && strcasecmp($classname, "pear")) {
+ $destructor = "_$classname";
+ if (method_exists($this, $destructor)) {
+ global $_PEAR_destructor_object_list;
+ $_PEAR_destructor_object_list[] = &$this;
+ if (!isset($GLOBALS['_PEAR_SHUTDOWN_REGISTERED'])) {
+ register_shutdown_function("_PEAR_call_destructors");
+ $GLOBALS['_PEAR_SHUTDOWN_REGISTERED'] = true;
+ }
+ break;
+ } else {
+ $classname = get_parent_class($classname);
+ }
+ }
+ }
+
+ // }}}
+ // {{{ destructor
+
+ /**
+ * Destructor (the emulated type of...). Does nothing right now,
+ * but is included for forward compatibility, so subclass
+ * destructors should always call it.
+ *
+ * See the note in the class desciption about output from
+ * destructors.
+ *
+ * @access public
+ * @return void
+ */
+ function _PEAR() {
+ if ($this->_debug) {
+ printf("PEAR destructor called, class=%s\n", strtolower(get_class($this)));
+ }
+ }
+
+ // }}}
+ // {{{ getStaticProperty()
+
+ /**
+ * If you have a class that's mostly/entirely static, and you need static
+ * properties, you can use this method to simulate them. Eg. in your method(s)
+ * do this: $myVar = &PEAR::getStaticProperty('myclass', 'myVar');
+ * You MUST use a reference, or they will not persist!
+ *
+ * @access public
+ * @param string $class The calling classname, to prevent clashes
+ * @param string $var The variable to retrieve.
+ * @return mixed A reference to the variable. If not set it will be
+ * auto initialised to NULL.
+ */
+ function &getStaticProperty($class, $var)
+ {
+ static $properties;
+ if (!isset($properties[$class])) {
+ $properties[$class] = array();
+ }
+ if (!array_key_exists($var, $properties[$class])) {
+ $properties[$class][$var] = null;
+ }
+ return $properties[$class][$var];
+ }
+
+ // }}}
+ // {{{ registerShutdownFunc()
+
+ /**
+ * Use this function to register a shutdown method for static
+ * classes.
+ *
+ * @access public
+ * @param mixed $func The function name (or array of class/method) to call
+ * @param mixed $args The arguments to pass to the function
+ * @return void
+ */
+ function registerShutdownFunc($func, $args = array())
+ {
+ // if we are called statically, there is a potential
+ // that no shutdown func is registered. Bug #6445
+ if (!isset($GLOBALS['_PEAR_SHUTDOWN_REGISTERED'])) {
+ register_shutdown_function("_PEAR_call_destructors");
+ $GLOBALS['_PEAR_SHUTDOWN_REGISTERED'] = true;
+ }
+ $GLOBALS['_PEAR_shutdown_funcs'][] = array($func, $args);
+ }
+
+ // }}}
+ // {{{ isError()
+
+ /**
+ * Tell whether a value is a PEAR error.
+ *
+ * @param mixed $data the value to test
+ * @param int $code if $data is an error object, return true
+ * only if $code is a string and
+ * $obj->getMessage() == $code or
+ * $code is an integer and $obj->getCode() == $code
+ * @access public
+ * @return bool true if parameter is an error
+ */
+ function isError($data, $code = null)
+ {
+ if (is_a($data, 'PEAR_Error')) {
+ if (is_null($code)) {
+ return true;
+ } elseif (is_string($code)) {
+ return $data->getMessage() == $code;
+ } else {
+ return $data->getCode() == $code;
+ }
+ }
+ return false;
+ }
+
+ // }}}
+ // {{{ setErrorHandling()
+
+ /**
+ * Sets how errors generated by this object should be handled.
+ * Can be invoked both in objects and statically. If called
+ * statically, setErrorHandling sets the default behaviour for all
+ * PEAR objects. If called in an object, setErrorHandling sets
+ * the default behaviour for that object.
+ *
+ * @param int $mode
+ * One of PEAR_ERROR_RETURN, PEAR_ERROR_PRINT,
+ * PEAR_ERROR_TRIGGER, PEAR_ERROR_DIE,
+ * PEAR_ERROR_CALLBACK or PEAR_ERROR_EXCEPTION.
+ *
+ * @param mixed $options
+ * When $mode is PEAR_ERROR_TRIGGER, this is the error level (one
+ * of E_USER_NOTICE, E_USER_WARNING or E_USER_ERROR).
+ *
+ * When $mode is PEAR_ERROR_CALLBACK, this parameter is expected
+ * to be the callback function or method. A callback
+ * function is a string with the name of the function, a
+ * callback method is an array of two elements: the element
+ * at index 0 is the object, and the element at index 1 is
+ * the name of the method to call in the object.
+ *
+ * When $mode is PEAR_ERROR_PRINT or PEAR_ERROR_DIE, this is
+ * a printf format string used when printing the error
+ * message.
+ *
+ * @access public
+ * @return void
+ * @see PEAR_ERROR_RETURN
+ * @see PEAR_ERROR_PRINT
+ * @see PEAR_ERROR_TRIGGER
+ * @see PEAR_ERROR_DIE
+ * @see PEAR_ERROR_CALLBACK
+ * @see PEAR_ERROR_EXCEPTION
+ *
+ * @since PHP 4.0.5
+ */
+
+ function setErrorHandling($mode = null, $options = null)
+ {
+ if (isset($this) && is_a($this, 'PEAR')) {
+ $setmode = &$this->_default_error_mode;
+ $setoptions = &$this->_default_error_options;
+ } else {
+ $setmode = &$GLOBALS['_PEAR_default_error_mode'];
+ $setoptions = &$GLOBALS['_PEAR_default_error_options'];
+ }
+
+ switch ($mode) {
+ case PEAR_ERROR_EXCEPTION:
+ case PEAR_ERROR_RETURN:
+ case PEAR_ERROR_PRINT:
+ case PEAR_ERROR_TRIGGER:
+ case PEAR_ERROR_DIE:
+ case null:
+ $setmode = $mode;
+ $setoptions = $options;
+ break;
+
+ case PEAR_ERROR_CALLBACK:
+ $setmode = $mode;
+ // class/object method callback
+ if (is_callable($options)) {
+ $setoptions = $options;
+ } else {
+ trigger_error("invalid error callback", E_USER_WARNING);
+ }
+ break;
+
+ default:
+ trigger_error("invalid error mode", E_USER_WARNING);
+ break;
+ }
+ }
+
+ // }}}
+ // {{{ expectError()
+
+ /**
+ * This method is used to tell which errors you expect to get.
+ * Expected errors are always returned with error mode
+ * PEAR_ERROR_RETURN. Expected error codes are stored in a stack,
+ * and this method pushes a new element onto it. The list of
+ * expected errors are in effect until they are popped off the
+ * stack with the popExpect() method.
+ *
+ * Note that this method can not be called statically
+ *
+ * @param mixed $code a single error code or an array of error codes to expect
+ *
+ * @return int the new depth of the "expected errors" stack
+ * @access public
+ */
+ function expectError($code = '*')
+ {
+ if (is_array($code)) {
+ array_push($this->_expected_errors, $code);
+ } else {
+ array_push($this->_expected_errors, array($code));
+ }
+ return sizeof($this->_expected_errors);
+ }
+
+ // }}}
+ // {{{ popExpect()
+
+ /**
+ * This method pops one element off the expected error codes
+ * stack.
+ *
+ * @return array the list of error codes that were popped
+ */
+ function popExpect()
+ {
+ return array_pop($this->_expected_errors);
+ }
+
+ // }}}
+ // {{{ _checkDelExpect()
+
+ /**
+ * This method checks unsets an error code if available
+ *
+ * @param mixed error code
+ * @return bool true if the error code was unset, false otherwise
+ * @access private
+ * @since PHP 4.3.0
+ */
+ function _checkDelExpect($error_code)
+ {
+ $deleted = false;
+
+ foreach ($this->_expected_errors AS $key => $error_array) {
+ if (in_array($error_code, $error_array)) {
+ unset($this->_expected_errors[$key][array_search($error_code, $error_array)]);
+ $deleted = true;
+ }
+
+ // clean up empty arrays
+ if (0 == count($this->_expected_errors[$key])) {
+ unset($this->_expected_errors[$key]);
+ }
+ }
+ return $deleted;
+ }
+
+ // }}}
+ // {{{ delExpect()
+
+ /**
+ * This method deletes all occurences of the specified element from
+ * the expected error codes stack.
+ *
+ * @param mixed $error_code error code that should be deleted
+ * @return mixed list of error codes that were deleted or error
+ * @access public
+ * @since PHP 4.3.0
+ */
+ function delExpect($error_code)
+ {
+ $deleted = false;
+
+ if ((is_array($error_code) && (0 != count($error_code)))) {
+ // $error_code is a non-empty array here;
+ // we walk through it trying to unset all
+ // values
+ foreach($error_code as $key => $error) {
+ if ($this->_checkDelExpect($error)) {
+ $deleted = true;
+ } else {
+ $deleted = false;
+ }
+ }
+ return $deleted ? true : PEAR::raiseError("The expected error you submitted does not exist"); // IMPROVE ME
+ } elseif (!empty($error_code)) {
+ // $error_code comes alone, trying to unset it
+ if ($this->_checkDelExpect($error_code)) {
+ return true;
+ } else {
+ return PEAR::raiseError("The expected error you submitted does not exist"); // IMPROVE ME
+ }
+ } else {
+ // $error_code is empty
+ return PEAR::raiseError("The expected error you submitted is empty"); // IMPROVE ME
+ }
+ }
+
+ // }}}
+ // {{{ raiseError()
+
+ /**
+ * This method is a wrapper that returns an instance of the
+ * configured error class with this object's default error
+ * handling applied. If the $mode and $options parameters are not
+ * specified, the object's defaults are used.
+ *
+ * @param mixed $message a text error message or a PEAR error object
+ *
+ * @param int $code a numeric error code (it is up to your class
+ * to define these if you want to use codes)
+ *
+ * @param int $mode One of PEAR_ERROR_RETURN, PEAR_ERROR_PRINT,
+ * PEAR_ERROR_TRIGGER, PEAR_ERROR_DIE,
+ * PEAR_ERROR_CALLBACK, PEAR_ERROR_EXCEPTION.
+ *
+ * @param mixed $options If $mode is PEAR_ERROR_TRIGGER, this parameter
+ * specifies the PHP-internal error level (one of
+ * E_USER_NOTICE, E_USER_WARNING or E_USER_ERROR).
+ * If $mode is PEAR_ERROR_CALLBACK, this
+ * parameter specifies the callback function or
+ * method. In other error modes this parameter
+ * is ignored.
+ *
+ * @param string $userinfo If you need to pass along for example debug
+ * information, this parameter is meant for that.
+ *
+ * @param string $error_class The returned error object will be
+ * instantiated from this class, if specified.
+ *
+ * @param bool $skipmsg If true, raiseError will only pass error codes,
+ * the error message parameter will be dropped.
+ *
+ * @access public
+ * @return object a PEAR error object
+ * @see PEAR::setErrorHandling
+ * @since PHP 4.0.5
+ */
+ function &raiseError($message = null,
+ $code = null,
+ $mode = null,
+ $options = null,
+ $userinfo = null,
+ $error_class = null,
+ $skipmsg = false)
+ {
+ // The error is yet a PEAR error object
+ if (is_object($message)) {
+ $code = $message->getCode();
+ $userinfo = $message->getUserInfo();
+ $error_class = $message->getType();
+ $message->error_message_prefix = '';
+ $message = $message->getMessage();
+ }
+
+ if (isset($this) && isset($this->_expected_errors) && sizeof($this->_expected_errors) > 0 && sizeof($exp = end($this->_expected_errors))) {
+ if ($exp[0] == "*" ||
+ (is_int(reset($exp)) && in_array($code, $exp)) ||
+ (is_string(reset($exp)) && in_array($message, $exp))) {
+ $mode = PEAR_ERROR_RETURN;
+ }
+ }
+ // No mode given, try global ones
+ if ($mode === null) {
+ // Class error handler
+ if (isset($this) && isset($this->_default_error_mode)) {
+ $mode = $this->_default_error_mode;
+ $options = $this->_default_error_options;
+ // Global error handler
+ } elseif (isset($GLOBALS['_PEAR_default_error_mode'])) {
+ $mode = $GLOBALS['_PEAR_default_error_mode'];
+ $options = $GLOBALS['_PEAR_default_error_options'];
+ }
+ }
+
+ if ($error_class !== null) {
+ $ec = $error_class;
+ } elseif (isset($this) && isset($this->_error_class)) {
+ $ec = $this->_error_class;
+ } else {
+ $ec = 'PEAR_Error';
+ }
+ if (intval(PHP_VERSION) < 5) {
+ // little non-eval hack to fix bug #12147
+ include 'PEAR/FixPHP5PEARWarnings.php';
+ return $a;
+ }
+ if ($skipmsg) {
+ $a = new $ec($code, $mode, $options, $userinfo);
+ } else {
+ $a = new $ec($message, $code, $mode, $options, $userinfo);
+ }
+ return $a;
+ }
+
+ // }}}
+ // {{{ throwError()
+
+ /**
+ * Simpler form of raiseError with fewer options. In most cases
+ * message, code and userinfo are enough.
+ *
+ * @param string $message
+ *
+ */
+ function &throwError($message = null,
+ $code = null,
+ $userinfo = null)
+ {
+ if (isset($this) && is_a($this, 'PEAR')) {
+ $a = &$this->raiseError($message, $code, null, null, $userinfo);
+ return $a;
+ } else {
+ $a = &PEAR::raiseError($message, $code, null, null, $userinfo);
+ return $a;
+ }
+ }
+
+ // }}}
+ function staticPushErrorHandling($mode, $options = null)
+ {
+ $stack = &$GLOBALS['_PEAR_error_handler_stack'];
+ $def_mode = &$GLOBALS['_PEAR_default_error_mode'];
+ $def_options = &$GLOBALS['_PEAR_default_error_options'];
+ $stack[] = array($def_mode, $def_options);
+ switch ($mode) {
+ case PEAR_ERROR_EXCEPTION:
+ case PEAR_ERROR_RETURN:
+ case PEAR_ERROR_PRINT:
+ case PEAR_ERROR_TRIGGER:
+ case PEAR_ERROR_DIE:
+ case null:
+ $def_mode = $mode;
+ $def_options = $options;
+ break;
+
+ case PEAR_ERROR_CALLBACK:
+ $def_mode = $mode;
+ // class/object method callback
+ if (is_callable($options)) {
+ $def_options = $options;
+ } else {
+ trigger_error("invalid error callback", E_USER_WARNING);
+ }
+ break;
+
+ default:
+ trigger_error("invalid error mode", E_USER_WARNING);
+ break;
+ }
+ $stack[] = array($mode, $options);
+ return true;
+ }
+
+ function staticPopErrorHandling()
+ {
+ $stack = &$GLOBALS['_PEAR_error_handler_stack'];
+ $setmode = &$GLOBALS['_PEAR_default_error_mode'];
+ $setoptions = &$GLOBALS['_PEAR_default_error_options'];
+ array_pop($stack);
+ list($mode, $options) = $stack[sizeof($stack) - 1];
+ array_pop($stack);
+ switch ($mode) {
+ case PEAR_ERROR_EXCEPTION:
+ case PEAR_ERROR_RETURN:
+ case PEAR_ERROR_PRINT:
+ case PEAR_ERROR_TRIGGER:
+ case PEAR_ERROR_DIE:
+ case null:
+ $setmode = $mode;
+ $setoptions = $options;
+ break;
+
+ case PEAR_ERROR_CALLBACK:
+ $setmode = $mode;
+ // class/object method callback
+ if (is_callable($options)) {
+ $setoptions = $options;
+ } else {
+ trigger_error("invalid error callback", E_USER_WARNING);
+ }
+ break;
+
+ default:
+ trigger_error("invalid error mode", E_USER_WARNING);
+ break;
+ }
+ return true;
+ }
+
+ // {{{ pushErrorHandling()
+
+ /**
+ * Push a new error handler on top of the error handler options stack. With this
+ * you can easily override the actual error handler for some code and restore
+ * it later with popErrorHandling.
+ *
+ * @param mixed $mode (same as setErrorHandling)
+ * @param mixed $options (same as setErrorHandling)
+ *
+ * @return bool Always true
+ *
+ * @see PEAR::setErrorHandling
+ */
+ function pushErrorHandling($mode, $options = null)
+ {
+ $stack = &$GLOBALS['_PEAR_error_handler_stack'];
+ if (isset($this) && is_a($this, 'PEAR')) {
+ $def_mode = &$this->_default_error_mode;
+ $def_options = &$this->_default_error_options;
+ } else {
+ $def_mode = &$GLOBALS['_PEAR_default_error_mode'];
+ $def_options = &$GLOBALS['_PEAR_default_error_options'];
+ }
+ $stack[] = array($def_mode, $def_options);
+
+ if (isset($this) && is_a($this, 'PEAR')) {
+ $this->setErrorHandling($mode, $options);
+ } else {
+ PEAR::setErrorHandling($mode, $options);
+ }
+ $stack[] = array($mode, $options);
+ return true;
+ }
+
+ // }}}
+ // {{{ popErrorHandling()
+
+ /**
+ * Pop the last error handler used
+ *
+ * @return bool Always true
+ *
+ * @see PEAR::pushErrorHandling
+ */
+ function popErrorHandling()
+ {
+ $stack = &$GLOBALS['_PEAR_error_handler_stack'];
+ array_pop($stack);
+ list($mode, $options) = $stack[sizeof($stack) - 1];
+ array_pop($stack);
+ if (isset($this) && is_a($this, 'PEAR')) {
+ $this->setErrorHandling($mode, $options);
+ } else {
+ PEAR::setErrorHandling($mode, $options);
+ }
+ return true;
+ }
+
+ // }}}
+ // {{{ loadExtension()
+
+ /**
+ * OS independant PHP extension load. Remember to take care
+ * on the correct extension name for case sensitive OSes.
+ *
+ * @param string $ext The extension name
+ * @return bool Success or not on the dl() call
+ */
+ function loadExtension($ext)
+ {
+ if (!extension_loaded($ext)) {
+ // if either returns true dl() will produce a FATAL error, stop that
+ if ((ini_get('enable_dl') != 1) || (ini_get('safe_mode') == 1)) {
+ return false;
+ }
+ if (OS_WINDOWS) {
+ $suffix = '.dll';
+ } elseif (PHP_OS == 'HP-UX') {
+ $suffix = '.sl';
+ } elseif (PHP_OS == 'AIX') {
+ $suffix = '.a';
+ } elseif (PHP_OS == 'OSX') {
+ $suffix = '.bundle';
+ } else {
+ $suffix = '.so';
+ }
+ return @dl('php_'.$ext.$suffix) || @dl($ext.$suffix);
+ }
+ return true;
+ }
+
+ // }}}
+}
+
+// {{{ _PEAR_call_destructors()
+
+function _PEAR_call_destructors()
+{
+ global $_PEAR_destructor_object_list;
+ if (is_array($_PEAR_destructor_object_list) &&
+ sizeof($_PEAR_destructor_object_list))
+ {
+ reset($_PEAR_destructor_object_list);
+ if (PEAR::getStaticProperty('PEAR', 'destructlifo')) {
+ $_PEAR_destructor_object_list = array_reverse($_PEAR_destructor_object_list);
+ }
+ while (list($k, $objref) = each($_PEAR_destructor_object_list)) {
+ $classname = get_class($objref);
+ while ($classname) {
+ $destructor = "_$classname";
+ if (method_exists($objref, $destructor)) {
+ $objref->$destructor();
+ break;
+ } else {
+ $classname = get_parent_class($classname);
+ }
+ }
+ }
+ // Empty the object list to ensure that destructors are
+ // not called more than once.
+ $_PEAR_destructor_object_list = array();
+ }
+
+ // Now call the shutdown functions
+ if (is_array($GLOBALS['_PEAR_shutdown_funcs']) AND !empty($GLOBALS['_PEAR_shutdown_funcs'])) {
+ foreach ($GLOBALS['_PEAR_shutdown_funcs'] as $value) {
+ call_user_func_array($value[0], $value[1]);
+ }
+ }
+}
+
+// }}}
+/**
+ * Standard PEAR error class for PHP 4
+ *
+ * This class is supserseded by {@link PEAR_Exception} in PHP 5
+ *
+ * @category pear
+ * @package PEAR
+ * @author Stig Bakken <ssb@php.net>
+ * @author Tomas V.V. Cox <cox@idecnet.com>
+ * @author Gregory Beaver <cellog@php.net>
+ * @copyright 1997-2006 The PHP Group
+ * @license http://www.php.net/license/3_0.txt PHP License 3.0
+ * @version Release: 1.7.2
+ * @link http://pear.php.net/manual/en/core.pear.pear-error.php
+ * @see PEAR::raiseError(), PEAR::throwError()
+ * @since Class available since PHP 4.0.2
+ */
+class PEAR_Error
+{
+ // {{{ properties
+
+ var $error_message_prefix = '';
+ var $mode = PEAR_ERROR_RETURN;
+ var $level = E_USER_NOTICE;
+ var $code = -1;
+ var $message = '';
+ var $userinfo = '';
+ var $backtrace = null;
+
+ // }}}
+ // {{{ constructor
+
+ /**
+ * PEAR_Error constructor
+ *
+ * @param string $message message
+ *
+ * @param int $code (optional) error code
+ *
+ * @param int $mode (optional) error mode, one of: PEAR_ERROR_RETURN,
+ * PEAR_ERROR_PRINT, PEAR_ERROR_DIE, PEAR_ERROR_TRIGGER,
+ * PEAR_ERROR_CALLBACK or PEAR_ERROR_EXCEPTION
+ *
+ * @param mixed $options (optional) error level, _OR_ in the case of
+ * PEAR_ERROR_CALLBACK, the callback function or object/method
+ * tuple.
+ *
+ * @param string $userinfo (optional) additional user/debug info
+ *
+ * @access public
+ *
+ */
+ function PEAR_Error($message = 'unknown error', $code = null,
+ $mode = null, $options = null, $userinfo = null)
+ {
+ if ($mode === null) {
+ $mode = PEAR_ERROR_RETURN;
+ }
+ $this->message = $message;
+ $this->code = $code;
+ $this->mode = $mode;
+ $this->userinfo = $userinfo;
+ if (!PEAR::getStaticProperty('PEAR_Error', 'skiptrace')) {
+ $this->backtrace = debug_backtrace();
+ if (isset($this->backtrace[0]) && isset($this->backtrace[0]['object'])) {
+ unset($this->backtrace[0]['object']);
+ }
+ }
+ if ($mode & PEAR_ERROR_CALLBACK) {
+ $this->level = E_USER_NOTICE;
+ $this->callback = $options;
+ } else {
+ if ($options === null) {
+ $options = E_USER_NOTICE;
+ }
+ $this->level = $options;
+ $this->callback = null;
+ }
+ if ($this->mode & PEAR_ERROR_PRINT) {
+ if (is_null($options) || is_int($options)) {
+ $format = "%s";
+ } else {
+ $format = $options;
+ }
+ printf($format, $this->getMessage());
+ }
+ if ($this->mode & PEAR_ERROR_TRIGGER) {
+ trigger_error($this->getMessage(), $this->level);
+ }
+ if ($this->mode & PEAR_ERROR_DIE) {
+ $msg = $this->getMessage();
+ if (is_null($options) || is_int($options)) {
+ $format = "%s";
+ if (substr($msg, -1) != "\n") {
+ $msg .= "\n";
+ }
+ } else {
+ $format = $options;
+ }
+ die(sprintf($format, $msg));
+ }
+ if ($this->mode & PEAR_ERROR_CALLBACK) {
+ if (is_callable($this->callback)) {
+ call_user_func($this->callback, $this);
+ }
+ }
+ if ($this->mode & PEAR_ERROR_EXCEPTION) {
+ trigger_error("PEAR_ERROR_EXCEPTION is obsolete, use class PEAR_Exception for exceptions", E_USER_WARNING);
+ eval('$e = new Exception($this->message, $this->code);throw($e);');
+ }
+ }
+
+ // }}}
+ // {{{ getMode()
+
+ /**
+ * Get the error mode from an error object.
+ *
+ * @return int error mode
+ * @access public
+ */
+ function getMode() {
+ return $this->mode;
+ }
+
+ // }}}
+ // {{{ getCallback()
+
+ /**
+ * Get the callback function/method from an error object.
+ *
+ * @return mixed callback function or object/method array
+ * @access public
+ */
+ function getCallback() {
+ return $this->callback;
+ }
+
+ // }}}
+ // {{{ getMessage()
+
+
+ /**
+ * Get the error message from an error object.
+ *
+ * @return string full error message
+ * @access public
+ */
+ function getMessage()
+ {
+ return ($this->error_message_prefix . $this->message);
+ }
+
+
+ // }}}
+ // {{{ getCode()
+
+ /**
+ * Get error code from an error object
+ *
+ * @return int error code
+ * @access public
+ */
+ function getCode()
+ {
+ return $this->code;
+ }
+
+ // }}}
+ // {{{ getType()
+
+ /**
+ * Get the name of this error/exception.
+ *
+ * @return string error/exception name (type)
+ * @access public
+ */
+ function getType()
+ {
+ return get_class($this);
+ }
+
+ // }}}
+ // {{{ getUserInfo()
+
+ /**
+ * Get additional user-supplied information.
+ *
+ * @return string user-supplied information
+ * @access public
+ */
+ function getUserInfo()
+ {
+ return $this->userinfo;
+ }
+
+ // }}}
+ // {{{ getDebugInfo()
+
+ /**
+ * Get additional debug information supplied by the application.
+ *
+ * @return string debug information
+ * @access public
+ */
+ function getDebugInfo()
+ {
+ return $this->getUserInfo();
+ }
+
+ // }}}
+ // {{{ getBacktrace()
+
+ /**
+ * Get the call backtrace from where the error was generated.
+ * Supported with PHP 4.3.0 or newer.
+ *
+ * @param int $frame (optional) what frame to fetch
+ * @return array Backtrace, or NULL if not available.
+ * @access public
+ */
+ function getBacktrace($frame = null)
+ {
+ if (defined('PEAR_IGNORE_BACKTRACE')) {
+ return null;
+ }
+ if ($frame === null) {
+ return $this->backtrace;
+ }
+ return $this->backtrace[$frame];
+ }
+
+ // }}}
+ // {{{ addUserInfo()
+
+ function addUserInfo($info)
+ {
+ if (empty($this->userinfo)) {
+ $this->userinfo = $info;
+ } else {
+ $this->userinfo .= " ** $info";
+ }
+ }
+
+ // }}}
+ // {{{ toString()
+ function __toString()
+ {
+ return $this->getMessage();
+ }
+ // }}}
+ // {{{ toString()
+
+ /**
+ * Make a string representation of this object.
+ *
+ * @return string a string with an object summary
+ * @access public
+ */
+ function toString() {
+ $modes = array();
+ $levels = array(E_USER_NOTICE => 'notice',
+ E_USER_WARNING => 'warning',
+ E_USER_ERROR => 'error');
+ if ($this->mode & PEAR_ERROR_CALLBACK) {
+ if (is_array($this->callback)) {
+ $callback = (is_object($this->callback[0]) ?
+ strtolower(get_class($this->callback[0])) :
+ $this->callback[0]) . '::' .
+ $this->callback[1];
+ } else {
+ $callback = $this->callback;
+ }
+ return sprintf('[%s: message="%s" code=%d mode=callback '.
+ 'callback=%s prefix="%s" info="%s"]',
+ strtolower(get_class($this)), $this->message, $this->code,
+ $callback, $this->error_message_prefix,
+ $this->userinfo);
+ }
+ if ($this->mode & PEAR_ERROR_PRINT) {
+ $modes[] = 'print';
+ }
+ if ($this->mode & PEAR_ERROR_TRIGGER) {
+ $modes[] = 'trigger';
+ }
+ if ($this->mode & PEAR_ERROR_DIE) {
+ $modes[] = 'die';
+ }
+ if ($this->mode & PEAR_ERROR_RETURN) {
+ $modes[] = 'return';
+ }
+ return sprintf('[%s: message="%s" code=%d mode=%s level=%s '.
+ 'prefix="%s" info="%s"]',
+ strtolower(get_class($this)), $this->message, $this->code,
+ implode("|", $modes), $levels[$this->level],
+ $this->error_message_prefix,
+ $this->userinfo);
+ }
+
+ // }}}
+}
+
+/*
+ * Local Variables:
+ * mode: php
+ * tab-width: 4
+ * c-basic-offset: 4
+ * End:
+ */
+?>
diff --git a/extlib/PHP_License_2_02.txt b/extlib/PHP_License_2_02.txt
new file mode 100644
index 000000000..af5b01c49
--- /dev/null
+++ b/extlib/PHP_License_2_02.txt
@@ -0,0 +1,75 @@
+--------------------------------------------------------------------
+ The PHP License, version 2.02
+Copyright (c) 1999 - 2002 The PHP Group. All rights reserved.
+--------------------------------------------------------------------
+
+Redistribution and use in source and binary forms, with or without
+modification, is permitted provided that the following conditions
+are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+
+ 3. The name "PHP" must not be used to endorse or promote products
+ derived from this software without prior permission from the
+ PHP Group. This does not apply to add-on libraries or tools
+ that work in conjunction with PHP. In such a case the PHP
+ name may be used to indicate that the product supports PHP.
+
+ 4. The PHP Group may publish revised and/or new versions of the
+ license from time to time. Each version will be given a
+ distinguishing version number.
+ Once covered code has been published under a particular version
+ of the license, you may always continue to use it under the
+ terms of that version. You may also choose to use such covered
+ code under the terms of any subsequent version of the license
+ published by the PHP Group. No one other than the PHP Group has
+ the right to modify the terms applicable to covered code created
+ under this License.
+
+ 5. Redistributions of any form whatsoever must retain the following
+ acknowledgment:
+ "This product includes PHP, freely available from
+ http://www.php.net/".
+
+ 6. The software incorporates the Zend Engine, a product of Zend
+ Technologies, Ltd. ("Zend"). The Zend Engine is licensed to the
+ PHP Association (pursuant to a grant from Zend that can be
+ found at http://www.php.net/license/ZendGrant/) for
+ distribution to you under this license agreement, only as a
+ part of PHP. In the event that you separate the Zend Engine
+ (or any portion thereof) from the rest of the software, or
+ modify the Zend Engine, or any portion thereof, your use of the
+ separated or modified Zend Engine software shall not be governed
+ by this license, and instead shall be governed by the license
+ set forth at http://www.zend.com/license/ZendLicense/.
+
+
+
+THIS SOFTWARE IS PROVIDED BY THE PHP DEVELOPMENT TEAM ``AS IS'' AND
+ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE PHP
+DEVELOPMENT TEAM OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+OF THE POSSIBILITY OF SUCH DAMAGE.
+
+--------------------------------------------------------------------
+
+This software consists of voluntary contributions made by many
+individuals on behalf of the PHP Group.
+
+The PHP Group can be contacted via Email at group@php.net.
+
+For more information on the PHP Group and the PHP project,
+please see <http://www.php.net>.
diff --git a/extlib/PHP_License_3.01.txt b/extlib/PHP_License_3.01.txt
new file mode 100644
index 000000000..25662b2b9
--- /dev/null
+++ b/extlib/PHP_License_3.01.txt
@@ -0,0 +1,68 @@
+--------------------------------------------------------------------
+ The PHP License, version 3.01
+Copyright (c) 1999 - 2008 The PHP Group. All rights reserved.
+--------------------------------------------------------------------
+
+Redistribution and use in source and binary forms, with or without
+modification, is permitted provided that the following conditions
+are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+ 3. The name "PHP" must not be used to endorse or promote products
+ derived from this software without prior written permission. For
+ written permission, please contact group@php.net.
+
+ 4. Products derived from this software may not be called "PHP", nor
+ may "PHP" appear in their name, without prior written permission
+ from group@php.net. You may indicate that your software works in
+ conjunction with PHP by saying "Foo for PHP" instead of calling
+ it "PHP Foo" or "phpfoo"
+
+ 5. The PHP Group may publish revised and/or new versions of the
+ license from time to time. Each version will be given a
+ distinguishing version number.
+ Once covered code has been published under a particular version
+ of the license, you may always continue to use it under the terms
+ of that version. You may also choose to use such covered code
+ under the terms of any subsequent version of the license
+ published by the PHP Group. No one other than the PHP Group has
+ the right to modify the terms applicable to covered code created
+ under this License.
+
+ 6. Redistributions of any form whatsoever must retain the following
+ acknowledgment:
+ "This product includes PHP software, freely available from
+ <http://www.php.net/software/>".
+
+THIS SOFTWARE IS PROVIDED BY THE PHP DEVELOPMENT TEAM ``AS IS'' AND
+ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE PHP
+DEVELOPMENT TEAM OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+OF THE POSSIBILITY OF SUCH DAMAGE.
+
+--------------------------------------------------------------------
+
+This software consists of voluntary contributions made by many
+individuals on behalf of the PHP Group.
+
+The PHP Group can be contacted via Email at group@php.net.
+
+For more information on the PHP Group and the PHP project,
+please see <http://www.php.net>.
+
+PHP includes the Zend Engine, freely available at
+<http://www.zend.com>.
diff --git a/extlib/PHP_Markdown_License.text b/extlib/PHP_Markdown_License.text
new file mode 100644
index 000000000..fbde2c04d
--- /dev/null
+++ b/extlib/PHP_Markdown_License.text
@@ -0,0 +1,36 @@
+PHP Markdown
+Copyright (c) 2004-2008 Michel Fortin
+<http://www.michelf.com/>
+All rights reserved.
+
+Based on Markdown
+Copyright (c) 2003-2006 John Gruber
+<http://daringfireball.net/>
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name "Markdown" nor the names of its contributors may
+ be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+This software is provided by the copyright holders and contributors "as
+is" and any express or implied warranties, including, but not limited
+to, the implied warranties of merchantability and fitness for a
+particular purpose are disclaimed. In no event shall the copyright owner
+or contributors be liable for any direct, indirect, incidental, special,
+exemplary, or consequential damages (including, but not limited to,
+procurement of substitute goods or services; loss of use, data, or
+profits; or business interruption) however caused and on any theory of
+liability, whether in contract, strict liability, or tort (including
+negligence or otherwise) arising in any way out of the use of this
+software, even if advised of the possibility of such damage.
diff --git a/extlib/Validate.php b/extlib/Validate.php
new file mode 100644
index 000000000..4c05506b3
--- /dev/null
+++ b/extlib/Validate.php
@@ -0,0 +1,1051 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4: */
+// +----------------------------------------------------------------------+
+// | Copyright (c) 1997-2006 Pierre-Alain Joye,Tomas V.V.Cox, Amir Saied |
+// +----------------------------------------------------------------------+
+// | This source file is subject to the New BSD license, That is bundled |
+// | with this package in the file LICENSE, and is available through |
+// | the world-wide-web at |
+// | http://www.opensource.org/licenses/bsd-license.php |
+// | If you did not receive a copy of the new BSDlicense and are unable |
+// | to obtain it through the world-wide-web, please send a note to |
+// | pajoye@php.net so we can mail you a copy immediately. |
+// +----------------------------------------------------------------------+
+// | Author: Tomas V.V.Cox <cox@idecnet.com> |
+// | Pierre-Alain Joye <pajoye@php.net> |
+// | Amir Mohammad Saied <amir@php.net> |
+// +----------------------------------------------------------------------+
+//
+/**
+ * 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 <cox@idecnet.com>
+ * @author Pierre-Alain Joye <pajoye@php.net>
+ * @author Amir Mohammad Saied <amir@php.net>
+ * @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 <cox@idecnet.com>
+ * @author Pierre-Alain Joye <pajoye@php.net>
+ * @author Amir Mohammad Saied <amir@php.net>
+ * @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*<any CHAR except specials, SPACE and CTLs>
+ $atom = '[^][()<>@,;:\\".\s\000-\037\177-\377]+\s*';
+ // qtext = <any CHAR excepting <">, ; => 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 = <any CHAR excluding "[", ; => 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 .
+ ')?)*)((?<!\\\\)\((?:(?2)|.)*?(?<!\\\\)\))/';
+ }
+ // strip comments
+ $email = preg_replace($uncomment, '$1 ', $email);
+ return preg_match($address, $email);
+ }
+
+ /**
+ * Full TLD Validation function
+ *
+ * This function is used to make a much more proficient validation
+ * against all types of official domain names.
+ *
+ * @access protected
+ * @param string $email The email address to check.
+ * @param array $options The options for validation
+ * @return bool True if validating succeeds
+ */
+ function _fullTLDValidation($email, $options)
+ {
+ $validate = array();
+
+ switch ($options) {
+ /** 1 */
+ case VALIDATE_ITLD_EMAILS:
+ array_push($validate, 'itld');
+ break;
+
+ /** 2 */
+ case VALIDATE_GTLD_EMAILS:
+ array_push($validate, 'gtld');
+ break;
+
+ /** 3 */
+ case VALIDATE_ITLD_EMAILS | VALIDATE_GTLD_EMAILS:
+ array_push($validate, 'itld');
+ array_push($validate, 'gtld');
+ break;
+
+ /** 4 */
+ case VALIDATE_CCTLD_EMAILS:
+ array_push($validate, 'cctld');
+ break;
+
+ /** 5 */
+ case VALIDATE_CCTLD_EMAILS | VALIDATE_ITLD_EMAILS:
+ array_push($validate, 'cctld');
+ array_push($validate, 'itld');
+ break;
+
+ /** 6 */
+ case VALIDATE_CCTLD_EMAILS ^ VALIDATE_ITLD_EMAILS:
+ array_push($validate, 'cctld');
+ array_push($validate, 'itld');
+ break;
+
+ /** 7 - 8 */
+ case VALIDATE_CCTLD_EMAILS | VALIDATE_ITLD_EMAILS | VALIDATE_GTLD_EMAILS:
+ case VALIDATE_ALL_EMAILS:
+ array_push($validate, 'cctld');
+ array_push($validate, 'itld');
+ array_push($validate, 'gtld');
+ break;
+ }
+
+ /**
+ * Debugging still, not implemented but code is somewhat here.
+ */
+
+ $self = new Validate;
+
+ $toValidate = array();
+
+ foreach ($validate as $valid) {
+ $tmpVar = '_' . (string)$valid;
+ $toValidate[$valid] = $self->{$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:
+ * <code>
+ * $options = array('allowed_schemes' => array('http', 'https', 'ftp'))
+ * var_dump(Validate::uri('http://www.example.org', $options));
+ * </code>
+ *
+ * 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:
+ * "<class name><underscore><method name>"
+ * 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;
+ }
+}
+
diff --git a/extlib/XMPPHP/BOSH.php b/extlib/XMPPHP/BOSH.php
new file mode 100644
index 000000000..b147443d7
--- /dev/null
+++ b/extlib/XMPPHP/BOSH.php
@@ -0,0 +1,188 @@
+<?php
+/**
+ * XMPPHP: The PHP XMPP Library
+ * Copyright (C) 2008 Nathanael C. Fritz
+ * This file is part of SleekXMPP.
+ *
+ * XMPPHP is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * XMPPHP is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with XMPPHP; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * @category xmpphp
+ * @package XMPPHP
+ * @author Nathanael C. Fritz <JID: fritzy@netflint.net>
+ * @author Stephan Wentz <JID: stephan@jabber.wentz.it>
+ * @author Michael Garvin <JID: gar@netflint.net>
+ * @copyright 2008 Nathanael C. Fritz
+ */
+
+/** XMPPHP_XMLStream */
+require_once "XMPP.php";
+
+/**
+ * XMPPHP Main Class
+ *
+ * @category xmpphp
+ * @package XMPPHP
+ * @author Nathanael C. Fritz <JID: fritzy@netflint.net>
+ * @author Stephan Wentz <JID: stephan@jabber.wentz.it>
+ * @author Michael Garvin <JID: gar@netflint.net>
+ * @copyright 2008 Nathanael C. Fritz
+ * @version $Id$
+ */
+class XMPPHP_BOSH extends XMPPHP_XMPP {
+
+ protected $rid;
+ protected $sid;
+ protected $http_server;
+ protected $http_buffer = Array();
+ protected $session = false;
+
+ public function connect($server, $wait='1', $session=false) {
+ $this->http_server = $server;
+ $this->use_encryption = false;
+ $this->session = $session;
+
+ $this->rid = 3001;
+ $this->sid = null;
+ if($session)
+ {
+ $this->loadSession();
+ }
+ if(!$this->sid) {
+ $body = $this->__buildBody();
+ $body->addAttribute('hold','1');
+ $body->addAttribute('to', $this->host);
+ $body->addAttribute('route', "xmpp:{$this->host}:{$this->port}");
+ $body->addAttribute('secure','true');
+ $body->addAttribute('xmpp:version','1.6', 'urn:xmpp:xbosh');
+ $body->addAttribute('wait', strval($wait));
+ $body->addAttribute('ack','1');
+ $body->addAttribute('xmlns:xmpp','urn:xmpp:xbosh');
+ $buff = "<stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams'>";
+ xml_parse($this->parser, $buff, false);
+ $response = $this->__sendBody($body);
+ $rxml = new SimpleXMLElement($response);
+ $this->sid = $rxml['sid'];
+
+ } else {
+ $buff = "<stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams'>";
+ xml_parse($this->parser, $buff, false);
+ }
+ }
+
+ public function __sendBody($body=null, $recv=true) {
+ if(!$body) {
+ $body = $this->__buildBody();
+ }
+ $ch = curl_init($this->http_server);
+ curl_setopt($ch, CURLOPT_HEADER, 0);
+ curl_setopt($ch, CURLOPT_POST, 1);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $body->asXML());
+ curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
+ $header = array('Accept-Encoding: gzip, deflate','Content-Type: text/xml; charset=utf-8');
+ curl_setopt($ch, CURLOPT_HTTPHEADER, $header );
+ curl_setopt($ch, CURLOPT_VERBOSE, 0);
+ $output = '';
+ if($recv) {
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+ $output = curl_exec($ch);
+ $this->http_buffer[] = $output;
+ }
+ curl_close($ch);
+ return $output;
+ }
+
+ public function __buildBody($sub=null) {
+ $xml = new SimpleXMLElement("<body xmlns='http://jabber.org/protocol/httpbind' xmlns:xmpp='urn:xmpp:xbosh' />");
+ $xml->addAttribute('content', 'text/xml; charset=utf-8');
+ $xml->addAttribute('rid', $this->rid);
+ $this->rid += 1;
+ if($this->sid) $xml->addAttribute('sid', $this->sid);
+ #if($this->sid) $xml->addAttribute('xmlns', 'http://jabber.org/protocol/httpbind');
+ $xml->addAttribute('xml:lang', 'en');
+ if($sub) { // ok, so simplexml is lame
+ $p = dom_import_simplexml($xml);
+ $c = dom_import_simplexml($sub);
+ $cn = $p->ownerDocument->importNode($c, true);
+ $p->appendChild($cn);
+ $xml = simplexml_import_dom($p);
+ }
+ return $xml;
+ }
+
+ public function __process() {
+ if($this->http_buffer) {
+ $this->__parseBuffer();
+ } else {
+ $this->__sendBody();
+ $this->__parseBuffer();
+ }
+ }
+
+ public function __parseBuffer() {
+ while ($this->http_buffer) {
+ $idx = key($this->http_buffer);
+ $buffer = $this->http_buffer[$idx];
+ unset($this->http_buffer[$idx]);
+ if($buffer) {
+ $xml = new SimpleXMLElement($buffer);
+ $children = $xml->xpath('child::node()');
+ foreach ($children as $child) {
+ $buff = $child->asXML();
+ $this->log->log("RECV: $buff", XMPPHP_Log::LEVEL_VERBOSE);
+ xml_parse($this->parser, $buff, false);
+ }
+ }
+ }
+ }
+
+ public function send($msg) {
+ $this->log->log("SEND: $msg", XMPPHP_Log::LEVEL_VERBOSE);
+ $msg = new SimpleXMLElement($msg);
+ #$msg->addAttribute('xmlns', 'jabber:client');
+ $this->__sendBody($this->__buildBody($msg), true);
+ #$this->__parseBuffer();
+ }
+
+ public function reset() {
+ $this->xml_depth = 0;
+ unset($this->xmlobj);
+ $this->xmlobj = array();
+ $this->setupParser();
+ #$this->send($this->stream_start);
+ $body = $this->__buildBody();
+ $body->addAttribute('to', $this->host);
+ $body->addAttribute('xmpp:restart', 'true', 'urn:xmpp:xbosh');
+ $buff = "<stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams'>";
+ $response = $this->__sendBody($body);
+ $this->been_reset = true;
+ xml_parse($this->parser, $buff, false);
+ }
+
+ public function loadSession() {
+ if(isset($_SESSION['XMPPHP_BOSH_RID'])) $this->rid = $_SESSION['XMPPHP_BOSH_RID'];
+ if(isset($_SESSION['XMPPHP_BOSH_SID'])) $this->sid = $_SESSION['XMPPHP_BOSH_SID'];
+ if(isset($_SESSION['XMPPHP_BOSH_authed'])) $this->authed = $_SESSION['XMPPHP_BOSH_authed'];
+ if(isset($_SESSION['XMPPHP_BOSH_jid'])) $this->jid = $_SESSION['XMPPHP_BOSH_jid'];
+ if(isset($_SESSION['XMPPHP_BOSH_fulljid'])) $this->fulljid = $_SESSION['XMPPHP_BOSH_fulljid'];
+ }
+
+ public function saveSession() {
+ $_SESSION['XMPPHP_BOSH_RID'] = (string) $this->rid;
+ $_SESSION['XMPPHP_BOSH_SID'] = (string) $this->sid;
+ $_SESSION['XMPPHP_BOSH_authed'] = (boolean) $this->authed;
+ $_SESSION['XMPPHP_BOSH_jid'] = (string) $this->jid;
+ $_SESSION['XMPPHP_BOSH_fulljid'] = (string) $this->fulljid;
+ }
+}
diff --git a/extlib/XMPPHP/Exception.php b/extlib/XMPPHP/Exception.php
new file mode 100644
index 000000000..da59bc791
--- /dev/null
+++ b/extlib/XMPPHP/Exception.php
@@ -0,0 +1,41 @@
+<?php
+/**
+ * XMPPHP: The PHP XMPP Library
+ * Copyright (C) 2008 Nathanael C. Fritz
+ * This file is part of SleekXMPP.
+ *
+ * XMPPHP is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * XMPPHP is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with XMPPHP; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * @category xmpphp
+ * @package XMPPHP
+ * @author Nathanael C. Fritz <JID: fritzy@netflint.net>
+ * @author Stephan Wentz <JID: stephan@jabber.wentz.it>
+ * @author Michael Garvin <JID: gar@netflint.net>
+ * @copyright 2008 Nathanael C. Fritz
+ */
+
+/**
+ * XMPPHP Exception
+ *
+ * @category xmpphp
+ * @package XMPPHP
+ * @author Nathanael C. Fritz <JID: fritzy@netflint.net>
+ * @author Stephan Wentz <JID: stephan@jabber.wentz.it>
+ * @author Michael Garvin <JID: gar@netflint.net>
+ * @copyright 2008 Nathanael C. Fritz
+ * @version $Id$
+ */
+class XMPPHP_Exception extends Exception {
+}
diff --git a/extlib/XMPPHP/Log.php b/extlib/XMPPHP/Log.php
new file mode 100644
index 000000000..a9bce3d84
--- /dev/null
+++ b/extlib/XMPPHP/Log.php
@@ -0,0 +1,119 @@
+<?php
+/**
+ * XMPPHP: The PHP XMPP Library
+ * Copyright (C) 2008 Nathanael C. Fritz
+ * This file is part of SleekXMPP.
+ *
+ * XMPPHP is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * XMPPHP is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with XMPPHP; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * @category xmpphp
+ * @package XMPPHP
+ * @author Nathanael C. Fritz <JID: fritzy@netflint.net>
+ * @author Stephan Wentz <JID: stephan@jabber.wentz.it>
+ * @author Michael Garvin <JID: gar@netflint.net>
+ * @copyright 2008 Nathanael C. Fritz
+ */
+
+/**
+ * XMPPHP Log
+ *
+ * @package XMPPHP
+ * @author Nathanael C. Fritz <JID: fritzy@netflint.net>
+ * @author Stephan Wentz <JID: stephan@jabber.wentz.it>
+ * @author Michael Garvin <JID: gar@netflint.net>
+ * @copyright 2008 Nathanael C. Fritz
+ * @version $Id$
+ */
+class XMPPHP_Log {
+
+ const LEVEL_ERROR = 0;
+ const LEVEL_WARNING = 1;
+ const LEVEL_INFO = 2;
+ const LEVEL_DEBUG = 3;
+ const LEVEL_VERBOSE = 4;
+
+ /**
+ * @var array
+ */
+ protected $data = array();
+
+ /**
+ * @var array
+ */
+ protected $names = array('ERROR', 'WARNING', 'INFO', 'DEBUG', 'VERBOSE');
+
+ /**
+ * @var integer
+ */
+ protected $runlevel;
+
+ /**
+ * @var boolean
+ */
+ protected $printout;
+
+ /**
+ * Constructor
+ *
+ * @param boolean $printout
+ * @param string $runlevel
+ */
+ public function __construct($printout = false, $runlevel = self::LEVEL_INFO) {
+ $this->printout = (boolean)$printout;
+ $this->runlevel = (int)$runlevel;
+ }
+
+ /**
+ * Add a message to the log data array
+ * If printout in this instance is set to true, directly output the message
+ *
+ * @param string $msg
+ * @param integer $runlevel
+ */
+ public function log($msg, $runlevel = self::LEVEL_INFO) {
+ $time = time();
+ #$this->data[] = array($this->runlevel, $msg, $time);
+ if($this->printout and $runlevel <= $this->runlevel) {
+ $this->writeLine($msg, $runlevel, $time);
+ }
+ }
+
+ /**
+ * Output the complete log.
+ * Log will be cleared if $clear = true
+ *
+ * @param boolean $clear
+ * @param integer $runlevel
+ */
+ public function printout($clear = true, $runlevel = null) {
+ if($runlevel === null) {
+ $runlevel = $this->runlevel;
+ }
+ foreach($this->data as $data) {
+ if($runlevel <= $data[0]) {
+ $this->writeLine($data[1], $runlevel, $data[2]);
+ }
+ }
+ if($clear) {
+ $this->data = array();
+ }
+ }
+
+ protected function writeLine($msg, $runlevel, $time) {
+ //echo date('Y-m-d H:i:s', $time)." [".$this->names[$runlevel]."]: ".$msg."\n";
+ echo $time." [".$this->names[$runlevel]."]: ".$msg."\n";
+ flush();
+ }
+}
diff --git a/extlib/XMPPHP/Roster.php b/extlib/XMPPHP/Roster.php
new file mode 100644
index 000000000..2e459e2a2
--- /dev/null
+++ b/extlib/XMPPHP/Roster.php
@@ -0,0 +1,163 @@
+<?php
+/**
+ * XMPPHP: The PHP XMPP Library
+ * Copyright (C) 2008 Nathanael C. Fritz
+ * This file is part of SleekXMPP.
+ *
+ * XMPPHP is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * XMPPHP is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with XMPPHP; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * @category xmpphp
+ * @package XMPPHP
+ * @author Nathanael C. Fritz <JID: fritzy@netflint.net>
+ * @author Stephan Wentz <JID: stephan@jabber.wentz.it>
+ * @author Michael Garvin <JID: gar@netflint.net>
+ * @copyright 2008 Nathanael C. Fritz
+ */
+
+/**
+ * XMPPHP Roster Object
+ *
+ * @category xmpphp
+ * @package XMPPHP
+ * @author Nathanael C. Fritz <JID: fritzy@netflint.net>
+ * @author Stephan Wentz <JID: stephan@jabber.wentz.it>
+ * @author Michael Garvin <JID: gar@netflint.net>
+ * @copyright 2008 Nathanael C. Fritz
+ * @version $Id$
+ */
+
+class Roster {
+ /**
+ * Roster array, handles contacts and presence. Indexed by jid.
+ * Contains array with potentially two indexes 'contact' and 'presence'
+ * @var array
+ */
+ protected $roster_array = array();
+ /**
+ * Constructor
+ *
+ */
+ public function __construct($roster_array = array()) {
+ if ($this->verifyRoster($roster_array)) {
+ $this->roster_array = $roster_array; //Allow for prepopulation with existing roster
+ } else {
+ $this->roster_array = array();
+ }
+ }
+
+ /**
+ *
+ * Check that a given roster array is of a valid structure (empty is still valid)
+ *
+ * @param array $roster_array
+ */
+ protected function verifyRoster($roster_array) {
+ #TODO once we know *what* a valid roster array looks like
+ return True;
+ }
+
+ /**
+ *
+ * Add given contact to roster
+ *
+ * @param string $jid
+ * @param string $subscription
+ * @param string $name
+ * @param array $groups
+ */
+ public function addContact($jid, $subscription, $name='', $groups=array()) {
+ $contact = array('jid' => $jid, 'subscription' => $subscription, 'name' => $name, 'groups' => $groups);
+ if ($this->isContact($jid)) {
+ $this->roster_array[$jid]['contact'] = $contact;
+ } else {
+ $this->roster_array[$jid] = array('contact' => $contact);
+ }
+ }
+
+ /**
+ *
+ * Retrieve contact via jid
+ *
+ * @param string $jid
+ */
+ public function getContact($jid) {
+ if ($this->isContact($jid)) {
+ return $this->roster_array[$jid]['contact'];
+ }
+ }
+
+ /**
+ *
+ * Discover if a contact exists in the roster via jid
+ *
+ * @param string $jid
+ */
+ public function isContact($jid) {
+ return (array_key_exists($jid, $this->roster_array));
+ }
+
+ /**
+ *
+ * Set presence
+ *
+ * @param string $presence
+ * @param integer $priority
+ * @param string $show
+ * @param string $status
+ */
+ public function setPresence($presence, $priority, $show, $status) {
+ list($jid, $resource) = split("/", $presence);
+ if ($show != 'unavailable') {
+ if (!$this->isContact($jid)) {
+ $this->addContact($jid, 'not-in-roster');
+ }
+ $resource = $resource ? $resource : '';
+ $this->roster_array[$jid]['presence'][$resource] = array('priority' => $priority, 'show' => $show, 'status' => $status);
+ } else { //Nuke unavailable resources to save memory
+ unset($this->roster_array[$jid]['resource'][$resource]);
+ }
+ }
+
+ /*
+ *
+ * Return best presence for jid
+ *
+ * @param string $jid
+ */
+ public function getPresence($jid) {
+ $split = split("/", $jid);
+ $jid = $split[0];
+ if($this->isContact($jid)) {
+ $current = array('resource' => '', 'active' => '', 'priority' => -129, 'show' => '', 'status' => ''); //Priorities can only be -128 = 127
+ foreach($this->roster_array[$jid]['presence'] as $resource => $presence) {
+ //Highest available priority or just highest priority
+ if ($presence['priority'] > $current['priority'] and (($presence['show'] == "chat" or $presence['show'] == "available") or ($current['show'] != "chat" or $current['show'] != "available"))) {
+ $current = $presence;
+ $current['resource'] = $resource;
+ }
+ }
+ return $current;
+ }
+ }
+ /**
+ *
+ * Get roster
+ *
+ */
+ public function getRoster() {
+ return $this->roster_array;
+ }
+}
+?>
diff --git a/extlib/XMPPHP/XMLObj.php b/extlib/XMPPHP/XMLObj.php
new file mode 100644
index 000000000..0d3e21991
--- /dev/null
+++ b/extlib/XMPPHP/XMLObj.php
@@ -0,0 +1,158 @@
+<?php
+/**
+ * XMPPHP: The PHP XMPP Library
+ * Copyright (C) 2008 Nathanael C. Fritz
+ * This file is part of SleekXMPP.
+ *
+ * XMPPHP is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * XMPPHP is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with XMPPHP; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * @category xmpphp
+ * @package XMPPHP
+ * @author Nathanael C. Fritz <JID: fritzy@netflint.net>
+ * @author Stephan Wentz <JID: stephan@jabber.wentz.it>
+ * @author Michael Garvin <JID: gar@netflint.net>
+ * @copyright 2008 Nathanael C. Fritz
+ */
+
+/**
+ * XMPPHP XML Object
+ *
+ * @category xmpphp
+ * @package XMPPHP
+ * @author Nathanael C. Fritz <JID: fritzy@netflint.net>
+ * @author Stephan Wentz <JID: stephan@jabber.wentz.it>
+ * @author Michael Garvin <JID: gar@netflint.net>
+ * @copyright 2008 Nathanael C. Fritz
+ * @version $Id$
+ */
+class XMPPHP_XMLObj {
+ /**
+ * Tag name
+ *
+ * @var string
+ */
+ public $name;
+
+ /**
+ * Namespace
+ *
+ * @var string
+ */
+ public $ns;
+
+ /**
+ * Attributes
+ *
+ * @var array
+ */
+ public $attrs = array();
+
+ /**
+ * Subs?
+ *
+ * @var array
+ */
+ public $subs = array();
+
+ /**
+ * Node data
+ *
+ * @var string
+ */
+ public $data = '';
+
+ /**
+ * Constructor
+ *
+ * @param string $name
+ * @param string $ns
+ * @param array $attrs
+ * @param string $data
+ */
+ public function __construct($name, $ns = '', $attrs = array(), $data = '') {
+ $this->name = strtolower($name);
+ $this->ns = $ns;
+ if(is_array($attrs) && count($attrs)) {
+ foreach($attrs as $key => $value) {
+ $this->attrs[strtolower($key)] = $value;
+ }
+ }
+ $this->data = $data;
+ }
+
+ /**
+ * Dump this XML Object to output.
+ *
+ * @param integer $depth
+ */
+ public function printObj($depth = 0) {
+ print str_repeat("\t", $depth) . $this->name . " " . $this->ns . ' ' . $this->data;
+ print "\n";
+ foreach($this->subs as $sub) {
+ $sub->printObj($depth + 1);
+ }
+ }
+
+ /**
+ * Return this XML Object in xml notation
+ *
+ * @param string $str
+ */
+ public function toString($str = '') {
+ $str .= "<{$this->name} xmlns='{$this->ns}' ";
+ foreach($this->attrs as $key => $value) {
+ if($key != 'xmlns') {
+ $value = htmlspecialchars($value);
+ $str .= "$key='$value' ";
+ }
+ }
+ $str .= ">";
+ foreach($this->subs as $sub) {
+ $str .= $sub->toString();
+ }
+ $body = htmlspecialchars($this->data);
+ $str .= "$body</{$this->name}>";
+ return $str;
+ }
+
+ /**
+ * Has this XML Object the given sub?
+ *
+ * @param string $name
+ * @return boolean
+ */
+ public function hasSub($name, $ns = null) {
+ foreach($this->subs as $sub) {
+ if(($name == "*" or $sub->name == $name) and ($ns == null or $sub->ns == $ns)) return true;
+ }
+ return false;
+ }
+
+ /**
+ * Return a sub
+ *
+ * @param string $name
+ * @param string $attrs
+ * @param string $ns
+ */
+ public function sub($name, $attrs = null, $ns = null) {
+ #TODO attrs is ignored
+ foreach($this->subs as $sub) {
+ if($sub->name == $name and ($ns == null or $sub->ns == $ns)) {
+ return $sub;
+ }
+ }
+ }
+}
diff --git a/extlib/XMPPHP/XMLStream.php b/extlib/XMPPHP/XMLStream.php
new file mode 100644
index 000000000..0fcfea375
--- /dev/null
+++ b/extlib/XMPPHP/XMLStream.php
@@ -0,0 +1,763 @@
+<?php
+/**
+ * XMPPHP: The PHP XMPP Library
+ * Copyright (C) 2008 Nathanael C. Fritz
+ * This file is part of SleekXMPP.
+ *
+ * XMPPHP is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * XMPPHP is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with XMPPHP; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * @category xmpphp
+ * @package XMPPHP
+ * @author Nathanael C. Fritz <JID: fritzy@netflint.net>
+ * @author Stephan Wentz <JID: stephan@jabber.wentz.it>
+ * @author Michael Garvin <JID: gar@netflint.net>
+ * @copyright 2008 Nathanael C. Fritz
+ */
+
+/** XMPPHP_Exception */
+require_once 'Exception.php';
+
+/** XMPPHP_XMLObj */
+require_once 'XMLObj.php';
+
+/** XMPPHP_Log */
+require_once 'Log.php';
+
+/**
+ * XMPPHP XML Stream
+ *
+ * @category xmpphp
+ * @package XMPPHP
+ * @author Nathanael C. Fritz <JID: fritzy@netflint.net>
+ * @author Stephan Wentz <JID: stephan@jabber.wentz.it>
+ * @author Michael Garvin <JID: gar@netflint.net>
+ * @copyright 2008 Nathanael C. Fritz
+ * @version $Id$
+ */
+class XMPPHP_XMLStream {
+ /**
+ * @var resource
+ */
+ protected $socket;
+ /**
+ * @var resource
+ */
+ protected $parser;
+ /**
+ * @var string
+ */
+ protected $buffer;
+ /**
+ * @var integer
+ */
+ protected $xml_depth = 0;
+ /**
+ * @var string
+ */
+ protected $host;
+ /**
+ * @var integer
+ */
+ protected $port;
+ /**
+ * @var string
+ */
+ protected $stream_start = '<stream>';
+ /**
+ * @var string
+ */
+ protected $stream_end = '</stream>';
+ /**
+ * @var boolean
+ */
+ protected $disconnected = false;
+ /**
+ * @var boolean
+ */
+ protected $sent_disconnect = false;
+ /**
+ * @var array
+ */
+ protected $ns_map = array();
+ /**
+ * @var array
+ */
+ protected $current_ns = array();
+ /**
+ * @var array
+ */
+ protected $xmlobj = null;
+ /**
+ * @var array
+ */
+ protected $nshandlers = array();
+ /**
+ * @var array
+ */
+ protected $xpathhandlers = array();
+ /**
+ * @var array
+ */
+ protected $idhandlers = array();
+ /**
+ * @var array
+ */
+ protected $eventhandlers = array();
+ /**
+ * @var integer
+ */
+ protected $lastid = 0;
+ /**
+ * @var string
+ */
+ protected $default_ns;
+ /**
+ * @var string
+ */
+ protected $until = '';
+ /**
+ * @var string
+ */
+ protected $until_count = '';
+ /**
+ * @var array
+ */
+ protected $until_happened = false;
+ /**
+ * @var array
+ */
+ protected $until_payload = array();
+ /**
+ * @var XMPPHP_Log
+ */
+ protected $log;
+ /**
+ * @var boolean
+ */
+ protected $reconnect = true;
+ /**
+ * @var boolean
+ */
+ protected $been_reset = false;
+ /**
+ * @var boolean
+ */
+ protected $is_server;
+ /**
+ * @var float
+ */
+ protected $last_send = 0;
+ /**
+ * @var boolean
+ */
+ protected $use_ssl = false;
+ /**
+ * @var integer
+ */
+ protected $reconnectTimeout = 30;
+
+ /**
+ * Constructor
+ *
+ * @param string $host
+ * @param string $port
+ * @param boolean $printlog
+ * @param string $loglevel
+ * @param boolean $is_server
+ */
+ public function __construct($host = null, $port = null, $printlog = false, $loglevel = null, $is_server = false) {
+ $this->reconnect = !$is_server;
+ $this->is_server = $is_server;
+ $this->host = $host;
+ $this->port = $port;
+ $this->setupParser();
+ $this->log = new XMPPHP_Log($printlog, $loglevel);
+ }
+
+ /**
+ * Destructor
+ * Cleanup connection
+ */
+ public function __destruct() {
+ if(!$this->disconnected && $this->socket) {
+ $this->disconnect();
+ }
+ }
+
+ /**
+ * Return the log instance
+ *
+ * @return XMPPHP_Log
+ */
+ public function getLog() {
+ return $this->log;
+ }
+
+ /**
+ * Get next ID
+ *
+ * @return integer
+ */
+ public function getId() {
+ $this->lastid++;
+ return $this->lastid;
+ }
+
+ /**
+ * Set SSL
+ *
+ * @return integer
+ */
+ public function useSSL($use=true) {
+ $this->use_ssl = $use;
+ }
+
+ /**
+ * Add ID Handler
+ *
+ * @param integer $id
+ * @param string $pointer
+ * @param string $obj
+ */
+ public function addIdHandler($id, $pointer, $obj = null) {
+ $this->idhandlers[$id] = array($pointer, $obj);
+ }
+
+ /**
+ * Add Handler
+ *
+ * @param string $name
+ * @param string $ns
+ * @param string $pointer
+ * @param string $obj
+ * @param integer $depth
+ */
+ public function addHandler($name, $ns, $pointer, $obj = null, $depth = 1) {
+ #TODO deprication warning
+ $this->nshandlers[] = array($name,$ns,$pointer,$obj, $depth);
+ }
+
+ /**
+ * Add XPath Handler
+ *
+ * @param string $xpath
+ * @param string $pointer
+ * @param
+ */
+ public function addXPathHandler($xpath, $pointer, $obj = null) {
+ if (preg_match_all("/\(?{[^\}]+}\)?(\/?)[^\/]+/", $xpath, $regs)) {
+ $ns_tags = $regs[0];
+ } else {
+ $ns_tags = array($xpath);
+ }
+ foreach($ns_tags as $ns_tag) {
+ list($l, $r) = split("}", $ns_tag);
+ if ($r != null) {
+ $xpart = array(substr($l, 1), $r);
+ } else {
+ $xpart = array(null, $l);
+ }
+ $xpath_array[] = $xpart;
+ }
+ $this->xpathhandlers[] = array($xpath_array, $pointer, $obj);
+ }
+
+ /**
+ * Add Event Handler
+ *
+ * @param integer $id
+ * @param string $pointer
+ * @param string $obj
+ */
+ public function addEventHandler($name, $pointer, $obj) {
+ $this->eventhandlers[] = array($name, $pointer, $obj);
+ }
+
+ /**
+ * Connect to XMPP Host
+ *
+ * @param integer $timeout
+ * @param boolean $persistent
+ * @param boolean $sendinit
+ */
+ public function connect($timeout = 30, $persistent = false, $sendinit = true) {
+ $this->sent_disconnect = false;
+ $starttime = time();
+
+ do {
+ $this->disconnected = false;
+ $this->sent_disconnect = false;
+ if($persistent) {
+ $conflag = STREAM_CLIENT_CONNECT | STREAM_CLIENT_PERSISTENT;
+ } else {
+ $conflag = STREAM_CLIENT_CONNECT;
+ }
+ $conntype = 'tcp';
+ if($this->use_ssl) $conntype = 'ssl';
+ $this->log->log("Connecting to $conntype://{$this->host}:{$this->port}");
+ try {
+ $this->socket = @stream_socket_client("$conntype://{$this->host}:{$this->port}", $errno, $errstr, $timeout, $conflag);
+ } catch (Exception $e) {
+ throw new XMPPHP_Exception($e->getMessage());
+ }
+ if(!$this->socket) {
+ $this->log->log("Could not connect.", XMPPHP_Log::LEVEL_ERROR);
+ $this->disconnected = true;
+ # Take it easy for a few seconds
+ sleep(min($timeout, 5));
+ }
+ } while (!$this->socket && (time() - $starttime) < $timeout);
+
+ if ($this->socket) {
+ stream_set_blocking($this->socket, 1);
+ if($sendinit) $this->send($this->stream_start);
+ } else {
+ throw new XMPPHP_Exception("Could not connect before timeout.");
+ }
+ }
+
+ /**
+ * Reconnect XMPP Host
+ */
+ public function doReconnect() {
+ if(!$this->is_server) {
+ $this->log->log("Reconnecting ($this->reconnectTimeout)...", XMPPHP_Log::LEVEL_WARNING);
+ $this->connect($this->reconnectTimeout, false, false);
+ $this->reset();
+ $this->event('reconnect');
+ }
+ }
+
+ public function setReconnectTimeout($timeout) {
+ $this->reconnectTimeout = $timeout;
+ }
+
+ /**
+ * Disconnect from XMPP Host
+ */
+ public function disconnect() {
+ $this->log->log("Disconnecting...", XMPPHP_Log::LEVEL_VERBOSE);
+ if(false == (bool) $this->socket) {
+ return;
+ }
+ $this->reconnect = false;
+ $this->send($this->stream_end);
+ $this->sent_disconnect = true;
+ $this->processUntil('end_stream', 5);
+ $this->disconnected = true;
+ }
+
+ /**
+ * Are we are disconnected?
+ *
+ * @return boolean
+ */
+ public function isDisconnected() {
+ return $this->disconnected;
+ }
+
+ /**
+ * Core reading tool
+ * 0 -> only read if data is immediately ready
+ * NULL -> wait forever and ever
+ * integer -> process for this amount of time
+ */
+
+ private function __process($maximum=0) {
+
+ $remaining = $maximum;
+
+ do {
+ $starttime = (microtime(true) * 1000000);
+ $read = array($this->socket);
+ $write = array();
+ $except = array();
+ if (is_null($maximum)) {
+ $secs = NULL;
+ $usecs = NULL;
+ } else if ($maximum == 0) {
+ $secs = 0;
+ $usecs = 0;
+ } else {
+ $usecs = $remaining % 1000000;
+ $secs = floor(($remaining - $usecs) / 1000000);
+ }
+ $updated = @stream_select($read, $write, $except, $secs, $usecs);
+ if ($updated === false) {
+ $this->log->log("Error on stream_select()", XMPPHP_Log::LEVEL_VERBOSE);
+ if ($this->reconnect) {
+ $this->doReconnect();
+ } else {
+ fclose($this->socket);
+ $this->socket = NULL;
+ return false;
+ }
+ } else if ($updated > 0) {
+ # XXX: Is this big enough?
+ $buff = @fread($this->socket, 4096);
+ if(!$buff) {
+ if($this->reconnect) {
+ $this->doReconnect();
+ } else {
+ fclose($this->socket);
+ $this->socket = NULL;
+ return false;
+ }
+ }
+ $this->log->log("RECV: $buff", XMPPHP_Log::LEVEL_VERBOSE);
+ xml_parse($this->parser, $buff, false);
+ } else {
+ # $updated == 0 means no changes during timeout.
+ }
+ $endtime = (microtime(true)*1000000);
+ $time_past = $endtime - $starttime;
+ $remaining = $remaining - $time_past;
+ } while (is_null($maximum) || $remaining > 0);
+ return true;
+ }
+
+ /**
+ * Process
+ *
+ * @return string
+ */
+ public function process() {
+ $this->__process(NULL);
+ }
+
+ /**
+ * Process until a timeout occurs
+ *
+ * @param integer $timeout
+ * @return string
+ */
+ public function processTime($timeout=NULL) {
+ if (is_null($timeout)) {
+ return $this->__process(NULL);
+ } else {
+ return $this->__process($timeout * 1000000);
+ }
+ }
+
+ /**
+ * Process until a specified event or a timeout occurs
+ *
+ * @param string|array $event
+ * @param integer $timeout
+ * @return string
+ */
+ public function processUntil($event, $timeout=-1) {
+ $start = time();
+ if(!is_array($event)) $event = array($event);
+ $this->until[] = $event;
+ end($this->until);
+ $event_key = key($this->until);
+ reset($this->until);
+ $this->until_count[$event_key] = 0;
+ $updated = '';
+ while(!$this->disconnected and $this->until_count[$event_key] < 1 and (time() - $start < $timeout or $timeout == -1)) {
+ $this->__process();
+ }
+ if(array_key_exists($event_key, $this->until_payload)) {
+ $payload = $this->until_payload[$event_key];
+ unset($this->until_payload[$event_key]);
+ unset($this->until_count[$event_key]);
+ unset($this->until[$event_key]);
+ } else {
+ $payload = array();
+ }
+ return $payload;
+ }
+
+ /**
+ * Obsolete?
+ */
+ public function Xapply_socket($socket) {
+ $this->socket = $socket;
+ }
+
+ /**
+ * XML start callback
+ *
+ * @see xml_set_element_handler
+ *
+ * @param resource $parser
+ * @param string $name
+ */
+ public function startXML($parser, $name, $attr) {
+ if($this->been_reset) {
+ $this->been_reset = false;
+ $this->xml_depth = 0;
+ }
+ $this->xml_depth++;
+ if(array_key_exists('XMLNS', $attr)) {
+ $this->current_ns[$this->xml_depth] = $attr['XMLNS'];
+ } else {
+ $this->current_ns[$this->xml_depth] = $this->current_ns[$this->xml_depth - 1];
+ if(!$this->current_ns[$this->xml_depth]) $this->current_ns[$this->xml_depth] = $this->default_ns;
+ }
+ $ns = $this->current_ns[$this->xml_depth];
+ foreach($attr as $key => $value) {
+ if(strstr($key, ":")) {
+ $key = explode(':', $key);
+ $key = $key[1];
+ $this->ns_map[$key] = $value;
+ }
+ }
+ if(!strstr($name, ":") === false)
+ {
+ $name = explode(':', $name);
+ $ns = $this->ns_map[$name[0]];
+ $name = $name[1];
+ }
+ $obj = new XMPPHP_XMLObj($name, $ns, $attr);
+ if($this->xml_depth > 1) {
+ $this->xmlobj[$this->xml_depth - 1]->subs[] = $obj;
+ }
+ $this->xmlobj[$this->xml_depth] = $obj;
+ }
+
+ /**
+ * XML end callback
+ *
+ * @see xml_set_element_handler
+ *
+ * @param resource $parser
+ * @param string $name
+ */
+ public function endXML($parser, $name) {
+ #$this->log->log("Ending $name", XMPPHP_Log::LEVEL_DEBUG);
+ #print "$name\n";
+ if($this->been_reset) {
+ $this->been_reset = false;
+ $this->xml_depth = 0;
+ }
+ $this->xml_depth--;
+ if($this->xml_depth == 1) {
+ #clean-up old objects
+ #$found = false; #FIXME This didn't appear to be in use --Gar
+ foreach($this->xpathhandlers as $handler) {
+ if (is_array($this->xmlobj) && array_key_exists(2, $this->xmlobj)) {
+ $searchxml = $this->xmlobj[2];
+ $nstag = array_shift($handler[0]);
+ if (($nstag[0] == null or $searchxml->ns == $nstag[0]) and ($nstag[1] == "*" or $nstag[1] == $searchxml->name)) {
+ foreach($handler[0] as $nstag) {
+ if ($searchxml !== null and $searchxml->hasSub($nstag[1], $ns=$nstag[0])) {
+ $searchxml = $searchxml->sub($nstag[1], $ns=$nstag[0]);
+ } else {
+ $searchxml = null;
+ break;
+ }
+ }
+ if ($searchxml !== null) {
+ if($handler[2] === null) $handler[2] = $this;
+ $this->log->log("Calling {$handler[1]}", XMPPHP_Log::LEVEL_DEBUG);
+ $handler[2]->$handler[1]($this->xmlobj[2]);
+ }
+ }
+ }
+ }
+ foreach($this->nshandlers as $handler) {
+ if($handler[4] != 1 and array_key_exists(2, $this->xmlobj) and $this->xmlobj[2]->hasSub($handler[0])) {
+ $searchxml = $this->xmlobj[2]->sub($handler[0]);
+ } elseif(is_array($this->xmlobj) and array_key_exists(2, $this->xmlobj)) {
+ $searchxml = $this->xmlobj[2];
+ }
+ if($searchxml !== null and $searchxml->name == $handler[0] and ($searchxml->ns == $handler[1] or (!$handler[1] and $searchxml->ns == $this->default_ns))) {
+ if($handler[3] === null) $handler[3] = $this;
+ $this->log->log("Calling {$handler[2]}", XMPPHP_Log::LEVEL_DEBUG);
+ $handler[3]->$handler[2]($this->xmlobj[2]);
+ }
+ }
+ foreach($this->idhandlers as $id => $handler) {
+ if(array_key_exists('id', $this->xmlobj[2]->attrs) and $this->xmlobj[2]->attrs['id'] == $id) {
+ if($handler[1] === null) $handler[1] = $this;
+ $handler[1]->$handler[0]($this->xmlobj[2]);
+ #id handlers are only used once
+ unset($this->idhandlers[$id]);
+ break;
+ }
+ }
+ if(is_array($this->xmlobj)) {
+ $this->xmlobj = array_slice($this->xmlobj, 0, 1);
+ if(isset($this->xmlobj[0]) && $this->xmlobj[0] instanceof XMPPHP_XMLObj) {
+ $this->xmlobj[0]->subs = null;
+ }
+ }
+ unset($this->xmlobj[2]);
+ }
+ if($this->xml_depth == 0 and !$this->been_reset) {
+ if(!$this->disconnected) {
+ if(!$this->sent_disconnect) {
+ $this->send($this->stream_end);
+ }
+ $this->disconnected = true;
+ $this->sent_disconnect = true;
+ fclose($this->socket);
+ if($this->reconnect) {
+ $this->doReconnect();
+ }
+ }
+ $this->event('end_stream');
+ }
+ }
+
+ /**
+ * XML character callback
+ * @see xml_set_character_data_handler
+ *
+ * @param resource $parser
+ * @param string $data
+ */
+ public function charXML($parser, $data) {
+ if(array_key_exists($this->xml_depth, $this->xmlobj)) {
+ $this->xmlobj[$this->xml_depth]->data .= $data;
+ }
+ }
+
+ /**
+ * Event?
+ *
+ * @param string $name
+ * @param string $payload
+ */
+ public function event($name, $payload = null) {
+ $this->log->log("EVENT: $name", XMPPHP_Log::LEVEL_DEBUG);
+ foreach($this->eventhandlers as $handler) {
+ if($name == $handler[0]) {
+ if($handler[2] === null) {
+ $handler[2] = $this;
+ }
+ $handler[2]->$handler[1]($payload);
+ }
+ }
+ foreach($this->until as $key => $until) {
+ if(is_array($until)) {
+ if(in_array($name, $until)) {
+ $this->until_payload[$key][] = array($name, $payload);
+ if(!isset($this->until_count[$key])) {
+ $this->until_count[$key] = 0;
+ }
+ $this->until_count[$key] += 1;
+ #$this->until[$key] = false;
+ }
+ }
+ }
+ }
+
+ /**
+ * Read from socket
+ */
+ public function read() {
+ $buff = @fread($this->socket, 1024);
+ if(!$buff) {
+ if($this->reconnect) {
+ $this->doReconnect();
+ } else {
+ fclose($this->socket);
+ return false;
+ }
+ }
+ $this->log->log("RECV: $buff", XMPPHP_Log::LEVEL_VERBOSE);
+ xml_parse($this->parser, $buff, false);
+ }
+
+ /**
+ * Send to socket
+ *
+ * @param string $msg
+ */
+ public function send($msg, $timeout=NULL) {
+
+ if (is_null($timeout)) {
+ $secs = NULL;
+ $usecs = NULL;
+ } else if ($timeout == 0) {
+ $secs = 0;
+ $usecs = 0;
+ } else {
+ $maximum = $timeout * 1000000;
+ $usecs = $maximum % 1000000;
+ $secs = floor(($maximum - $usecs) / 1000000);
+ }
+
+ $read = array();
+ $write = array($this->socket);
+ $except = array();
+
+ $select = @stream_select($read, $write, $except, $secs, $usecs);
+
+ if($select === False) {
+ $this->log->log("ERROR sending message; reconnecting.");
+ $this->doReconnect();
+ # TODO: retry send here
+ return false;
+ } elseif ($select > 0) {
+ $this->log->log("Socket is ready; send it.", XMPPHP_Log::LEVEL_VERBOSE);
+ } else {
+ $this->log->log("Socket is not ready; break.", XMPPHP_Log::LEVEL_ERROR);
+ return false;
+ }
+
+ $sentbytes = @fwrite($this->socket, $msg);
+ $this->log->log("SENT: " . mb_substr($msg, 0, $sentbytes, '8bit'), XMPPHP_Log::LEVEL_VERBOSE);
+ if($sentbytes === FALSE) {
+ $this->log->log("ERROR sending message; reconnecting.", XMPPHP_Log::LEVEL_ERROR);
+ $this->doReconnect();
+ return false;
+ }
+ $this->log->log("Successfully sent $sentbytes bytes.", XMPPHP_Log::LEVEL_VERBOSE);
+ return $sentbytes;
+ }
+
+ public function time() {
+ list($usec, $sec) = explode(" ", microtime());
+ return (float)$sec + (float)$usec;
+ }
+
+ /**
+ * Reset connection
+ */
+ public function reset() {
+ $this->xml_depth = 0;
+ unset($this->xmlobj);
+ $this->xmlobj = array();
+ $this->setupParser();
+ if(!$this->is_server) {
+ $this->send($this->stream_start);
+ }
+ $this->been_reset = true;
+ }
+
+ /**
+ * Setup the XML parser
+ */
+ public function setupParser() {
+ $this->parser = xml_parser_create('UTF-8');
+ xml_parser_set_option($this->parser, XML_OPTION_SKIP_WHITE, 1);
+ xml_parser_set_option($this->parser, XML_OPTION_TARGET_ENCODING, 'UTF-8');
+ xml_set_object($this->parser, $this);
+ xml_set_element_handler($this->parser, 'startXML', 'endXML');
+ xml_set_character_data_handler($this->parser, 'charXML');
+ }
+
+ public function readyToProcess() {
+ $read = array($this->socket);
+ $write = array();
+ $except = array();
+ $updated = @stream_select($read, $write, $except, 0);
+ return (($updated !== false) && ($updated > 0));
+ }
+}
diff --git a/extlib/XMPPHP/XMPP.php b/extlib/XMPPHP/XMPP.php
new file mode 100644
index 000000000..73fbd2658
--- /dev/null
+++ b/extlib/XMPPHP/XMPP.php
@@ -0,0 +1,423 @@
+<?php
+/**
+ * XMPPHP: The PHP XMPP Library
+ * Copyright (C) 2008 Nathanael C. Fritz
+ * This file is part of SleekXMPP.
+ *
+ * XMPPHP is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * XMPPHP is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with XMPPHP; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * @category xmpphp
+ * @package XMPPHP
+ * @author Nathanael C. Fritz <JID: fritzy@netflint.net>
+ * @author Stephan Wentz <JID: stephan@jabber.wentz.it>
+ * @author Michael Garvin <JID: gar@netflint.net>
+ * @copyright 2008 Nathanael C. Fritz
+ */
+
+/** XMPPHP_XMLStream */
+require_once "XMLStream.php";
+require_once "Roster.php";
+
+/**
+ * XMPPHP Main Class
+ *
+ * @category xmpphp
+ * @package XMPPHP
+ * @author Nathanael C. Fritz <JID: fritzy@netflint.net>
+ * @author Stephan Wentz <JID: stephan@jabber.wentz.it>
+ * @author Michael Garvin <JID: gar@netflint.net>
+ * @copyright 2008 Nathanael C. Fritz
+ * @version $Id$
+ */
+class XMPPHP_XMPP extends XMPPHP_XMLStream {
+ /**
+ * @var string
+ */
+ public $server;
+
+ /**
+ * @var string
+ */
+ public $user;
+
+ /**
+ * @var string
+ */
+ protected $password;
+
+ /**
+ * @var string
+ */
+ protected $resource;
+
+ /**
+ * @var string
+ */
+ protected $fulljid;
+
+ /**
+ * @var string
+ */
+ protected $basejid;
+
+ /**
+ * @var boolean
+ */
+ protected $authed = false;
+ protected $session_started = false;
+
+ /**
+ * @var boolean
+ */
+ protected $auto_subscribe = false;
+
+ /**
+ * @var boolean
+ */
+ protected $use_encryption = true;
+
+ /**
+ * @var boolean
+ */
+ public $track_presence = true;
+
+ /**
+ * @var object
+ */
+ public $roster;
+
+ /**
+ * Constructor
+ *
+ * @param string $host
+ * @param integer $port
+ * @param string $user
+ * @param string $password
+ * @param string $resource
+ * @param string $server
+ * @param boolean $printlog
+ * @param string $loglevel
+ */
+ public function __construct($host, $port, $user, $password, $resource, $server = null, $printlog = false, $loglevel = null) {
+ parent::__construct($host, $port, $printlog, $loglevel);
+
+ $this->user = $user;
+ $this->password = $password;
+ $this->resource = $resource;
+ if(!$server) $server = $host;
+ $this->basejid = $this->user . '@' . $this->host;
+
+ $this->roster = new Roster();
+ $this->track_presence = true;
+
+ $this->stream_start = '<stream:stream to="' . $server . '" xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client" version="1.0">';
+ $this->stream_end = '</stream:stream>';
+ $this->default_ns = 'jabber:client';
+
+ $this->addXPathHandler('{http://etherx.jabber.org/streams}features', 'features_handler');
+ $this->addXPathHandler('{urn:ietf:params:xml:ns:xmpp-sasl}success', 'sasl_success_handler');
+ $this->addXPathHandler('{urn:ietf:params:xml:ns:xmpp-sasl}failure', 'sasl_failure_handler');
+ $this->addXPathHandler('{urn:ietf:params:xml:ns:xmpp-tls}proceed', 'tls_proceed_handler');
+ $this->addXPathHandler('{jabber:client}message', 'message_handler');
+ $this->addXPathHandler('{jabber:client}presence', 'presence_handler');
+ $this->addXPathHandler('iq/{jabber:iq:roster}query', 'roster_iq_handler');
+ }
+
+ /**
+ * Turn encryption on/ff
+ *
+ * @param boolean $useEncryption
+ */
+ public function useEncryption($useEncryption = true) {
+ $this->use_encryption = $useEncryption;
+ }
+
+ /**
+ * Turn on auto-authorization of subscription requests.
+ *
+ * @param boolean $autoSubscribe
+ */
+ public function autoSubscribe($autoSubscribe = true) {
+ $this->auto_subscribe = $autoSubscribe;
+ }
+
+ /**
+ * Send XMPP Message
+ *
+ * @param string $to
+ * @param string $body
+ * @param string $type
+ * @param string $subject
+ */
+ public function message($to, $body, $type = 'chat', $subject = null, $payload = null) {
+ if(is_null($type))
+ {
+ $type = 'chat';
+ }
+
+ $to = htmlspecialchars($to);
+ $body = htmlspecialchars($body);
+ $subject = htmlspecialchars($subject);
+
+ $out = "<message from='{$this->fulljid}' to='$to' type='$type'>";
+ if($subject) $out .= "<subject>$subject</subject>";
+ $out .= "<body>$body</body>";
+ if($payload) $out .= $payload;
+ $out .= "</message>";
+
+ $this->send($out);
+ }
+
+ /**
+ * Set Presence
+ *
+ * @param string $status
+ * @param string $show
+ * @param string $to
+ */
+ public function presence($status = null, $show = 'available', $to = null, $type='available', $priority=0) {
+ if($type == 'available') $type = '';
+ $to = htmlspecialchars($to);
+ $status = htmlspecialchars($status);
+ if($show == 'unavailable') $type = 'unavailable';
+
+ $out = "<presence";
+ if($to) $out .= " to='$to'";
+ if($type) $out .= " type='$type'";
+ if($show == 'available' and !$status) {
+ $out .= "/>";
+ } else {
+ $out .= ">";
+ if($show != 'available') $out .= "<show>$show</show>";
+ if($status) $out .= "<status>$status</status>";
+ if($priority) $out .= "<priority>$priority</priority>";
+ $out .= "</presence>";
+ }
+
+ $this->send($out);
+ }
+
+ /**
+ * Message handler
+ *
+ * @param string $xml
+ */
+ public function message_handler($xml) {
+ if(isset($xml->attrs['type'])) {
+ $payload['type'] = $xml->attrs['type'];
+ } else {
+ $payload['type'] = 'chat';
+ }
+ $payload['from'] = $xml->attrs['from'];
+ $payload['body'] = $xml->sub('body')->data;
+ $payload['xml'] = $xml;
+ $this->log->log("Message: {$xml->sub('body')->data}", XMPPHP_Log::LEVEL_DEBUG);
+ $this->event('message', $payload);
+ }
+
+ /**
+ * Presence handler
+ *
+ * @param string $xml
+ */
+ public function presence_handler($xml) {
+ $payload['type'] = (isset($xml->attrs['type'])) ? $xml->attrs['type'] : 'available';
+ $payload['show'] = (isset($xml->sub('show')->data)) ? $xml->sub('show')->data : $payload['type'];
+ $payload['from'] = $xml->attrs['from'];
+ $payload['status'] = (isset($xml->sub('status')->data)) ? $xml->sub('status')->data : '';
+ $payload['priority'] = (isset($xml->sub('priority')->data)) ? intval($xml->sub('priority')->data) : 0;
+ $payload['xml'] = $xml;
+ if($this->track_presence) {
+ $this->roster->setPresence($payload['from'], $payload['priority'], $payload['show'], $payload['status']);
+ }
+ $this->log->log("Presence: {$payload['from']} [{$payload['show']}] {$payload['status']}", XMPPHP_Log::LEVEL_DEBUG);
+ if(array_key_exists('type', $xml->attrs) and $xml->attrs['type'] == 'subscribe') {
+ if($this->auto_subscribe) {
+ $this->send("<presence type='subscribed' to='{$xml->attrs['from']}' from='{$this->fulljid}' />");
+ $this->send("<presence type='subscribe' to='{$xml->attrs['from']}' from='{$this->fulljid}' />");
+ }
+ $this->event('subscription_requested', $payload);
+ } elseif(array_key_exists('type', $xml->attrs) and $xml->attrs['type'] == 'subscribed') {
+ $this->event('subscription_accepted', $payload);
+ } else {
+ $this->event('presence', $payload);
+ }
+ }
+
+ /**
+ * Features handler
+ *
+ * @param string $xml
+ */
+ protected function features_handler($xml) {
+ if($xml->hasSub('starttls') and $this->use_encryption) {
+ $this->send("<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'><required /></starttls>");
+ } elseif($xml->hasSub('bind') and $this->authed) {
+ $id = $this->getId();
+ $this->addIdHandler($id, 'resource_bind_handler');
+ $this->send("<iq xmlns=\"jabber:client\" type=\"set\" id=\"$id\"><bind xmlns=\"urn:ietf:params:xml:ns:xmpp-bind\"><resource>{$this->resource}</resource></bind></iq>");
+ } else {
+ $this->log->log("Attempting Auth...");
+ if ($this->password) {
+ $this->send("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>" . base64_encode("\x00" . $this->user . "\x00" . $this->password) . "</auth>");
+ } else {
+ $this->send("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='ANONYMOUS'/>");
+ }
+ }
+ }
+
+ /**
+ * SASL success handler
+ *
+ * @param string $xml
+ */
+ protected function sasl_success_handler($xml) {
+ $this->log->log("Auth success!");
+ $this->authed = true;
+ $this->reset();
+ }
+
+ /**
+ * SASL feature handler
+ *
+ * @param string $xml
+ */
+ protected function sasl_failure_handler($xml) {
+ $this->log->log("Auth failed!", XMPPHP_Log::LEVEL_ERROR);
+ $this->disconnect();
+
+ throw new XMPPHP_Exception('Auth failed!');
+ }
+
+ /**
+ * Resource bind handler
+ *
+ * @param string $xml
+ */
+ protected function resource_bind_handler($xml) {
+ if($xml->attrs['type'] == 'result') {
+ $this->log->log("Bound to " . $xml->sub('bind')->sub('jid')->data);
+ $this->fulljid = $xml->sub('bind')->sub('jid')->data;
+ $jidarray = explode('/',$this->fulljid);
+ $this->jid = $jidarray[0];
+ }
+ $id = $this->getId();
+ $this->addIdHandler($id, 'session_start_handler');
+ $this->send("<iq xmlns='jabber:client' type='set' id='$id'><session xmlns='urn:ietf:params:xml:ns:xmpp-session' /></iq>");
+ }
+
+ /**
+ * Retrieves the roster
+ *
+ */
+ public function getRoster() {
+ $id = $this->getID();
+ $this->send("<iq xmlns='jabber:client' type='get' id='$id'><query xmlns='jabber:iq:roster' /></iq>");
+ }
+
+ /**
+ * Roster iq handler
+ * Gets all packets matching XPath "iq/{jabber:iq:roster}query'
+ *
+ * @param string $xml
+ */
+ protected function roster_iq_handler($xml) {
+ $status = "result";
+ $xmlroster = $xml->sub('query');
+ foreach($xmlroster->subs as $item) {
+ $groups = array();
+ if ($item->name == 'item') {
+ $jid = $item->attrs['jid']; //REQUIRED
+ $name = $item->attrs['name']; //MAY
+ $subscription = $item->attrs['subscription'];
+ foreach($item->subs as $subitem) {
+ if ($subitem->name == 'group') {
+ $groups[] = $subitem->data;
+ }
+ }
+ $contacts[] = array($jid, $subscription, $name, $groups); //Store for action if no errors happen
+ } else {
+ $status = "error";
+ }
+ }
+ if ($status == "result") { //No errors, add contacts
+ foreach($contacts as $contact) {
+ $this->roster->addContact($contact[0], $contact[1], $contact[2], $contact[3]);
+ }
+ }
+ if ($xml->attrs['type'] == 'set') {
+ $this->send("<iq type=\"reply\" id=\"{$xml->attrs['id']}\" to=\"{$xml->attrs['from']}\" />");
+ }
+ }
+
+ /**
+ * Session start handler
+ *
+ * @param string $xml
+ */
+ protected function session_start_handler($xml) {
+ $this->log->log("Session started");
+ $this->session_started = true;
+ $this->event('session_start');
+ }
+
+ /**
+ * TLS proceed handler
+ *
+ * @param string $xml
+ */
+ protected function tls_proceed_handler($xml) {
+ $this->log->log("Starting TLS encryption");
+ stream_socket_enable_crypto($this->socket, true, STREAM_CRYPTO_METHOD_SSLv23_CLIENT);
+ $this->reset();
+ }
+
+ /**
+ * Retrieves the vcard
+ *
+ */
+ public function getVCard($jid = Null) {
+ $id = $this->getID();
+ $this->addIdHandler($id, 'vcard_get_handler');
+ if($jid) {
+ $this->send("<iq type='get' id='$id' to='$jid'><vCard xmlns='vcard-temp' /></iq>");
+ } else {
+ $this->send("<iq type='get' id='$id'><vCard xmlns='vcard-temp' /></iq>");
+ }
+ }
+
+ /**
+ * VCard retrieval handler
+ *
+ * @param XML Object $xml
+ */
+ protected function vcard_get_handler($xml) {
+ $vcard_array = array();
+ $vcard = $xml->sub('vcard');
+ // go through all of the sub elements and add them to the vcard array
+ foreach ($vcard->subs as $sub) {
+ if ($sub->subs) {
+ $vcard_array[$sub->name] = array();
+ foreach ($sub->subs as $sub_child) {
+ $vcard_array[$sub->name][$sub_child->name] = $sub_child->data;
+ }
+ } else {
+ $vcard_array[$sub->name] = $sub->data;
+ }
+ }
+ $vcard_array['from'] = $xml->attrs['from'];
+ $this->event('vcard', $vcard_array);
+ }
+}
diff --git a/extlib/XMPPHP/XMPP_Old.php b/extlib/XMPPHP/XMPP_Old.php
new file mode 100644
index 000000000..43f56b154
--- /dev/null
+++ b/extlib/XMPPHP/XMPP_Old.php
@@ -0,0 +1,114 @@
+<?php
+/**
+ * XMPPHP: The PHP XMPP Library
+ * Copyright (C) 2008 Nathanael C. Fritz
+ * This file is part of SleekXMPP.
+ *
+ * XMPPHP is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * XMPPHP is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with XMPPHP; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * @category xmpphp
+ * @package XMPPHP
+ * @author Nathanael C. Fritz <JID: fritzy@netflint.net>
+ * @author Stephan Wentz <JID: stephan@jabber.wentz.it>
+ * @author Michael Garvin <JID: gar@netflint.net>
+ * @copyright 2008 Nathanael C. Fritz
+ */
+
+/** XMPPHP_XMPP
+ *
+ * This file is unnecessary unless you need to connect to older, non-XMPP-compliant servers like Dreamhost's.
+ * In this case, use instead of XMPPHP_XMPP, otherwise feel free to delete it.
+ * The old Jabber protocol wasn't standardized, so use at your own risk.
+ *
+ */
+require_once "XMPP.php";
+
+ class XMPPHP_XMPPOld extends XMPPHP_XMPP {
+ /**
+ *
+ * @var string
+ */
+ protected $session_id;
+
+ public function __construct($host, $port, $user, $password, $resource, $server = null, $printlog = false, $loglevel = null) {
+ parent::__construct($host, $port, $user, $password, $resource, $server, $printlog, $loglevel);
+ if(!$server) $server = $host;
+ $this->stream_start = '<stream:stream to="' . $server . '" xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client">';
+ $this->fulljid = "{$user}@{$server}/{$resource}";
+ }
+
+ /**
+ * Override XMLStream's startXML
+ *
+ * @param parser $parser
+ * @param string $name
+ * @param array $attr
+ */
+ public function startXML($parser, $name, $attr) {
+ if($this->xml_depth == 0) {
+ $this->session_id = $attr['ID'];
+ $this->authenticate();
+ }
+ parent::startXML($parser, $name, $attr);
+ }
+
+ /**
+ * Send Authenticate Info Request
+ *
+ */
+ public function authenticate() {
+ $id = $this->getId();
+ $this->addidhandler($id, 'authfieldshandler');
+ $this->send("<iq type='get' id='$id'><query xmlns='jabber:iq:auth'><username>{$this->user}</username></query></iq>");
+ }
+
+ /**
+ * Retrieve auth fields and send auth attempt
+ *
+ * @param XMLObj $xml
+ */
+ public function authFieldsHandler($xml) {
+ $id = $this->getId();
+ $this->addidhandler($id, 'oldAuthResultHandler');
+ if($xml->sub('query')->hasSub('digest')) {
+ $hash = sha1($this->session_id . $this->password);
+ print "{$this->session_id} {$this->password}\n";
+ $out = "<iq type='set' id='$id'><query xmlns='jabber:iq:auth'><username>{$this->user}</username><digest>{$hash}</digest><resource>{$this->resource}</resource></query></iq>";
+ } else {
+ $out = "<iq type='set' id='$id'><query xmlns='jabber:iq:auth'><username>{$this->user}</username><password>{$this->password}</password><resource>{$this->resource}</resource></query></iq>";
+ }
+ $this->send($out);
+
+ }
+
+ /**
+ * Determine authenticated or failure
+ *
+ * @param XMLObj $xml
+ */
+ public function oldAuthResultHandler($xml) {
+ if($xml->attrs['type'] != 'result') {
+ $this->log->log("Auth failed!", XMPPHP_Log::LEVEL_ERROR);
+ $this->disconnect();
+ throw new XMPPHP_Exception('Auth failed!');
+ } else {
+ $this->log->log("Session started");
+ $this->event('session_start');
+ }
+ }
+ }
+
+
+?>
diff --git a/extlib/facebook/facebook.php b/extlib/facebook/facebook.php
new file mode 100644
index 000000000..35de6be50
--- /dev/null
+++ b/extlib/facebook/facebook.php
@@ -0,0 +1,499 @@
+<?php
+// Copyright 2004-2008 Facebook. All Rights Reserved.
+//
+// +---------------------------------------------------------------------------+
+// | Facebook Platform PHP5 client |
+// +---------------------------------------------------------------------------+
+// | Copyright (c) 2007 Facebook, Inc. |
+// | All rights reserved. |
+// | |
+// | Redistribution and use in source and binary forms, with or without |
+// | modification, are permitted provided that the following conditions |
+// | are met: |
+// | |
+// | 1. Redistributions of source code must retain the above copyright |
+// | notice, this list of conditions and the following disclaimer. |
+// | 2. Redistributions in binary form must reproduce the above copyright |
+// | notice, this list of conditions and the following disclaimer in the |
+// | documentation and/or other materials provided with the distribution. |
+// | |
+// | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR |
+// | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
+// | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. |
+// | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, |
+// | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
+// | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
+// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
+// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
+// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF |
+// | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
+// +---------------------------------------------------------------------------+
+// | For help with this library, contact developers-help@facebook.com |
+// +---------------------------------------------------------------------------+
+//
+include_once 'facebookapi_php5_restlib.php';
+
+define('FACEBOOK_API_VALIDATION_ERROR', 1);
+class Facebook {
+ public $api_client;
+
+ public $api_key;
+ public $secret;
+ public $generate_session_secret;
+ public $session_expires;
+
+ public $fb_params;
+ public $user;
+ public $profile_user;
+ public $canvas_user;
+ protected $base_domain;
+ /*
+ * Create a Facebook client like this:
+ *
+ * $fb = new Facebook(API_KEY, SECRET);
+ *
+ * This will automatically pull in any parameters, validate them against the
+ * session signature, and chuck them in the public $fb_params member variable.
+ *
+ * @param api_key your Developer API key
+ * @param secret your Developer API secret
+ * @param generate_session_secret whether to automatically generate a session
+ * if the user doesn't have one, but
+ * there is an auth token present in the url,
+ */
+ public function __construct($api_key, $secret, $generate_session_secret=false) {
+ $this->api_key = $api_key;
+ $this->secret = $secret;
+ $this->generate_session_secret = $generate_session_secret;
+ $this->api_client = new FacebookRestClient($api_key, $secret, null);
+ $this->validate_fb_params();
+
+ // Set the default user id for methods that allow the caller to
+ // pass an explicit uid instead of using a session key.
+ $defaultUser = null;
+ if ($this->user) {
+ $defaultUser = $this->user;
+ } else if ($this->profile_user) {
+ $defaultUser = $this->profile_user;
+ } else if ($this->canvas_user) {
+ $defaultUser = $this->canvas_user;
+ }
+
+ $this->api_client->set_user($defaultUser);
+
+
+ if (isset($this->fb_params['friends'])) {
+ $this->api_client->friends_list = explode(',', $this->fb_params['friends']);
+ }
+ if (isset($this->fb_params['added'])) {
+ $this->api_client->added = $this->fb_params['added'];
+ }
+ if (isset($this->fb_params['canvas_user'])) {
+ $this->api_client->canvas_user = $this->fb_params['canvas_user'];
+ }
+ }
+
+ /*
+ * Validates that the parameters passed in were sent from Facebook. It does so
+ * by validating that the signature matches one that could only be generated
+ * by using your application's secret key.
+ *
+ * Facebook-provided parameters will come from $_POST, $_GET, or $_COOKIE,
+ * in that order. $_POST and $_GET are always more up-to-date than cookies,
+ * so we prefer those if they are available.
+ *
+ * For nitty-gritty details of when each of these is used, check out
+ * http://wiki.developers.facebook.com/index.php/Verifying_The_Signature
+ *
+ * @param bool resolve_auth_token convert an auth token into a session
+ */
+ public function validate_fb_params($resolve_auth_token=true) {
+ $this->fb_params = $this->get_valid_fb_params($_POST, 48*3600, 'fb_sig');
+
+ // note that with preload FQL, it's possible to receive POST params in
+ // addition to GET, so use a different prefix to differentiate them
+ if (!$this->fb_params) {
+ $fb_params = $this->get_valid_fb_params($_GET, 48*3600, 'fb_sig');
+ $fb_post_params = $this->get_valid_fb_params($_POST, 48*3600, 'fb_post_sig');
+ $this->fb_params = array_merge($fb_params, $fb_post_params);
+ }
+
+ // Okay, something came in via POST or GET
+ if ($this->fb_params) {
+ $user = isset($this->fb_params['user']) ?
+ $this->fb_params['user'] : null;
+ $this->profile_user = isset($this->fb_params['profile_user']) ?
+ $this->fb_params['profile_user'] : null;
+ $this->canvas_user = isset($this->fb_params['canvas_user']) ?
+ $this->fb_params['canvas_user'] : null;
+ $this->base_domain = isset($this->fb_params['base_domain']) ?
+ $this->fb_params['base_domain'] : null;
+
+ if (isset($this->fb_params['session_key'])) {
+ $session_key = $this->fb_params['session_key'];
+ } else if (isset($this->fb_params['profile_session_key'])) {
+ $session_key = $this->fb_params['profile_session_key'];
+ } else {
+ $session_key = null;
+ }
+ $expires = isset($this->fb_params['expires']) ?
+ $this->fb_params['expires'] : null;
+ $this->set_user($user,
+ $session_key,
+ $expires);
+ }
+ // if no Facebook parameters were found in the GET or POST variables,
+ // then fall back to cookies, which may have cached user information
+ // Cookies are also used to receive session data via the Javascript API
+ else if ($cookies =
+ $this->get_valid_fb_params($_COOKIE, null, $this->api_key)) {
+
+ $base_domain_cookie = 'base_domain_' . $this->api_key;
+ if (isset($_COOKIE[$base_domain_cookie])) {
+ $this->base_domain = $_COOKIE[$base_domain_cookie];
+ }
+
+ // use $api_key . '_' as a prefix for the cookies in case there are
+ // multiple facebook clients on the same domain.
+ $expires = isset($cookies['expires']) ? $cookies['expires'] : null;
+ $this->set_user($cookies['user'],
+ $cookies['session_key'],
+ $expires);
+ }
+ // finally, if we received no parameters, but the 'auth_token' GET var
+ // is present, then we are in the middle of auth handshake,
+ // so go ahead and create the session
+ else if ($resolve_auth_token && isset($_GET['auth_token']) &&
+ $session = $this->do_get_session($_GET['auth_token'])) {
+ if ($this->generate_session_secret &&
+ !empty($session['secret'])) {
+ $session_secret = $session['secret'];
+ }
+
+ if (isset($session['base_domain'])) {
+ $this->base_domain = $session['base_domain'];
+ }
+
+ $this->set_user($session['uid'],
+ $session['session_key'],
+ $session['expires'],
+ isset($session_secret) ? $session_secret : null);
+ }
+
+ return !empty($this->fb_params);
+ }
+
+ // Store a temporary session secret for the current session
+ // for use with the JS client library
+ public function promote_session() {
+ try {
+ $session_secret = $this->api_client->auth_promoteSession();
+ if (!$this->in_fb_canvas()) {
+ $this->set_cookies($this->user, $this->api_client->session_key, $this->session_expires, $session_secret);
+ }
+ return $session_secret;
+ } catch (FacebookRestClientException $e) {
+ // API_EC_PARAM means we don't have a logged in user, otherwise who
+ // knows what it means, so just throw it.
+ if ($e->getCode() != FacebookAPIErrorCodes::API_EC_PARAM) {
+ throw $e;
+ }
+ }
+ }
+
+ public function do_get_session($auth_token) {
+ try {
+ return $this->api_client->auth_getSession($auth_token, $this->generate_session_secret);
+ } catch (FacebookRestClientException $e) {
+ // API_EC_PARAM means we don't have a logged in user, otherwise who
+ // knows what it means, so just throw it.
+ if ($e->getCode() != FacebookAPIErrorCodes::API_EC_PARAM) {
+ throw $e;
+ }
+ }
+ }
+
+ // Invalidate the session currently being used, and clear any state associated with it
+ public function expire_session() {
+ if ($this->api_client->auth_expireSession()) {
+ if (!$this->in_fb_canvas() && isset($_COOKIE[$this->api_key . '_user'])) {
+ $cookies = array('user', 'session_key', 'expires', 'ss');
+ foreach ($cookies as $name) {
+ setcookie($this->api_key . '_' . $name, false, time() - 3600);
+ unset($_COOKIE[$this->api_key . '_' . $name]);
+ }
+ setcookie($this->api_key, false, time() - 3600);
+ unset($_COOKIE[$this->api_key]);
+ }
+
+ // now, clear the rest of the stored state
+ $this->user = 0;
+ $this->api_client->session_key = 0;
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public function redirect($url) {
+ if ($this->in_fb_canvas()) {
+ echo '<fb:redirect url="' . $url . '"/>';
+ } else if (preg_match('/^https?:\/\/([^\/]*\.)?facebook\.com(:\d+)?/i', $url)) {
+ // make sure facebook.com url's load in the full frame so that we don't
+ // get a frame within a frame.
+ echo "<script type=\"text/javascript\">\ntop.location.href = \"$url\";\n</script>";
+ } else {
+ header('Location: ' . $url);
+ }
+ exit;
+ }
+
+ public function in_frame() {
+ return isset($this->fb_params['in_canvas']) || isset($this->fb_params['in_iframe']);
+ }
+ public function in_fb_canvas() {
+ return isset($this->fb_params['in_canvas']);
+ }
+
+ public function get_loggedin_user() {
+ return $this->user;
+ }
+
+ public function get_canvas_user() {
+ return $this->canvas_user;
+ }
+
+ public function get_profile_user() {
+ return $this->profile_user;
+ }
+
+ public static function current_url() {
+ return 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
+ }
+
+ // require_add and require_install have been removed.
+ // see http://developer.facebook.com/news.php?blog=1&story=116 for more details
+ public function require_login() {
+ if ($user = $this->get_loggedin_user()) {
+ return $user;
+ }
+ $this->redirect($this->get_login_url(self::current_url(), $this->in_frame()));
+ }
+
+ public function require_frame() {
+ if (!$this->in_frame()) {
+ $this->redirect($this->get_login_url(self::current_url(), true));
+ }
+ }
+
+ public static function get_facebook_url($subdomain='www') {
+ return 'http://' . $subdomain . '.facebook.com';
+ }
+
+ public function get_install_url($next=null) {
+ // this was renamed, keeping for compatibility's sake
+ return $this->get_add_url($next);
+ }
+
+ public function get_add_url($next=null) {
+ return self::get_facebook_url().'/add.php?api_key='.$this->api_key .
+ ($next ? '&next=' . urlencode($next) : '');
+ }
+
+ public function get_login_url($next, $canvas) {
+ return self::get_facebook_url().'/login.php?v=1.0&api_key=' . $this->api_key .
+ ($next ? '&next=' . urlencode($next) : '') .
+ ($canvas ? '&canvas' : '');
+ }
+
+ public function set_user($user, $session_key, $expires=null, $session_secret=null) {
+ if (!$this->in_fb_canvas() && (!isset($_COOKIE[$this->api_key . '_user'])
+ || $_COOKIE[$this->api_key . '_user'] != $user)) {
+ $this->set_cookies($user, $session_key, $expires, $session_secret);
+ }
+ $this->user = $user;
+ $this->api_client->session_key = $session_key;
+ $this->session_expires = $expires;
+ }
+
+ public function set_cookies($user, $session_key, $expires=null, $session_secret=null) {
+ $cookies = array();
+ $cookies['user'] = $user;
+ $cookies['session_key'] = $session_key;
+ if ($expires != null) {
+ $cookies['expires'] = $expires;
+ }
+ if ($session_secret != null) {
+ $cookies['ss'] = $session_secret;
+ }
+
+ foreach ($cookies as $name => $val) {
+ setcookie($this->api_key . '_' . $name, $val, (int)$expires, '', $this->base_domain);
+ $_COOKIE[$this->api_key . '_' . $name] = $val;
+ }
+ $sig = self::generate_sig($cookies, $this->secret);
+ setcookie($this->api_key, $sig, (int)$expires, '', $this->base_domain);
+ $_COOKIE[$this->api_key] = $sig;
+
+ if ($this->base_domain != null) {
+ $base_domain_cookie = 'base_domain_' . $this->api_key;
+ setcookie($base_domain_cookie, $this->base_domain, (int)$expires, '', $this->base_domain);
+ $_COOKIE[$base_domain_cookie] = $this->base_domain;
+ }
+ }
+
+ /**
+ * Tries to undo the badness of magic quotes as best we can
+ * @param string $val Should come directly from $_GET, $_POST, etc.
+ * @return string val without added slashes
+ */
+ public static function no_magic_quotes($val) {
+ if (get_magic_quotes_gpc()) {
+ return stripslashes($val);
+ } else {
+ return $val;
+ }
+ }
+
+ /*
+ * Get the signed parameters that were sent from Facebook. Validates the set
+ * of parameters against the included signature.
+ *
+ * Since Facebook sends data to your callback URL via unsecured means, the
+ * signature is the only way to make sure that the data actually came from
+ * Facebook. So if an app receives a request at the callback URL, it should
+ * always verify the signature that comes with against your own secret key.
+ * Otherwise, it's possible for someone to spoof a request by
+ * pretending to be someone else, i.e.:
+ * www.your-callback-url.com/?fb_user=10101
+ *
+ * This is done automatically by verify_fb_params.
+ *
+ * @param assoc $params a full array of external parameters.
+ * presumed $_GET, $_POST, or $_COOKIE
+ * @param int $timeout number of seconds that the args are good for.
+ * Specifically good for forcing cookies to expire.
+ * @param string $namespace prefix string for the set of parameters we want
+ * to verify. i.e., fb_sig or fb_post_sig
+ *
+ * @return assoc the subset of parameters containing the given prefix,
+ * and also matching the signature associated with them.
+ * OR an empty array if the params do not validate
+ */
+ public function get_valid_fb_params($params, $timeout=null, $namespace='fb_sig') {
+ $prefix = $namespace . '_';
+ $prefix_len = strlen($prefix);
+ $fb_params = array();
+ if (empty($params)) {
+ return array();
+ }
+
+ foreach ($params as $name => $val) {
+ // pull out only those parameters that match the prefix
+ // note that the signature itself ($params[$namespace]) is not in the list
+ if (strpos($name, $prefix) === 0) {
+ $fb_params[substr($name, $prefix_len)] = self::no_magic_quotes($val);
+ }
+ }
+
+ // validate that the request hasn't expired. this is most likely
+ // for params that come from $_COOKIE
+ if ($timeout && (!isset($fb_params['time']) || time() - $fb_params['time'] > $timeout)) {
+ return array();
+ }
+
+ // validate that the params match the signature
+ $signature = isset($params[$namespace]) ? $params[$namespace] : null;
+ if (!$signature || (!$this->verify_signature($fb_params, $signature))) {
+ return array();
+ }
+ return $fb_params;
+ }
+
+ /*
+ * Validates that a given set of parameters match their signature.
+ * Parameters all match a given input prefix, such as "fb_sig".
+ *
+ * @param $fb_params an array of all Facebook-sent parameters,
+ * not including the signature itself
+ * @param $expected_sig the expected result to check against
+ */
+ public function verify_signature($fb_params, $expected_sig) {
+ return self::generate_sig($fb_params, $this->secret) == $expected_sig;
+ }
+
+ /*
+ * Generate a signature using the application secret key.
+ *
+ * The only two entities that know your secret key are you and Facebook,
+ * according to the Terms of Service. Since nobody else can generate
+ * the signature, you can rely on it to verify that the information
+ * came from Facebook.
+ *
+ * @param $params_array an array of all Facebook-sent parameters,
+ * NOT INCLUDING the signature itself
+ * @param $secret your app's secret key
+ *
+ * @return a hash to be checked against the signature provided by Facebook
+ */
+ public static function generate_sig($params_array, $secret) {
+ $str = '';
+
+ ksort($params_array);
+ // Note: make sure that the signature parameter is not already included in
+ // $params_array.
+ foreach ($params_array as $k=>$v) {
+ $str .= "$k=$v";
+ }
+ $str .= $secret;
+
+ return md5($str);
+ }
+
+ public function encode_validationError($summary, $message) {
+ return json_encode(
+ array('errorCode' => FACEBOOK_API_VALIDATION_ERROR,
+ 'errorTitle' => $summary,
+ 'errorMessage' => $message));
+ }
+
+ public function encode_multiFeedStory($feed, $next) {
+ return json_encode(
+ array('method' => 'multiFeedStory',
+ 'content' =>
+ array('next' => $next,
+ 'feed' => $feed)));
+ }
+
+ public function encode_feedStory($feed, $next) {
+ return json_encode(
+ array('method' => 'feedStory',
+ 'content' =>
+ array('next' => $next,
+ 'feed' => $feed)));
+ }
+
+ public function create_templatizedFeedStory($title_template, $title_data=array(),
+ $body_template='', $body_data = array(), $body_general=null,
+ $image_1=null, $image_1_link=null,
+ $image_2=null, $image_2_link=null,
+ $image_3=null, $image_3_link=null,
+ $image_4=null, $image_4_link=null) {
+ return array('title_template'=> $title_template,
+ 'title_data' => $title_data,
+ 'body_template'=> $body_template,
+ 'body_data' => $body_data,
+ 'body_general' => $body_general,
+ 'image_1' => $image_1,
+ 'image_1_link' => $image_1_link,
+ 'image_2' => $image_2,
+ 'image_2_link' => $image_2_link,
+ 'image_3' => $image_3,
+ 'image_3_link' => $image_3_link,
+ 'image_4' => $image_4,
+ 'image_4_link' => $image_4_link);
+ }
+
+
+}
+
diff --git a/extlib/facebook/facebook_desktop.php b/extlib/facebook/facebook_desktop.php
new file mode 100644
index 000000000..90cdf66bd
--- /dev/null
+++ b/extlib/facebook/facebook_desktop.php
@@ -0,0 +1,104 @@
+<?php
+// Copyright 2004-2008 Facebook. All Rights Reserved.
+//
+// +---------------------------------------------------------------------------+
+// | Facebook Platform PHP5 client |
+// +---------------------------------------------------------------------------+
+// | Copyright (c) 2007 Facebook, Inc. |
+// | All rights reserved. |
+// | |
+// | Redistribution and use in source and binary forms, with or without |
+// | modification, are permitted provided that the following conditions |
+// | are met: |
+// | |
+// | 1. Redistributions of source code must retain the above copyright |
+// | notice, this list of conditions and the following disclaimer. |
+// | 2. Redistributions in binary form must reproduce the above copyright |
+// | notice, this list of conditions and the following disclaimer in the |
+// | documentation and/or other materials provided with the distribution. |
+// | |
+// | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR |
+// | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
+// | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. |
+// | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, |
+// | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
+// | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
+// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
+// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
+// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF |
+// | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
+// +---------------------------------------------------------------------------+
+// | For help with this library, contact developers-help@facebook.com |
+// +---------------------------------------------------------------------------+
+//
+
+/**
+ * This class extends and modifies the "Facebook" class to better
+ * suit desktop apps.
+ */
+class FacebookDesktop extends Facebook {
+ // the application secret, which differs from the session secret
+ public $app_secret;
+ public $verify_sig;
+
+ public function __construct($api_key, $secret) {
+ $this->app_secret = $secret;
+ $this->verify_sig = false;
+ parent::__construct($api_key, $secret);
+ }
+
+ public function do_get_session($auth_token) {
+ $this->api_client->secret = $this->app_secret;
+ $this->api_client->session_key = null;
+ $session_info = parent::do_get_session($auth_token);
+ if (!empty($session_info['secret'])) {
+ // store the session secret
+ $this->set_session_secret($session_info['secret']);
+ }
+ return $session_info;
+ }
+
+ public function set_session_secret($session_secret) {
+ $this->secret = $session_secret;
+ $this->api_client->secret = $session_secret;
+ }
+
+ public function require_login() {
+ if ($this->get_loggedin_user()) {
+ try {
+ // try a session-based API call to ensure that we have the correct
+ // session secret
+ $user = $this->api_client->users_getLoggedInUser();
+
+ // now that we have a valid session secret, verify the signature
+ $this->verify_sig = true;
+ if ($this->validate_fb_params(false)) {
+ return $user;
+ } else {
+ // validation failed
+ return null;
+ }
+ } catch (FacebookRestClientException $ex) {
+ if (isset($_GET['auth_token'])) {
+ // if we have an auth_token, use it to establish a session
+ $session_info = $this->do_get_session($_GET['auth_token']);
+ if ($session_info) {
+ return $session_info['uid'];
+ }
+ }
+ }
+ }
+ // if we get here, we need to redirect the user to log in
+ $this->redirect($this->get_login_url(self::current_url(), $this->in_fb_canvas()));
+ }
+
+ public function verify_signature($fb_params, $expected_sig) {
+ // we don't want to verify the signature until we have a valid
+ // session secret
+ if ($this->verify_sig) {
+ return parent::verify_signature($fb_params, $expected_sig);
+ } else {
+ return true;
+ }
+ }
+}
diff --git a/extlib/facebook/facebookapi_php5_restlib.php b/extlib/facebook/facebookapi_php5_restlib.php
new file mode 100644
index 000000000..389f40a9d
--- /dev/null
+++ b/extlib/facebook/facebookapi_php5_restlib.php
@@ -0,0 +1,2632 @@
+<?php
+//
+// +---------------------------------------------------------------------------+
+// | Facebook Platform PHP5 client |
+// +---------------------------------------------------------------------------+
+// | Copyright (c) 2007-2008 Facebook, Inc. |
+// | All rights reserved. |
+// | |
+// | Redistribution and use in source and binary forms, with or without |
+// | modification, are permitted provided that the following conditions |
+// | are met: |
+// | |
+// | 1. Redistributions of source code must retain the above copyright |
+// | notice, this list of conditions and the following disclaimer. |
+// | 2. Redistributions in binary form must reproduce the above copyright |
+// | notice, this list of conditions and the following disclaimer in the |
+// | documentation and/or other materials provided with the distribution. |
+// | |
+// | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR |
+// | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
+// | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. |
+// | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, |
+// | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
+// | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
+// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
+// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
+// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF |
+// | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
+// +---------------------------------------------------------------------------+
+// | For help with this library, contact developers-help@facebook.com |
+// +---------------------------------------------------------------------------+
+//
+
+include_once 'jsonwrapper/jsonwrapper.php';
+class FacebookRestClient {
+ public $secret;
+ public $session_key;
+ public $api_key;
+ // to save making the friends.get api call, this will get prepopulated on
+ // canvas pages
+ public $friends_list;
+ public $user;
+ // to save making the pages.isAppAdded api call, this will get prepopulated
+ // on canvas pages
+ public $added;
+ public $is_user;
+ // we don't pass friends list to iframes, but we want to make
+ // friends_get really simple in the canvas_user (non-logged in) case.
+ // So we use the canvas_user as default arg to friends_get
+ public $canvas_user;
+ public $batch_mode;
+ private $batch_queue;
+ private $call_as_apikey;
+
+ const BATCH_MODE_DEFAULT = 0;
+ const BATCH_MODE_SERVER_PARALLEL = 0;
+ const BATCH_MODE_SERIAL_ONLY = 2;
+
+ /**
+ * Create the client.
+ * @param string $session_key if you haven't gotten a session key yet, leave
+ * this as null and then set it later by just
+ * directly accessing the $session_key member
+ * variable.
+ */
+ public function __construct($api_key, $secret, $session_key=null) {
+ $this->secret = $secret;
+ $this->session_key = $session_key;
+ $this->api_key = $api_key;
+ $this->batch_mode = FacebookRestClient::BATCH_MODE_DEFAULT;
+ $this->last_call_id = 0;
+ $this->call_as_apikey = '';
+ $this->server_addr = Facebook::get_facebook_url('api') . '/restserver.php';
+
+ if (!empty($GLOBALS['facebook_config']['debug'])) {
+ $this->cur_id = 0;
+ ?>
+<script type="text/javascript">
+var types = ['params', 'xml', 'php', 'sxml'];
+function getStyle(elem, style) {
+ if (elem.getStyle) {
+ return elem.getStyle(style);
+ } else {
+ return elem.style[style];
+ }
+}
+function setStyle(elem, style, value) {
+ if (elem.setStyle) {
+ elem.setStyle(style, value);
+ } else {
+ elem.style[style] = value;
+ }
+}
+function toggleDisplay(id, type) {
+ for (var i = 0; i < types.length; i++) {
+ var t = types[i];
+ var pre = document.getElementById(t + id);
+ if (pre) {
+ if (t != type || getStyle(pre, 'display') == 'block') {
+ setStyle(pre, 'display', 'none');
+ } else {
+ setStyle(pre, 'display', 'block');
+ }
+ }
+ }
+ return false;
+}
+</script>
+<?php
+ }
+ }
+
+ /**
+ * Set the default user id for methods that allow the caller
+ * to pass an uid parameter to identify the target user
+ * instead of a session key. This currently applies to
+ * the user preferences methods.
+ *
+ * @param $uid int the user id
+ */
+ public function set_user($uid) {
+ $this->user = $uid;
+ }
+
+ /**
+ * Start a batch operation.
+ */
+ public function begin_batch() {
+ if($this->batch_queue !== null) {
+ $code = FacebookAPIErrorCodes::API_EC_BATCH_ALREADY_STARTED;
+ throw new FacebookRestClientException($code,
+ FacebookAPIErrorCodes::$api_error_descriptions[$code]);
+ }
+
+ $this->batch_queue = array();
+ }
+
+ /*
+ * End current batch operation
+ */
+ public function end_batch() {
+ if($this->batch_queue === null) {
+ $code = FacebookAPIErrorCodes::API_EC_BATCH_NOT_STARTED;
+ throw new FacebookRestClientException($code,
+ FacebookAPIErrorCodes::$api_error_descriptions[$code]);
+ }
+
+ $this->execute_server_side_batch();
+
+ $this->batch_queue = null;
+ }
+
+ private function execute_server_side_batch() {
+ $item_count = count($this->batch_queue);
+ $method_feed = array();
+ foreach($this->batch_queue as $batch_item) {
+ $method_feed[] = $this->create_post_string($batch_item['m'],
+ $batch_item['p']);
+ }
+
+ $method_feed_json = json_encode($method_feed);
+
+ $serial_only =
+ ($this->batch_mode == FacebookRestClient::BATCH_MODE_SERIAL_ONLY);
+ $params = array('method_feed' => $method_feed_json,
+ 'serial_only' => $serial_only);
+ if ($this->call_as_apikey) {
+ $params['call_as_apikey'] = $this->call_as_apikey;
+ }
+
+ $xml = $this->post_request('batch.run', $params);
+
+ $result = $this->convert_xml_to_result($xml, 'batch.run', $params);
+
+
+ if (is_array($result) && isset($result['error_code'])) {
+ throw new FacebookRestClientException($result['error_msg'],
+ $result['error_code']);
+ }
+
+ for($i = 0; $i < $item_count; $i++) {
+ $batch_item = $this->batch_queue[$i];
+ $batch_item_result_xml = $result[$i];
+ $batch_item_result = $this->convert_xml_to_result($batch_item_result_xml,
+ $batch_item['m'],
+ $batch_item['p']);
+
+ if (is_array($batch_item_result) &&
+ isset($batch_item_result['error_code'])) {
+ throw new FacebookRestClientException($batch_item_result['error_msg'],
+ $batch_item_result['error_code']);
+ }
+ $batch_item['r'] = $batch_item_result;
+ }
+ }
+
+ public function begin_permissions_mode($permissions_apikey) {
+ $this->call_as_apikey = $permissions_apikey;
+ }
+
+ public function end_permissions_mode() {
+ $this->call_as_apikey = '';
+ }
+
+ /**
+ * Returns public information for an application (as shown in the application
+ * directory) by either application ID, API key, or canvas page name.
+ *
+ * @param int $application_id (Optional) app id
+ * @param string $application_api_key (Optional) api key
+ * @param string $application_canvas_name (Optional) canvas name
+ *
+ * Exactly one argument must be specified, otherwise it is an error.
+ *
+ * @return array An array of public information about the application.
+ */
+ public function application_getPublicInfo($application_id=null,
+ $application_api_key=null,
+ $application_canvas_name=null) {
+ return $this->call_method('facebook.application.getPublicInfo',
+ array('application_id' => $application_id,
+ 'application_api_key' => $application_api_key,
+ 'application_canvas_name' => $application_canvas_name));
+ }
+
+ /**
+ * Creates an authentication token to be used as part of the desktop login
+ * flow. For more information, please see
+ * http://wiki.developers.facebook.com/index.php/Auth.createToken.
+ *
+ * @return string An authentication token.
+ */
+ public function auth_createToken() {
+ return $this->call_method('facebook.auth.createToken', array());
+ }
+
+ /**
+ * Returns the session information available after current user logs in.
+ *
+ * @param string $auth_token the token returned by
+ * auth_createToken or passed back to
+ * your callback_url.
+ * @param bool $generate_session_secret whether the session returned should
+ * include a session secret
+ *
+ * @return array An assoc array containing session_key, uid
+ */
+ public function auth_getSession($auth_token, $generate_session_secret=false) {
+ //Check if we are in batch mode
+ if($this->batch_queue === null) {
+ $result = $this->call_method('facebook.auth.getSession',
+ array('auth_token' => $auth_token,
+ 'generate_session_secret' => $generate_session_secret));
+ $this->session_key = $result['session_key'];
+
+ if (!empty($result['secret']) && !$generate_session_secret) {
+ // desktop apps have a special secret
+ $this->secret = $result['secret'];
+ }
+ return $result;
+ }
+ }
+
+ /**
+ * Generates a session-specific secret. This is for integration with
+ * client-side API calls, such as the JS library.
+ *
+ * @return array A session secret for the current promoted session
+ *
+ * @error API_EC_PARAM_SESSION_KEY
+ * API_EC_PARAM_UNKNOWN
+ */
+ public function auth_promoteSession() {
+ return $this->call_method('facebook.auth.promoteSession', array());
+ }
+
+ /**
+ * Expires the session that is currently being used. If this call is
+ * successful, no further calls to the API (which require a session) can be
+ * made until a valid session is created.
+ *
+ * @return bool true if session expiration was successful, false otherwise
+ */
+ public function auth_expireSession() {
+ return $this->call_method('facebook.auth.expireSession', array());
+ }
+
+ /**
+ * Revokes the user's agreement to the Facebook Terms of Service for your
+ * application. If you call this method for one of your users, you will no
+ * longer be able to make API requests on their behalf until they again
+ * authorize your application. Use with care. Note that if this method is
+ * called without a user parameter, then it will revoke access for the
+ * current session's user.
+ *
+ * @param int $uid (Optional) User to revoke
+ *
+ * @return bool true if revocation succeeds, false otherwise
+ */
+ public function auth_revokeAuthorization($uid=null) {
+ return $this->call_method('facebook.auth.revokeAuthorization',
+ array('uid' => $uid));
+ }
+
+ /**
+ * Returns the number of unconnected friends that exist in this application.
+ * This number is determined based on the accounts registered through
+ * connect.registerUsers() (see below).
+ */
+ public function connect_getUnconnectedFriendsCount() {
+ return $this->call_method('facebook.connect.getUnconnectedFriendsCount',
+ array());
+ }
+
+ /**
+ * This method is used to create an association between an external user
+ * account and a Facebook user account, as per Facebook Connect.
+ *
+ * This method takes an array of account data, including a required email_hash
+ * and optional account data. For each connected account, if the user exists,
+ * the information is added to the set of the user's connected accounts.
+ * If the user has already authorized the site, the connected account is added
+ * in the confirmed state. If the user has not yet authorized the site, the
+ * connected account is added in the pending state.
+ *
+ * This is designed to help Facebook Connect recognize when two Facebook
+ * friends are both members of a external site, but perhaps are not aware of
+ * it. The Connect dialog (see fb:connect-form) is used when friends can be
+ * identified through these email hashes. See the following url for details:
+ *
+ * http://wiki.developers.facebook.com/index.php/Connect.registerUsers
+ *
+ * @param mixed $accounts A (JSON-encoded) array of arrays, where each array
+ * has three properties:
+ * 'email_hash' (req) - public email hash of account
+ * 'account_id' (opt) - remote account id;
+ * 'account_url' (opt) - url to remote account;
+ *
+ * @return array The list of email hashes for the successfully registered
+ * accounts.
+ */
+ public function connect_registerUsers($accounts) {
+ return $this->call_method('facebook.connect.registerUsers',
+ array('accounts' => $accounts));
+ }
+
+ /**
+ * Unregisters a set of accounts registered using connect.registerUsers.
+ *
+ * @param array $email_hashes The (JSON-encoded) list of email hashes to be
+ * unregistered.
+ *
+ * @return array The list of email hashes which have been successfully
+ * unregistered.
+ */
+ public function connect_unregisterUsers($email_hashes) {
+ return $this->call_method('facebook.connect.unregisterUsers',
+ array('email_hashes' => $email_hashes));
+ }
+
+ /**
+ * Returns events according to the filters specified.
+ *
+ * @param int $uid (Optional) User associated with events. A null
+ * parameter will default to the session user.
+ * @param array $eids (Optional) Filter by these event ids. A null
+ * parameter will get all events for the user.
+ * @param int $start_time (Optional) Filter with this unix time as lower
+ * bound. A null or zero parameter indicates no
+ * lower bound.
+ * @param int $end_time (Optional) Filter with this UTC as upper bound.
+ * A null or zero parameter indicates no upper
+ * bound.
+ * @param string $rsvp_status (Optional) Only show events where the given uid
+ * has this rsvp status. This only works if you
+ * have specified a value for $uid. Values are as
+ * in events.getMembers. Null indicates to ignore
+ * rsvp status when filtering.
+ *
+ * @return array The events matching the query.
+ */
+ public function &events_get($uid=null,
+ $eids=null,
+ $start_time=null,
+ $end_time=null,
+ $rsvp_status=null) {
+ return $this->call_method('facebook.events.get',
+ array('uid' => $uid,
+ 'eids' => $eids,
+ 'start_time' => $start_time,
+ 'end_time' => $end_time,
+ 'rsvp_status' => $rsvp_status));
+ }
+
+ /**
+ * Returns membership list data associated with an event.
+ *
+ * @param int $eid event id
+ *
+ * @return array An assoc array of four membership lists, with keys
+ * 'attending', 'unsure', 'declined', and 'not_replied'
+ */
+ public function &events_getMembers($eid) {
+ return $this->call_method('facebook.events.getMembers',
+ array('eid' => $eid));
+ }
+
+ /**
+ * RSVPs the current user to this event.
+ *
+ * @param int $eid event id
+ * @param string $rsvp_status 'attending', 'unsure', or 'declined'
+ *
+ * @return bool true if successful
+ */
+ public function &events_rsvp($eid, $rsvp_status) {
+ return $this->call_method('facebook.events.rsvp',
+ array(
+ 'eid' => $eid,
+ 'rsvp_status' => $rsvp_status));
+ }
+
+ /**
+ * Cancels an event. Only works for events where application is the admin.
+ *
+ * @param int $eid event id
+ * @param string $cancel_message (Optional) message to send to members of
+ * the event about why it is cancelled
+ *
+ * @return bool true if successful
+ */
+ public function &events_cancel($eid, $cancel_message='') {
+ return $this->call_method('facebook.events.cancel',
+ array('eid' => $eid,
+ 'cancel_message' => $cancel_message));
+ }
+
+ /**
+ * Creates an event on behalf of the user is there is a session, otherwise on
+ * behalf of app. Successful creation guarantees app will be admin.
+ *
+ * @param assoc array $event_info json encoded event information
+ *
+ * @return int event id
+ */
+ public function &events_create($event_info) {
+ return $this->call_method('facebook.events.create',
+ array('event_info' => $event_info));
+ }
+
+ /**
+ * Edits an existing event. Only works for events where application is admin.
+ *
+ * @param int $eid event id
+ * @param assoc array $event_info json encoded event information
+ *
+ * @return bool true if successful
+ */
+ public function &events_edit($eid, $event_info) {
+ return $this->call_method('facebook.events.edit',
+ array('eid' => $eid,
+ 'event_info' => $event_info));
+ }
+
+ /**
+ * Fetches and re-caches the image stored at the given URL, for use in images
+ * published to non-canvas pages via the API (for example, to user profiles
+ * via profile.setFBML, or to News Feed via feed.publishUserAction).
+ *
+ * @param string $url The absolute URL from which to refresh the image.
+ *
+ * @return bool true on success
+ */
+ public function &fbml_refreshImgSrc($url) {
+ return $this->call_method('facebook.fbml.refreshImgSrc',
+ array('url' => $url));
+ }
+
+ /**
+ * Fetches and re-caches the content stored at the given URL, for use in an
+ * fb:ref FBML tag.
+ *
+ * @param string $url The absolute URL from which to fetch content. This URL
+ * should be used in a fb:ref FBML tag.
+ *
+ * @return bool true on success
+ */
+ public function &fbml_refreshRefUrl($url) {
+ return $this->call_method('facebook.fbml.refreshRefUrl',
+ array('url' => $url));
+ }
+
+ /**
+ * Lets you insert text strings in their native language into the Facebook
+ * Translations database so they can be translated.
+ *
+ * @param array $native_strings An array of maps, where each map has a 'text'
+ * field and a 'description' field.
+ *
+ * @return int Number of strings uploaded.
+ */
+ public function &fbml_uploadNativeStrings($native_strings) {
+ return $this->call_method('facebook.fbml.uploadNativeStrings',
+ array('native_strings' => json_encode($native_strings)));
+ }
+
+ /**
+ * Associates a given "handle" with FBML markup so that the handle can be
+ * used within the fb:ref FBML tag. A handle is unique within an application
+ * and allows an application to publish identical FBML to many user profiles
+ * and do subsequent updates without having to republish FBML on behalf of
+ * each user.
+ *
+ * @param string $handle The handle to associate with the given FBML.
+ * @param string $fbml The FBML to associate with the given handle.
+ *
+ * @return bool true on success
+ */
+ public function &fbml_setRefHandle($handle, $fbml) {
+ return $this->call_method('facebook.fbml.setRefHandle',
+ array('handle' => $handle, 'fbml' => $fbml));
+ }
+
+ /**
+ * Register custom tags for the application. Custom tags can be used
+ * to extend the set of tags available to applications in FBML
+ * markup.
+ *
+ * Before you call this function,
+ * make sure you read the full documentation at
+ *
+ * http://wiki.developers.facebook.com/index.php/Fbml.RegisterCustomTags
+ *
+ * IMPORTANT: This function overwrites the values of
+ * existing tags if the names match. Use this function with care because
+ * it may break the FBML of any application that is using the
+ * existing version of the tags.
+ *
+ * @param mixed $tags an array of tag objects (the full description is on the
+ * wiki page)
+ *
+ * @return int the number of tags that were registered
+ */
+ public function &fbml_registerCustomTags($tags) {
+ $tags = json_encode($tags);
+ return $this->call_method('facebook.fbml.registerCustomTags',
+ array('tags' => $tags));
+ }
+
+ /**
+ * Get the custom tags for an application. If $app_id
+ * is not specified, the calling app's tags are returned.
+ * If $app_id is different from the id of the calling app,
+ * only the app's public tags are returned.
+ * The return value is an array of the same type as
+ * the $tags parameter of fbml_registerCustomTags().
+ *
+ * @param int $app_id the application's id (optional)
+ *
+ * @return mixed an array containing the custom tag objects
+ */
+ public function &fbml_getCustomTags($app_id = null) {
+ return $this->call_method('facebook.fbml.getCustomTags',
+ array('app_id' => $app_id));
+ }
+
+
+ /**
+ * Delete custom tags the application has registered. If
+ * $tag_names is null, all the application's custom tags will be
+ * deleted.
+ *
+ * IMPORTANT: If your application has registered public tags
+ * that other applications may be using, don't delete those tags!
+ * Doing so can break the FBML ofapplications that are using them.
+ *
+ * @param array $tag_names the names of the tags to delete (optinal)
+ * @return bool true on success
+ */
+ public function &fbml_deleteCustomTags($tag_names = null) {
+ return $this->call_method('facebook.fbml.deleteCustomTags',
+ array('tag_names' => json_encode($tag_names)));
+ }
+
+
+
+ /**
+ * This method is deprecated for calls made on behalf of users. This method
+ * works only for publishing stories on a Facebook Page that has installed
+ * your application. To publish stories to a user's profile, use
+ * feed.publishUserAction instead.
+ *
+ * For more details on this call, please visit the wiki page:
+ *
+ * http://wiki.developers.facebook.com/index.php/Feed.publishTemplatizedAction
+ */
+ public function &feed_publishTemplatizedAction($title_template,
+ $title_data,
+ $body_template,
+ $body_data,
+ $body_general,
+ $image_1=null,
+ $image_1_link=null,
+ $image_2=null,
+ $image_2_link=null,
+ $image_3=null,
+ $image_3_link=null,
+ $image_4=null,
+ $image_4_link=null,
+ $target_ids='',
+ $page_actor_id=null) {
+ return $this->call_method('facebook.feed.publishTemplatizedAction',
+ array('title_template' => $title_template,
+ 'title_data' => $title_data,
+ 'body_template' => $body_template,
+ 'body_data' => $body_data,
+ 'body_general' => $body_general,
+ 'image_1' => $image_1,
+ 'image_1_link' => $image_1_link,
+ 'image_2' => $image_2,
+ 'image_2_link' => $image_2_link,
+ 'image_3' => $image_3,
+ 'image_3_link' => $image_3_link,
+ 'image_4' => $image_4,
+ 'image_4_link' => $image_4_link,
+ 'target_ids' => $target_ids,
+ 'page_actor_id' => $page_actor_id));
+ }
+
+ /**
+ * Registers a template bundle. Template bundles are somewhat involved, so
+ * it's recommended you check out the wiki for more details:
+ *
+ * http://wiki.developers.facebook.com/index.php/Feed.registerTemplateBundle
+ *
+ * @return string A template bundle id
+ */
+ public function &feed_registerTemplateBundle($one_line_story_templates,
+ $short_story_templates = array(),
+ $full_story_template = null,
+ $action_links = array()) {
+
+ $one_line_story_templates = json_encode($one_line_story_templates);
+
+ if (!empty($short_story_templates)) {
+ $short_story_templates = json_encode($short_story_templates);
+ }
+
+ if (isset($full_story_template)) {
+ $full_story_template = json_encode($full_story_template);
+ }
+
+ if (isset($action_links)) {
+ $action_links = json_encode($action_links);
+ }
+
+ return $this->call_method('facebook.feed.registerTemplateBundle',
+ array('one_line_story_templates' => $one_line_story_templates,
+ 'short_story_templates' => $short_story_templates,
+ 'full_story_template' => $full_story_template,
+ 'action_links' => $action_links));
+ }
+
+ /**
+ * Retrieves the full list of active template bundles registered by the
+ * requesting application.
+ *
+ * @return array An array of template bundles
+ */
+ public function &feed_getRegisteredTemplateBundles() {
+ return $this->call_method('facebook.feed.getRegisteredTemplateBundles',
+ array());
+ }
+
+ /**
+ * Retrieves information about a specified template bundle previously
+ * registered by the requesting application.
+ *
+ * @param string $template_bundle_id The template bundle id
+ *
+ * @return array Template bundle
+ */
+ public function &feed_getRegisteredTemplateBundleByID($template_bundle_id) {
+ return $this->call_method('facebook.feed.getRegisteredTemplateBundleByID',
+ array('template_bundle_id' => $template_bundle_id));
+ }
+
+ /**
+ * Deactivates a previously registered template bundle.
+ *
+ * @param string $template_bundle_id The template bundle id
+ *
+ * @return bool true on success
+ */
+ public function &feed_deactivateTemplateBundleByID($template_bundle_id) {
+ return $this->call_method('facebook.feed.deactivateTemplateBundleByID',
+ array('template_bundle_id' => $template_bundle_id));
+ }
+
+ const STORY_SIZE_ONE_LINE = 1;
+ const STORY_SIZE_SHORT = 2;
+ const STORY_SIZE_FULL = 4;
+
+ /**
+ * Publishes a story on behalf of the user owning the session, using the
+ * specified template bundle. This method requires an active session key in
+ * order to be called.
+ *
+ * The parameters to this method ($templata_data in particular) are somewhat
+ * involved. It's recommended you visit the wiki for details:
+ *
+ * http://wiki.developers.facebook.com/index.php/Feed.publishUserAction
+ *
+ * @param int $template_bundle_id A template bundle id previously registered
+ * @param array $template_data See wiki article for syntax
+ * @param array $target_ids (Optional) An array of friend uids of the
+ * user who shared in this action.
+ * @param string $body_general (Optional) Additional markup that extends
+ * the body of a short story.
+ * @param int $story_size (Optional) A story size (see above)
+ *
+ * @return bool true on success
+ */
+ public function &feed_publishUserAction(
+ $template_bundle_id, $template_data, $target_ids='', $body_general='',
+ $story_size=FacebookRestClient::STORY_SIZE_ONE_LINE) {
+
+ if (is_array($template_data)) {
+ $template_data = json_encode($template_data);
+ } // allow client to either pass in JSON or an assoc that we JSON for them
+
+ if (is_array($target_ids)) {
+ $target_ids = json_encode($target_ids);
+ $target_ids = trim($target_ids, "[]"); // we don't want square brackets
+ }
+
+ return $this->call_method('facebook.feed.publishUserAction',
+ array('template_bundle_id' => $template_bundle_id,
+ 'template_data' => $template_data,
+ 'target_ids' => $target_ids,
+ 'body_general' => $body_general,
+ 'story_size' => $story_size));
+ }
+
+ /**
+ * For the current user, retrieves stories generated by the user's friends
+ * while using this application. This can be used to easily create a
+ * "News Feed" like experience.
+ *
+ * @return array An array of feed story objects.
+ */
+ public function &feed_getAppFriendStories() {
+ return $this->call_method('facebook.feed.getAppFriendStories', array());
+ }
+
+ /**
+ * Makes an FQL query. This is a generalized way of accessing all the data
+ * in the API, as an alternative to most of the other method calls. More
+ * info at http://developers.facebook.com/documentation.php?v=1.0&doc=fql
+ *
+ * @param string $query the query to evaluate
+ *
+ * @return array generalized array representing the results
+ */
+ public function &fql_query($query) {
+ return $this->call_method('facebook.fql.query',
+ array('query' => $query));
+ }
+
+ /**
+ * Returns whether or not pairs of users are friends.
+ * Note that the Facebook friend relationship is symmetric.
+ *
+ * @param array $uids1 array of ids (id_1, id_2,...) of some length X
+ * @param array $uids2 array of ids (id_A, id_B,...) of SAME length X
+ *
+ * @return array An array with uid1, uid2, and bool if friends, e.g.:
+ * array(0 => array('uid1' => id_1, 'uid2' => id_A, 'are_friends' => 1),
+ * 1 => array('uid1' => id_2, 'uid2' => id_B, 'are_friends' => 0)
+ * ...)
+ */
+ public function &friends_areFriends($uids1, $uids2) {
+ return $this->call_method('facebook.friends.areFriends',
+ array('uids1' => $uids1, 'uids2' => $uids2));
+ }
+
+ /**
+ * Returns the friends of the current session user.
+ *
+ * @param int $flid (Optional) Only return friends on this friend list.
+ *
+ * @return array An array of friends
+ */
+ public function &friends_get($flid=null) {
+ if (isset($this->friends_list)) {
+ return $this->friends_list;
+ }
+ $params = array();
+ if (isset($this->canvas_user)) {
+ $params['uid'] = $this->canvas_user;
+ }
+ if ($flid) {
+ $params['flid'] = $flid;
+ }
+ return $this->call_method('facebook.friends.get', $params);
+
+ }
+
+ /**
+ * Returns the set of friend lists for the current session user.
+ *
+ * @return array An array of friend list objects
+ */
+ public function &friends_getLists() {
+ return $this->call_method('facebook.friends.getLists', array());
+ }
+
+ /**
+ * Returns the friends of the session user, who are also users
+ * of the calling application.
+ *
+ * @return array An array of friends also using the app
+ */
+ public function &friends_getAppUsers() {
+ return $this->call_method('facebook.friends.getAppUsers', array());
+ }
+
+ /**
+ * Returns groups according to the filters specified.
+ *
+ * @param int $uid (Optional) User associated with groups. A null
+ * parameter will default to the session user.
+ * @param array $gids (Optional) Group ids to query. A null parameter will
+ * get all groups for the user.
+ *
+ * @return array An array of group objects
+ */
+ public function &groups_get($uid, $gids) {
+ return $this->call_method('facebook.groups.get',
+ array('uid' => $uid,
+ 'gids' => $gids));
+ }
+
+ /**
+ * Returns the membership list of a group.
+ *
+ * @param int $gid Group id
+ *
+ * @return array An array with four membership lists, with keys 'members',
+ * 'admins', 'officers', and 'not_replied'
+ */
+ public function &groups_getMembers($gid) {
+ return $this->call_method('facebook.groups.getMembers',
+ array('gid' => $gid));
+ }
+
+ /**
+ * Returns cookies according to the filters specified.
+ *
+ * @param int $uid User for which the cookies are needed.
+ * @param string $name (Optional) A null parameter will get all cookies
+ * for the user.
+ *
+ * @return array Cookies! Nom nom nom nom nom.
+ */
+ public function data_getCookies($uid, $name) {
+ return $this->call_method('facebook.data.getCookies',
+ array('uid' => $uid,
+ 'name' => $name));
+ }
+
+ /**
+ * Sets cookies according to the params specified.
+ *
+ * @param int $uid User for which the cookies are needed.
+ * @param string $name Name of the cookie
+ * @param string $value (Optional) if expires specified and is in the past
+ * @param int $expires (Optional) Expiry time
+ * @param string $path (Optional) Url path to associate with (default is /)
+ *
+ * @return bool true on success
+ */
+ public function data_setCookie($uid, $name, $value, $expires, $path) {
+ return $this->call_method('facebook.data.setCookie',
+ array('uid' => $uid,
+ 'name' => $name,
+ 'value' => $value,
+ 'expires' => $expires,
+ 'path' => $path));
+ }
+
+ /**
+ * Permissions API
+ */
+
+ /**
+ * Checks API-access granted by self to the specified application.
+ *
+ * @param string $permissions_apikey Other application key
+ *
+ * @return array API methods/namespaces which are allowed access
+ */
+ public function permissions_checkGrantedApiAccess($permissions_apikey) {
+ return $this->call_method('facebook.permissions.checkGrantedApiAccess',
+ array('permissions_apikey' => $permissions_apikey));
+ }
+
+ /**
+ * Checks API-access granted to self by the specified application.
+ *
+ * @param string $permissions_apikey Other application key
+ *
+ * @return array API methods/namespaces which are allowed access
+ */
+ public function permissions_checkAvailableApiAccess($permissions_apikey) {
+ return $this->call_method('facebook.permissions.checkAvailableApiAccess',
+ array('permissions_apikey' => $permissions_apikey));
+ }
+
+ /**
+ * Grant API-access to the specified methods/namespaces to the specified
+ * application.
+ *
+ * @param string $permissions_apikey Other application key
+ * @param array(string) $method_arr (Optional) API methods/namespaces
+ * allowed
+ *
+ * @return array API methods/namespaces which are allowed access
+ */
+ public function permissions_grantApiAccess($permissions_apikey, $method_arr) {
+ return $this->call_method('facebook.permissions.grantApiAccess',
+ array('permissions_apikey' => $permissions_apikey,
+ 'method_arr' => $method_arr));
+ }
+
+ /**
+ * Revoke API-access granted to the specified application.
+ *
+ * @param string $permissions_apikey Other application key
+ *
+ * @return bool true on success
+ */
+ public function permissions_revokeApiAccess($permissions_apikey) {
+ return $this->call_method('facebook.permissions.revokeApiAccess',
+ array('permissions_apikey' => $permissions_apikey));
+ }
+
+ /**
+ * Returns the outstanding notifications for the session user.
+ *
+ * @return array An assoc array of notification count objects for
+ * 'messages', 'pokes' and 'shares', a uid list of
+ * 'friend_requests', a gid list of 'group_invites',
+ * and an eid list of 'event_invites'
+ */
+ public function &notifications_get() {
+ return $this->call_method('facebook.notifications.get', array());
+ }
+
+ /**
+ * Sends a notification to the specified users.
+ *
+ * @return A comma separated list of successful recipients
+ */
+ public function &notifications_send($to_ids, $notification, $type) {
+ return $this->call_method('facebook.notifications.send',
+ array('to_ids' => $to_ids,
+ 'notification' => $notification,
+ 'type' => $type));
+ }
+
+ /**
+ * Sends an email to the specified user of the application.
+ *
+ * @param array $recipients id of the recipients
+ * @param string $subject subject of the email
+ * @param string $text (plain text) body of the email
+ * @param string $fbml fbml markup for an html version of the email
+ *
+ * @return string A comma separated list of successful recipients
+ */
+ public function &notifications_sendEmail($recipients,
+ $subject,
+ $text,
+ $fbml) {
+ return $this->call_method('facebook.notifications.sendEmail',
+ array('recipients' => $recipients,
+ 'subject' => $subject,
+ 'text' => $text,
+ 'fbml' => $fbml));
+ }
+
+ /**
+ * Returns the requested info fields for the requested set of pages.
+ *
+ * @param array $page_ids an array of page ids
+ * @param array $fields an array of strings describing the info fields
+ * desired
+ * @param int $uid (Optional) limit results to pages of which this
+ * user is a fan.
+ * @param string type limits results to a particular type of page.
+ *
+ * @return array An array of pages
+ */
+ public function &pages_getInfo($page_ids, $fields, $uid, $type) {
+ return $this->call_method('facebook.pages.getInfo',
+ array('page_ids' => $page_ids,
+ 'fields' => $fields,
+ 'uid' => $uid,
+ 'type' => $type));
+ }
+
+ /**
+ * Returns true if the given user is an admin for the passed page.
+ *
+ * @param int $page_id target page id
+ * @param int $uid (Optional) user id (defaults to the logged-in user)
+ *
+ * @return bool true on success
+ */
+ public function &pages_isAdmin($page_id, $uid = null) {
+ return $this->call_method('facebook.pages.isAdmin',
+ array('page_id' => $page_id,
+ 'uid' => $uid));
+ }
+
+ /**
+ * Returns whether or not the given page has added the application.
+ *
+ * @param int $page_id target page id
+ *
+ * @return bool true on success
+ */
+ public function &pages_isAppAdded($page_id) {
+ return $this->call_method('facebook.pages.isAppAdded',
+ array('page_id' => $page_id));
+ }
+
+ /**
+ * Returns true if logged in user is a fan for the passed page.
+ *
+ * @param int $page_id target page id
+ * @param int $uid user to compare. If empty, the logged in user.
+ *
+ * @return bool true on success
+ */
+ public function &pages_isFan($page_id, $uid = null) {
+ return $this->call_method('facebook.pages.isFan',
+ array('page_id' => $page_id,
+ 'uid' => $uid));
+ }
+
+ /**
+ * Adds a tag with the given information to a photo. See the wiki for details:
+ *
+ * http://wiki.developers.facebook.com/index.php/Photos.addTag
+ *
+ * @param int $pid The ID of the photo to be tagged
+ * @param int $tag_uid The ID of the user being tagged. You must specify
+ * either the $tag_uid or the $tag_text parameter
+ * (unless $tags is specified).
+ * @param string $tag_text Some text identifying the person being tagged.
+ * You must specify either the $tag_uid or $tag_text
+ * parameter (unless $tags is specified).
+ * @param float $x The horizontal position of the tag, as a
+ * percentage from 0 to 100, from the left of the
+ * photo.
+ * @param float $y The vertical position of the tag, as a percentage
+ * from 0 to 100, from the top of the photo.
+ * @param array $tags (Optional) An array of maps, where each map
+ * can contain the tag_uid, tag_text, x, and y
+ * parameters defined above. If specified, the
+ * individual arguments are ignored.
+ * @param int $owner_uid (Optional) The user ID of the user whose photo
+ * you are tagging. If this parameter is not
+ * specified, then it defaults to the session user.
+ *
+ * @return bool true on success
+ */
+ public function &photos_addTag($pid,
+ $tag_uid,
+ $tag_text,
+ $x,
+ $y,
+ $tags,
+ $owner_uid=0) {
+ return $this->call_method('facebook.photos.addTag',
+ array('pid' => $pid,
+ 'tag_uid' => $tag_uid,
+ 'tag_text' => $tag_text,
+ 'x' => $x,
+ 'y' => $y,
+ 'tags' => json_encode($tags),
+ 'owner_uid' => $this->get_uid($owner_uid)));
+ }
+
+ /**
+ * Creates and returns a new album owned by the specified user or the current
+ * session user.
+ *
+ * @param string $name The name of the album.
+ * @param string $description (Optional) A description of the album.
+ * @param string $location (Optional) A description of the location.
+ * @param string $visible (Optional) A privacy setting for the album.
+ * One of 'friends', 'friends-of-friends',
+ * 'networks', or 'everyone'. Default 'everyone'.
+ * @param int $uid (Optional) User id for creating the album; if
+ * not specified, the session user is used.
+ *
+ * @return array An album object
+ */
+ public function &photos_createAlbum($name,
+ $description='',
+ $location='',
+ $visible='',
+ $uid=0) {
+ return $this->call_method('facebook.photos.createAlbum',
+ array('name' => $name,
+ 'description' => $description,
+ 'location' => $location,
+ 'visible' => $visible,
+ 'uid' => $this->get_uid($uid)));
+ }
+
+ /**
+ * Returns photos according to the filters specified.
+ *
+ * @param int $subj_id (Optional) Filter by uid of user tagged in the photos.
+ * @param int $aid (Optional) Filter by an album, as returned by
+ * photos_getAlbums.
+ * @param array $pids (Optional) Restrict to a list of pids
+ *
+ * Note that at least one of these parameters needs to be specified, or an
+ * error is returned.
+ *
+ * @return array An array of photo objects.
+ */
+ public function &photos_get($subj_id, $aid, $pids) {
+ return $this->call_method('facebook.photos.get',
+ array('subj_id' => $subj_id, 'aid' => $aid, 'pids' => $pids));
+ }
+
+ /**
+ * Returns the albums created by the given user.
+ *
+ * @param int $uid (Optional) The uid of the user whose albums you want.
+ * A null will return the albums of the session user.
+ * @param array $aids (Optional) A list of aids to restrict the query.
+ *
+ * Note that at least one of the (uid, aids) parameters must be specified.
+ *
+ * @returns an array of album objects.
+ */
+ public function &photos_getAlbums($uid, $aids) {
+ return $this->call_method('facebook.photos.getAlbums',
+ array('uid' => $uid,
+ 'aids' => $aids));
+ }
+
+ /**
+ * Returns the tags on all photos specified.
+ *
+ * @param string $pids A list of pids to query
+ *
+ * @return array An array of photo tag objects, which include pid,
+ * subject uid, and two floating-point numbers (xcoord, ycoord)
+ * for tag pixel location.
+ */
+ public function &photos_getTags($pids) {
+ return $this->call_method('facebook.photos.getTags',
+ array('pids' => $pids));
+ }
+
+ /**
+ * Returns the requested info fields for the requested set of users.
+ *
+ * @param array $uids An array of user ids
+ * @param array $fields An array of info field names desired
+ *
+ * @return array An array of user objects
+ */
+ public function &users_getInfo($uids, $fields) {
+ return $this->call_method('facebook.users.getInfo',
+ array('uids' => $uids, 'fields' => $fields));
+ }
+
+ /**
+ * Returns the requested info fields for the requested set of users. A
+ * session key must not be specified. Only data about users that have
+ * authorized your application will be returned.
+ *
+ * Check the wiki for fields that can be queried through this API call.
+ * Data returned from here should not be used for rendering to application
+ * users, use users.getInfo instead, so that proper privacy rules will be
+ * applied.
+ *
+ * @param array $uids An array of user ids
+ * @param array $fields An array of info field names desired
+ *
+ * @return array An array of user objects
+ */
+ public function &users_getStandardInfo($uids, $fields) {
+ return $this->call_method('facebook.users.getStandardInfo',
+ array('uids' => $uids, 'fields' => $fields));
+ }
+
+ /**
+ * Returns the user corresponding to the current session object.
+ *
+ * @return integer User id
+ */
+ public function &users_getLoggedInUser() {
+ return $this->call_method('facebook.users.getLoggedInUser', array());
+ }
+
+ /**
+ * Returns 1 if the user has the specified permission, 0 otherwise.
+ * http://wiki.developers.facebook.com/index.php/Users.hasAppPermission
+ *
+ * @return integer 1 or 0
+ */
+ public function &users_hasAppPermission($ext_perm, $uid=null) {
+ return $this->call_method('facebook.users.hasAppPermission',
+ array('ext_perm' => $ext_perm, 'uid' => $uid));
+ }
+
+ /**
+ * Returns whether or not the user corresponding to the current
+ * session object has the give the app basic authorization.
+ *
+ * @return boolean true if the user has authorized the app
+ */
+ public function &users_isAppUser($uid=null) {
+ if ($uid === null && isset($this->is_user)) {
+ return $this->is_user;
+ }
+
+ return $this->call_method('facebook.users.isAppUser', array('uid' => $uid));
+ }
+
+ /**
+ * Sets the users' current status message. Message does NOT contain the
+ * word "is" , so make sure to include a verb.
+ *
+ * Example: setStatus("is loving the API!")
+ * will produce the status "Luke is loving the API!"
+ *
+ * @param string $status text-only message to set
+ * @param int $uid user to set for (defaults to the
+ * logged-in user)
+ * @param bool $clear whether or not to clear the status,
+ * instead of setting it
+ * @param bool $status_includes_verb if true, the word "is" will *not* be
+ * prepended to the status message
+ *
+ * @return boolean
+ */
+ public function &users_setStatus($status,
+ $uid = null,
+ $clear = false,
+ $status_includes_verb = true) {
+ $args = array(
+ 'status' => $status,
+ 'uid' => $uid,
+ 'clear' => $clear,
+ 'status_includes_verb' => $status_includes_verb,
+ );
+ return $this->call_method('facebook.users.setStatus', $args);
+ }
+
+ /**
+ * Sets the FBML for the profile of the user attached to this session.
+ *
+ * @param string $markup The FBML that describes the profile
+ * presence of this app for the user
+ * @param int $uid The user
+ * @param string $profile Profile FBML
+ * @param string $profile_action Profile action FBML (deprecated)
+ * @param string $mobile_profile Mobile profile FBML
+ * @param string $profile_main Main Tab profile FBML
+ *
+ * @return array A list of strings describing any compile errors for the
+ * submitted FBML
+ */
+ function profile_setFBML($markup,
+ $uid=null,
+ $profile='',
+ $profile_action='',
+ $mobile_profile='',
+ $profile_main='') {
+ return $this->call_method('facebook.profile.setFBML',
+ array('markup' => $markup,
+ 'uid' => $uid,
+ 'profile' => $profile,
+ 'profile_action' => $profile_action,
+ 'mobile_profile' => $mobile_profile,
+ 'profile_main' => $profile_main));
+ }
+
+ /**
+ * Gets the FBML for the profile box that is currently set for a user's
+ * profile (your application set the FBML previously by calling the
+ * profile.setFBML method).
+ *
+ * @param int $uid (Optional) User id to lookup; defaults to session.
+ * @param int $type (Optional) 1 for original style, 2 for profile_main boxes
+ *
+ * @return string The FBML
+ */
+ public function &profile_getFBML($uid=null, $type=null) {
+ return $this->call_method('facebook.profile.getFBML',
+ array('uid' => $uid,
+ 'type' => $type));
+ }
+
+ /**
+ * Returns the specified user's application info section for the calling
+ * application. These info sections have either been set via a previous
+ * profile.setInfo call or by the user editing them directly.
+ *
+ * @param int $uid (Optional) User id to lookup; defaults to session.
+ *
+ * @return array Info fields for the current user. See wiki for structure:
+ *
+ * http://wiki.developers.facebook.com/index.php/Profile.getInfo
+ *
+ */
+ public function &profile_getInfo($uid=null) {
+ return $this->call_method('facebook.profile.getInfo',
+ array('uid' => $uid));
+ }
+
+ /**
+ * Returns the options associated with the specified info field for an
+ * application info section.
+ *
+ * @param string $field The title of the field
+ *
+ * @return array An array of info options.
+ */
+ public function &profile_getInfoOptions($field) {
+ return $this->call_method('facebook.profile.getInfoOptions',
+ array('field' => $field));
+ }
+
+ /**
+ * Configures an application info section that the specified user can install
+ * on the Info tab of her profile. For details on the structure of an info
+ * field, please see:
+ *
+ * http://wiki.developers.facebook.com/index.php/Profile.setInfo
+ *
+ * @param string $title Title / header of the info section
+ * @param int $type 1 for text-only, 5 for thumbnail views
+ * @param array $info_fields An array of info fields. See wiki for details.
+ * @param int $uid (Optional)
+ *
+ * @return bool true on success
+ */
+ public function &profile_setInfo($title, $type, $info_fields, $uid=null) {
+ return $this->call_method('facebook.profile.setInfo',
+ array('uid' => $uid,
+ 'type' => $type,
+ 'title' => $title,
+ 'info_fields' => json_encode($info_fields)));
+ }
+
+ /**
+ * Specifies the objects for a field for an application info section. These
+ * options populate the typeahead for a thumbnail.
+ *
+ * @param string $field The title of the field
+ * @param array $options An array of items for a thumbnail, including
+ * 'label', 'link', and optionally 'image',
+ * 'description' and 'sublabel'
+ *
+ * @return bool true on success
+ */
+ public function profile_setInfoOptions($field, $options) {
+ return $this->call_method('facebook.profile.setInfoOptions',
+ array('field' => $field,
+ 'options' => json_encode($options)));
+ }
+
+ /**
+ * Get all the marketplace categories.
+ *
+ * @return array A list of category names
+ */
+ function marketplace_getCategories() {
+ return $this->call_method('facebook.marketplace.getCategories',
+ array());
+ }
+
+ /**
+ * Get all the marketplace subcategories for a particular category.
+ *
+ * @param category The category for which we are pulling subcategories
+ *
+ * @return array A list of subcategory names
+ */
+ function marketplace_getSubCategories($category) {
+ return $this->call_method('facebook.marketplace.getSubCategories',
+ array('category' => $category));
+ }
+
+ /**
+ * Get listings by either listing_id or user.
+ *
+ * @param listing_ids An array of listing_ids (optional)
+ * @param uids An array of user ids (optional)
+ *
+ * @return array The data for matched listings
+ */
+ function marketplace_getListings($listing_ids, $uids) {
+ return $this->call_method('facebook.marketplace.getListings',
+ array('listing_ids' => $listing_ids, 'uids' => $uids));
+ }
+
+ /**
+ * Search for Marketplace listings. All arguments are optional, though at
+ * least one must be filled out to retrieve results.
+ *
+ * @param category The category in which to search (optional)
+ * @param subcategory The subcategory in which to search (optional)
+ * @param query A query string (optional)
+ *
+ * @return array The data for matched listings
+ */
+ function marketplace_search($category, $subcategory, $query) {
+ return $this->call_method('facebook.marketplace.search',
+ array('category' => $category,
+ 'subcategory' => $subcategory,
+ 'query' => $query));
+ }
+
+ /**
+ * Remove a listing from Marketplace.
+ *
+ * @param listing_id The id of the listing to be removed
+ * @param status 'SUCCESS', 'NOT_SUCCESS', or 'DEFAULT'
+ *
+ * @return bool True on success
+ */
+ function marketplace_removeListing($listing_id,
+ $status='DEFAULT',
+ $uid=null) {
+ return $this->call_method('facebook.marketplace.removeListing',
+ array('listing_id' => $listing_id,
+ 'status' => $status,
+ 'uid' => $uid));
+ }
+
+ /**
+ * Create/modify a Marketplace listing for the loggedinuser.
+ *
+ * @param int listing_id The id of a listing to be modified, 0
+ * for a new listing.
+ * @param show_on_profile bool Should we show this listing on the
+ * user's profile
+ * @param listing_attrs array An array of the listing data
+ *
+ * @return int The listing_id (unchanged if modifying an existing listing).
+ */
+ function marketplace_createListing($listing_id,
+ $show_on_profile,
+ $attrs,
+ $uid=null) {
+ return $this->call_method('facebook.marketplace.createListing',
+ array('listing_id' => $listing_id,
+ 'show_on_profile' => $show_on_profile,
+ 'listing_attrs' => json_encode($attrs),
+ 'uid' => $uid));
+ }
+
+ /////////////////////////////////////////////////////////////////////////////
+ // Data Store API
+
+ /**
+ * Set a user preference.
+ *
+ * @param pref_id preference identifier (0-200)
+ * @param value preferece's value
+ * @param uid the user id (defaults to current session user)
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_PARAM
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ * API_EC_PERMISSION_OTHER_USER
+ */
+ public function &data_setUserPreference($pref_id, $value, $uid = null) {
+ return $this->call_method('facebook.data.setUserPreference',
+ array('pref_id' => $pref_id,
+ 'value' => $value,
+ 'uid' => $this->get_uid($uid)));
+ }
+
+ /**
+ * Set a user's all preferences for this application.
+ *
+ * @param values preferece values in an associative arrays
+ * @param replace whether to replace all existing preferences or
+ * merge into them.
+ * @param uid the user id (defaults to current session user)
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_PARAM
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ * API_EC_PERMISSION_OTHER_USER
+ */
+ public function &data_setUserPreferences($values,
+ $replace = false,
+ $uid = null) {
+ return $this->call_method('facebook.data.setUserPreferences',
+ array('values' => json_encode($values),
+ 'replace' => $replace,
+ 'uid' => $this->get_uid($uid)));
+ }
+
+ /**
+ * Get a user preference.
+ *
+ * @param pref_id preference identifier (0-200)
+ * @param uid the user id (defaults to current session user)
+ * @return preference's value
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_PARAM
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ * API_EC_PERMISSION_OTHER_USER
+ */
+ public function &data_getUserPreference($pref_id, $uid = null) {
+ return $this->call_method('facebook.data.getUserPreference',
+ array('pref_id' => $pref_id,
+ 'uid' => $this->get_uid($uid)));
+ }
+
+ /**
+ * Get a user preference.
+ *
+ * @param uid the user id (defaults to current session user)
+ * @return preference values
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ * API_EC_PERMISSION_OTHER_USER
+ */
+ public function &data_getUserPreferences($uid = null) {
+ return $this->call_method('facebook.data.getUserPreferences',
+ array('uid' => $this->get_uid($uid)));
+ }
+
+ /**
+ * Create a new object type.
+ *
+ * @param name object type's name
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_ALREADY_EXISTS
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_createObjectType($name) {
+ return $this->call_method('facebook.data.createObjectType',
+ array('name' => $name));
+ }
+
+ /**
+ * Delete an object type.
+ *
+ * @param obj_type object type's name
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_NOT_FOUND
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_dropObjectType($obj_type) {
+ return $this->call_method('facebook.data.dropObjectType',
+ array('obj_type' => $obj_type));
+ }
+
+ /**
+ * Rename an object type.
+ *
+ * @param obj_type object type's name
+ * @param new_name new object type's name
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_NOT_FOUND
+ * API_EC_DATA_OBJECT_ALREADY_EXISTS
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_renameObjectType($obj_type, $new_name) {
+ return $this->call_method('facebook.data.renameObjectType',
+ array('obj_type' => $obj_type,
+ 'new_name' => $new_name));
+ }
+
+ /**
+ * Add a new property to an object type.
+ *
+ * @param obj_type object type's name
+ * @param prop_name name of the property to add
+ * @param prop_type 1: integer; 2: string; 3: text blob
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_ALREADY_EXISTS
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_defineObjectProperty($obj_type,
+ $prop_name,
+ $prop_type) {
+ return $this->call_method('facebook.data.defineObjectProperty',
+ array('obj_type' => $obj_type,
+ 'prop_name' => $prop_name,
+ 'prop_type' => $prop_type));
+ }
+
+ /**
+ * Remove a previously defined property from an object type.
+ *
+ * @param obj_type object type's name
+ * @param prop_name name of the property to remove
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_NOT_FOUND
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_undefineObjectProperty($obj_type, $prop_name) {
+ return $this->call_method('facebook.data.undefineObjectProperty',
+ array('obj_type' => $obj_type,
+ 'prop_name' => $prop_name));
+ }
+
+ /**
+ * Rename a previously defined property of an object type.
+ *
+ * @param obj_type object type's name
+ * @param prop_name name of the property to rename
+ * @param new_name new name to use
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_NOT_FOUND
+ * API_EC_DATA_OBJECT_ALREADY_EXISTS
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_renameObjectProperty($obj_type, $prop_name,
+ $new_name) {
+ return $this->call_method('facebook.data.renameObjectProperty',
+ array('obj_type' => $obj_type,
+ 'prop_name' => $prop_name,
+ 'new_name' => $new_name));
+ }
+
+ /**
+ * Retrieve a list of all object types that have defined for the application.
+ *
+ * @return a list of object type names
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_PERMISSION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_getObjectTypes() {
+ return $this->call_method('facebook.data.getObjectTypes', array());
+ }
+
+ /**
+ * Get definitions of all properties of an object type.
+ *
+ * @param obj_type object type's name
+ * @return pairs of property name and property types
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_OBJECT_NOT_FOUND
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_getObjectType($obj_type) {
+ return $this->call_method('facebook.data.getObjectType',
+ array('obj_type' => $obj_type));
+ }
+
+ /**
+ * Create a new object.
+ *
+ * @param obj_type object type's name
+ * @param properties (optional) properties to set initially
+ * @return newly created object's id
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_createObject($obj_type, $properties = null) {
+ return $this->call_method('facebook.data.createObject',
+ array('obj_type' => $obj_type,
+ 'properties' => json_encode($properties)));
+ }
+
+ /**
+ * Update an existing object.
+ *
+ * @param obj_id object's id
+ * @param properties new properties
+ * @param replace true for replacing existing properties;
+ * false for merging
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_NOT_FOUND
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_updateObject($obj_id, $properties, $replace = false) {
+ return $this->call_method('facebook.data.updateObject',
+ array('obj_id' => $obj_id,
+ 'properties' => json_encode($properties),
+ 'replace' => $replace));
+ }
+
+ /**
+ * Delete an existing object.
+ *
+ * @param obj_id object's id
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_NOT_FOUND
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_deleteObject($obj_id) {
+ return $this->call_method('facebook.data.deleteObject',
+ array('obj_id' => $obj_id));
+ }
+
+ /**
+ * Delete a list of objects.
+ *
+ * @param obj_ids objects to delete
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_deleteObjects($obj_ids) {
+ return $this->call_method('facebook.data.deleteObjects',
+ array('obj_ids' => json_encode($obj_ids)));
+ }
+
+ /**
+ * Get a single property value of an object.
+ *
+ * @param obj_id object's id
+ * @param prop_name individual property's name
+ * @return individual property's value
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_NOT_FOUND
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_getObjectProperty($obj_id, $prop_name) {
+ return $this->call_method('facebook.data.getObjectProperty',
+ array('obj_id' => $obj_id,
+ 'prop_name' => $prop_name));
+ }
+
+ /**
+ * Get properties of an object.
+ *
+ * @param obj_id object's id
+ * @param prop_names (optional) properties to return; null for all.
+ * @return specified properties of an object
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_NOT_FOUND
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_getObject($obj_id, $prop_names = null) {
+ return $this->call_method('facebook.data.getObject',
+ array('obj_id' => $obj_id,
+ 'prop_names' => json_encode($prop_names)));
+ }
+
+ /**
+ * Get properties of a list of objects.
+ *
+ * @param obj_ids object ids
+ * @param prop_names (optional) properties to return; null for all.
+ * @return specified properties of an object
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_NOT_FOUND
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_getObjects($obj_ids, $prop_names = null) {
+ return $this->call_method('facebook.data.getObjects',
+ array('obj_ids' => json_encode($obj_ids),
+ 'prop_names' => json_encode($prop_names)));
+ }
+
+ /**
+ * Set a single property value of an object.
+ *
+ * @param obj_id object's id
+ * @param prop_name individual property's name
+ * @param prop_value new value to set
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_NOT_FOUND
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_setObjectProperty($obj_id, $prop_name,
+ $prop_value) {
+ return $this->call_method('facebook.data.setObjectProperty',
+ array('obj_id' => $obj_id,
+ 'prop_name' => $prop_name,
+ 'prop_value' => $prop_value));
+ }
+
+ /**
+ * Read hash value by key.
+ *
+ * @param obj_type object type's name
+ * @param key hash key
+ * @param prop_name (optional) individual property's name
+ * @return hash value
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_getHashValue($obj_type, $key, $prop_name = null) {
+ return $this->call_method('facebook.data.getHashValue',
+ array('obj_type' => $obj_type,
+ 'key' => $key,
+ 'prop_name' => $prop_name));
+ }
+
+ /**
+ * Write hash value by key.
+ *
+ * @param obj_type object type's name
+ * @param key hash key
+ * @param value hash value
+ * @param prop_name (optional) individual property's name
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_setHashValue($obj_type,
+ $key,
+ $value,
+ $prop_name = null) {
+ return $this->call_method('facebook.data.setHashValue',
+ array('obj_type' => $obj_type,
+ 'key' => $key,
+ 'value' => $value,
+ 'prop_name' => $prop_name));
+ }
+
+ /**
+ * Increase a hash value by specified increment atomically.
+ *
+ * @param obj_type object type's name
+ * @param key hash key
+ * @param prop_name individual property's name
+ * @param increment (optional) default is 1
+ * @return incremented hash value
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_incHashValue($obj_type,
+ $key,
+ $prop_name,
+ $increment = 1) {
+ return $this->call_method('facebook.data.incHashValue',
+ array('obj_type' => $obj_type,
+ 'key' => $key,
+ 'prop_name' => $prop_name,
+ 'increment' => $increment));
+ }
+
+ /**
+ * Remove a hash key and its values.
+ *
+ * @param obj_type object type's name
+ * @param key hash key
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_removeHashKey($obj_type, $key) {
+ return $this->call_method('facebook.data.removeHashKey',
+ array('obj_type' => $obj_type,
+ 'key' => $key));
+ }
+
+ /**
+ * Remove hash keys and their values.
+ *
+ * @param obj_type object type's name
+ * @param keys hash keys
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_removeHashKeys($obj_type, $keys) {
+ return $this->call_method('facebook.data.removeHashKeys',
+ array('obj_type' => $obj_type,
+ 'keys' => json_encode($keys)));
+ }
+
+ /**
+ * Define an object association.
+ *
+ * @param name name of this association
+ * @param assoc_type 1: one-way 2: two-way symmetric 3: two-way asymmetric
+ * @param assoc_info1 needed info about first object type
+ * @param assoc_info2 needed info about second object type
+ * @param inverse (optional) name of reverse association
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_ALREADY_EXISTS
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_defineAssociation($name, $assoc_type, $assoc_info1,
+ $assoc_info2, $inverse = null) {
+ return $this->call_method('facebook.data.defineAssociation',
+ array('name' => $name,
+ 'assoc_type' => $assoc_type,
+ 'assoc_info1' => json_encode($assoc_info1),
+ 'assoc_info2' => json_encode($assoc_info2),
+ 'inverse' => $inverse));
+ }
+
+ /**
+ * Undefine an object association.
+ *
+ * @param name name of this association
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_NOT_FOUND
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_undefineAssociation($name) {
+ return $this->call_method('facebook.data.undefineAssociation',
+ array('name' => $name));
+ }
+
+ /**
+ * Rename an object association or aliases.
+ *
+ * @param name name of this association
+ * @param new_name (optional) new name of this association
+ * @param new_alias1 (optional) new alias for object type 1
+ * @param new_alias2 (optional) new alias for object type 2
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_ALREADY_EXISTS
+ * API_EC_DATA_OBJECT_NOT_FOUND
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_renameAssociation($name, $new_name, $new_alias1 = null,
+ $new_alias2 = null) {
+ return $this->call_method('facebook.data.renameAssociation',
+ array('name' => $name,
+ 'new_name' => $new_name,
+ 'new_alias1' => $new_alias1,
+ 'new_alias2' => $new_alias2));
+ }
+
+ /**
+ * Get definition of an object association.
+ *
+ * @param name name of this association
+ * @return specified association
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_NOT_FOUND
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_getAssociationDefinition($name) {
+ return $this->call_method('facebook.data.getAssociationDefinition',
+ array('name' => $name));
+ }
+
+ /**
+ * Get definition of all associations.
+ *
+ * @return all defined associations
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_PERMISSION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_getAssociationDefinitions() {
+ return $this->call_method('facebook.data.getAssociationDefinitions',
+ array());
+ }
+
+ /**
+ * Create or modify an association between two objects.
+ *
+ * @param name name of association
+ * @param obj_id1 id of first object
+ * @param obj_id2 id of second object
+ * @param data (optional) extra string data to store
+ * @param assoc_time (optional) extra time data; default to creation time
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_setAssociation($name, $obj_id1, $obj_id2, $data = null,
+ $assoc_time = null) {
+ return $this->call_method('facebook.data.setAssociation',
+ array('name' => $name,
+ 'obj_id1' => $obj_id1,
+ 'obj_id2' => $obj_id2,
+ 'data' => $data,
+ 'assoc_time' => $assoc_time));
+ }
+
+ /**
+ * Create or modify associations between objects.
+ *
+ * @param assocs associations to set
+ * @param name (optional) name of association
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_setAssociations($assocs, $name = null) {
+ return $this->call_method('facebook.data.setAssociations',
+ array('assocs' => json_encode($assocs),
+ 'name' => $name));
+ }
+
+ /**
+ * Remove an association between two objects.
+ *
+ * @param name name of association
+ * @param obj_id1 id of first object
+ * @param obj_id2 id of second object
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_NOT_FOUND
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_removeAssociation($name, $obj_id1, $obj_id2) {
+ return $this->call_method('facebook.data.removeAssociation',
+ array('name' => $name,
+ 'obj_id1' => $obj_id1,
+ 'obj_id2' => $obj_id2));
+ }
+
+ /**
+ * Remove associations between objects by specifying pairs of object ids.
+ *
+ * @param assocs associations to remove
+ * @param name (optional) name of association
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_NOT_FOUND
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_removeAssociations($assocs, $name = null) {
+ return $this->call_method('facebook.data.removeAssociations',
+ array('assocs' => json_encode($assocs),
+ 'name' => $name));
+ }
+
+ /**
+ * Remove associations between objects by specifying one object id.
+ *
+ * @param name name of association
+ * @param obj_id who's association to remove
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_NOT_FOUND
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_removeAssociatedObjects($name, $obj_id) {
+ return $this->call_method('facebook.data.removeAssociatedObjects',
+ array('name' => $name,
+ 'obj_id' => $obj_id));
+ }
+
+ /**
+ * Retrieve a list of associated objects.
+ *
+ * @param name name of association
+ * @param obj_id who's association to retrieve
+ * @param no_data only return object ids
+ * @return associated objects
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_NOT_FOUND
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_getAssociatedObjects($name, $obj_id, $no_data = true) {
+ return $this->call_method('facebook.data.getAssociatedObjects',
+ array('name' => $name,
+ 'obj_id' => $obj_id,
+ 'no_data' => $no_data));
+ }
+
+ /**
+ * Count associated objects.
+ *
+ * @param name name of association
+ * @param obj_id who's association to retrieve
+ * @return associated object's count
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_NOT_FOUND
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_getAssociatedObjectCount($name, $obj_id) {
+ return $this->call_method('facebook.data.getAssociatedObjectCount',
+ array('name' => $name,
+ 'obj_id' => $obj_id));
+ }
+
+ /**
+ * Get a list of associated object counts.
+ *
+ * @param name name of association
+ * @param obj_ids whose association to retrieve
+ * @return associated object counts
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_NOT_FOUND
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_getAssociatedObjectCounts($name, $obj_ids) {
+ return $this->call_method('facebook.data.getAssociatedObjectCounts',
+ array('name' => $name,
+ 'obj_ids' => json_encode($obj_ids)));
+ }
+
+ /**
+ * Find all associations between two objects.
+ *
+ * @param obj_id1 id of first object
+ * @param obj_id2 id of second object
+ * @param no_data only return association names without data
+ * @return all associations between objects
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_getAssociations($obj_id1, $obj_id2, $no_data = true) {
+ return $this->call_method('facebook.data.getAssociations',
+ array('obj_id1' => $obj_id1,
+ 'obj_id2' => $obj_id2,
+ 'no_data' => $no_data));
+ }
+
+ /**
+ * Get the properties that you have set for an app.
+ *
+ * @param properties List of properties names to fetch
+ *
+ * @return array A map from property name to value
+ */
+ public function admin_getAppProperties($properties) {
+ return json_decode(
+ $this->call_method('facebook.admin.getAppProperties',
+ array('properties' => json_encode($properties))), true);
+ }
+
+ /**
+ * Set properties for an app.
+ *
+ * @param properties A map from property names to values
+ *
+ * @return bool true on success
+ */
+ public function admin_setAppProperties($properties) {
+ return $this->call_method('facebook.admin.setAppProperties',
+ array('properties' => json_encode($properties)));
+ }
+
+ /**
+ * Returns the allocation limit value for a specified integration point name
+ * Integration point names are defined in lib/api/karma/constants.php in the
+ * limit_map.
+ *
+ * @param string $integration_point_name Name of an integration point
+ * (see developer wiki for list).
+ *
+ * @return int Integration point allocation value
+ */
+ public function &admin_getAllocation($integration_point_name) {
+ return $this->call_method('facebook.admin.getAllocation',
+ array('integration_point_name' => $integration_point_name));
+ }
+
+ /**
+ * Returns values for the specified metrics for the current application, in
+ * the given time range. The metrics are collected for fixed-length periods,
+ * and the times represent midnight at the end of each period.
+ *
+ * @param start_time unix time for the start of the range
+ * @param end_time unix time for the end of the range
+ * @param period number of seconds in the desired period
+ * @param metrics list of metrics to look up
+ *
+ * @return array A map of the names and values for those metrics
+ */
+ public function &admin_getMetrics($start_time, $end_time, $period, $metrics) {
+ return $this->call_method('admin.getMetrics',
+ array('start_time' => $start_time,
+ 'end_time' => $end_time,
+ 'period' => $period,
+ 'metrics' => json_encode($metrics)));
+ }
+
+ /**
+ * Sets application restriction info.
+ *
+ * Applications can restrict themselves to only a limited user demographic
+ * based on users' age and/or location or based on static predefined types
+ * specified by facebook for specifying diff age restriction for diff
+ * locations.
+ *
+ * @param array $restriction_info The age restriction settings to set.
+ *
+ * @return bool true on success
+ */
+ public function admin_setRestrictionInfo($restriction_info = null) {
+ $restriction_str = null;
+ if (!empty($restriction_info)) {
+ $restriction_str = json_encode($restriction_info);
+ }
+ return $this->call_method('admin.setRestrictionInfo',
+ array('restriction_str' => $restriction_str));
+ }
+
+ /**
+ * Gets application restriction info.
+ *
+ * Applications can restrict themselves to only a limited user demographic
+ * based on users' age and/or location or based on static predefined types
+ * specified by facebook for specifying diff age restriction for diff
+ * locations.
+ *
+ * @return array The age restriction settings for this application.
+ */
+ public function admin_getRestrictionInfo() {
+ return json_decode(
+ $this->call_method('admin.getRestrictionInfo', array()),
+ true);
+ }
+
+ /* UTILITY FUNCTIONS */
+
+ /**
+ * Calls the specified method with the specified parameters.
+ *
+ * @param string $method Name of the Facebook method to invoke
+ * @param array $params A map of param names => param values
+ *
+ * @return mixed Result of method call
+ */
+ public function & call_method($method, $params) {
+ //Check if we are in batch mode
+ if($this->batch_queue === null) {
+ if ($this->call_as_apikey) {
+ $params['call_as_apikey'] = $this->call_as_apikey;
+ }
+ $xml = $this->post_request($method, $params);
+ $result = $this->convert_xml_to_result($xml, $method, $params);
+
+ if (is_array($result) && isset($result['error_code'])) {
+ throw new FacebookRestClientException($result['error_msg'],
+ $result['error_code']);
+ }
+ }
+ else {
+ $result = null;
+ $batch_item = array('m' => $method, 'p' => $params, 'r' => & $result);
+ $this->batch_queue[] = $batch_item;
+ }
+
+ return $result;
+ }
+
+ private function convert_xml_to_result($xml, $method, $params) {
+ $sxml = simplexml_load_string($xml);
+ $result = self::convert_simplexml_to_array($sxml);
+
+
+ if (!empty($GLOBALS['facebook_config']['debug'])) {
+ // output the raw xml and its corresponding php object, for debugging:
+ print '<div style="margin: 10px 30px; padding: 5px; border: 2px solid black; background: gray; color: white; font-size: 12px; font-weight: bold;">';
+ $this->cur_id++;
+ print $this->cur_id . ': Called ' . $method . ', show ' .
+ '<a href=# onclick="return toggleDisplay(' . $this->cur_id . ', \'params\');">Params</a> | '.
+ '<a href=# onclick="return toggleDisplay(' . $this->cur_id . ', \'xml\');">XML</a> | '.
+ '<a href=# onclick="return toggleDisplay(' . $this->cur_id . ', \'sxml\');">SXML</a> | '.
+ '<a href=# onclick="return toggleDisplay(' . $this->cur_id . ', \'php\');">PHP</a>';
+ print '<pre id="params'.$this->cur_id.'" style="display: none; overflow: auto;">'.print_r($params, true).'</pre>';
+ print '<pre id="xml'.$this->cur_id.'" style="display: none; overflow: auto;">'.htmlspecialchars($xml).'</pre>';
+ print '<pre id="php'.$this->cur_id.'" style="display: none; overflow: auto;">'.print_r($result, true).'</pre>';
+ print '<pre id="sxml'.$this->cur_id.'" style="display: none; overflow: auto;">'.print_r($sxml, true).'</pre>';
+ print '</div>';
+ }
+ return $result;
+ }
+
+ private function create_post_string($method, $params) {
+ $params['method'] = $method;
+ $params['session_key'] = $this->session_key;
+ $params['api_key'] = $this->api_key;
+ $params['call_id'] = microtime(true);
+ if ($params['call_id'] <= $this->last_call_id) {
+ $params['call_id'] = $this->last_call_id + 0.001;
+ }
+ $this->last_call_id = $params['call_id'];
+ if (!isset($params['v'])) {
+ $params['v'] = '1.0';
+ }
+ $post_params = array();
+ foreach ($params as $key => &$val) {
+ if (is_array($val)) $val = implode(',', $val);
+ $post_params[] = $key.'='.urlencode($val);
+ }
+ $secret = $this->secret;
+ $post_params[] = 'sig='.Facebook::generate_sig($params, $secret);
+ return implode('&', $post_params);
+ }
+
+ public function post_request($method, $params) {
+
+ $post_string = $this->create_post_string($method, $params);
+
+ if (function_exists('curl_init')) {
+ // Use CURL if installed...
+ $useragent = 'Facebook API PHP5 Client 1.1 (curl) ' . phpversion();
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, $this->server_addr);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $post_string);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_USERAGENT, $useragent);
+ $result = curl_exec($ch);
+ curl_close($ch);
+ } else {
+ // Non-CURL based version...
+ $content_type = 'application/x-www-form-urlencoded';
+ $user_agent = 'Facebook API PHP5 Client 1.1 (non-curl) '.phpversion();
+ $context =
+ array('http' =>
+ array('method' => 'POST',
+ 'header' => 'Content-type: '.$content_type."\r\n".
+ 'User-Agent: '.$user_agent."\r\n".
+ 'Content-length: ' . strlen($post_string),
+ 'content' => $post_string));
+ $contextid=stream_context_create($context);
+ $sock=fopen($this->server_addr, 'r', false, $contextid);
+ if ($sock) {
+ $result='';
+ while (!feof($sock))
+ $result.=fgets($sock, 4096);
+
+ fclose($sock);
+ }
+ }
+ return $result;
+ }
+
+ public static function convert_simplexml_to_array($sxml) {
+ $arr = array();
+ if ($sxml) {
+ foreach ($sxml as $k => $v) {
+ if ($sxml['list']) {
+ $arr[] = self::convert_simplexml_to_array($v);
+ } else {
+ $arr[$k] = self::convert_simplexml_to_array($v);
+ }
+ }
+ }
+ if (sizeof($arr) > 0) {
+ return $arr;
+ } else {
+ return (string)$sxml;
+ }
+ }
+
+ private function get_uid($uid) {
+ return $uid ? $uid : $this->user;
+ }
+}
+
+
+class FacebookRestClientException extends Exception {
+}
+
+// Supporting methods and values------
+
+/**
+ * Error codes and descriptions for the Facebook API.
+ */
+
+class FacebookAPIErrorCodes {
+
+ const API_EC_SUCCESS = 0;
+
+ /*
+ * GENERAL ERRORS
+ */
+ const API_EC_UNKNOWN = 1;
+ const API_EC_SERVICE = 2;
+ const API_EC_METHOD = 3;
+ const API_EC_TOO_MANY_CALLS = 4;
+ const API_EC_BAD_IP = 5;
+
+ /*
+ * PARAMETER ERRORS
+ */
+ const API_EC_PARAM = 100;
+ const API_EC_PARAM_API_KEY = 101;
+ const API_EC_PARAM_SESSION_KEY = 102;
+ const API_EC_PARAM_CALL_ID = 103;
+ const API_EC_PARAM_SIGNATURE = 104;
+ const API_EC_PARAM_USER_ID = 110;
+ const API_EC_PARAM_USER_FIELD = 111;
+ const API_EC_PARAM_SOCIAL_FIELD = 112;
+ const API_EC_PARAM_ALBUM_ID = 120;
+ const API_EC_PARAM_BAD_EID = 150;
+ const API_EC_PARAM_UNKNOWN_CITY = 151;
+
+ /*
+ * USER PERMISSIONS ERRORS
+ */
+ const API_EC_PERMISSION = 200;
+ const API_EC_PERMISSION_USER = 210;
+ const API_EC_PERMISSION_ALBUM = 220;
+ const API_EC_PERMISSION_PHOTO = 221;
+ const API_EC_PERMISSION_EVENT = 290;
+ const API_EC_PERMISSION_RSVP_EVENT = 299;
+
+ const FQL_EC_PARSER = 601;
+ const FQL_EC_UNKNOWN_FIELD = 602;
+ const FQL_EC_UNKNOWN_TABLE = 603;
+ const FQL_EC_NOT_INDEXABLE = 604;
+
+ /**
+ * DATA STORE API ERRORS
+ */
+ const API_EC_DATA_UNKNOWN_ERROR = 800;
+ const API_EC_DATA_INVALID_OPERATION = 801;
+ const API_EC_DATA_QUOTA_EXCEEDED = 802;
+ const API_EC_DATA_OBJECT_NOT_FOUND = 803;
+ const API_EC_DATA_OBJECT_ALREADY_EXISTS = 804;
+ const API_EC_DATA_DATABASE_ERROR = 805;
+
+ /*
+ * Batch ERROR
+ */
+ const API_EC_BATCH_ALREADY_STARTED = 900;
+ const API_EC_BATCH_NOT_STARTED = 901;
+ const API_EC_BATCH_METHOD_NOT_ALLOWED_IN_BATCH_MODE = 902;
+
+ public static $api_error_descriptions = array(
+ API_EC_SUCCESS => 'Success',
+ API_EC_UNKNOWN => 'An unknown error occurred',
+ API_EC_SERVICE => 'Service temporarily unavailable',
+ API_EC_METHOD => 'Unknown method',
+ API_EC_TOO_MANY_CALLS => 'Application request limit reached',
+ API_EC_BAD_IP => 'Unauthorized source IP address',
+ API_EC_PARAM => 'Invalid parameter',
+ API_EC_PARAM_API_KEY => 'Invalid API key',
+ API_EC_PARAM_SESSION_KEY => 'Session key invalid or no longer valid',
+ API_EC_PARAM_CALL_ID => 'Call_id must be greater than previous',
+ API_EC_PARAM_SIGNATURE => 'Incorrect signature',
+ API_EC_PARAM_USER_ID => 'Invalid user id',
+ API_EC_PARAM_USER_FIELD => 'Invalid user info field',
+ API_EC_PARAM_SOCIAL_FIELD => 'Invalid user field',
+ API_EC_PARAM_ALBUM_ID => 'Invalid album id',
+ API_EC_PARAM_BAD_EID => 'Invalid eid',
+ API_EC_PARAM_UNKNOWN_CITY => 'Unknown city',
+ API_EC_PERMISSION => 'Permissions error',
+ API_EC_PERMISSION_USER => 'User not visible',
+ API_EC_PERMISSION_ALBUM => 'Album not visible',
+ API_EC_PERMISSION_PHOTO => 'Photo not visible',
+ API_EC_PERMISSION_EVENT => 'Creating and modifying events required the extended permission create_event',
+ API_EC_PERMISSION_RSVP_EVENT => 'RSVPing to events required the extended permission rsvp_event',
+ FQL_EC_PARSER => 'FQL: Parser Error',
+ FQL_EC_UNKNOWN_FIELD => 'FQL: Unknown Field',
+ FQL_EC_UNKNOWN_TABLE => 'FQL: Unknown Table',
+ FQL_EC_NOT_INDEXABLE => 'FQL: Statement not indexable',
+ FQL_EC_UNKNOWN_FUNCTION => 'FQL: Attempted to call unknown function',
+ FQL_EC_INVALID_PARAM => 'FQL: Invalid parameter passed in',
+ API_EC_DATA_UNKNOWN_ERROR => 'Unknown data store API error',
+ API_EC_DATA_INVALID_OPERATION => 'Invalid operation',
+ API_EC_DATA_QUOTA_EXCEEDED => 'Data store allowable quota was exceeded',
+ API_EC_DATA_OBJECT_NOT_FOUND => 'Specified object cannot be found',
+ API_EC_DATA_OBJECT_ALREADY_EXISTS => 'Specified object already exists',
+ API_EC_DATA_DATABASE_ERROR => 'A database error occurred. Please try again',
+ API_EC_BATCH_ALREADY_STARTED => 'begin_batch already called, please make sure to call end_batch first',
+ API_EC_BATCH_NOT_STARTED => 'end_batch called before start_batch',
+ API_EC_BATCH_METHOD_NOT_ALLOWED_IN_BATCH_MODE => 'This method is not allowed in batch mode',
+ );
+}
diff --git a/extlib/facebook/jsonwrapper/JSON/JSON.php b/extlib/facebook/jsonwrapper/JSON/JSON.php
new file mode 100644
index 000000000..0cddbddb4
--- /dev/null
+++ b/extlib/facebook/jsonwrapper/JSON/JSON.php
@@ -0,0 +1,806 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * Converts to and from JSON format.
+ *
+ * JSON (JavaScript Object Notation) is a lightweight data-interchange
+ * format. It is easy for humans to read and write. It is easy for machines
+ * to parse and generate. It is based on a subset of the JavaScript
+ * Programming Language, Standard ECMA-262 3rd Edition - December 1999.
+ * This feature can also be found in Python. JSON is a text format that is
+ * completely language independent but uses conventions that are familiar
+ * to programmers of the C-family of languages, including C, C++, C#, Java,
+ * JavaScript, Perl, TCL, and many others. These properties make JSON an
+ * ideal data-interchange language.
+ *
+ * This package provides a simple encoder and decoder for JSON notation. It
+ * is intended for use with client-side Javascript applications that make
+ * use of HTTPRequest to perform server communication functions - data can
+ * be encoded into JSON notation for use in a client-side javascript, or
+ * decoded from incoming Javascript requests. JSON format is native to
+ * Javascript, and can be directly eval()'ed with no further parsing
+ * overhead
+ *
+ * All strings should be in ASCII or UTF-8 format!
+ *
+ * LICENSE: Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met: Redistributions of source code must retain the
+ * above copyright notice, this list of conditions and the following
+ * disclaimer. Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
+ * NO EVENT SHALL CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+ * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+ * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+ * DAMAGE.
+ *
+ * @category
+ * @package Services_JSON
+ * @author Michal Migurski <mike-json@teczno.com>
+ * @author Matt Knapp <mdknapp[at]gmail[dot]com>
+ * @author Brett Stimmerman <brettstimmerman[at]gmail[dot]com>
+ * @copyright 2005 Michal Migurski
+ * @version CVS: $Id: JSON.php,v 1.31 2006/06/28 05:54:17 migurski Exp $
+ * @license http://www.opensource.org/licenses/bsd-license.php
+ * @link http://pear.php.net/pepr/pepr-proposal-show.php?id=198
+ */
+
+/**
+ * Marker constant for Services_JSON::decode(), used to flag stack state
+ */
+define('SERVICES_JSON_SLICE', 1);
+
+/**
+ * Marker constant for Services_JSON::decode(), used to flag stack state
+ */
+define('SERVICES_JSON_IN_STR', 2);
+
+/**
+ * Marker constant for Services_JSON::decode(), used to flag stack state
+ */
+define('SERVICES_JSON_IN_ARR', 3);
+
+/**
+ * Marker constant for Services_JSON::decode(), used to flag stack state
+ */
+define('SERVICES_JSON_IN_OBJ', 4);
+
+/**
+ * Marker constant for Services_JSON::decode(), used to flag stack state
+ */
+define('SERVICES_JSON_IN_CMT', 5);
+
+/**
+ * Behavior switch for Services_JSON::decode()
+ */
+define('SERVICES_JSON_LOOSE_TYPE', 16);
+
+/**
+ * Behavior switch for Services_JSON::decode()
+ */
+define('SERVICES_JSON_SUPPRESS_ERRORS', 32);
+
+/**
+ * Converts to and from JSON format.
+ *
+ * Brief example of use:
+ *
+ * <code>
+ * // create a new instance of Services_JSON
+ * $json = new Services_JSON();
+ *
+ * // convert a complexe value to JSON notation, and send it to the browser
+ * $value = array('foo', 'bar', array(1, 2, 'baz'), array(3, array(4)));
+ * $output = $json->encode($value);
+ *
+ * print($output);
+ * // prints: ["foo","bar",[1,2,"baz"],[3,[4]]]
+ *
+ * // accept incoming POST data, assumed to be in JSON notation
+ * $input = file_get_contents('php://input', 1000000);
+ * $value = $json->decode($input);
+ * </code>
+ */
+class Services_JSON
+{
+ /**
+ * constructs a new JSON instance
+ *
+ * @param int $use object behavior flags; combine with boolean-OR
+ *
+ * possible values:
+ * - SERVICES_JSON_LOOSE_TYPE: loose typing.
+ * "{...}" syntax creates associative arrays
+ * instead of objects in decode().
+ * - SERVICES_JSON_SUPPRESS_ERRORS: error suppression.
+ * Values which can't be encoded (e.g. resources)
+ * appear as NULL instead of throwing errors.
+ * By default, a deeply-nested resource will
+ * bubble up with an error, so all return values
+ * from encode() should be checked with isError()
+ */
+ function Services_JSON($use = 0)
+ {
+ $this->use = $use;
+ }
+
+ /**
+ * convert a string from one UTF-16 char to one UTF-8 char
+ *
+ * Normally should be handled by mb_convert_encoding, but
+ * provides a slower PHP-only method for installations
+ * that lack the multibye string extension.
+ *
+ * @param string $utf16 UTF-16 character
+ * @return string UTF-8 character
+ * @access private
+ */
+ function utf162utf8($utf16)
+ {
+ // oh please oh please oh please oh please oh please
+ if(function_exists('mb_convert_encoding')) {
+ return mb_convert_encoding($utf16, 'UTF-8', 'UTF-16');
+ }
+
+ $bytes = (ord($utf16{0}) << 8) | ord($utf16{1});
+
+ switch(true) {
+ case ((0x7F & $bytes) == $bytes):
+ // this case should never be reached, because we are in ASCII range
+ // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ return chr(0x7F & $bytes);
+
+ case (0x07FF & $bytes) == $bytes:
+ // return a 2-byte UTF-8 character
+ // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ return chr(0xC0 | (($bytes >> 6) & 0x1F))
+ . chr(0x80 | ($bytes & 0x3F));
+
+ case (0xFFFF & $bytes) == $bytes:
+ // return a 3-byte UTF-8 character
+ // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ return chr(0xE0 | (($bytes >> 12) & 0x0F))
+ . chr(0x80 | (($bytes >> 6) & 0x3F))
+ . chr(0x80 | ($bytes & 0x3F));
+ }
+
+ // ignoring UTF-32 for now, sorry
+ return '';
+ }
+
+ /**
+ * convert a string from one UTF-8 char to one UTF-16 char
+ *
+ * Normally should be handled by mb_convert_encoding, but
+ * provides a slower PHP-only method for installations
+ * that lack the multibye string extension.
+ *
+ * @param string $utf8 UTF-8 character
+ * @return string UTF-16 character
+ * @access private
+ */
+ function utf82utf16($utf8)
+ {
+ // oh please oh please oh please oh please oh please
+ if(function_exists('mb_convert_encoding')) {
+ return mb_convert_encoding($utf8, 'UTF-16', 'UTF-8');
+ }
+
+ switch(strlen($utf8)) {
+ case 1:
+ // this case should never be reached, because we are in ASCII range
+ // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ return $utf8;
+
+ case 2:
+ // return a UTF-16 character from a 2-byte UTF-8 char
+ // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ return chr(0x07 & (ord($utf8{0}) >> 2))
+ . chr((0xC0 & (ord($utf8{0}) << 6))
+ | (0x3F & ord($utf8{1})));
+
+ case 3:
+ // return a UTF-16 character from a 3-byte UTF-8 char
+ // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ return chr((0xF0 & (ord($utf8{0}) << 4))
+ | (0x0F & (ord($utf8{1}) >> 2)))
+ . chr((0xC0 & (ord($utf8{1}) << 6))
+ | (0x7F & ord($utf8{2})));
+ }
+
+ // ignoring UTF-32 for now, sorry
+ return '';
+ }
+
+ /**
+ * encodes an arbitrary variable into JSON format
+ *
+ * @param mixed $var any number, boolean, string, array, or object to be encoded.
+ * see argument 1 to Services_JSON() above for array-parsing behavior.
+ * if var is a strng, note that encode() always expects it
+ * to be in ASCII or UTF-8 format!
+ *
+ * @return mixed JSON string representation of input var or an error if a problem occurs
+ * @access public
+ */
+ function encode($var)
+ {
+ switch (gettype($var)) {
+ case 'boolean':
+ return $var ? 'true' : 'false';
+
+ case 'NULL':
+ return 'null';
+
+ case 'integer':
+ return (int) $var;
+
+ case 'double':
+ case 'float':
+ return (float) $var;
+
+ case 'string':
+ // STRINGS ARE EXPECTED TO BE IN ASCII OR UTF-8 FORMAT
+ $ascii = '';
+ $strlen_var = strlen($var);
+
+ /*
+ * Iterate over every character in the string,
+ * escaping with a slash or encoding to UTF-8 where necessary
+ */
+ for ($c = 0; $c < $strlen_var; ++$c) {
+
+ $ord_var_c = ord($var{$c});
+
+ switch (true) {
+ case $ord_var_c == 0x08:
+ $ascii .= '\b';
+ break;
+ case $ord_var_c == 0x09:
+ $ascii .= '\t';
+ break;
+ case $ord_var_c == 0x0A:
+ $ascii .= '\n';
+ break;
+ case $ord_var_c == 0x0C:
+ $ascii .= '\f';
+ break;
+ case $ord_var_c == 0x0D:
+ $ascii .= '\r';
+ break;
+
+ case $ord_var_c == 0x22:
+ case $ord_var_c == 0x2F:
+ case $ord_var_c == 0x5C:
+ // double quote, slash, slosh
+ $ascii .= '\\'.$var{$c};
+ break;
+
+ case (($ord_var_c >= 0x20) && ($ord_var_c <= 0x7F)):
+ // characters U-00000000 - U-0000007F (same as ASCII)
+ $ascii .= $var{$c};
+ break;
+
+ case (($ord_var_c & 0xE0) == 0xC0):
+ // characters U-00000080 - U-000007FF, mask 110XXXXX
+ // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ $char = pack('C*', $ord_var_c, ord($var{$c + 1}));
+ $c += 1;
+ $utf16 = $this->utf82utf16($char);
+ $ascii .= sprintf('\u%04s', bin2hex($utf16));
+ break;
+
+ case (($ord_var_c & 0xF0) == 0xE0):
+ // characters U-00000800 - U-0000FFFF, mask 1110XXXX
+ // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ $char = pack('C*', $ord_var_c,
+ ord($var{$c + 1}),
+ ord($var{$c + 2}));
+ $c += 2;
+ $utf16 = $this->utf82utf16($char);
+ $ascii .= sprintf('\u%04s', bin2hex($utf16));
+ break;
+
+ case (($ord_var_c & 0xF8) == 0xF0):
+ // characters U-00010000 - U-001FFFFF, mask 11110XXX
+ // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ $char = pack('C*', $ord_var_c,
+ ord($var{$c + 1}),
+ ord($var{$c + 2}),
+ ord($var{$c + 3}));
+ $c += 3;
+ $utf16 = $this->utf82utf16($char);
+ $ascii .= sprintf('\u%04s', bin2hex($utf16));
+ break;
+
+ case (($ord_var_c & 0xFC) == 0xF8):
+ // characters U-00200000 - U-03FFFFFF, mask 111110XX
+ // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ $char = pack('C*', $ord_var_c,
+ ord($var{$c + 1}),
+ ord($var{$c + 2}),
+ ord($var{$c + 3}),
+ ord($var{$c + 4}));
+ $c += 4;
+ $utf16 = $this->utf82utf16($char);
+ $ascii .= sprintf('\u%04s', bin2hex($utf16));
+ break;
+
+ case (($ord_var_c & 0xFE) == 0xFC):
+ // characters U-04000000 - U-7FFFFFFF, mask 1111110X
+ // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ $char = pack('C*', $ord_var_c,
+ ord($var{$c + 1}),
+ ord($var{$c + 2}),
+ ord($var{$c + 3}),
+ ord($var{$c + 4}),
+ ord($var{$c + 5}));
+ $c += 5;
+ $utf16 = $this->utf82utf16($char);
+ $ascii .= sprintf('\u%04s', bin2hex($utf16));
+ break;
+ }
+ }
+
+ return '"'.$ascii.'"';
+
+ case 'array':
+ /*
+ * As per JSON spec if any array key is not an integer
+ * we must treat the the whole array as an object. We
+ * also try to catch a sparsely populated associative
+ * array with numeric keys here because some JS engines
+ * will create an array with empty indexes up to
+ * max_index which can cause memory issues and because
+ * the keys, which may be relevant, will be remapped
+ * otherwise.
+ *
+ * As per the ECMA and JSON specification an object may
+ * have any string as a property. Unfortunately due to
+ * a hole in the ECMA specification if the key is a
+ * ECMA reserved word or starts with a digit the
+ * parameter is only accessible using ECMAScript's
+ * bracket notation.
+ */
+
+ // treat as a JSON object
+ if (is_array($var) && count($var) && (array_keys($var) !== range(0, sizeof($var) - 1))) {
+ $properties = array_map(array($this, 'name_value'),
+ array_keys($var),
+ array_values($var));
+
+ foreach($properties as $property) {
+ if(Services_JSON::isError($property)) {
+ return $property;
+ }
+ }
+
+ return '{' . join(',', $properties) . '}';
+ }
+
+ // treat it like a regular array
+ $elements = array_map(array($this, 'encode'), $var);
+
+ foreach($elements as $element) {
+ if(Services_JSON::isError($element)) {
+ return $element;
+ }
+ }
+
+ return '[' . join(',', $elements) . ']';
+
+ case 'object':
+ $vars = get_object_vars($var);
+
+ $properties = array_map(array($this, 'name_value'),
+ array_keys($vars),
+ array_values($vars));
+
+ foreach($properties as $property) {
+ if(Services_JSON::isError($property)) {
+ return $property;
+ }
+ }
+
+ return '{' . join(',', $properties) . '}';
+
+ default:
+ return ($this->use & SERVICES_JSON_SUPPRESS_ERRORS)
+ ? 'null'
+ : new Services_JSON_Error(gettype($var)." can not be encoded as JSON string");
+ }
+ }
+
+ /**
+ * array-walking function for use in generating JSON-formatted name-value pairs
+ *
+ * @param string $name name of key to use
+ * @param mixed $value reference to an array element to be encoded
+ *
+ * @return string JSON-formatted name-value pair, like '"name":value'
+ * @access private
+ */
+ function name_value($name, $value)
+ {
+ $encoded_value = $this->encode($value);
+
+ if(Services_JSON::isError($encoded_value)) {
+ return $encoded_value;
+ }
+
+ return $this->encode(strval($name)) . ':' . $encoded_value;
+ }
+
+ /**
+ * reduce a string by removing leading and trailing comments and whitespace
+ *
+ * @param $str string string value to strip of comments and whitespace
+ *
+ * @return string string value stripped of comments and whitespace
+ * @access private
+ */
+ function reduce_string($str)
+ {
+ $str = preg_replace(array(
+
+ // eliminate single line comments in '// ...' form
+ '#^\s*//(.+)$#m',
+
+ // eliminate multi-line comments in '/* ... */' form, at start of string
+ '#^\s*/\*(.+)\*/#Us',
+
+ // eliminate multi-line comments in '/* ... */' form, at end of string
+ '#/\*(.+)\*/\s*$#Us'
+
+ ), '', $str);
+
+ // eliminate extraneous space
+ return trim($str);
+ }
+
+ /**
+ * decodes a JSON string into appropriate variable
+ *
+ * @param string $str JSON-formatted string
+ *
+ * @return mixed number, boolean, string, array, or object
+ * corresponding to given JSON input string.
+ * See argument 1 to Services_JSON() above for object-output behavior.
+ * Note that decode() always returns strings
+ * in ASCII or UTF-8 format!
+ * @access public
+ */
+ function decode($str)
+ {
+ $str = $this->reduce_string($str);
+
+ switch (strtolower($str)) {
+ case 'true':
+ return true;
+
+ case 'false':
+ return false;
+
+ case 'null':
+ return null;
+
+ default:
+ $m = array();
+
+ if (is_numeric($str)) {
+ // Lookie-loo, it's a number
+
+ // This would work on its own, but I'm trying to be
+ // good about returning integers where appropriate:
+ // return (float)$str;
+
+ // Return float or int, as appropriate
+ return ((float)$str == (integer)$str)
+ ? (integer)$str
+ : (float)$str;
+
+ } elseif (preg_match('/^("|\').*(\1)$/s', $str, $m) && $m[1] == $m[2]) {
+ // STRINGS RETURNED IN UTF-8 FORMAT
+ $delim = substr($str, 0, 1);
+ $chrs = substr($str, 1, -1);
+ $utf8 = '';
+ $strlen_chrs = strlen($chrs);
+
+ for ($c = 0; $c < $strlen_chrs; ++$c) {
+
+ $substr_chrs_c_2 = substr($chrs, $c, 2);
+ $ord_chrs_c = ord($chrs{$c});
+
+ switch (true) {
+ case $substr_chrs_c_2 == '\b':
+ $utf8 .= chr(0x08);
+ ++$c;
+ break;
+ case $substr_chrs_c_2 == '\t':
+ $utf8 .= chr(0x09);
+ ++$c;
+ break;
+ case $substr_chrs_c_2 == '\n':
+ $utf8 .= chr(0x0A);
+ ++$c;
+ break;
+ case $substr_chrs_c_2 == '\f':
+ $utf8 .= chr(0x0C);
+ ++$c;
+ break;
+ case $substr_chrs_c_2 == '\r':
+ $utf8 .= chr(0x0D);
+ ++$c;
+ break;
+
+ case $substr_chrs_c_2 == '\\"':
+ case $substr_chrs_c_2 == '\\\'':
+ case $substr_chrs_c_2 == '\\\\':
+ case $substr_chrs_c_2 == '\\/':
+ if (($delim == '"' && $substr_chrs_c_2 != '\\\'') ||
+ ($delim == "'" && $substr_chrs_c_2 != '\\"')) {
+ $utf8 .= $chrs{++$c};
+ }
+ break;
+
+ case preg_match('/\\\u[0-9A-F]{4}/i', substr($chrs, $c, 6)):
+ // single, escaped unicode character
+ $utf16 = chr(hexdec(substr($chrs, ($c + 2), 2)))
+ . chr(hexdec(substr($chrs, ($c + 4), 2)));
+ $utf8 .= $this->utf162utf8($utf16);
+ $c += 5;
+ break;
+
+ case ($ord_chrs_c >= 0x20) && ($ord_chrs_c <= 0x7F):
+ $utf8 .= $chrs{$c};
+ break;
+
+ case ($ord_chrs_c & 0xE0) == 0xC0:
+ // characters U-00000080 - U-000007FF, mask 110XXXXX
+ //see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ $utf8 .= substr($chrs, $c, 2);
+ ++$c;
+ break;
+
+ case ($ord_chrs_c & 0xF0) == 0xE0:
+ // characters U-00000800 - U-0000FFFF, mask 1110XXXX
+ // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ $utf8 .= substr($chrs, $c, 3);
+ $c += 2;
+ break;
+
+ case ($ord_chrs_c & 0xF8) == 0xF0:
+ // characters U-00010000 - U-001FFFFF, mask 11110XXX
+ // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ $utf8 .= substr($chrs, $c, 4);
+ $c += 3;
+ break;
+
+ case ($ord_chrs_c & 0xFC) == 0xF8:
+ // characters U-00200000 - U-03FFFFFF, mask 111110XX
+ // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ $utf8 .= substr($chrs, $c, 5);
+ $c += 4;
+ break;
+
+ case ($ord_chrs_c & 0xFE) == 0xFC:
+ // characters U-04000000 - U-7FFFFFFF, mask 1111110X
+ // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ $utf8 .= substr($chrs, $c, 6);
+ $c += 5;
+ break;
+
+ }
+
+ }
+
+ return $utf8;
+
+ } elseif (preg_match('/^\[.*\]$/s', $str) || preg_match('/^\{.*\}$/s', $str)) {
+ // array, or object notation
+
+ if ($str{0} == '[') {
+ $stk = array(SERVICES_JSON_IN_ARR);
+ $arr = array();
+ } else {
+ if ($this->use & SERVICES_JSON_LOOSE_TYPE) {
+ $stk = array(SERVICES_JSON_IN_OBJ);
+ $obj = array();
+ } else {
+ $stk = array(SERVICES_JSON_IN_OBJ);
+ $obj = new stdClass();
+ }
+ }
+
+ array_push($stk, array('what' => SERVICES_JSON_SLICE,
+ 'where' => 0,
+ 'delim' => false));
+
+ $chrs = substr($str, 1, -1);
+ $chrs = $this->reduce_string($chrs);
+
+ if ($chrs == '') {
+ if (reset($stk) == SERVICES_JSON_IN_ARR) {
+ return $arr;
+
+ } else {
+ return $obj;
+
+ }
+ }
+
+ //print("\nparsing {$chrs}\n");
+
+ $strlen_chrs = strlen($chrs);
+
+ for ($c = 0; $c <= $strlen_chrs; ++$c) {
+
+ $top = end($stk);
+ $substr_chrs_c_2 = substr($chrs, $c, 2);
+
+ if (($c == $strlen_chrs) || (($chrs{$c} == ',') && ($top['what'] == SERVICES_JSON_SLICE))) {
+ // found a comma that is not inside a string, array, etc.,
+ // OR we've reached the end of the character list
+ $slice = substr($chrs, $top['where'], ($c - $top['where']));
+ array_push($stk, array('what' => SERVICES_JSON_SLICE, 'where' => ($c + 1), 'delim' => false));
+ //print("Found split at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n");
+
+ if (reset($stk) == SERVICES_JSON_IN_ARR) {
+ // we are in an array, so just push an element onto the stack
+ array_push($arr, $this->decode($slice));
+
+ } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) {
+ // we are in an object, so figure
+ // out the property name and set an
+ // element in an associative array,
+ // for now
+ $parts = array();
+
+ if (preg_match('/^\s*(["\'].*[^\\\]["\'])\s*:\s*(\S.*),?$/Uis', $slice, $parts)) {
+ // "name":value pair
+ $key = $this->decode($parts[1]);
+ $val = $this->decode($parts[2]);
+
+ if ($this->use & SERVICES_JSON_LOOSE_TYPE) {
+ $obj[$key] = $val;
+ } else {
+ $obj->$key = $val;
+ }
+ } elseif (preg_match('/^\s*(\w+)\s*:\s*(\S.*),?$/Uis', $slice, $parts)) {
+ // name:value pair, where name is unquoted
+ $key = $parts[1];
+ $val = $this->decode($parts[2]);
+
+ if ($this->use & SERVICES_JSON_LOOSE_TYPE) {
+ $obj[$key] = $val;
+ } else {
+ $obj->$key = $val;
+ }
+ }
+
+ }
+
+ } elseif ((($chrs{$c} == '"') || ($chrs{$c} == "'")) && ($top['what'] != SERVICES_JSON_IN_STR)) {
+ // found a quote, and we are not inside a string
+ array_push($stk, array('what' => SERVICES_JSON_IN_STR, 'where' => $c, 'delim' => $chrs{$c}));
+ //print("Found start of string at {$c}\n");
+
+ } elseif (($chrs{$c} == $top['delim']) &&
+ ($top['what'] == SERVICES_JSON_IN_STR) &&
+ ((strlen(substr($chrs, 0, $c)) - strlen(rtrim(substr($chrs, 0, $c), '\\'))) % 2 != 1)) {
+ // found a quote, we're in a string, and it's not escaped
+ // we know that it's not escaped becase there is _not_ an
+ // odd number of backslashes at the end of the string so far
+ array_pop($stk);
+ //print("Found end of string at {$c}: ".substr($chrs, $top['where'], (1 + 1 + $c - $top['where']))."\n");
+
+ } elseif (($chrs{$c} == '[') &&
+ in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) {
+ // found a left-bracket, and we are in an array, object, or slice
+ array_push($stk, array('what' => SERVICES_JSON_IN_ARR, 'where' => $c, 'delim' => false));
+ //print("Found start of array at {$c}\n");
+
+ } elseif (($chrs{$c} == ']') && ($top['what'] == SERVICES_JSON_IN_ARR)) {
+ // found a right-bracket, and we're in an array
+ array_pop($stk);
+ //print("Found end of array at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n");
+
+ } elseif (($chrs{$c} == '{') &&
+ in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) {
+ // found a left-brace, and we are in an array, object, or slice
+ array_push($stk, array('what' => SERVICES_JSON_IN_OBJ, 'where' => $c, 'delim' => false));
+ //print("Found start of object at {$c}\n");
+
+ } elseif (($chrs{$c} == '}') && ($top['what'] == SERVICES_JSON_IN_OBJ)) {
+ // found a right-brace, and we're in an object
+ array_pop($stk);
+ //print("Found end of object at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n");
+
+ } elseif (($substr_chrs_c_2 == '/*') &&
+ in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) {
+ // found a comment start, and we are in an array, object, or slice
+ array_push($stk, array('what' => SERVICES_JSON_IN_CMT, 'where' => $c, 'delim' => false));
+ $c++;
+ //print("Found start of comment at {$c}\n");
+
+ } elseif (($substr_chrs_c_2 == '*/') && ($top['what'] == SERVICES_JSON_IN_CMT)) {
+ // found a comment end, and we're in one now
+ array_pop($stk);
+ $c++;
+
+ for ($i = $top['where']; $i <= $c; ++$i)
+ $chrs = substr_replace($chrs, ' ', $i, 1);
+
+ //print("Found end of comment at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n");
+
+ }
+
+ }
+
+ if (reset($stk) == SERVICES_JSON_IN_ARR) {
+ return $arr;
+
+ } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) {
+ return $obj;
+
+ }
+
+ }
+ }
+ }
+
+ /**
+ * @todo Ultimately, this should just call PEAR::isError()
+ */
+ function isError($data, $code = null)
+ {
+ if (class_exists('pear')) {
+ return PEAR::isError($data, $code);
+ } elseif (is_object($data) && (get_class($data) == 'services_json_error' ||
+ is_subclass_of($data, 'services_json_error'))) {
+ return true;
+ }
+
+ return false;
+ }
+}
+
+if (class_exists('PEAR_Error')) {
+
+ class Services_JSON_Error extends PEAR_Error
+ {
+ function Services_JSON_Error($message = 'unknown error', $code = null,
+ $mode = null, $options = null, $userinfo = null)
+ {
+ parent::PEAR_Error($message, $code, $mode, $options, $userinfo);
+ }
+ }
+
+} else {
+
+ /**
+ * @todo Ultimately, this class shall be descended from PEAR_Error
+ */
+ class Services_JSON_Error
+ {
+ function Services_JSON_Error($message = 'unknown error', $code = null,
+ $mode = null, $options = null, $userinfo = null)
+ {
+
+ }
+ }
+
+}
+
+?>
diff --git a/extlib/facebook/jsonwrapper/JSON/LICENSE b/extlib/facebook/jsonwrapper/JSON/LICENSE
new file mode 100644
index 000000000..4ae6bef55
--- /dev/null
+++ b/extlib/facebook/jsonwrapper/JSON/LICENSE
@@ -0,0 +1,21 @@
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+Redistributions of source code must retain the above copyright notice,
+this list of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED
+WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
+NO EVENT SHALL CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/extlib/facebook/jsonwrapper/jsonwrapper.php b/extlib/facebook/jsonwrapper/jsonwrapper.php
new file mode 100644
index 000000000..29509deba
--- /dev/null
+++ b/extlib/facebook/jsonwrapper/jsonwrapper.php
@@ -0,0 +1,6 @@
+<?php
+# In PHP 5.2 or higher we don't need to bring this in
+if (!function_exists('json_encode')) {
+ require_once 'jsonwrapper_inner.php';
+}
+?>
diff --git a/extlib/facebook/jsonwrapper/jsonwrapper_inner.php b/extlib/facebook/jsonwrapper/jsonwrapper_inner.php
new file mode 100644
index 000000000..36a3f2863
--- /dev/null
+++ b/extlib/facebook/jsonwrapper/jsonwrapper_inner.php
@@ -0,0 +1,23 @@
+<?php
+
+require_once 'JSON/JSON.php';
+
+function json_encode($arg)
+{
+ global $services_json;
+ if (!isset($services_json)) {
+ $services_json = new Services_JSON();
+ }
+ return $services_json->encode($arg);
+}
+
+function json_decode($arg)
+{
+ global $services_json;
+ if (!isset($services_json)) {
+ $services_json = new Services_JSON();
+ }
+ return $services_json->decode($arg);
+}
+
+?>
diff --git a/extlib/get_temp_dir.php b/extlib/get_temp_dir.php
new file mode 100644
index 000000000..4ec96e522
--- /dev/null
+++ b/extlib/get_temp_dir.php
@@ -0,0 +1,14 @@
+<?php
+if ( !function_exists('sys_get_temp_dir')) {
+ function sys_get_temp_dir() {
+ if (!empty($_ENV['TMP'])) { return realpath($_ENV['TMP']); }
+ if (!empty($_ENV['TMPDIR'])) { return realpath( $_ENV['TMPDIR']); }
+ if (!empty($_ENV['TEMP'])) { return realpath( $_ENV['TEMP']); }
+ $tempfile=tempnam(uniqid(rand(),TRUE),'');
+ if (file_exists($tempfile)) {
+ unlink($tempfile);
+ }
+ return realpath(dirname($tempfile));
+ }
+}
+?>
diff --git a/extlib/gpl-2.0.txt b/extlib/gpl-2.0.txt
new file mode 100644
index 000000000..d511905c1
--- /dev/null
+++ b/extlib/gpl-2.0.txt
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/extlib/markdown.php b/extlib/markdown.php
new file mode 100644
index 000000000..8179b568b
--- /dev/null
+++ b/extlib/markdown.php
@@ -0,0 +1,1710 @@
+<?php
+#
+# Markdown - A text-to-HTML conversion tool for web writers
+#
+# PHP Markdown
+# Copyright (c) 2004-2008 Michel Fortin
+# <http://www.michelf.com/projects/php-markdown/>
+#
+# Original Markdown
+# Copyright (c) 2004-2006 John Gruber
+# <http://daringfireball.net/projects/markdown/>
+#
+
+
+define( 'MARKDOWN_VERSION', "1.0.1m" ); # Sat 21 Jun 2008
+
+
+#
+# Global default settings:
+#
+
+# Change to ">" for HTML output
+@define( 'MARKDOWN_EMPTY_ELEMENT_SUFFIX', " />");
+
+# Define the width of a tab for code blocks.
+@define( 'MARKDOWN_TAB_WIDTH', 4 );
+
+
+#
+# WordPress settings:
+#
+
+# Change to false to remove Markdown from posts and/or comments.
+@define( 'MARKDOWN_WP_POSTS', true );
+@define( 'MARKDOWN_WP_COMMENTS', true );
+
+
+
+### Standard Function Interface ###
+
+@define( 'MARKDOWN_PARSER_CLASS', 'Markdown_Parser' );
+
+function Markdown($text) {
+#
+# Initialize the parser and return the result of its transform method.
+#
+ # Setup static parser variable.
+ static $parser;
+ if (!isset($parser)) {
+ $parser_class = MARKDOWN_PARSER_CLASS;
+ $parser = new $parser_class;
+ }
+
+ # Transform text using parser.
+ return $parser->transform($text);
+}
+
+
+### WordPress Plugin Interface ###
+
+/*
+Plugin Name: Markdown
+Plugin URI: http://www.michelf.com/projects/php-markdown/
+Description: <a href="http://daringfireball.net/projects/markdown/syntax">Markdown syntax</a> allows you to write using an easy-to-read, easy-to-write plain text format. Based on the original Perl version by <a href="http://daringfireball.net/">John Gruber</a>. <a href="http://www.michelf.com/projects/php-markdown/">More...</a>
+Version: 1.0.1m
+Author: Michel Fortin
+Author URI: http://www.michelf.com/
+*/
+
+if (isset($wp_version)) {
+ # More details about how it works here:
+ # <http://www.michelf.com/weblog/2005/wordpress-text-flow-vs-markdown/>
+
+ # Post content and excerpts
+ # - Remove WordPress paragraph generator.
+ # - Run Markdown on excerpt, then remove all tags.
+ # - Add paragraph tag around the excerpt, but remove it for the excerpt rss.
+ if (MARKDOWN_WP_POSTS) {
+ remove_filter('the_content', 'wpautop');
+ remove_filter('the_content_rss', 'wpautop');
+ remove_filter('the_excerpt', 'wpautop');
+ add_filter('the_content', 'Markdown', 6);
+ add_filter('the_content_rss', 'Markdown', 6);
+ add_filter('get_the_excerpt', 'Markdown', 6);
+ add_filter('get_the_excerpt', 'trim', 7);
+ add_filter('the_excerpt', 'mdwp_add_p');
+ add_filter('the_excerpt_rss', 'mdwp_strip_p');
+
+ remove_filter('content_save_pre', 'balanceTags', 50);
+ remove_filter('excerpt_save_pre', 'balanceTags', 50);
+ add_filter('the_content', 'balanceTags', 50);
+ add_filter('get_the_excerpt', 'balanceTags', 9);
+ }
+
+ # Comments
+ # - Remove WordPress paragraph generator.
+ # - Remove WordPress auto-link generator.
+ # - Scramble important tags before passing them to the kses filter.
+ # - Run Markdown on excerpt then remove paragraph tags.
+ if (MARKDOWN_WP_COMMENTS) {
+ remove_filter('comment_text', 'wpautop', 30);
+ remove_filter('comment_text', 'make_clickable');
+ add_filter('pre_comment_content', 'Markdown', 6);
+ add_filter('pre_comment_content', 'mdwp_hide_tags', 8);
+ add_filter('pre_comment_content', 'mdwp_show_tags', 12);
+ add_filter('get_comment_text', 'Markdown', 6);
+ add_filter('get_comment_excerpt', 'Markdown', 6);
+ add_filter('get_comment_excerpt', 'mdwp_strip_p', 7);
+
+ global $mdwp_hidden_tags, $mdwp_placeholders;
+ $mdwp_hidden_tags = explode(' ',
+ '<p> </p> <pre> </pre> <ol> </ol> <ul> </ul> <li> </li>');
+ $mdwp_placeholders = explode(' ', str_rot13(
+ 'pEj07ZbbBZ U1kqgh4w4p pre2zmeN6K QTi31t9pre ol0MP1jzJR '.
+ 'ML5IjmbRol ulANi1NsGY J7zRLJqPul liA8ctl16T K9nhooUHli'));
+ }
+
+ function mdwp_add_p($text) {
+ if (!preg_match('{^$|^<(p|ul|ol|dl|pre|blockquote)>}i', $text)) {
+ $text = '<p>'.$text.'</p>';
+ $text = preg_replace('{\n{2,}}', "</p>\n\n<p>", $text);
+ }
+ return $text;
+ }
+
+ function mdwp_strip_p($t) { return preg_replace('{</?p>}i', '', $t); }
+
+ function mdwp_hide_tags($text) {
+ global $mdwp_hidden_tags, $mdwp_placeholders;
+ return str_replace($mdwp_hidden_tags, $mdwp_placeholders, $text);
+ }
+ function mdwp_show_tags($text) {
+ global $mdwp_hidden_tags, $mdwp_placeholders;
+ return str_replace($mdwp_placeholders, $mdwp_hidden_tags, $text);
+ }
+}
+
+
+### bBlog Plugin Info ###
+
+function identify_modifier_markdown() {
+ return array(
+ 'name' => 'markdown',
+ 'type' => 'modifier',
+ 'nicename' => 'Markdown',
+ 'description' => 'A text-to-HTML conversion tool for web writers',
+ 'authors' => 'Michel Fortin and John Gruber',
+ 'licence' => 'BSD-like',
+ 'version' => MARKDOWN_VERSION,
+ 'help' => '<a href="http://daringfireball.net/projects/markdown/syntax">Markdown syntax</a> allows you to write using an easy-to-read, easy-to-write plain text format. Based on the original Perl version by <a href="http://daringfireball.net/">John Gruber</a>. <a href="http://www.michelf.com/projects/php-markdown/">More...</a>'
+ );
+}
+
+
+### Smarty Modifier Interface ###
+
+function smarty_modifier_markdown($text) {
+ return Markdown($text);
+}
+
+
+### Textile Compatibility Mode ###
+
+# Rename this file to "classTextile.php" and it can replace Textile everywhere.
+
+if (strcasecmp(substr(__FILE__, -16), "classTextile.php") == 0) {
+ # Try to include PHP SmartyPants. Should be in the same directory.
+ @include_once 'smartypants.php';
+ # Fake Textile class. It calls Markdown instead.
+ class Textile {
+ function TextileThis($text, $lite='', $encode='') {
+ if ($lite == '' && $encode == '') $text = Markdown($text);
+ if (function_exists('SmartyPants')) $text = SmartyPants($text);
+ return $text;
+ }
+ # Fake restricted version: restrictions are not supported for now.
+ function TextileRestricted($text, $lite='', $noimage='') {
+ return $this->TextileThis($text, $lite);
+ }
+ # Workaround to ensure compatibility with TextPattern 4.0.3.
+ function blockLite($text) { return $text; }
+ }
+}
+
+
+
+#
+# Markdown Parser Class
+#
+
+class Markdown_Parser {
+
+ # Regex to match balanced [brackets].
+ # Needed to insert a maximum bracked depth while converting to PHP.
+ var $nested_brackets_depth = 6;
+ var $nested_brackets_re;
+
+ var $nested_url_parenthesis_depth = 4;
+ var $nested_url_parenthesis_re;
+
+ # Table of hash values for escaped characters:
+ var $escape_chars = '\`*_{}[]()>#+-.!';
+ var $escape_chars_re;
+
+ # Change to ">" for HTML output.
+ var $empty_element_suffix = MARKDOWN_EMPTY_ELEMENT_SUFFIX;
+ var $tab_width = MARKDOWN_TAB_WIDTH;
+
+ # Change to `true` to disallow markup or entities.
+ var $no_markup = false;
+ var $no_entities = false;
+
+ # Predefined urls and titles for reference links and images.
+ var $predef_urls = array();
+ var $predef_titles = array();
+
+
+ function Markdown_Parser() {
+ #
+ # Constructor function. Initialize appropriate member variables.
+ #
+ $this->_initDetab();
+ $this->prepareItalicsAndBold();
+
+ $this->nested_brackets_re =
+ str_repeat('(?>[^\[\]]+|\[', $this->nested_brackets_depth).
+ str_repeat('\])*', $this->nested_brackets_depth);
+
+ $this->nested_url_parenthesis_re =
+ str_repeat('(?>[^()\s]+|\(', $this->nested_url_parenthesis_depth).
+ str_repeat('(?>\)))*', $this->nested_url_parenthesis_depth);
+
+ $this->escape_chars_re = '['.preg_quote($this->escape_chars).']';
+
+ # Sort document, block, and span gamut in ascendent priority order.
+ asort($this->document_gamut);
+ asort($this->block_gamut);
+ asort($this->span_gamut);
+ }
+
+
+ # Internal hashes used during transformation.
+ var $urls = array();
+ var $titles = array();
+ var $html_hashes = array();
+
+ # Status flag to avoid invalid nesting.
+ var $in_anchor = false;
+
+
+ function setup() {
+ #
+ # Called before the transformation process starts to setup parser
+ # states.
+ #
+ # Clear global hashes.
+ $this->urls = $this->predef_urls;
+ $this->titles = $this->predef_titles;
+ $this->html_hashes = array();
+
+ $in_anchor = false;
+ }
+
+ function teardown() {
+ #
+ # Called after the transformation process to clear any variable
+ # which may be taking up memory unnecessarly.
+ #
+ $this->urls = array();
+ $this->titles = array();
+ $this->html_hashes = array();
+ }
+
+
+ function transform($text) {
+ #
+ # Main function. Performs some preprocessing on the input text
+ # and pass it through the document gamut.
+ #
+ $this->setup();
+
+ # Remove UTF-8 BOM and marker character in input, if present.
+ $text = preg_replace('{^\xEF\xBB\xBF|\x1A}', '', $text);
+
+ # Standardize line endings:
+ # DOS to Unix and Mac to Unix
+ $text = preg_replace('{\r\n?}', "\n", $text);
+
+ # Make sure $text ends with a couple of newlines:
+ $text .= "\n\n";
+
+ # Convert all tabs to spaces.
+ $text = $this->detab($text);
+
+ # Turn block-level HTML blocks into hash entries
+ $text = $this->hashHTMLBlocks($text);
+
+ # Strip any lines consisting only of spaces and tabs.
+ # This makes subsequent regexen easier to write, because we can
+ # match consecutive blank lines with /\n+/ instead of something
+ # contorted like /[ ]*\n+/ .
+ $text = preg_replace('/^[ ]+$/m', '', $text);
+
+ # Run document gamut methods.
+ foreach ($this->document_gamut as $method => $priority) {
+ $text = $this->$method($text);
+ }
+
+ $this->teardown();
+
+ return $text . "\n";
+ }
+
+ var $document_gamut = array(
+ # Strip link definitions, store in hashes.
+ "stripLinkDefinitions" => 20,
+
+ "runBasicBlockGamut" => 30,
+ );
+
+
+ function stripLinkDefinitions($text) {
+ #
+ # Strips link definitions from text, stores the URLs and titles in
+ # hash references.
+ #
+ $less_than_tab = $this->tab_width - 1;
+
+ # Link defs are in the form: ^[id]: url "optional title"
+ $text = preg_replace_callback('{
+ ^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?: # id = $1
+ [ ]*
+ \n? # maybe *one* newline
+ [ ]*
+ <?(\S+?)>? # url = $2
+ [ ]*
+ \n? # maybe one newline
+ [ ]*
+ (?:
+ (?<=\s) # lookbehind for whitespace
+ ["(]
+ (.*?) # title = $3
+ [")]
+ [ ]*
+ )? # title is optional
+ (?:\n+|\Z)
+ }xm',
+ array(&$this, '_stripLinkDefinitions_callback'),
+ $text);
+ return $text;
+ }
+ function _stripLinkDefinitions_callback($matches) {
+ $link_id = strtolower($matches[1]);
+ $this->urls[$link_id] = $matches[2];
+ $this->titles[$link_id] =& $matches[3];
+ return ''; # String that will replace the block
+ }
+
+
+ function hashHTMLBlocks($text) {
+ if ($this->no_markup) return $text;
+
+ $less_than_tab = $this->tab_width - 1;
+
+ # Hashify HTML blocks:
+ # We only want to do this for block-level HTML tags, such as headers,
+ # lists, and tables. That's because we still want to wrap <p>s around
+ # "paragraphs" that are wrapped in non-block-level tags, such as anchors,
+ # phrase emphasis, and spans. The list of tags we're looking for is
+ # hard-coded:
+ #
+ # * List "a" is made of tags which can be both inline or block-level.
+ # These will be treated block-level when the start tag is alone on
+ # its line, otherwise they're not matched here and will be taken as
+ # inline later.
+ # * List "b" is made of tags which are always block-level;
+ #
+ $block_tags_a_re = 'ins|del';
+ $block_tags_b_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|'.
+ 'script|noscript|form|fieldset|iframe|math';
+
+ # Regular expression for the content of a block tag.
+ $nested_tags_level = 4;
+ $attr = '
+ (?> # optional tag attributes
+ \s # starts with whitespace
+ (?>
+ [^>"/]+ # text outside quotes
+ |
+ /+(?!>) # slash not followed by ">"
+ |
+ "[^"]*" # text inside double quotes (tolerate ">")
+ |
+ \'[^\']*\' # text inside single quotes (tolerate ">")
+ )*
+ )?
+ ';
+ $content =
+ str_repeat('
+ (?>
+ [^<]+ # content without tag
+ |
+ <\2 # nested opening tag
+ '.$attr.' # attributes
+ (?>
+ />
+ |
+ >', $nested_tags_level). # end of opening tag
+ '.*?'. # last level nested tag content
+ str_repeat('
+ </\2\s*> # closing nested tag
+ )
+ |
+ <(?!/\2\s*> # other tags with a different name
+ )
+ )*',
+ $nested_tags_level);
+ $content2 = str_replace('\2', '\3', $content);
+
+ # First, look for nested blocks, e.g.:
+ # <div>
+ # <div>
+ # tags for inner block must be indented.
+ # </div>
+ # </div>
+ #
+ # The outermost tags must start at the left margin for this to match, and
+ # the inner nested divs must be indented.
+ # We need to do this before the next, more liberal match, because the next
+ # match will start at the first `<div>` and stop at the first `</div>`.
+ $text = preg_replace_callback('{(?>
+ (?>
+ (?<=\n\n) # Starting after a blank line
+ | # or
+ \A\n? # the beginning of the doc
+ )
+ ( # save in $1
+
+ # Match from `\n<tag>` to `</tag>\n`, handling nested tags
+ # in between.
+
+ [ ]{0,'.$less_than_tab.'}
+ <('.$block_tags_b_re.')# start tag = $2
+ '.$attr.'> # attributes followed by > and \n
+ '.$content.' # content, support nesting
+ </\2> # the matching end tag
+ [ ]* # trailing spaces/tabs
+ (?=\n+|\Z) # followed by a newline or end of document
+
+ | # Special version for tags of group a.
+
+ [ ]{0,'.$less_than_tab.'}
+ <('.$block_tags_a_re.')# start tag = $3
+ '.$attr.'>[ ]*\n # attributes followed by >
+ '.$content2.' # content, support nesting
+ </\3> # the matching end tag
+ [ ]* # trailing spaces/tabs
+ (?=\n+|\Z) # followed by a newline or end of document
+
+ | # Special case just for <hr />. It was easier to make a special
+ # case than to make the other regex more complicated.
+
+ [ ]{0,'.$less_than_tab.'}
+ <(hr) # start tag = $2
+ '.$attr.' # attributes
+ /?> # the matching end tag
+ [ ]*
+ (?=\n{2,}|\Z) # followed by a blank line or end of document
+
+ | # Special case for standalone HTML comments:
+
+ [ ]{0,'.$less_than_tab.'}
+ (?s:
+ <!-- .*? -->
+ )
+ [ ]*
+ (?=\n{2,}|\Z) # followed by a blank line or end of document
+
+ | # PHP and ASP-style processor instructions (<? and <%)
+
+ [ ]{0,'.$less_than_tab.'}
+ (?s:
+ <([?%]) # $2
+ .*?
+ \2>
+ )
+ [ ]*
+ (?=\n{2,}|\Z) # followed by a blank line or end of document
+
+ )
+ )}Sxmi',
+ array(&$this, '_hashHTMLBlocks_callback'),
+ $text);
+
+ return $text;
+ }
+ function _hashHTMLBlocks_callback($matches) {
+ $text = $matches[1];
+ $key = $this->hashBlock($text);
+ return "\n\n$key\n\n";
+ }
+
+
+ function hashPart($text, $boundary = 'X') {
+ #
+ # Called whenever a tag must be hashed when a function insert an atomic
+ # element in the text stream. Passing $text to through this function gives
+ # a unique text-token which will be reverted back when calling unhash.
+ #
+ # The $boundary argument specify what character should be used to surround
+ # the token. By convension, "B" is used for block elements that needs not
+ # to be wrapped into paragraph tags at the end, ":" is used for elements
+ # that are word separators and "X" is used in the general case.
+ #
+ # Swap back any tag hash found in $text so we do not have to `unhash`
+ # multiple times at the end.
+ $text = $this->unhash($text);
+
+ # Then hash the block.
+ static $i = 0;
+ $key = "$boundary\x1A" . ++$i . $boundary;
+ $this->html_hashes[$key] = $text;
+ return $key; # String that will replace the tag.
+ }
+
+
+ function hashBlock($text) {
+ #
+ # Shortcut function for hashPart with block-level boundaries.
+ #
+ return $this->hashPart($text, 'B');
+ }
+
+
+ var $block_gamut = array(
+ #
+ # These are all the transformations that form block-level
+ # tags like paragraphs, headers, and list items.
+ #
+ "doHeaders" => 10,
+ "doHorizontalRules" => 20,
+
+ "doLists" => 40,
+ "doCodeBlocks" => 50,
+ "doBlockQuotes" => 60,
+ );
+
+ function runBlockGamut($text) {
+ #
+ # Run block gamut tranformations.
+ #
+ # We need to escape raw HTML in Markdown source before doing anything
+ # else. This need to be done for each block, and not only at the
+ # begining in the Markdown function since hashed blocks can be part of
+ # list items and could have been indented. Indented blocks would have
+ # been seen as a code block in a previous pass of hashHTMLBlocks.
+ $text = $this->hashHTMLBlocks($text);
+
+ return $this->runBasicBlockGamut($text);
+ }
+
+ function runBasicBlockGamut($text) {
+ #
+ # Run block gamut tranformations, without hashing HTML blocks. This is
+ # useful when HTML blocks are known to be already hashed, like in the first
+ # whole-document pass.
+ #
+ foreach ($this->block_gamut as $method => $priority) {
+ $text = $this->$method($text);
+ }
+
+ # Finally form paragraph and restore hashed blocks.
+ $text = $this->formParagraphs($text);
+
+ return $text;
+ }
+
+
+ function doHorizontalRules($text) {
+ # Do Horizontal Rules:
+ return preg_replace(
+ '{
+ ^[ ]{0,3} # Leading space
+ ([-*_]) # $1: First marker
+ (?> # Repeated marker group
+ [ ]{0,2} # Zero, one, or two spaces.
+ \1 # Marker character
+ ){2,} # Group repeated at least twice
+ [ ]* # Tailing spaces
+ $ # End of line.
+ }mx',
+ "\n".$this->hashBlock("<hr$this->empty_element_suffix")."\n",
+ $text);
+ }
+
+
+ var $span_gamut = array(
+ #
+ # These are all the transformations that occur *within* block-level
+ # tags like paragraphs, headers, and list items.
+ #
+ # Process character escapes, code spans, and inline HTML
+ # in one shot.
+ "parseSpan" => -30,
+
+ # Process anchor and image tags. Images must come first,
+ # because ![foo][f] looks like an anchor.
+ "doImages" => 10,
+ "doAnchors" => 20,
+
+ # Make links out of things like `<http://example.com/>`
+ # Must come after doAnchors, because you can use < and >
+ # delimiters in inline links like [this](<url>).
+ "doAutoLinks" => 30,
+ "encodeAmpsAndAngles" => 40,
+
+ "doItalicsAndBold" => 50,
+ "doHardBreaks" => 60,
+ );
+
+ function runSpanGamut($text) {
+ #
+ # Run span gamut tranformations.
+ #
+ foreach ($this->span_gamut as $method => $priority) {
+ $text = $this->$method($text);
+ }
+
+ return $text;
+ }
+
+
+ function doHardBreaks($text) {
+ # Do hard breaks:
+ return preg_replace_callback('/ {2,}\n/',
+ array(&$this, '_doHardBreaks_callback'), $text);
+ }
+ function _doHardBreaks_callback($matches) {
+ return $this->hashPart("<br$this->empty_element_suffix\n");
+ }
+
+
+ function doAnchors($text) {
+ #
+ # Turn Markdown link shortcuts into XHTML <a> tags.
+ #
+ if ($this->in_anchor) return $text;
+ $this->in_anchor = true;
+
+ #
+ # First, handle reference-style links: [link text] [id]
+ #
+ $text = preg_replace_callback('{
+ ( # wrap whole match in $1
+ \[
+ ('.$this->nested_brackets_re.') # link text = $2
+ \]
+
+ [ ]? # one optional space
+ (?:\n[ ]*)? # one optional newline followed by spaces
+
+ \[
+ (.*?) # id = $3
+ \]
+ )
+ }xs',
+ array(&$this, '_doAnchors_reference_callback'), $text);
+
+ #
+ # Next, inline-style links: [link text](url "optional title")
+ #
+ $text = preg_replace_callback('{
+ ( # wrap whole match in $1
+ \[
+ ('.$this->nested_brackets_re.') # link text = $2
+ \]
+ \( # literal paren
+ [ ]*
+ (?:
+ <(\S*)> # href = $3
+ |
+ ('.$this->nested_url_parenthesis_re.') # href = $4
+ )
+ [ ]*
+ ( # $5
+ ([\'"]) # quote char = $6
+ (.*?) # Title = $7
+ \6 # matching quote
+ [ ]* # ignore any spaces/tabs between closing quote and )
+ )? # title is optional
+ \)
+ )
+ }xs',
+ array(&$this, '_DoAnchors_inline_callback'), $text);
+
+ #
+ # Last, handle reference-style shortcuts: [link text]
+ # These must come last in case you've also got [link test][1]
+ # or [link test](/foo)
+ #
+// $text = preg_replace_callback('{
+// ( # wrap whole match in $1
+// \[
+// ([^\[\]]+) # link text = $2; can\'t contain [ or ]
+// \]
+// )
+// }xs',
+// array(&$this, '_doAnchors_reference_callback'), $text);
+
+ $this->in_anchor = false;
+ return $text;
+ }
+ function _doAnchors_reference_callback($matches) {
+ $whole_match = $matches[1];
+ $link_text = $matches[2];
+ $link_id =& $matches[3];
+
+ if ($link_id == "") {
+ # for shortcut links like [this][] or [this].
+ $link_id = $link_text;
+ }
+
+ # lower-case and turn embedded newlines into spaces
+ $link_id = strtolower($link_id);
+ $link_id = preg_replace('{[ ]?\n}', ' ', $link_id);
+
+ if (isset($this->urls[$link_id])) {
+ $url = $this->urls[$link_id];
+ $url = $this->encodeAttribute($url);
+
+ $result = "<a href=\"$url\"";
+ if ( isset( $this->titles[$link_id] ) ) {
+ $title = $this->titles[$link_id];
+ $title = $this->encodeAttribute($title);
+ $result .= " title=\"$title\"";
+ }
+
+ $link_text = $this->runSpanGamut($link_text);
+ $result .= ">$link_text</a>";
+ $result = $this->hashPart($result);
+ }
+ else {
+ $result = $whole_match;
+ }
+ return $result;
+ }
+ function _doAnchors_inline_callback($matches) {
+ $whole_match = $matches[1];
+ $link_text = $this->runSpanGamut($matches[2]);
+ $url = $matches[3] == '' ? $matches[4] : $matches[3];
+ $title =& $matches[7];
+
+ $url = $this->encodeAttribute($url);
+
+ $result = "<a href=\"$url\"";
+ if (isset($title)) {
+ $title = $this->encodeAttribute($title);
+ $result .= " title=\"$title\"";
+ }
+
+ $link_text = $this->runSpanGamut($link_text);
+ $result .= ">$link_text</a>";
+
+ return $this->hashPart($result);
+ }
+
+
+ function doImages($text) {
+ #
+ # Turn Markdown image shortcuts into <img> tags.
+ #
+ #
+ # First, handle reference-style labeled images: ![alt text][id]
+ #
+ $text = preg_replace_callback('{
+ ( # wrap whole match in $1
+ !\[
+ ('.$this->nested_brackets_re.') # alt text = $2
+ \]
+
+ [ ]? # one optional space
+ (?:\n[ ]*)? # one optional newline followed by spaces
+
+ \[
+ (.*?) # id = $3
+ \]
+
+ )
+ }xs',
+ array(&$this, '_doImages_reference_callback'), $text);
+
+ #
+ # Next, handle inline images: ![alt text](url "optional title")
+ # Don't forget: encode * and _
+ #
+ $text = preg_replace_callback('{
+ ( # wrap whole match in $1
+ !\[
+ ('.$this->nested_brackets_re.') # alt text = $2
+ \]
+ \s? # One optional whitespace character
+ \( # literal paren
+ [ ]*
+ (?:
+ <(\S*)> # src url = $3
+ |
+ ('.$this->nested_url_parenthesis_re.') # src url = $4
+ )
+ [ ]*
+ ( # $5
+ ([\'"]) # quote char = $6
+ (.*?) # title = $7
+ \6 # matching quote
+ [ ]*
+ )? # title is optional
+ \)
+ )
+ }xs',
+ array(&$this, '_doImages_inline_callback'), $text);
+
+ return $text;
+ }
+ function _doImages_reference_callback($matches) {
+ $whole_match = $matches[1];
+ $alt_text = $matches[2];
+ $link_id = strtolower($matches[3]);
+
+ if ($link_id == "") {
+ $link_id = strtolower($alt_text); # for shortcut links like ![this][].
+ }
+
+ $alt_text = $this->encodeAttribute($alt_text);
+ if (isset($this->urls[$link_id])) {
+ $url = $this->encodeAttribute($this->urls[$link_id]);
+ $result = "<img src=\"$url\" alt=\"$alt_text\"";
+ if (isset($this->titles[$link_id])) {
+ $title = $this->titles[$link_id];
+ $title = $this->encodeAttribute($title);
+ $result .= " title=\"$title\"";
+ }
+ $result .= $this->empty_element_suffix;
+ $result = $this->hashPart($result);
+ }
+ else {
+ # If there's no such link ID, leave intact:
+ $result = $whole_match;
+ }
+
+ return $result;
+ }
+ function _doImages_inline_callback($matches) {
+ $whole_match = $matches[1];
+ $alt_text = $matches[2];
+ $url = $matches[3] == '' ? $matches[4] : $matches[3];
+ $title =& $matches[7];
+
+ $alt_text = $this->encodeAttribute($alt_text);
+ $url = $this->encodeAttribute($url);
+ $result = "<img src=\"$url\" alt=\"$alt_text\"";
+ if (isset($title)) {
+ $title = $this->encodeAttribute($title);
+ $result .= " title=\"$title\""; # $title already quoted
+ }
+ $result .= $this->empty_element_suffix;
+
+ return $this->hashPart($result);
+ }
+
+
+ function doHeaders($text) {
+ # Setext-style headers:
+ # Header 1
+ # ========
+ #
+ # Header 2
+ # --------
+ #
+ $text = preg_replace_callback('{ ^(.+?)[ ]*\n(=+|-+)[ ]*\n+ }mx',
+ array(&$this, '_doHeaders_callback_setext'), $text);
+
+ # atx-style headers:
+ # # Header 1
+ # ## Header 2
+ # ## Header 2 with closing hashes ##
+ # ...
+ # ###### Header 6
+ #
+ $text = preg_replace_callback('{
+ ^(\#{1,6}) # $1 = string of #\'s
+ [ ]*
+ (.+?) # $2 = Header text
+ [ ]*
+ \#* # optional closing #\'s (not counted)
+ \n+
+ }xm',
+ array(&$this, '_doHeaders_callback_atx'), $text);
+
+ return $text;
+ }
+ function _doHeaders_callback_setext($matches) {
+ # Terrible hack to check we haven't found an empty list item.
+ if ($matches[2] == '-' && preg_match('{^-(?: |$)}', $matches[1]))
+ return $matches[0];
+
+ $level = $matches[2]{0} == '=' ? 1 : 2;
+ $block = "<h$level>".$this->runSpanGamut($matches[1])."</h$level>";
+ return "\n" . $this->hashBlock($block) . "\n\n";
+ }
+ function _doHeaders_callback_atx($matches) {
+ $level = strlen($matches[1]);
+ $block = "<h$level>".$this->runSpanGamut($matches[2])."</h$level>";
+ return "\n" . $this->hashBlock($block) . "\n\n";
+ }
+
+
+ function doLists($text) {
+ #
+ # Form HTML ordered (numbered) and unordered (bulleted) lists.
+ #
+ $less_than_tab = $this->tab_width - 1;
+
+ # Re-usable patterns to match list item bullets and number markers:
+ $marker_ul_re = '[*+-]';
+ $marker_ol_re = '\d+[.]';
+ $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)";
+
+ $markers_relist = array($marker_ul_re, $marker_ol_re);
+
+ foreach ($markers_relist as $marker_re) {
+ # Re-usable pattern to match any entirel ul or ol list:
+ $whole_list_re = '
+ ( # $1 = whole list
+ ( # $2
+ [ ]{0,'.$less_than_tab.'}
+ ('.$marker_re.') # $3 = first list item marker
+ [ ]+
+ )
+ (?s:.+?)
+ ( # $4
+ \z
+ |
+ \n{2,}
+ (?=\S)
+ (?! # Negative lookahead for another list item marker
+ [ ]*
+ '.$marker_re.'[ ]+
+ )
+ )
+ )
+ '; // mx
+
+ # We use a different prefix before nested lists than top-level lists.
+ # See extended comment in _ProcessListItems().
+
+ if ($this->list_level) {
+ $text = preg_replace_callback('{
+ ^
+ '.$whole_list_re.'
+ }mx',
+ array(&$this, '_doLists_callback'), $text);
+ }
+ else {
+ $text = preg_replace_callback('{
+ (?:(?<=\n)\n|\A\n?) # Must eat the newline
+ '.$whole_list_re.'
+ }mx',
+ array(&$this, '_doLists_callback'), $text);
+ }
+ }
+
+ return $text;
+ }
+ function _doLists_callback($matches) {
+ # Re-usable patterns to match list item bullets and number markers:
+ $marker_ul_re = '[*+-]';
+ $marker_ol_re = '\d+[.]';
+ $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)";
+
+ $list = $matches[1];
+ $list_type = preg_match("/$marker_ul_re/", $matches[3]) ? "ul" : "ol";
+
+ $marker_any_re = ( $list_type == "ul" ? $marker_ul_re : $marker_ol_re );
+
+ $list .= "\n";
+ $result = $this->processListItems($list, $marker_any_re);
+
+ $result = $this->hashBlock("<$list_type>\n" . $result . "</$list_type>");
+ return "\n". $result ."\n\n";
+ }
+
+ var $list_level = 0;
+
+ function processListItems($list_str, $marker_any_re) {
+ #
+ # Process the contents of a single ordered or unordered list, splitting it
+ # into individual list items.
+ #
+ # The $this->list_level global keeps track of when we're inside a list.
+ # Each time we enter a list, we increment it; when we leave a list,
+ # we decrement. If it's zero, we're not in a list anymore.
+ #
+ # We do this because when we're not inside a list, we want to treat
+ # something like this:
+ #
+ # I recommend upgrading to version
+ # 8. Oops, now this line is treated
+ # as a sub-list.
+ #
+ # As a single paragraph, despite the fact that the second line starts
+ # with a digit-period-space sequence.
+ #
+ # Whereas when we're inside a list (or sub-list), that line will be
+ # treated as the start of a sub-list. What a kludge, huh? This is
+ # an aspect of Markdown's syntax that's hard to parse perfectly
+ # without resorting to mind-reading. Perhaps the solution is to
+ # change the syntax rules such that sub-lists must start with a
+ # starting cardinal number; e.g. "1." or "a.".
+
+ $this->list_level++;
+
+ # trim trailing blank lines:
+ $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str);
+
+ $list_str = preg_replace_callback('{
+ (\n)? # leading line = $1
+ (^[ ]*) # leading whitespace = $2
+ ('.$marker_any_re.' # list marker and space = $3
+ (?:[ ]+|(?=\n)) # space only required if item is not empty
+ )
+ ((?s:.*?)) # list item text = $4
+ (?:(\n+(?=\n))|\n) # tailing blank line = $5
+ (?= \n* (\z | \2 ('.$marker_any_re.') (?:[ ]+|(?=\n))))
+ }xm',
+ array(&$this, '_processListItems_callback'), $list_str);
+
+ $this->list_level--;
+ return $list_str;
+ }
+ function _processListItems_callback($matches) {
+ $item = $matches[4];
+ $leading_line =& $matches[1];
+ $leading_space =& $matches[2];
+ $marker_space = $matches[3];
+ $tailing_blank_line =& $matches[5];
+
+ if ($leading_line || $tailing_blank_line ||
+ preg_match('/\n{2,}/', $item))
+ {
+ # Replace marker with the appropriate whitespace indentation
+ $item = $leading_space . str_repeat(' ', strlen($marker_space)) . $item;
+ $item = $this->runBlockGamut($this->outdent($item)."\n");
+ }
+ else {
+ # Recursion for sub-lists:
+ $item = $this->doLists($this->outdent($item));
+ $item = preg_replace('/\n+$/', '', $item);
+ $item = $this->runSpanGamut($item);
+ }
+
+ return "<li>" . $item . "</li>\n";
+ }
+
+
+ function doCodeBlocks($text) {
+ #
+ # Process Markdown `<pre><code>` blocks.
+ #
+ $text = preg_replace_callback('{
+ (?:\n\n|\A\n?)
+ ( # $1 = the code block -- one or more lines, starting with a space/tab
+ (?>
+ [ ]{'.$this->tab_width.'} # Lines must start with a tab or a tab-width of spaces
+ .*\n+
+ )+
+ )
+ ((?=^[ ]{0,'.$this->tab_width.'}\S)|\Z) # Lookahead for non-space at line-start, or end of doc
+ }xm',
+ array(&$this, '_doCodeBlocks_callback'), $text);
+
+ return $text;
+ }
+ function _doCodeBlocks_callback($matches) {
+ $codeblock = $matches[1];
+
+ $codeblock = $this->outdent($codeblock);
+ $codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES);
+
+ # trim leading newlines and trailing newlines
+ $codeblock = preg_replace('/\A\n+|\n+\z/', '', $codeblock);
+
+ $codeblock = "<pre><code>$codeblock\n</code></pre>";
+ return "\n\n".$this->hashBlock($codeblock)."\n\n";
+ }
+
+
+ function makeCodeSpan($code) {
+ #
+ # Create a code span markup for $code. Called from handleSpanToken.
+ #
+ $code = htmlspecialchars(trim($code), ENT_NOQUOTES);
+ return $this->hashPart("<code>$code</code>");
+ }
+
+
+ var $em_relist = array(
+ '' => '(?:(?<!\*)\*(?!\*)|(?<!_)_(?!_))(?=\S)(?![.,:;]\s)',
+ '*' => '(?<=\S)(?<!\*)\*(?!\*)',
+ '_' => '(?<=\S)(?<!_)_(?!_)',
+ );
+ var $strong_relist = array(
+ '' => '(?:(?<!\*)\*\*(?!\*)|(?<!_)__(?!_))(?=\S)(?![.,:;]\s)',
+ '**' => '(?<=\S)(?<!\*)\*\*(?!\*)',
+ '__' => '(?<=\S)(?<!_)__(?!_)',
+ );
+ var $em_strong_relist = array(
+ '' => '(?:(?<!\*)\*\*\*(?!\*)|(?<!_)___(?!_))(?=\S)(?![.,:;]\s)',
+ '***' => '(?<=\S)(?<!\*)\*\*\*(?!\*)',
+ '___' => '(?<=\S)(?<!_)___(?!_)',
+ );
+ var $em_strong_prepared_relist;
+
+ function prepareItalicsAndBold() {
+ #
+ # Prepare regular expressions for seraching emphasis tokens in any
+ # context.
+ #
+ foreach ($this->em_relist as $em => $em_re) {
+ foreach ($this->strong_relist as $strong => $strong_re) {
+ # Construct list of allowed token expressions.
+ $token_relist = array();
+ if (isset($this->em_strong_relist["$em$strong"])) {
+ $token_relist[] = $this->em_strong_relist["$em$strong"];
+ }
+ $token_relist[] = $em_re;
+ $token_relist[] = $strong_re;
+
+ # Construct master expression from list.
+ $token_re = '{('. implode('|', $token_relist) .')}';
+ $this->em_strong_prepared_relist["$em$strong"] = $token_re;
+ }
+ }
+ }
+
+ function doItalicsAndBold($text) {
+ $token_stack = array('');
+ $text_stack = array('');
+ $em = '';
+ $strong = '';
+ $tree_char_em = false;
+
+ while (1) {
+ #
+ # Get prepared regular expression for seraching emphasis tokens
+ # in current context.
+ #
+ $token_re = $this->em_strong_prepared_relist["$em$strong"];
+
+ #
+ # Each loop iteration seach for the next emphasis token.
+ # Each token is then passed to handleSpanToken.
+ #
+ $parts = preg_split($token_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE);
+ $text_stack[0] .= $parts[0];
+ $token =& $parts[1];
+ $text =& $parts[2];
+
+ if (empty($token)) {
+ # Reached end of text span: empty stack without emitting.
+ # any more emphasis.
+ while ($token_stack[0]) {
+ $text_stack[1] .= array_shift($token_stack);
+ $text_stack[0] .= array_shift($text_stack);
+ }
+ break;
+ }
+
+ $token_len = strlen($token);
+ if ($tree_char_em) {
+ # Reached closing marker while inside a three-char emphasis.
+ if ($token_len == 3) {
+ # Three-char closing marker, close em and strong.
+ array_shift($token_stack);
+ $span = array_shift($text_stack);
+ $span = $this->runSpanGamut($span);
+ $span = "<strong><em>$span</em></strong>";
+ $text_stack[0] .= $this->hashPart($span);
+ $em = '';
+ $strong = '';
+ } else {
+ # Other closing marker: close one em or strong and
+ # change current token state to match the other
+ $token_stack[0] = str_repeat($token{0}, 3-$token_len);
+ $tag = $token_len == 2 ? "strong" : "em";
+ $span = $text_stack[0];
+ $span = $this->runSpanGamut($span);
+ $span = "<$tag>$span</$tag>";
+ $text_stack[0] = $this->hashPart($span);
+ $$tag = ''; # $$tag stands for $em or $strong
+ }
+ $tree_char_em = false;
+ } else if ($token_len == 3) {
+ if ($em) {
+ # Reached closing marker for both em and strong.
+ # Closing strong marker:
+ for ($i = 0; $i < 2; ++$i) {
+ $shifted_token = array_shift($token_stack);
+ $tag = strlen($shifted_token) == 2 ? "strong" : "em";
+ $span = array_shift($text_stack);
+ $span = $this->runSpanGamut($span);
+ $span = "<$tag>$span</$tag>";
+ $text_stack[0] .= $this->hashPart($span);
+ $$tag = ''; # $$tag stands for $em or $strong
+ }
+ } else {
+ # Reached opening three-char emphasis marker. Push on token
+ # stack; will be handled by the special condition above.
+ $em = $token{0};
+ $strong = "$em$em";
+ array_unshift($token_stack, $token);
+ array_unshift($text_stack, '');
+ $tree_char_em = true;
+ }
+ } else if ($token_len == 2) {
+ if ($strong) {
+ # Unwind any dangling emphasis marker:
+ if (strlen($token_stack[0]) == 1) {
+ $text_stack[1] .= array_shift($token_stack);
+ $text_stack[0] .= array_shift($text_stack);
+ }
+ # Closing strong marker:
+ array_shift($token_stack);
+ $span = array_shift($text_stack);
+ $span = $this->runSpanGamut($span);
+ $span = "<strong>$span</strong>";
+ $text_stack[0] .= $this->hashPart($span);
+ $strong = '';
+ } else {
+ array_unshift($token_stack, $token);
+ array_unshift($text_stack, '');
+ $strong = $token;
+ }
+ } else {
+ # Here $token_len == 1
+ if ($em) {
+ if (strlen($token_stack[0]) == 1) {
+ # Closing emphasis marker:
+ array_shift($token_stack);
+ $span = array_shift($text_stack);
+ $span = $this->runSpanGamut($span);
+ $span = "<em>$span</em>";
+ $text_stack[0] .= $this->hashPart($span);
+ $em = '';
+ } else {
+ $text_stack[0] .= $token;
+ }
+ } else {
+ array_unshift($token_stack, $token);
+ array_unshift($text_stack, '');
+ $em = $token;
+ }
+ }
+ }
+ return $text_stack[0];
+ }
+
+
+ function doBlockQuotes($text) {
+ $text = preg_replace_callback('/
+ ( # Wrap whole match in $1
+ (?>
+ ^[ ]*>[ ]? # ">" at the start of a line
+ .+\n # rest of the first line
+ (.+\n)* # subsequent consecutive lines
+ \n* # blanks
+ )+
+ )
+ /xm',
+ array(&$this, '_doBlockQuotes_callback'), $text);
+
+ return $text;
+ }
+ function _doBlockQuotes_callback($matches) {
+ $bq = $matches[1];
+ # trim one level of quoting - trim whitespace-only lines
+ $bq = preg_replace('/^[ ]*>[ ]?|^[ ]+$/m', '', $bq);
+ $bq = $this->runBlockGamut($bq); # recurse
+
+ $bq = preg_replace('/^/m', " ", $bq);
+ # These leading spaces cause problem with <pre> content,
+ # so we need to fix that:
+ $bq = preg_replace_callback('{(\s*<pre>.+?</pre>)}sx',
+ array(&$this, '_DoBlockQuotes_callback2'), $bq);
+
+ return "\n". $this->hashBlock("<blockquote>\n$bq\n</blockquote>")."\n\n";
+ }
+ function _doBlockQuotes_callback2($matches) {
+ $pre = $matches[1];
+ $pre = preg_replace('/^ /m', '', $pre);
+ return $pre;
+ }
+
+
+ function formParagraphs($text) {
+ #
+ # Params:
+ # $text - string to process with html <p> tags
+ #
+ # Strip leading and trailing lines:
+ $text = preg_replace('/\A\n+|\n+\z/', '', $text);
+
+ $grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY);
+
+ #
+ # Wrap <p> tags and unhashify HTML blocks
+ #
+ foreach ($grafs as $key => $value) {
+ if (!preg_match('/^B\x1A[0-9]+B$/', $value)) {
+ # Is a paragraph.
+ $value = $this->runSpanGamut($value);
+ $value = preg_replace('/^([ ]*)/', "<p>", $value);
+ $value .= "</p>";
+ $grafs[$key] = $this->unhash($value);
+ }
+ else {
+ # Is a block.
+ # Modify elements of @grafs in-place...
+ $graf = $value;
+ $block = $this->html_hashes[$graf];
+ $graf = $block;
+// if (preg_match('{
+// \A
+// ( # $1 = <div> tag
+// <div \s+
+// [^>]*
+// \b
+// markdown\s*=\s* ([\'"]) # $2 = attr quote char
+// 1
+// \2
+// [^>]*
+// >
+// )
+// ( # $3 = contents
+// .*
+// )
+// (</div>) # $4 = closing tag
+// \z
+// }xs', $block, $matches))
+// {
+// list(, $div_open, , $div_content, $div_close) = $matches;
+//
+// # We can't call Markdown(), because that resets the hash;
+// # that initialization code should be pulled into its own sub, though.
+// $div_content = $this->hashHTMLBlocks($div_content);
+//
+// # Run document gamut methods on the content.
+// foreach ($this->document_gamut as $method => $priority) {
+// $div_content = $this->$method($div_content);
+// }
+//
+// $div_open = preg_replace(
+// '{\smarkdown\s*=\s*([\'"]).+?\1}', '', $div_open);
+//
+// $graf = $div_open . "\n" . $div_content . "\n" . $div_close;
+// }
+ $grafs[$key] = $graf;
+ }
+ }
+
+ return implode("\n\n", $grafs);
+ }
+
+
+ function encodeAttribute($text) {
+ #
+ # Encode text for a double-quoted HTML attribute. This function
+ # is *not* suitable for attributes enclosed in single quotes.
+ #
+ $text = $this->encodeAmpsAndAngles($text);
+ $text = str_replace('"', '&quot;', $text);
+ return $text;
+ }
+
+
+ function encodeAmpsAndAngles($text) {
+ #
+ # Smart processing for ampersands and angle brackets that need to
+ # be encoded. Valid character entities are left alone unless the
+ # no-entities mode is set.
+ #
+ if ($this->no_entities) {
+ $text = str_replace('&', '&amp;', $text);
+ } else {
+ # Ampersand-encoding based entirely on Nat Irons's Amputator
+ # MT plugin: <http://bumppo.net/projects/amputator/>
+ $text = preg_replace('/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/',
+ '&amp;', $text);;
+ }
+ # Encode remaining <'s
+ $text = str_replace('<', '&lt;', $text);
+
+ return $text;
+ }
+
+
+ function doAutoLinks($text) {
+ $text = preg_replace_callback('{<((https?|ftp|dict):[^\'">\s]+)>}i',
+ array(&$this, '_doAutoLinks_url_callback'), $text);
+
+ # Email addresses: <address@domain.foo>
+ $text = preg_replace_callback('{
+ <
+ (?:mailto:)?
+ (
+ [-.\w\x80-\xFF]+
+ \@
+ [-a-z0-9\x80-\xFF]+(\.[-a-z0-9\x80-\xFF]+)*\.[a-z]+
+ )
+ >
+ }xi',
+ array(&$this, '_doAutoLinks_email_callback'), $text);
+
+ return $text;
+ }
+ function _doAutoLinks_url_callback($matches) {
+ $url = $this->encodeAttribute($matches[1]);
+ $link = "<a href=\"$url\">$url</a>";
+ return $this->hashPart($link);
+ }
+ function _doAutoLinks_email_callback($matches) {
+ $address = $matches[1];
+ $link = $this->encodeEmailAddress($address);
+ return $this->hashPart($link);
+ }
+
+
+ function encodeEmailAddress($addr) {
+ #
+ # Input: an email address, e.g. "foo@example.com"
+ #
+ # Output: the email address as a mailto link, with each character
+ # of the address encoded as either a decimal or hex entity, in
+ # the hopes of foiling most address harvesting spam bots. E.g.:
+ #
+ # <p><a href="&#109;&#x61;&#105;&#x6c;&#116;&#x6f;&#58;&#x66;o&#111;
+ # &#x40;&#101;&#x78;&#97;&#x6d;&#112;&#x6c;&#101;&#46;&#x63;&#111;
+ # &#x6d;">&#x66;o&#111;&#x40;&#101;&#x78;&#97;&#x6d;&#112;&#x6c;
+ # &#101;&#46;&#x63;&#111;&#x6d;</a></p>
+ #
+ # Based by a filter by Matthew Wickline, posted to BBEdit-Talk.
+ # With some optimizations by Milian Wolff.
+ #
+ $addr = "mailto:" . $addr;
+ $chars = preg_split('/(?<!^)(?!$)/', $addr);
+ $seed = (int)abs(crc32($addr) / strlen($addr)); # Deterministic seed.
+
+ foreach ($chars as $key => $char) {
+ $ord = ord($char);
+ # Ignore non-ascii chars.
+ if ($ord < 128) {
+ $r = ($seed * (1 + $key)) % 100; # Pseudo-random function.
+ # roughly 10% raw, 45% hex, 45% dec
+ # '@' *must* be encoded. I insist.
+ if ($r > 90 && $char != '@') /* do nothing */;
+ else if ($r < 45) $chars[$key] = '&#x'.dechex($ord).';';
+ else $chars[$key] = '&#'.$ord.';';
+ }
+ }
+
+ $addr = implode('', $chars);
+ $text = implode('', array_slice($chars, 7)); # text without `mailto:`
+ $addr = "<a href=\"$addr\">$text</a>";
+
+ return $addr;
+ }
+
+
+ function parseSpan($str) {
+ #
+ # Take the string $str and parse it into tokens, hashing embeded HTML,
+ # escaped characters and handling code spans.
+ #
+ $output = '';
+
+ $span_re = '{
+ (
+ \\\\'.$this->escape_chars_re.'
+ |
+ (?<![`\\\\])
+ `+ # code span marker
+ '.( $this->no_markup ? '' : '
+ |
+ <!-- .*? --> # comment
+ |
+ <\?.*?\?> | <%.*?%> # processing instruction
+ |
+ <[/!$]?[-a-zA-Z0-9:]+ # regular tags
+ (?>
+ \s
+ (?>[^"\'>]+|"[^"]*"|\'[^\']*\')*
+ )?
+ >
+ ').'
+ )
+ }xs';
+
+ while (1) {
+ #
+ # Each loop iteration seach for either the next tag, the next
+ # openning code span marker, or the next escaped character.
+ # Each token is then passed to handleSpanToken.
+ #
+ $parts = preg_split($span_re, $str, 2, PREG_SPLIT_DELIM_CAPTURE);
+
+ # Create token from text preceding tag.
+ if ($parts[0] != "") {
+ $output .= $parts[0];
+ }
+
+ # Check if we reach the end.
+ if (isset($parts[1])) {
+ $output .= $this->handleSpanToken($parts[1], $parts[2]);
+ $str = $parts[2];
+ }
+ else {
+ break;
+ }
+ }
+
+ return $output;
+ }
+
+
+ function handleSpanToken($token, &$str) {
+ #
+ # Handle $token provided by parseSpan by determining its nature and
+ # returning the corresponding value that should replace it.
+ #
+ switch ($token{0}) {
+ case "\\":
+ return $this->hashPart("&#". ord($token{1}). ";");
+ case "`":
+ # Search for end marker in remaining text.
+ if (preg_match('/^(.*?[^`])'.preg_quote($token).'(?!`)(.*)$/sm',
+ $str, $matches))
+ {
+ $str = $matches[2];
+ $codespan = $this->makeCodeSpan($matches[1]);
+ return $this->hashPart($codespan);
+ }
+ return $token; // return as text since no ending marker found.
+ default:
+ return $this->hashPart($token);
+ }
+ }
+
+
+ function outdent($text) {
+ #
+ # Remove one level of line-leading tabs or spaces
+ #
+ return preg_replace('/^(\t|[ ]{1,'.$this->tab_width.'})/m', '', $text);
+ }
+
+
+ # String length function for detab. `_initDetab` will create a function to
+ # hanlde UTF-8 if the default function does not exist.
+ var $utf8_strlen = 'mb_strlen';
+
+ function detab($text) {
+ #
+ # Replace tabs with the appropriate amount of space.
+ #
+ # For each line we separate the line in blocks delemited by
+ # tab characters. Then we reconstruct every line by adding the
+ # appropriate number of space between each blocks.
+
+ $text = preg_replace_callback('/^.*\t.*$/m',
+ array(&$this, '_detab_callback'), $text);
+
+ return $text;
+ }
+ function _detab_callback($matches) {
+ $line = $matches[0];
+ $strlen = $this->utf8_strlen; # strlen function for UTF-8.
+
+ # Split in blocks.
+ $blocks = explode("\t", $line);
+ # Add each blocks to the line.
+ $line = $blocks[0];
+ unset($blocks[0]); # Do not add first block twice.
+ foreach ($blocks as $block) {
+ # Calculate amount of space, insert spaces, insert block.
+ $amount = $this->tab_width -
+ $strlen($line, 'UTF-8') % $this->tab_width;
+ $line .= str_repeat(" ", $amount) . $block;
+ }
+ return $line;
+ }
+ function _initDetab() {
+ #
+ # Check for the availability of the function in the `utf8_strlen` property
+ # (initially `mb_strlen`). If the function is not available, create a
+ # function that will loosely count the number of UTF-8 characters with a
+ # regular expression.
+ #
+ if (function_exists($this->utf8_strlen)) return;
+ $this->utf8_strlen = create_function('$text', 'return preg_match_all(
+ "/[\\\\x00-\\\\xBF]|[\\\\xC0-\\\\xFF][\\\\x80-\\\\xBF]*/",
+ $text, $m);');
+ }
+
+
+ function unhash($text) {
+ #
+ # Swap back in all the tags hashed by _HashHTMLBlocks.
+ #
+ return preg_replace_callback('/(.)\x1A[0-9]+\1/',
+ array(&$this, '_unhash_callback'), $text);
+ }
+ function _unhash_callback($matches) {
+ return $this->html_hashes[$matches[0]];
+ }
+
+}
+
+/*
+
+PHP Markdown
+============
+
+Description
+-----------
+
+This is a PHP translation of the original Markdown formatter written in
+Perl by John Gruber.
+
+Markdown is a text-to-HTML filter; it translates an easy-to-read /
+easy-to-write structured text format into HTML. Markdown's text format
+is most similar to that of plain text email, and supports features such
+as headers, *emphasis*, code blocks, blockquotes, and links.
+
+Markdown's syntax is designed not as a generic markup language, but
+specifically to serve as a front-end to (X)HTML. You can use span-level
+HTML tags anywhere in a Markdown document, and you can use block level
+HTML tags (like <div> and <table> as well).
+
+For more information about Markdown's syntax, see:
+
+<http://daringfireball.net/projects/markdown/>
+
+
+Bugs
+----
+
+To file bug reports please send email to:
+
+<michel.fortin@michelf.com>
+
+Please include with your report: (1) the example input; (2) the output you
+expected; (3) the output Markdown actually produced.
+
+
+Version History
+---------------
+
+See the readme file for detailed release notes for this version.
+
+
+Copyright and License
+---------------------
+
+PHP Markdown
+Copyright (c) 2004-2008 Michel Fortin
+<http://www.michelf.com/>
+All rights reserved.
+
+Based on Markdown
+Copyright (c) 2003-2006 John Gruber
+<http://daringfireball.net/>
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name "Markdown" nor the names of its contributors may
+ be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+This software is provided by the copyright holders and contributors "as
+is" and any express or implied warranties, including, but not limited
+to, the implied warranties of merchantability and fitness for a
+particular purpose are disclaimed. In no event shall the copyright owner
+or contributors be liable for any direct, indirect, incidental, special,
+exemplary, or consequential damages (including, but not limited to,
+procurement of substitute goods or services; loss of use, data, or
+profits; or business interruption) however caused and on any theory of
+liability, whether in contract, strict liability, or tort (including
+negligence or otherwise) arising in any way out of the use of this
+software, even if advised of the possibility of such damage.
+
+*/
+?> \ No newline at end of file