summaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/Smarty/plugins/block.t.php13
-rw-r--r--lib/Smarty/plugins/function.gentime.php7
-rw-r--r--lib/actions/domains_list.php32
-rw-r--r--lib/actions/system_login.php5
-rw-r--r--lib/actions/user_add.php71
-rw-r--r--lib/actions/user_types_list.php23
-rw-r--r--lib/actions/users_list.php11
-rw-r--r--lib/api/kolab_admin_domains_actions.php (renamed from lib/kolab_admin_domains_actions.php)2
-rw-r--r--lib/api/kolab_admin_form_value_actions.php (renamed from lib/kolab_admin_form_value_actions.php)2
-rw-r--r--lib/api/kolab_admin_group_actions.php (renamed from lib/kolab_admin_group_actions.php)2
-rw-r--r--lib/api/kolab_admin_group_types_actions.php (renamed from lib/kolab_admin_group_types_actions.php)2
-rw-r--r--lib/api/kolab_admin_groups_actions.php (renamed from lib/kolab_admin_groups_actions.php)2
-rw-r--r--lib/api/kolab_admin_user_actions.php (renamed from lib/kolab_admin_user_actions.php)2
-rw-r--r--lib/api/kolab_admin_user_types_actions.php (renamed from lib/kolab_admin_user_types_actions.php)2
-rw-r--r--lib/api/kolab_admin_users_actions.php (renamed from lib/kolab_admin_users_actions.php)2
-rw-r--r--lib/client/kolab_admin_client_task_about.php34
-rw-r--r--lib/client/kolab_admin_client_task_group.php53
-rw-r--r--lib/client/kolab_admin_client_task_main.php26
-rw-r--r--lib/client/kolab_admin_client_task_user.php106
-rw-r--r--lib/ext/HTTP/Request2.php1015
-rw-r--r--lib/ext/HTTP/Request2/Adapter.php154
-rw-r--r--lib/ext/HTTP/Request2/Adapter/Curl.php560
-rw-r--r--lib/ext/HTTP/Request2/Adapter/Mock.php171
-rw-r--r--lib/ext/HTTP/Request2/Adapter/Socket.php1084
-rw-r--r--lib/ext/HTTP/Request2/CookieJar.php499
-rw-r--r--lib/ext/HTTP/Request2/Exception.php160
-rw-r--r--lib/ext/HTTP/Request2/MultipartBody.php274
-rw-r--r--lib/ext/HTTP/Request2/Observer/Log.php215
-rw-r--r--lib/ext/HTTP/Request2/Response.php643
-rwxr-xr-xlib/ext/Net/URL2.php942
-rw-r--r--lib/functions.php50
-rw-r--r--lib/kolab_admin_api.php157
-rw-r--r--lib/kolab_admin_api_controller.php (renamed from lib/kolab_admin_controller.php)51
-rw-r--r--lib/kolab_admin_api_result.php42
-rw-r--r--lib/kolab_admin_api_service.php (renamed from lib/kolab_admin_service.php)4
-rw-r--r--lib/kolab_admin_client_output.php162
-rw-r--r--lib/kolab_admin_client_task.php283
-rw-r--r--lib/kolab_html.php330
-rw-r--r--lib/kolab_utils.php78
-rw-r--r--lib/locale/en_US.php29
40 files changed, 7104 insertions, 196 deletions
diff --git a/lib/Smarty/plugins/block.t.php b/lib/Smarty/plugins/block.t.php
new file mode 100644
index 0000000..2d7c38e
--- /dev/null
+++ b/lib/Smarty/plugins/block.t.php
@@ -0,0 +1,13 @@
+<?php
+ function smarty_block_t($params, $content, $template, &$repeat)
+ {
+ if (!empty($content)) {
+
+ array_unshift($params, $content);
+ $content = kolab_admin_client_task::translate($params);
+
+ return trim($content);
+ }
+ }
+
+?>
diff --git a/lib/Smarty/plugins/function.gentime.php b/lib/Smarty/plugins/function.gentime.php
new file mode 100644
index 0000000..8ad5c65
--- /dev/null
+++ b/lib/Smarty/plugins/function.gentime.php
@@ -0,0 +1,7 @@
+<?php
+ function smarty_function_gentime($params, $template)
+ {
+ $time = microtime(true);
+ return sprintf('%.4f', $time - KADM_START);
+ }
+?>
diff --git a/lib/actions/domains_list.php b/lib/actions/domains_list.php
deleted file mode 100644
index 587cf68..0000000
--- a/lib/actions/domains_list.php
+++ /dev/null
@@ -1,32 +0,0 @@
-<?php
-
- require_once(dirname(__FILE__) . "/../functions.php");
-
- $auth = Auth::get_instance();
- $conf = Conf::get_instance();
-
- $domains = $auth->list_domains();
- $domains = LDAP::normalize_result($domains);
-
- if (isset($_GET['rest']) && !empty($_GET['rest'])) {
- $search_key = str_replace('/','',$_GET['rest']);
-
- $section = $conf->get('kolab', 'auth_mechanism');
- $domain_name_attr = $conf->get($section, 'domain_name_attribute');
-
- foreach ($domains as $id => $attributes) {
-
- if (is_array($attributes['associateddomain'])) {
- if (in_array($_GET['rest'], $attributes['associateddomain'])) {
- print json_encode($attributes);
- }
- } elseif ($attributes['associateddomain'] == $search_key) {
- print json_encode($attributes);
- }
- }
-
- } else {
- print json_encode($domains);
- }
-
-?>
diff --git a/lib/actions/system_login.php b/lib/actions/system_login.php
deleted file mode 100644
index 7457f43..0000000
--- a/lib/actions/system_login.php
+++ /dev/null
@@ -1,5 +0,0 @@
-<?php
- if (!$_SERVER["REQUEST_METHOD"] == "POST") {
- throw new Exception("You are not posting any information you twat!");
- }
-?>
diff --git a/lib/actions/user_add.php b/lib/actions/user_add.php
deleted file mode 100644
index 9986fde..0000000
--- a/lib/actions/user_add.php
+++ /dev/null
@@ -1,71 +0,0 @@
-<?php
- $type = 'kolab';
-
- $user_type = mysql_fetch_assoc(query("SELECT attributes FROM user_types WHERE id = '" . $_GET['user_type_id'] ."'"));
-
- $user_type_attributes = json_decode(base64_decode($user_type['attributes']));
-
-# echo "<pre>";
-# print_r($_POST);
-# echo "</pre>";
-
- $user = Array();
-
- #chars = ['Ä', 'Ü', 'Ö', 'ä', 'ü', 'ö', 'ß']
- #simple = ['Ae', 'Ue', 'Oe', 'ae', 'ue', 'oe', 'ss']
-
- $needles = Array(
- ' ',
- 'ö',
- 'ü',
- );
-
- $replaces = Array(
- '',
- 'oe',
- 'ue',
- );
-
- foreach ($user_type_attributes as $level => $attributes) {
- echo "<li>$level:<pre>";
- print_r($attributes);
- echo "</pre>";
- if ($level == "mandatory") {
- foreach ($attributes as $key => $value) {
- if (isset($_POST[strtolower($key)])) {
-# print $level . " key " . $key . " found\n";
- $user[$key] = $_POST[strtolower($key)];
- } elseif (isset($_POST[strtolower($value)])) {
-# print $level . " value for " . $value . " found\n";
- $user[$value] = $_POST[strtolower($value)];
- } else {
- $user[$key] = $value;
- }
- }
- }
- if ($level == "auto_generated") {
- // Only contains a list of attribute names...
- foreach ($attributes as $num => $attribute) {
- if ($attribute == "alias") {
- // Apply secondary mail routine
- }
-
- if ($attribute == "uid") {
- // Apply normalization
- $user['uid'] = strtolower(str_replace($needles, $replaces, $user['sn']));
- }
-
- if ($attribute == "cn") {
- $user['cn'] = $user['givenName'] . " " . $user['sn'];
- }
-
- if ($attribute == "displayName") {
- $user['displayName'] = $user['sn'] . ", " . $user['givenName'];
- }
- }
- }
- }
-
- print_r($user);
-
-?>
diff --git a/lib/actions/user_types_list.php b/lib/actions/user_types_list.php
deleted file mode 100644
index a504c72..0000000
--- a/lib/actions/user_types_list.php
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php
- $result = query("SELECT * FROM user_types");
-
- $user_types = Array();
-
- while ($row = mysql_fetch_assoc($result)) {
- $user_types[$row['id']] = Array();
-
- foreach ($row as $key => $value) {
- if ($key != "id") {
- if ($key == "attributes") {
- $user_types[$row['id']][$key] = json_decode(base64_decode($value));
- } else {
- $user_types[$row['id']][$key] = $value;
- }
- }
- }
- }
-
- #print base64_encode(json_encode($user_types));
- print json_encode($user_types);
-
-?>
diff --git a/lib/actions/users_list.php b/lib/actions/users_list.php
deleted file mode 100644
index 66903e0..0000000
--- a/lib/actions/users_list.php
+++ /dev/null
@@ -1,11 +0,0 @@
-<?php
- require_once(dirname(__FILE__) . "/../functions.php");
-
- require_once('Auth.php');
-
- $auth = Auth::get_instance();
- $users = $auth->list_users();
- $users = $auth->normalize_result($users);
-
- print json_encode($users);
-?>
diff --git a/lib/kolab_admin_domains_actions.php b/lib/api/kolab_admin_domains_actions.php
index e972e55..a55207a 100644
--- a/lib/kolab_admin_domains_actions.php
+++ b/lib/api/kolab_admin_domains_actions.php
@@ -3,7 +3,7 @@
/**
*
*/
- class kolab_admin_domains_actions extends kolab_admin_service
+ class kolab_admin_domains_actions extends kolab_admin_api_service
{
public function capabilities($domain)
{
diff --git a/lib/kolab_admin_form_value_actions.php b/lib/api/kolab_admin_form_value_actions.php
index 8803f74..16bf7dd 100644
--- a/lib/kolab_admin_form_value_actions.php
+++ b/lib/api/kolab_admin_form_value_actions.php
@@ -3,7 +3,7 @@
/**
*
*/
- class kolab_admin_form_value_actions extends kolab_admin_service
+ class kolab_admin_form_value_actions extends kolab_admin_api_service
{
public function capabilities($domain)
{
diff --git a/lib/kolab_admin_group_actions.php b/lib/api/kolab_admin_group_actions.php
index 3dacc48..2be062c 100644
--- a/lib/kolab_admin_group_actions.php
+++ b/lib/api/kolab_admin_group_actions.php
@@ -3,7 +3,7 @@
/**
*
*/
- class kolab_admin_group_actions extends kolab_admin_service
+ class kolab_admin_group_actions extends kolab_admin_api_service
{
public function capabilities($domain)
{
diff --git a/lib/kolab_admin_group_types_actions.php b/lib/api/kolab_admin_group_types_actions.php
index 885f4f4..0dce919 100644
--- a/lib/kolab_admin_group_types_actions.php
+++ b/lib/api/kolab_admin_group_types_actions.php
@@ -3,7 +3,7 @@
/**
*
*/
- class kolab_admin_group_types_actions extends kolab_admin_service
+ class kolab_admin_group_types_actions extends kolab_admin_api_service
{
public function capabilities($domain)
{
diff --git a/lib/kolab_admin_groups_actions.php b/lib/api/kolab_admin_groups_actions.php
index aa72659..9997b7f 100644
--- a/lib/kolab_admin_groups_actions.php
+++ b/lib/api/kolab_admin_groups_actions.php
@@ -3,7 +3,7 @@
/**
*
*/
- class kolab_admin_groups_actions extends kolab_admin_service
+ class kolab_admin_groups_actions extends kolab_admin_api_service
{
public function capabilities($domain)
{
diff --git a/lib/kolab_admin_user_actions.php b/lib/api/kolab_admin_user_actions.php
index 245bcc4..c3e55a7 100644
--- a/lib/kolab_admin_user_actions.php
+++ b/lib/api/kolab_admin_user_actions.php
@@ -3,7 +3,7 @@
/**
*
*/
- class kolab_admin_user_actions extends kolab_admin_service
+ class kolab_admin_user_actions extends kolab_admin_api_service
{
public function capabilities($domain)
{
diff --git a/lib/kolab_admin_user_types_actions.php b/lib/api/kolab_admin_user_types_actions.php
index bc05351..9834a29 100644
--- a/lib/kolab_admin_user_types_actions.php
+++ b/lib/api/kolab_admin_user_types_actions.php
@@ -3,7 +3,7 @@
/**
*
*/
- class kolab_admin_user_types_actions extends kolab_admin_service
+ class kolab_admin_user_types_actions extends kolab_admin_api_service
{
public function capabilities($domain)
{
diff --git a/lib/kolab_admin_users_actions.php b/lib/api/kolab_admin_users_actions.php
index 1e74490..ed19ab0 100644
--- a/lib/kolab_admin_users_actions.php
+++ b/lib/api/kolab_admin_users_actions.php
@@ -3,7 +3,7 @@
/**
*
*/
- class kolab_admin_users_actions extends kolab_admin_service
+ class kolab_admin_users_actions extends kolab_admin_api_service
{
public function capabilities($domain)
{
diff --git a/lib/client/kolab_admin_client_task_about.php b/lib/client/kolab_admin_client_task_about.php
new file mode 100644
index 0000000..f36e0cf
--- /dev/null
+++ b/lib/client/kolab_admin_client_task_about.php
@@ -0,0 +1,34 @@
+<?php
+
+class kolab_admin_client_task_about extends kolab_admin_client_task
+{
+ protected $ajax_only = true;
+
+ protected $menu = array(
+ 'kolab' => 'menu.kolab',
+ 'kolabsys' => 'menu.kolabsys',
+ 'technology' => 'menu.technology',
+ );
+
+ public function action_default()
+ {
+ $this->output->set_object('content', 'about', true);
+ $this->output->set_object('task_navigation', $this->menu());
+ }
+
+ public function action_kolab()
+ {
+ $this->output->set_object('content', 'about_kolab', true);
+ }
+
+ public function action_kolabsys()
+ {
+ $this->output->set_object('content', 'about_kolabsys', true);
+ }
+
+ public function action_technology()
+ {
+ $this->output->set_object('content', 'about_technology', true);
+ }
+
+}
diff --git a/lib/client/kolab_admin_client_task_group.php b/lib/client/kolab_admin_client_task_group.php
new file mode 100644
index 0000000..6d2a58f
--- /dev/null
+++ b/lib/client/kolab_admin_client_task_group.php
@@ -0,0 +1,53 @@
+<?php
+
+class kolab_admin_client_task_group extends kolab_admin_client_task
+{
+ protected $ajax_only = true;
+
+ protected $menu = array(
+ 'add' => 'group.add',
+ 'list' => 'group.list',
+ );
+
+ public function action_default()
+ {
+ $this->output->set_object('content', '');
+ $this->output->set_object('task_navigation', $this->menu());
+ }
+
+ public function action_list()
+ {
+// $content = 'test output from users.list';
+ $result = $this->api->post('groups.list');
+ $result = (array) $result->get();
+/*
+ foreach ($result as $idx => $item) {
+ if (!is_array($item) || empty($item['uid'])) {
+ unset($result[$idx]);
+ continue;
+ }
+ $result[$idx] = sprintf('<li><a href="#" onclick="kadm.command(\'user.info\', \'%s\')">%s</a></li>',
+ $idx, $item['uid']);
+ }
+
+ $result = '<ul id="userlist">' . implode("\n", $result) . '</ul>';
+ $this->output->set_object('content', $result);
+*/
+ }
+
+ public function action_info()
+ {
+/*
+ $id = $this->get_input('id', 'POST');
+ $result = $this->api->get('user.info', array('user' => $id));
+
+ $user = $result->get($id);
+ $this->output->set_object('content', print_r($user, true));
+*/
+ }
+
+ public function group_add()
+ {
+
+ }
+}
diff --git a/lib/client/kolab_admin_client_task_main.php b/lib/client/kolab_admin_client_task_main.php
new file mode 100644
index 0000000..fd682c6
--- /dev/null
+++ b/lib/client/kolab_admin_client_task_main.php
@@ -0,0 +1,26 @@
+<?php
+
+ class kolab_admin_client_task_main extends kolab_admin_client_task
+ {
+ protected $menu = array(
+ 'user.default' => 'menu.users',
+ 'group.default' => 'menu.groups',
+ 'about.default' => 'menu.about',
+ );
+
+
+ public function action_default()
+ {
+ // assign token
+ $this->output->set_env('token', $_SESSION['user']['token']);
+
+ // assign default set of translations
+ $this->output->add_translation('loading', 'servererror');
+
+ $this->output->assign('main_menu', $this->menu());
+ $this->output->assign('user', $_SESSION['user']);
+ }
+
+ }
+
+?>
diff --git a/lib/client/kolab_admin_client_task_user.php b/lib/client/kolab_admin_client_task_user.php
new file mode 100644
index 0000000..4ef167e
--- /dev/null
+++ b/lib/client/kolab_admin_client_task_user.php
@@ -0,0 +1,106 @@
+<?php
+
+class kolab_admin_client_task_user extends kolab_admin_client_task
+{
+ protected $ajax_only = true;
+
+ protected $menu = array(
+ 'add' => 'user.add',
+ 'list' => 'user.list',
+ );
+
+ public function action_default()
+ {
+ $this->output->set_object('content', 'user', true);
+ $this->output->set_object('task_navigation', $this->menu());
+ }
+
+ public function action_list()
+ {
+ $result = $this->api->post('users.list');
+ $result = (array) $result->get();
+
+ $rows = $head = array();
+ $cols = array('name', 'actions', 'test');
+ $i = 0;
+
+ foreach ($cols as $col) {
+ $body = $col != 'actions' ? $this->translate('user.' . $col) : '';
+ $head[0]['cells'][] = array('class' => $col, 'body' => $body);
+ }
+
+ if (!empty($result)) {
+ foreach ($result as $idx => $item) {
+ if (!is_array($item) || empty($item['uid'])) {
+ continue;
+ }
+
+ $i++;
+ $cells = array();
+ $cells[] = array('class' => 'name', 'body' => kolab_html::escape($item['uid']),
+ 'onclick' => "kadm.command('user.info', '$idx')");
+ $cells[] = array('class' => 'links', 'body' => '<a>test</a>');
+ $cells[] = array('class' => 'links', 'body' => 'test sdf sdf sd fs df sdf sd f');
+ $rows[] = array('id' => $i, 'class' => 'selectable', 'cells' => $cells);
+ }
+ }
+ else {
+ $rows[] = array('cells' => array(
+ 0 => array('class' => 'empty-body', 'colspan' => count($cols),
+ 'body' => $this->translate('user.norecords')
+ )));
+ }
+
+ $table = kolab_html::table(array('id' => 'userlist', 'class' => 'list',
+ 'head' => $head, 'body' => $rows));
+ $this->output->set_object('task_content', $table);
+ }
+
+ public function action_info()
+ {
+ $id = $this->get_input('id', 'POST');
+ $result = $this->api->get('user.info', array('user' => $id));
+
+ $user = $result->get($id);
+
+ $definition = array(
+ 'personal' => array(
+ 'label' => $this->translate('user.personal'),
+ 'fields' => array(
+ 'givenname' => array(
+ 'label' => $this->translate('user.givenname'),
+ 'description' => '',
+ 'type' => '',
+ 'value' => $user['givenname'],
+ ),
+ 'sn' => array(
+ 'label' => $this->translate('user.surname'),
+ 'description' => '',
+ 'type' => '',
+ 'value' => $user['sn'],
+ ),
+ 'mail' => array(
+ 'label' => $this->translate('user.email'),
+ 'description' => 'sdf sdf sd fs sdf s dfsdfsdf sdfsdfsfsfs df',
+ 'type' => '',
+ 'value' => $user['mail'],
+ ),
+ ),
+ ),
+ );
+
+ $form = kolab_html::form_table(null, $definition);
+
+ $this->output->set_object('content', $form);
+ }
+
+ public function user_add()
+ {
+
+ }
+
+ private function user_types()
+ {
+ $result = $this->api->post('user_types.list');
+ }
+}
diff --git a/lib/ext/HTTP/Request2.php b/lib/ext/HTTP/Request2.php
new file mode 100644
index 0000000..6d8d953
--- /dev/null
+++ b/lib/ext/HTTP/Request2.php
@@ -0,0 +1,1015 @@
+<?php
+/**
+ * Class representing a HTTP request message
+ *
+ * PHP version 5
+ *
+ * LICENSE:
+ *
+ * Copyright (c) 2008-2011, Alexey Borzov <avb@php.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.
+ * * The names of the authors may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @category HTTP
+ * @package HTTP_Request2
+ * @author Alexey Borzov <avb@php.net>
+ * @license http://opensource.org/licenses/bsd-license.php New BSD License
+ * @version SVN: $Id: Request2.php 315409 2011-08-24 07:29:23Z avb $
+ * @link http://pear.php.net/package/HTTP_Request2
+ */
+
+/**
+ * A class representing an URL as per RFC 3986.
+ */
+require_once 'Net/URL2.php';
+
+/**
+ * Exception class for HTTP_Request2 package
+ */
+require_once 'HTTP/Request2/Exception.php';
+
+/**
+ * Class representing a HTTP request message
+ *
+ * @category HTTP
+ * @package HTTP_Request2
+ * @author Alexey Borzov <avb@php.net>
+ * @version Release: 2.0.0
+ * @link http://tools.ietf.org/html/rfc2616#section-5
+ */
+class HTTP_Request2 implements SplSubject
+{
+ /**#@+
+ * Constants for HTTP request methods
+ *
+ * @link http://tools.ietf.org/html/rfc2616#section-5.1.1
+ */
+ const METHOD_OPTIONS = 'OPTIONS';
+ const METHOD_GET = 'GET';
+ const METHOD_HEAD = 'HEAD';
+ const METHOD_POST = 'POST';
+ const METHOD_PUT = 'PUT';
+ const METHOD_DELETE = 'DELETE';
+ const METHOD_TRACE = 'TRACE';
+ const METHOD_CONNECT = 'CONNECT';
+ /**#@-*/
+
+ /**#@+
+ * Constants for HTTP authentication schemes
+ *
+ * @link http://tools.ietf.org/html/rfc2617
+ */
+ const AUTH_BASIC = 'basic';
+ const AUTH_DIGEST = 'digest';
+ /**#@-*/
+
+ /**
+ * Regular expression used to check for invalid symbols in RFC 2616 tokens
+ * @link http://pear.php.net/bugs/bug.php?id=15630
+ */
+ const REGEXP_INVALID_TOKEN = '![\x00-\x1f\x7f-\xff()<>@,;:\\\\"/\[\]?={}\s]!';
+
+ /**
+ * Regular expression used to check for invalid symbols in cookie strings
+ * @link http://pear.php.net/bugs/bug.php?id=15630
+ * @link http://web.archive.org/web/20080331104521/http://cgi.netscape.com/newsref/std/cookie_spec.html
+ */
+ const REGEXP_INVALID_COOKIE = '/[\s,;]/';
+
+ /**
+ * Fileinfo magic database resource
+ * @var resource
+ * @see detectMimeType()
+ */
+ private static $_fileinfoDb;
+
+ /**
+ * Observers attached to the request (instances of SplObserver)
+ * @var array
+ */
+ protected $observers = array();
+
+ /**
+ * Request URL
+ * @var Net_URL2
+ */
+ protected $url;
+
+ /**
+ * Request method
+ * @var string
+ */
+ protected $method = self::METHOD_GET;
+
+ /**
+ * Authentication data
+ * @var array
+ * @see getAuth()
+ */
+ protected $auth;
+
+ /**
+ * Request headers
+ * @var array
+ */
+ protected $headers = array();
+
+ /**
+ * Configuration parameters
+ * @var array
+ * @see setConfig()
+ */
+ protected $config = array(
+ 'adapter' => 'HTTP_Request2_Adapter_Socket',
+ 'connect_timeout' => 10,
+ 'timeout' => 0,
+ 'use_brackets' => true,
+ 'protocol_version' => '1.1',
+ 'buffer_size' => 16384,
+ 'store_body' => true,
+
+ 'proxy_host' => '',
+ 'proxy_port' => '',
+ 'proxy_user' => '',
+ 'proxy_password' => '',
+ 'proxy_auth_scheme' => self::AUTH_BASIC,
+
+ 'ssl_verify_peer' => true,
+ 'ssl_verify_host' => true,
+ 'ssl_cafile' => null,
+ 'ssl_capath' => null,
+ 'ssl_local_cert' => null,
+ 'ssl_passphrase' => null,
+
+ 'digest_compat_ie' => false,
+
+ 'follow_redirects' => false,
+ 'max_redirects' => 5,
+ 'strict_redirects' => false
+ );
+
+ /**
+ * Last event in request / response handling, intended for observers
+ * @var array
+ * @see getLastEvent()
+ */
+ protected $lastEvent = array(
+ 'name' => 'start',
+ 'data' => null
+ );
+
+ /**
+ * Request body
+ * @var string|resource
+ * @see setBody()
+ */
+ protected $body = '';
+
+ /**
+ * Array of POST parameters
+ * @var array
+ */
+ protected $postParams = array();
+
+ /**
+ * Array of file uploads (for multipart/form-data POST requests)
+ * @var array
+ */
+ protected $uploads = array();
+
+ /**
+ * Adapter used to perform actual HTTP request
+ * @var HTTP_Request2_Adapter
+ */
+ protected $adapter;
+
+ /**
+ * Cookie jar to persist cookies between requests
+ * @var HTTP_Request2_CookieJar
+ */
+ protected $cookieJar = null;
+
+ /**
+ * Constructor. Can set request URL, method and configuration array.
+ *
+ * Also sets a default value for User-Agent header.
+ *
+ * @param string|Net_Url2 Request URL
+ * @param string Request method
+ * @param array Configuration for this Request instance
+ */
+ public function __construct($url = null, $method = self::METHOD_GET, array $config = array())
+ {
+ $this->setConfig($config);
+ if (!empty($url)) {
+ $this->setUrl($url);
+ }
+ if (!empty($method)) {
+ $this->setMethod($method);
+ }
+ $this->setHeader('user-agent', 'HTTP_Request2/2.0.0 ' .
+ '(http://pear.php.net/package/http_request2) ' .
+ 'PHP/' . phpversion());
+ }
+
+ /**
+ * Sets the URL for this request
+ *
+ * If the URL has userinfo part (username & password) these will be removed
+ * and converted to auth data. If the URL does not have a path component,
+ * that will be set to '/'.
+ *
+ * @param string|Net_URL2 Request URL
+ * @return HTTP_Request2
+ * @throws HTTP_Request2_LogicException
+ */
+ public function setUrl($url)
+ {
+ if (is_string($url)) {
+ $url = new Net_URL2(
+ $url, array(Net_URL2::OPTION_USE_BRACKETS => $this->config['use_brackets'])
+ );
+ }
+ if (!$url instanceof Net_URL2) {
+ throw new HTTP_Request2_LogicException(
+ 'Parameter is not a valid HTTP URL',
+ HTTP_Request2_Exception::INVALID_ARGUMENT
+ );
+ }
+ // URL contains username / password?
+ if ($url->getUserinfo()) {
+ $username = $url->getUser();
+ $password = $url->getPassword();
+ $this->setAuth(rawurldecode($username), $password? rawurldecode($password): '');
+ $url->setUserinfo('');
+ }
+ if ('' == $url->getPath()) {
+ $url->setPath('/');
+ }
+ $this->url = $url;
+
+ return $this;
+ }
+
+ /**
+ * Returns the request URL
+ *
+ * @return Net_URL2
+ */
+ public function getUrl()
+ {
+ return $this->url;
+ }
+
+ /**
+ * Sets the request method
+ *
+ * @param string
+ * @return HTTP_Request2
+ * @throws HTTP_Request2_LogicException if the method name is invalid
+ */
+ public function setMethod($method)
+ {
+ // Method name should be a token: http://tools.ietf.org/html/rfc2616#section-5.1.1
+ if (preg_match(self::REGEXP_INVALID_TOKEN, $method)) {
+ throw new HTTP_Request2_LogicException(
+ "Invalid request method '{$method}'",
+ HTTP_Request2_Exception::INVALID_ARGUMENT
+ );
+ }
+ $this->method = $method;
+
+ return $this;
+ }
+
+ /**
+ * Returns the request method
+ *
+ * @return string
+ */
+ public function getMethod()
+ {
+ return $this->method;
+ }
+
+ /**
+ * Sets the configuration parameter(s)
+ *
+ * The following parameters are available:
+ * <ul>
+ * <li> 'adapter' - adapter to use (string)</li>
+ * <li> 'connect_timeout' - Connection timeout in seconds (integer)</li>
+ * <li> 'timeout' - Total number of seconds a request can take.
+ * Use 0 for no limit, should be greater than
+ * 'connect_timeout' if set (integer)</li>
+ * <li> 'use_brackets' - Whether to append [] to array variable names (bool)</li>
+ * <li> 'protocol_version' - HTTP Version to use, '1.0' or '1.1' (string)</li>
+ * <li> 'buffer_size' - Buffer size to use for reading and writing (int)</li>
+ * <li> 'store_body' - Whether to store response body in response object.
+ * Set to false if receiving a huge response and
+ * using an Observer to save it (boolean)</li>
+ * <li> 'proxy_host' - Proxy server host (string)</li>
+ * <li> 'proxy_port' - Proxy server port (integer)</li>
+ * <li> 'proxy_user' - Proxy auth username (string)</li>
+ * <li> 'proxy_password' - Proxy auth password (string)</li>
+ * <li> 'proxy_auth_scheme' - Proxy auth scheme, one of HTTP_Request2::AUTH_* constants (string)</li>
+ * <li> 'ssl_verify_peer' - Whether to verify peer's SSL certificate (bool)</li>
+ * <li> 'ssl_verify_host' - Whether to check that Common Name in SSL
+ * certificate matches host name (bool)</li>
+ * <li> 'ssl_cafile' - Cerificate Authority file to verify the peer
+ * with (use with 'ssl_verify_peer') (string)</li>
+ * <li> 'ssl_capath' - Directory holding multiple Certificate
+ * Authority files (string)</li>
+ * <li> 'ssl_local_cert' - Name of a file containing local cerificate (string)</li>
+ * <li> 'ssl_passphrase' - Passphrase with which local certificate
+ * was encoded (string)</li>
+ * <li> 'digest_compat_ie' - Whether to imitate behaviour of MSIE 5 and 6
+ * in using URL without query string in digest
+ * authentication (boolean)</li>
+ * <li> 'follow_redirects' - Whether to automatically follow HTTP Redirects (boolean)</li>
+ * <li> 'max_redirects' - Maximum number of redirects to follow (integer)</li>
+ * <li> 'strict_redirects' - Whether to keep request method on redirects via status 301 and
+ * 302 (true, needed for compatibility with RFC 2616)
+ * or switch to GET (false, needed for compatibility with most
+ * browsers) (boolean)</li>
+ * </ul>
+ *
+ * @param string|array configuration parameter name or array
+ * ('parameter name' => 'parameter value')
+ * @param mixed parameter value if $nameOrConfig is not an array
+ * @return HTTP_Request2
+ * @throws HTTP_Request2_LogicException If the parameter is unknown
+ */
+ public function setConfig($nameOrConfig, $value = null)
+ {
+ if (is_array($nameOrConfig)) {
+ foreach ($nameOrConfig as $name => $value) {
+ $this->setConfig($name, $value);
+ }
+
+ } else {
+ if (!array_key_exists($nameOrConfig, $this->config)) {
+ throw new HTTP_Request2_LogicException(
+ "Unknown configuration parameter '{$nameOrConfig}'",
+ HTTP_Request2_Exception::INVALID_ARGUMENT
+ );
+ }
+ $this->config[$nameOrConfig] = $value;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Returns the value(s) of the configuration parameter(s)
+ *
+ * @param string parameter name
+ * @return mixed value of $name parameter, array of all configuration
+ * parameters if $name is not given
+ * @throws HTTP_Request2_LogicException If the parameter is unknown
+ */
+ public function getConfig($name = null)
+ {
+ if (null === $name) {
+ return $this->config;
+ } elseif (!array_key_exists($name, $this->config)) {
+ throw new HTTP_Request2_LogicException(
+ "Unknown configuration parameter '{$name}'",
+ HTTP_Request2_Exception::INVALID_ARGUMENT
+ );
+ }
+ return $this->config[$name];
+ }
+
+ /**
+ * Sets the autentification data
+ *
+ * @param string user name
+ * @param string password
+ * @param string authentication scheme
+ * @return HTTP_Request2
+ */
+ public function setAuth($user, $password = '', $scheme = self::AUTH_BASIC)
+ {
+ if (empty($user)) {
+ $this->auth = null;
+ } else {
+ $this->auth = array(
+ 'user' => (string)$user,
+ 'password' => (string)$password,
+ 'scheme' => $scheme
+ );
+ }
+
+ return $this;
+ }
+
+ /**
+ * Returns the authentication data
+ *
+ * The array has the keys 'user', 'password' and 'scheme', where 'scheme'
+ * is one of the HTTP_Request2::AUTH_* constants.
+ *
+ * @return array
+ */
+ public function getAuth()
+ {
+ return $this->auth;
+ }
+
+ /**
+ * Sets request header(s)
+ *
+ * The first parameter may be either a full header string 'header: value' or
+ * header name. In the former case $value parameter is ignored, in the latter
+ * the header's value will either be set to $value or the header will be
+ * removed if $value is null. The first parameter can also be an array of
+ * headers, in that case method will be called recursively.
+ *
+ * Note that headers are treated case insensitively as per RFC 2616.
+ *
+ * <code>
+ * $req->setHeader('Foo: Bar'); // sets the value of 'Foo' header to 'Bar'
+ * $req->setHeader('FoO', 'Baz'); // sets the value of 'Foo' header to 'Baz'
+ * $req->setHeader(array('foo' => 'Quux')); // sets the value of 'Foo' header to 'Quux'
+ * $req->setHeader('FOO'); // removes 'Foo' header from request
+ * </code>
+ *
+ * @param string|array header name, header string ('Header: value')
+ * or an array of headers
+ * @param string|array|null header value if $name is not an array,
+ * header will be removed if value is null
+ * @param bool whether to replace previous header with the
+ * same name or append to its value
+ * @return HTTP_Request2
+ * @throws HTTP_Request2_LogicException
+ */
+ public function setHeader($name, $value = null, $replace = true)
+ {
+ if (is_array($name)) {
+ foreach ($name as $k => $v) {
+ if (is_string($k)) {
+ $this->setHeader($k, $v, $replace);
+ } else {
+ $this->setHeader($v, null, $replace);
+ }
+ }
+ } else {
+ if (null === $value && strpos($name, ':')) {
+ list($name, $value) = array_map('trim', explode(':', $name, 2));
+ }
+ // Header name should be a token: http://tools.ietf.org/html/rfc2616#section-4.2
+ if (preg_match(self::REGEXP_INVALID_TOKEN, $name)) {
+ throw new HTTP_Request2_LogicException(
+ "Invalid header name '{$name}'",
+ HTTP_Request2_Exception::INVALID_ARGUMENT
+ );
+ }
+ // Header names are case insensitive anyway
+ $name = strtolower($name);
+ if (null === $value) {
+ unset($this->headers[$name]);
+
+ } else {
+ if (is_array($value)) {
+ $value = implode(', ', array_map('trim', $value));
+ } elseif (is_string($value)) {
+ $value = trim($value);
+ }
+ if (!isset($this->headers[$name]) || $replace) {
+ $this->headers[$name] = $value;
+ } else {
+ $this->headers[$name] .= ', ' . $value;
+ }
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Returns the request headers
+ *
+ * The array is of the form ('header name' => 'header value'), header names
+ * are lowercased
+ *
+ * @return array
+ */
+ public function getHeaders()
+ {
+ return $this->headers;
+ }
+
+ /**
+ * Adds a cookie to the request
+ *
+ * If the request does not have a CookieJar object set, this method simply
+ * appends a cookie to "Cookie:" header.
+ *
+ * If a CookieJar object is available, the cookie is stored in that object.
+ * Data from request URL will be used for setting its 'domain' and 'path'
+ * parameters, 'expires' and 'secure' will be set to null and false,
+ * respectively. If you need further control, use CookieJar's methods.
+ *
+ * @param string cookie name
+ * @param string cookie value
+ * @return HTTP_Request2
+ * @throws HTTP_Request2_LogicException
+ * @see setCookieJar()
+ */
+ public function addCookie($name, $value)
+ {
+ if (!empty($this->cookieJar)) {
+ $this->cookieJar->store(array('name' => $name, 'value' => $value),
+ $this->url);
+
+ } else {
+ $cookie = $name . '=' . $value;
+ if (preg_match(self::REGEXP_INVALID_COOKIE, $cookie)) {
+ throw new HTTP_Request2_LogicException(
+ "Invalid cookie: '{$cookie}'",
+ HTTP_Request2_Exception::INVALID_ARGUMENT
+ );
+ }
+ $cookies = empty($this->headers['cookie'])? '': $this->headers['cookie'] . '; ';
+ $this->setHeader('cookie', $cookies . $cookie);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Sets the request body
+ *
+ * If you provide file pointer rather than file name, it should support
+ * fstat() and rewind() operations.
+ *
+ * @param string|resource|HTTP_Request2_MultipartBody Either a string
+ * with the body or filename containing body or pointer to
+ * an open file or object with multipart body data
+ * @param bool Whether first parameter is a filename
+ * @return HTTP_Request2
+ * @throws HTTP_Request2_LogicException
+ */
+ public function setBody($body, $isFilename = false)
+ {
+ if (!$isFilename && !is_resource($body)) {
+ if (!$body instanceof HTTP_Request2_MultipartBody) {
+ $this->body = (string)$body;
+ } else {
+ $this->body = $body;
+ }
+ } else {
+ $fileData = $this->fopenWrapper($body, empty($this->headers['content-type']));
+ $this->body = $fileData['fp'];
+ if (empty($this->headers['content-type'])) {
+ $this->setHeader('content-type', $fileData['type']);
+ }
+ }
+ $this->postParams = $this->uploads = array();
+
+ return $this;
+ }
+
+ /**
+ * Returns the request body
+ *
+ * @return string|resource|HTTP_Request2_MultipartBody
+ */
+ public function getBody()
+ {
+ if (self::METHOD_POST == $this->method &&
+ (!empty($this->postParams) || !empty($this->uploads))
+ ) {
+ if (0 === strpos($this->headers['content-type'], 'application/x-www-form-urlencoded')) {
+ $body = http_build_query($this->postParams, '', '&');
+ if (!$this->getConfig('use_brackets')) {
+ $body = preg_replace('/%5B\d+%5D=/', '=', $body);
+ }
+ // support RFC 3986 by not encoding '~' symbol (request #15368)
+ return str_replace('%7E', '~', $body);
+
+ } elseif (0 === strpos($this->headers['content-type'], 'multipart/form-data')) {
+ require_once 'HTTP/Request2/MultipartBody.php';
+ return new HTTP_Request2_MultipartBody(
+ $this->postParams, $this->uploads, $this->getConfig('use_brackets')
+ );
+ }
+ }
+ return $this->body;
+ }
+
+ /**
+ * Adds a file to form-based file upload
+ *
+ * Used to emulate file upload via a HTML form. The method also sets
+ * Content-Type of HTTP request to 'multipart/form-data'.
+ *
+ * If you just want to send the contents of a file as the body of HTTP
+ * request you should use setBody() method.
+ *
+ * If you provide file pointers rather than file names, they should support
+ * fstat() and rewind() operations.
+ *
+ * @param string name of file-upload field
+ * @param string|resource|array full name of local file, pointer to
+ * open file or an array of files
+ * @param string filename to send in the request
+ * @param string content-type of file being uploaded
+ * @return HTTP_Request2
+ * @throws HTTP_Request2_LogicException
+ */
+ public function addUpload($fieldName, $filename, $sendFilename = null,
+ $contentType = null)
+ {
+ if (!is_array($filename)) {
+ $fileData = $this->fopenWrapper($filename, empty($contentType));
+ $this->uploads[$fieldName] = array(
+ 'fp' => $fileData['fp'],
+ 'filename' => !empty($sendFilename)? $sendFilename
+ :(is_string($filename)? basename($filename): 'anonymous.blob') ,
+ 'size' => $fileData['size'],
+ 'type' => empty($contentType)? $fileData['type']: $contentType
+ );
+ } else {
+ $fps = $names = $sizes = $types = array();
+ foreach ($filename as $f) {
+ if (!is_array($f)) {
+ $f = array($f);
+ }
+ $fileData = $this->fopenWrapper($f[0], empty($f[2]));
+ $fps[] = $fileData['fp'];
+ $names[] = !empty($f[1])? $f[1]
+ :(is_string($f[0])? basename($f[0]): 'anonymous.blob');
+ $sizes[] = $fileData['size'];
+ $types[] = empty($f[2])? $fileData['type']: $f[2];
+ }
+ $this->uploads[$fieldName] = array(
+ 'fp' => $fps, 'filename' => $names, 'size' => $sizes, 'type' => $types
+ );
+ }
+ if (empty($this->headers['content-type']) ||
+ 'application/x-www-form-urlencoded' == $this->headers['content-type']
+ ) {
+ $this->setHeader('content-type', 'multipart/form-data');
+ }
+
+ return $this;
+ }
+
+ /**
+ * Adds POST parameter(s) to the request.
+ *
+ * @param string|array parameter name or array ('name' => 'value')
+ * @param mixed parameter value (can be an array)
+ * @return HTTP_Request2
+ */
+ public function addPostParameter($name, $value = null)
+ {
+ if (!is_array($name)) {
+ $this->postParams[$name] = $value;
+ } else {
+ foreach ($name as $k => $v) {
+ $this->addPostParameter($k, $v);
+ }
+ }
+ if (empty($this->headers['content-type'])) {
+ $this->setHeader('content-type', 'application/x-www-form-urlencoded');
+ }
+
+ return $this;
+ }
+
+ /**
+ * Attaches a new observer
+ *
+ * @param SplObserver
+ */
+ public function attach(SplObserver $observer)
+ {
+ foreach ($this->observers as $attached) {
+ if ($attached === $observer) {
+ return;
+ }
+ }
+ $this->observers[] = $observer;
+ }
+
+ /**
+ * Detaches an existing observer
+ *
+ * @param SplObserver
+ */
+ public function detach(SplObserver $observer)
+ {
+ foreach ($this->observers as $key => $attached) {
+ if ($attached === $observer) {
+ unset($this->observers[$key]);
+ return;
+ }
+ }
+ }
+
+ /**
+ * Notifies all observers
+ */
+ public function notify()
+ {
+ foreach ($this->observers as $observer) {
+ $observer->update($this);
+ }
+ }
+
+ /**
+ * Sets the last event
+ *
+ * Adapters should use this method to set the current state of the request
+ * and notify the observers.
+ *
+ * @param string event name
+ * @param mixed event data
+ */
+ public function setLastEvent($name, $data = null)
+ {
+ $this->lastEvent = array(
+ 'name' => $name,
+ 'data' => $data
+ );
+ $this->notify();
+ }
+
+ /**
+ * Returns the last event
+ *
+ * Observers should use this method to access the last change in request.
+ * The following event names are possible:
+ * <ul>
+ * <li>'connect' - after connection to remote server,
+ * data is the destination (string)</li>
+ * <li>'disconnect' - after disconnection from server</li>
+ * <li>'sentHeaders' - after sending the request headers,
+ * data is the headers sent (string)</li>
+ * <li>'sentBodyPart' - after sending a part of the request body,
+ * data is the length of that part (int)</li>
+ * <li>'sentBody' - after sending the whole request body,
+ * data is request body length (int)</li>
+ * <li>'receivedHeaders' - after receiving the response headers,
+ * data is HTTP_Request2_Response object</li>
+ * <li>'receivedBodyPart' - after receiving a part of the response
+ * body, data is that part (string)</li>
+ * <li>'receivedEncodedBodyPart' - as 'receivedBodyPart', but data is still
+ * encoded by Content-Encoding</li>
+ * <li>'receivedBody' - after receiving the complete response
+ * body, data is HTTP_Request2_Response object</li>
+ * </ul>
+ * Different adapters may not send all the event types. Mock adapter does
+ * not send any events to the observers.
+ *
+ * @return array The array has two keys: 'name' and 'data'
+ */
+ public function getLastEvent()
+ {
+ return $this->lastEvent;
+ }
+
+ /**
+ * Sets the adapter used to actually perform the request
+ *
+ * You can pass either an instance of a class implementing HTTP_Request2_Adapter
+ * or a class name. The method will only try to include a file if the class
+ * name starts with HTTP_Request2_Adapter_, it will also try to prepend this
+ * prefix to the class name if it doesn't contain any underscores, so that
+ * <code>
+ * $request->setAdapter('curl');
+ * </code>
+ * will work.
+ *
+ * @param string|HTTP_Request2_Adapter
+ * @return HTTP_Request2
+ * @throws HTTP_Request2_LogicException
+ */
+ public function setAdapter($adapter)
+ {
+ if (is_string($adapter)) {
+ if (!class_exists($adapter, false)) {
+ if (false === strpos($adapter, '_')) {
+ $adapter = 'HTTP_Request2_Adapter_' . ucfirst($adapter);
+ }
+ if (preg_match('/^HTTP_Request2_Adapter_([a-zA-Z0-9]+)$/', $adapter)) {
+ include_once str_replace('_', DIRECTORY_SEPARATOR, $adapter) . '.php';
+ }
+ if (!class_exists($adapter, false)) {
+ throw new HTTP_Request2_LogicException(
+ "Class {$adapter} not found",
+ HTTP_Request2_Exception::MISSING_VALUE
+ );
+ }
+ }
+ $adapter = new $adapter;
+ }
+ if (!$adapter instanceof HTTP_Request2_Adapter) {
+ throw new HTTP_Request2_LogicException(
+ 'Parameter is not a HTTP request adapter',
+ HTTP_Request2_Exception::INVALID_ARGUMENT
+ );
+ }
+ $this->adapter = $adapter;
+
+ return $this;
+ }
+
+ /**
+ * Sets the cookie jar
+ *
+ * A cookie jar is used to maintain cookies across HTTP requests and
+ * responses. Cookies from jar will be automatically added to the request
+ * headers based on request URL.
+ *
+ * @param HTTP_Request2_CookieJar|bool Existing CookieJar object, true to
+ * create a new one, false to remove
+ */
+ public function setCookieJar($jar = true)
+ {
+ if (!class_exists('HTTP_Request2_CookieJar', false)) {
+ require_once 'HTTP/Request2/CookieJar.php';
+ }
+
+ if ($jar instanceof HTTP_Request2_CookieJar) {
+ $this->cookieJar = $jar;
+ } elseif (true === $jar) {
+ $this->cookieJar = new HTTP_Request2_CookieJar();
+ } elseif (!$jar) {
+ $this->cookieJar = null;
+ } else {
+ throw new HTTP_Request2_LogicException(
+ 'Invalid parameter passed to setCookieJar()',
+ HTTP_Request2_Exception::INVALID_ARGUMENT
+ );
+ }
+
+ return $this;
+ }
+
+ /**
+ * Returns current CookieJar object or null if none
+ *
+ * @return HTTP_Request2_CookieJar|null
+ */
+ public function getCookieJar()
+ {
+ return $this->cookieJar;
+ }
+
+ /**
+ * Sends the request and returns the response
+ *
+ * @throws HTTP_Request2_Exception
+ * @return HTTP_Request2_Response
+ */
+ public function send()
+ {
+ // Sanity check for URL
+ if (!$this->url instanceof Net_URL2
+ || !$this->url->isAbsolute()
+ || !in_array(strtolower($this->url->getScheme()), array('https', 'http'))
+ ) {
+ throw new HTTP_Request2_LogicException(
+ 'HTTP_Request2 needs an absolute HTTP(S) request URL, '
+ . ($this->url instanceof Net_URL2
+ ? "'" . $this->url->__toString() . "'" : 'none')
+ . ' given',
+ HTTP_Request2_Exception::INVALID_ARGUMENT
+ );
+ }
+ if (empty($this->adapter)) {
+ $this->setAdapter($this->getConfig('adapter'));
+ }
+ // magic_quotes_runtime may break file uploads and chunked response
+ // processing; see bug #4543. Don't use ini_get() here; see bug #16440.
+ if ($magicQuotes = get_magic_quotes_runtime()) {
+ set_magic_quotes_runtime(false);
+ }
+ // force using single byte encoding if mbstring extension overloads
+ // strlen() and substr(); see bug #1781, bug #10605
+ if (extension_loaded('mbstring') && (2 & ini_get('mbstring.func_overload'))) {
+ $oldEncoding = mb_internal_encoding();
+ mb_internal_encoding('iso-8859-1');
+ }
+
+ try {
+ $response = $this->adapter->sendRequest($this);
+ } catch (Exception $e) {
+ }
+ // cleanup in either case (poor man's "finally" clause)
+ if ($magicQuotes) {
+ set_magic_quotes_runtime(true);
+ }
+ if (!empty($oldEncoding)) {
+ mb_internal_encoding($oldEncoding);
+ }
+ // rethrow the exception
+ if (!empty($e)) {
+ throw $e;
+ }
+ return $response;
+ }
+
+ /**
+ * Wrapper around fopen()/fstat() used by setBody() and addUpload()
+ *
+ * @param string|resource file name or pointer to open file
+ * @param bool whether to try autodetecting MIME type of file,
+ * will only work if $file is a filename, not pointer
+ * @return array array('fp' => file pointer, 'size' => file size, 'type' => MIME type)
+ * @throws HTTP_Request2_LogicException
+ */
+ protected function fopenWrapper($file, $detectType = false)
+ {
+ if (!is_string($file) && !is_resource($file)) {
+ throw new HTTP_Request2_LogicException(
+ "Filename or file pointer resource expected",
+ HTTP_Request2_Exception::INVALID_ARGUMENT
+ );
+ }
+ $fileData = array(
+ 'fp' => is_string($file)? null: $file,
+ 'type' => 'application/octet-stream',
+ 'size' => 0
+ );
+ if (is_string($file)) {
+ $track = @ini_set('track_errors', 1);
+ if (!($fileData['fp'] = @fopen($file, 'rb'))) {
+ $e = new HTTP_Request2_LogicException(
+ $php_errormsg, HTTP_Request2_Exception::READ_ERROR
+ );
+ }
+ @ini_set('track_errors', $track);
+ if (isset($e)) {
+ throw $e;
+ }
+ if ($detectType) {
+ $fileData['type'] = self::detectMimeType($file);
+ }
+ }
+ if (!($stat = fstat($fileData['fp']))) {
+ throw new HTTP_Request2_LogicException(
+ "fstat() call failed", HTTP_Request2_Exception::READ_ERROR
+ );
+ }
+ $fileData['size'] = $stat['size'];
+
+ return $fileData;
+ }
+
+ /**
+ * Tries to detect MIME type of a file
+ *
+ * The method will try to use fileinfo extension if it is available,
+ * deprecated mime_content_type() function in the other case. If neither
+ * works, default 'application/octet-stream' MIME type is returned
+ *
+ * @param string filename
+ * @return string file MIME type
+ */
+ protected static function detectMimeType($filename)
+ {
+ // finfo extension from PECL available
+ if (function_exists('finfo_open')) {
+ if (!isset(self::$_fileinfoDb)) {
+ self::$_fileinfoDb = @finfo_open(FILEINFO_MIME);
+ }
+ if (self::$_fileinfoDb) {
+ $info = finfo_file(self::$_fileinfoDb, $filename);
+ }
+ }
+ // (deprecated) mime_content_type function available
+ if (empty($info) && function_exists('mime_content_type')) {
+ return mime_content_type($filename);
+ }
+ return empty($info)? 'application/octet-stream': $info;
+ }
+}
+?>
diff --git a/lib/ext/HTTP/Request2/Adapter.php b/lib/ext/HTTP/Request2/Adapter.php
new file mode 100644
index 0000000..56d81a9
--- /dev/null
+++ b/lib/ext/HTTP/Request2/Adapter.php
@@ -0,0 +1,154 @@
+<?php
+/**
+ * Base class for HTTP_Request2 adapters
+ *
+ * PHP version 5
+ *
+ * LICENSE:
+ *
+ * Copyright (c) 2008-2011, Alexey Borzov <avb@php.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.
+ * * The names of the authors may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @category HTTP
+ * @package HTTP_Request2
+ * @author Alexey Borzov <avb@php.net>
+ * @license http://opensource.org/licenses/bsd-license.php New BSD License
+ * @version SVN: $Id: Adapter.php 308322 2011-02-14 13:58:03Z avb $
+ * @link http://pear.php.net/package/HTTP_Request2
+ */
+
+/**
+ * Class representing a HTTP response
+ */
+require_once 'HTTP/Request2/Response.php';
+
+/**
+ * Base class for HTTP_Request2 adapters
+ *
+ * HTTP_Request2 class itself only defines methods for aggregating the request
+ * data, all actual work of sending the request to the remote server and
+ * receiving its response is performed by adapters.
+ *
+ * @category HTTP
+ * @package HTTP_Request2
+ * @author Alexey Borzov <avb@php.net>
+ * @version Release: 2.0.0
+ */
+abstract class HTTP_Request2_Adapter
+{
+ /**
+ * A list of methods that MUST NOT have a request body, per RFC 2616
+ * @var array
+ */
+ protected static $bodyDisallowed = array('TRACE');
+
+ /**
+ * Methods having defined semantics for request body
+ *
+ * Content-Length header (indicating that the body follows, section 4.3 of
+ * RFC 2616) will be sent for these methods even if no body was added
+ *
+ * @var array
+ * @link http://pear.php.net/bugs/bug.php?id=12900
+ * @link http://pear.php.net/bugs/bug.php?id=14740
+ */
+ protected static $bodyRequired = array('POST', 'PUT');
+
+ /**
+ * Request being sent
+ * @var HTTP_Request2
+ */
+ protected $request;
+
+ /**
+ * Request body
+ * @var string|resource|HTTP_Request2_MultipartBody
+ * @see HTTP_Request2::getBody()
+ */
+ protected $requestBody;
+
+ /**
+ * Length of the request body
+ * @var integer
+ */
+ protected $contentLength;
+
+ /**
+ * Sends request to the remote server and returns its response
+ *
+ * @param HTTP_Request2
+ * @return HTTP_Request2_Response
+ * @throws HTTP_Request2_Exception
+ */
+ abstract public function sendRequest(HTTP_Request2 $request);
+
+ /**
+ * Calculates length of the request body, adds proper headers
+ *
+ * @param array associative array of request headers, this method will
+ * add proper 'Content-Length' and 'Content-Type' headers
+ * to this array (or remove them if not needed)
+ */
+ protected function calculateRequestLength(&$headers)
+ {
+ $this->requestBody = $this->request->getBody();
+
+ if (is_string($this->requestBody)) {
+ $this->contentLength = strlen($this->requestBody);
+ } elseif (is_resource($this->requestBody)) {
+ $stat = fstat($this->requestBody);
+ $this->contentLength = $stat['size'];
+ rewind($this->requestBody);
+ } else {
+ $this->contentLength = $this->requestBody->getLength();
+ $headers['content-type'] = 'multipart/form-data; boundary=' .
+ $this->requestBody->getBoundary();
+ $this->requestBody->rewind();
+ }
+
+ if (in_array($this->request->getMethod(), self::$bodyDisallowed) ||
+ 0 == $this->contentLength
+ ) {
+ // No body: send a Content-Length header nonetheless (request #12900),
+ // but do that only for methods that require a body (bug #14740)
+ if (in_array($this->request->getMethod(), self::$bodyRequired)) {
+ $headers['content-length'] = 0;
+ } else {
+ unset($headers['content-length']);
+ // if the method doesn't require a body and doesn't have a
+ // body, don't send a Content-Type header. (request #16799)
+ unset($headers['content-type']);
+ }
+ } else {
+ if (empty($headers['content-type'])) {
+ $headers['content-type'] = 'application/x-www-form-urlencoded';
+ }
+ $headers['content-length'] = $this->contentLength;
+ }
+ }
+}
+?>
diff --git a/lib/ext/HTTP/Request2/Adapter/Curl.php b/lib/ext/HTTP/Request2/Adapter/Curl.php
new file mode 100644
index 0000000..82d227f
--- /dev/null
+++ b/lib/ext/HTTP/Request2/Adapter/Curl.php
@@ -0,0 +1,560 @@
+<?php
+/**
+ * Adapter for HTTP_Request2 wrapping around cURL extension
+ *
+ * PHP version 5
+ *
+ * LICENSE:
+ *
+ * Copyright (c) 2008-2011, Alexey Borzov <avb@php.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.
+ * * The names of the authors may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @category HTTP
+ * @package HTTP_Request2
+ * @author Alexey Borzov <avb@php.net>
+ * @license http://opensource.org/licenses/bsd-license.php New BSD License
+ * @version SVN: $Id: Curl.php 310800 2011-05-06 07:29:56Z avb $
+ * @link http://pear.php.net/package/HTTP_Request2
+ */
+
+/**
+ * Base class for HTTP_Request2 adapters
+ */
+require_once 'HTTP/Request2/Adapter.php';
+
+/**
+ * Adapter for HTTP_Request2 wrapping around cURL extension
+ *
+ * @category HTTP
+ * @package HTTP_Request2
+ * @author Alexey Borzov <avb@php.net>
+ * @version Release: 2.0.0
+ */
+class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter
+{
+ /**
+ * Mapping of header names to cURL options
+ * @var array
+ */
+ protected static $headerMap = array(
+ 'accept-encoding' => CURLOPT_ENCODING,
+ 'cookie' => CURLOPT_COOKIE,
+ 'referer' => CURLOPT_REFERER,
+ 'user-agent' => CURLOPT_USERAGENT
+ );
+
+ /**
+ * Mapping of SSL context options to cURL options
+ * @var array
+ */
+ protected static $sslContextMap = array(
+ 'ssl_verify_peer' => CURLOPT_SSL_VERIFYPEER,
+ 'ssl_cafile' => CURLOPT_CAINFO,
+ 'ssl_capath' => CURLOPT_CAPATH,
+ 'ssl_local_cert' => CURLOPT_SSLCERT,
+ 'ssl_passphrase' => CURLOPT_SSLCERTPASSWD
+ );
+
+ /**
+ * Mapping of CURLE_* constants to Exception subclasses and error codes
+ * @var array
+ */
+ protected static $errorMap = array(
+ CURLE_UNSUPPORTED_PROTOCOL => array('HTTP_Request2_MessageException',
+ HTTP_Request2_Exception::NON_HTTP_REDIRECT),
+ CURLE_COULDNT_RESOLVE_PROXY => array('HTTP_Request2_ConnectionException'),
+ CURLE_COULDNT_RESOLVE_HOST => array('HTTP_Request2_ConnectionException'),
+ CURLE_COULDNT_CONNECT => array('HTTP_Request2_ConnectionException'),
+ // error returned from write callback
+ CURLE_WRITE_ERROR => array('HTTP_Request2_MessageException',
+ HTTP_Request2_Exception::NON_HTTP_REDIRECT),
+ CURLE_OPERATION_TIMEOUTED => array('HTTP_Request2_MessageException',
+ HTTP_Request2_Exception::TIMEOUT),
+ CURLE_HTTP_RANGE_ERROR => array('HTTP_Request2_MessageException'),
+ CURLE_SSL_CONNECT_ERROR => array('HTTP_Request2_ConnectionException'),
+ CURLE_LIBRARY_NOT_FOUND => array('HTTP_Request2_LogicException',
+ HTTP_Request2_Exception::MISCONFIGURATION),
+ CURLE_FUNCTION_NOT_FOUND => array('HTTP_Request2_LogicException',
+ HTTP_Request2_Exception::MISCONFIGURATION),
+ CURLE_ABORTED_BY_CALLBACK => array('HTTP_Request2_MessageException',
+ HTTP_Request2_Exception::NON_HTTP_REDIRECT),
+ CURLE_TOO_MANY_REDIRECTS => array('HTTP_Request2_MessageException',
+ HTTP_Request2_Exception::TOO_MANY_REDIRECTS),
+ CURLE_SSL_PEER_CERTIFICATE => array('HTTP_Request2_ConnectionException'),
+ CURLE_GOT_NOTHING => array('HTTP_Request2_MessageException'),
+ CURLE_SSL_ENGINE_NOTFOUND => array('HTTP_Request2_LogicException',
+ HTTP_Request2_Exception::MISCONFIGURATION),
+ CURLE_SSL_ENGINE_SETFAILED => array('HTTP_Request2_LogicException',
+ HTTP_Request2_Exception::MISCONFIGURATION),
+ CURLE_SEND_ERROR => array('HTTP_Request2_MessageException'),
+ CURLE_RECV_ERROR => array('HTTP_Request2_MessageException'),
+ CURLE_SSL_CERTPROBLEM => array('HTTP_Request2_LogicException',
+ HTTP_Request2_Exception::INVALID_ARGUMENT),
+ CURLE_SSL_CIPHER => array('HTTP_Request2_ConnectionException'),
+ CURLE_SSL_CACERT => array('HTTP_Request2_ConnectionException'),
+ CURLE_BAD_CONTENT_ENCODING => array('HTTP_Request2_MessageException'),
+ );
+
+ /**
+ * Response being received
+ * @var HTTP_Request2_Response
+ */
+ protected $response;
+
+ /**
+ * Whether 'sentHeaders' event was sent to observers
+ * @var boolean
+ */
+ protected $eventSentHeaders = false;
+
+ /**
+ * Whether 'receivedHeaders' event was sent to observers
+ * @var boolean
+ */
+ protected $eventReceivedHeaders = false;
+
+ /**
+ * Position within request body
+ * @var integer
+ * @see callbackReadBody()
+ */
+ protected $position = 0;
+
+ /**
+ * Information about last transfer, as returned by curl_getinfo()
+ * @var array
+ */
+ protected $lastInfo;
+
+ /**
+ * Creates a subclass of HTTP_Request2_Exception from curl error data
+ *
+ * @param resource curl handle
+ * @return HTTP_Request2_Exception
+ */
+ protected static function wrapCurlError($ch)
+ {
+ $nativeCode = curl_errno($ch);
+ $message = 'Curl error: ' . curl_error($ch);
+ if (!isset(self::$errorMap[$nativeCode])) {
+ return new HTTP_Request2_Exception($message, 0, $nativeCode);
+ } else {
+ $class = self::$errorMap[$nativeCode][0];
+ $code = empty(self::$errorMap[$nativeCode][1])
+ ? 0 : self::$errorMap[$nativeCode][1];
+ return new $class($message, $code, $nativeCode);
+ }
+ }
+
+ /**
+ * Sends request to the remote server and returns its response
+ *
+ * @param HTTP_Request2
+ * @return HTTP_Request2_Response
+ * @throws HTTP_Request2_Exception
+ */
+ public function sendRequest(HTTP_Request2 $request)
+ {
+ if (!extension_loaded('curl')) {
+ throw new HTTP_Request2_LogicException(
+ 'cURL extension not available', HTTP_Request2_Exception::MISCONFIGURATION
+ );
+ }
+
+ $this->request = $request;
+ $this->response = null;
+ $this->position = 0;
+ $this->eventSentHeaders = false;
+ $this->eventReceivedHeaders = false;
+
+ try {
+ if (false === curl_exec($ch = $this->createCurlHandle())) {
+ $e = self::wrapCurlError($ch);
+ }
+ } catch (Exception $e) {
+ }
+ if (isset($ch)) {
+ $this->lastInfo = curl_getinfo($ch);
+ curl_close($ch);
+ }
+
+ $response = $this->response;
+ unset($this->request, $this->requestBody, $this->response);
+
+ if (!empty($e)) {
+ throw $e;
+ }
+
+ if ($jar = $request->getCookieJar()) {
+ $jar->addCookiesFromResponse($response, $request->getUrl());
+ }
+
+ if (0 < $this->lastInfo['size_download']) {
+ $request->setLastEvent('receivedBody', $response);
+ }
+ return $response;
+ }
+
+ /**
+ * Returns information about last transfer
+ *
+ * @return array associative array as returned by curl_getinfo()
+ */
+ public function getInfo()
+ {
+ return $this->lastInfo;
+ }
+
+ /**
+ * Creates a new cURL handle and populates it with data from the request
+ *
+ * @return resource a cURL handle, as created by curl_init()
+ * @throws HTTP_Request2_LogicException
+ */
+ protected function createCurlHandle()
+ {
+ $ch = curl_init();
+
+ curl_setopt_array($ch, array(
+ // setup write callbacks
+ CURLOPT_HEADERFUNCTION => array($this, 'callbackWriteHeader'),
+ CURLOPT_WRITEFUNCTION => array($this, 'callbackWriteBody'),
+ // buffer size
+ CURLOPT_BUFFERSIZE => $this->request->getConfig('buffer_size'),
+ // connection timeout
+ CURLOPT_CONNECTTIMEOUT => $this->request->getConfig('connect_timeout'),
+ // save full outgoing headers, in case someone is interested
+ CURLINFO_HEADER_OUT => true,
+ // request url
+ CURLOPT_URL => $this->request->getUrl()->getUrl()
+ ));
+
+ // set up redirects
+ if (!$this->request->getConfig('follow_redirects')) {
+ curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
+ } else {
+ if (!@curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true)) {
+ throw new HTTP_Request2_LogicException(
+ 'Redirect support in curl is unavailable due to open_basedir or safe_mode setting',
+ HTTP_Request2_Exception::MISCONFIGURATION
+ );
+ }
+ curl_setopt($ch, CURLOPT_MAXREDIRS, $this->request->getConfig('max_redirects'));
+ // limit redirects to http(s), works in 5.2.10+
+ if (defined('CURLOPT_REDIR_PROTOCOLS')) {
+ curl_setopt($ch, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
+ }
+ // works in 5.3.2+, http://bugs.php.net/bug.php?id=49571
+ if ($this->request->getConfig('strict_redirects') && defined('CURLOPT_POSTREDIR')) {
+ curl_setopt($ch, CURLOPT_POSTREDIR, 3);
+ }
+ }
+
+ // request timeout
+ if ($timeout = $this->request->getConfig('timeout')) {
+ curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
+ }
+
+ // set HTTP version
+ switch ($this->request->getConfig('protocol_version')) {
+ case '1.0':
+ curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
+ break;
+ case '1.1':
+ curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
+ }
+
+ // set request method
+ switch ($this->request->getMethod()) {
+ case HTTP_Request2::METHOD_GET:
+ curl_setopt($ch, CURLOPT_HTTPGET, true);
+ break;
+ case HTTP_Request2::METHOD_POST:
+ curl_setopt($ch, CURLOPT_POST, true);
+ break;
+ case HTTP_Request2::METHOD_HEAD:
+ curl_setopt($ch, CURLOPT_NOBODY, true);
+ break;
+ case HTTP_Request2::METHOD_PUT:
+ curl_setopt($ch, CURLOPT_UPLOAD, true);
+ break;
+ default:
+ curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $this->request->getMethod());
+ }
+
+ // set proxy, if needed
+ if ($host = $this->request->getConfig('proxy_host')) {
+ if (!($port = $this->request->getConfig('proxy_port'))) {
+ throw new HTTP_Request2_LogicException(
+ 'Proxy port not provided', HTTP_Request2_Exception::MISSING_VALUE
+ );
+ }
+ curl_setopt($ch, CURLOPT_PROXY, $host . ':' . $port);
+ if ($user = $this->request->getConfig('proxy_user')) {
+ curl_setopt($ch, CURLOPT_PROXYUSERPWD, $user . ':' .
+ $this->request->getConfig('proxy_password'));
+ switch ($this->request->getConfig('proxy_auth_scheme')) {
+ case HTTP_Request2::AUTH_BASIC:
+ curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_BASIC);
+ break;
+ case HTTP_Request2::AUTH_DIGEST:
+ curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_DIGEST);
+ }
+ }
+ }
+
+ // set authentication data
+ if ($auth = $this->request->getAuth()) {
+ curl_setopt($ch, CURLOPT_USERPWD, $auth['user'] . ':' . $auth['password']);
+ switch ($auth['scheme']) {
+ case HTTP_Request2::AUTH_BASIC:
+ curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
+ break;
+ case HTTP_Request2::AUTH_DIGEST:
+ curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST);
+ }
+ }
+
+ // set SSL options
+ foreach ($this->request->getConfig() as $name => $value) {
+ if ('ssl_verify_host' == $name && null !== $value) {
+ curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $value? 2: 0);
+ } elseif (isset(self::$sslContextMap[$name]) && null !== $value) {
+ curl_setopt($ch, self::$sslContextMap[$name], $value);
+ }
+ }
+
+ $headers = $this->request->getHeaders();
+ // make cURL automagically send proper header
+ if (!isset($headers['accept-encoding'])) {
+ $headers['accept-encoding'] = '';
+ }
+
+ if (($jar = $this->request->getCookieJar())
+ && ($cookies = $jar->getMatching($this->request->getUrl(), true))
+ ) {
+ $headers['cookie'] = (empty($headers['cookie'])? '': $headers['cookie'] . '; ') . $cookies;
+ }
+
+ // set headers having special cURL keys
+ foreach (self::$headerMap as $name => $option) {
+ if (isset($headers[$name])) {
+ curl_setopt($ch, $option, $headers[$name]);
+ unset($headers[$name]);
+ }
+ }
+
+ $this->calculateRequestLength($headers);
+ if (isset($headers['content-length'])) {
+ $this->workaroundPhpBug47204($ch, $headers);
+ }
+
+ // set headers not having special keys
+ $headersFmt = array();
+ foreach ($headers as $name => $value) {
+ $canonicalName = implode('-', array_map('ucfirst', explode('-', $name)));
+ $headersFmt[] = $canonicalName . ': ' . $value;
+ }
+ curl_setopt($ch, CURLOPT_HTTPHEADER, $headersFmt);
+
+ return $ch;
+ }
+
+ /**
+ * Workaround for PHP bug #47204 that prevents rewinding request body
+ *
+ * The workaround consists of reading the entire request body into memory
+ * and setting it as CURLOPT_POSTFIELDS, so it isn't recommended for large
+ * file uploads, use Socket adapter instead.
+ *
+ * @param resource cURL handle
+ * @param array Request headers
+ */
+ protected function workaroundPhpBug47204($ch, &$headers)
+ {
+ // no redirects, no digest auth -> probably no rewind needed
+ if (!$this->request->getConfig('follow_redirects')
+ && (!($auth = $this->request->getAuth())
+ || HTTP_Request2::AUTH_DIGEST != $auth['scheme'])
+ ) {
+ curl_setopt($ch, CURLOPT_READFUNCTION, array($this, 'callbackReadBody'));
+
+ // rewind may be needed, read the whole body into memory
+ } else {
+ if ($this->requestBody instanceof HTTP_Request2_MultipartBody) {
+ $this->requestBody = $this->requestBody->__toString();
+
+ } elseif (is_resource($this->requestBody)) {
+ $fp = $this->requestBody;
+ $this->requestBody = '';
+ while (!feof($fp)) {
+ $this->requestBody .= fread($fp, 16384);
+ }
+ }
+ // curl hangs up if content-length is present
+ unset($headers['content-length']);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $this->requestBody);
+ }
+ }
+
+ /**
+ * Callback function called by cURL for reading the request body
+ *
+ * @param resource cURL handle
+ * @param resource file descriptor (not used)
+ * @param integer maximum length of data to return
+ * @return string part of the request body, up to $length bytes
+ */
+ protected function callbackReadBody($ch, $fd, $length)
+ {
+ if (!$this->eventSentHeaders) {
+ $this->request->setLastEvent(
+ 'sentHeaders', curl_getinfo($ch, CURLINFO_HEADER_OUT)
+ );
+ $this->eventSentHeaders = true;
+ }
+ if (in_array($this->request->getMethod(), self::$bodyDisallowed) ||
+ 0 == $this->contentLength || $this->position >= $this->contentLength
+ ) {
+ return '';
+ }
+ if (is_string($this->requestBody)) {
+ $string = substr($this->requestBody, $this->position, $length);
+ } elseif (is_resource($this->requestBody)) {
+ $string = fread($this->requestBody, $length);
+ } else {
+ $string = $this->requestBody->read($length);
+ }
+ $this->request->setLastEvent('sentBodyPart', strlen($string));
+ $this->position += strlen($string);
+ return $string;
+ }
+
+ /**
+ * Callback function called by cURL for saving the response headers
+ *
+ * @param resource cURL handle
+ * @param string response header (with trailing CRLF)
+ * @return integer number of bytes saved
+ * @see HTTP_Request2_Response::parseHeaderLine()
+ */
+ protected function callbackWriteHeader($ch, $string)
+ {
+ // we may receive a second set of headers if doing e.g. digest auth
+ if ($this->eventReceivedHeaders || !$this->eventSentHeaders) {
+ // don't bother with 100-Continue responses (bug #15785)
+ if (!$this->eventSentHeaders ||
+ $this->response->getStatus() >= 200
+ ) {
+ $this->request->setLastEvent(
+ 'sentHeaders', curl_getinfo($ch, CURLINFO_HEADER_OUT)
+ );
+ }
+ $upload = curl_getinfo($ch, CURLINFO_SIZE_UPLOAD);
+ // if body wasn't read by a callback, send event with total body size
+ if ($upload > $this->position) {
+ $this->request->setLastEvent(
+ 'sentBodyPart', $upload - $this->position
+ );
+ $this->position = $upload;
+ }
+ if ($upload && (!$this->eventSentHeaders
+ || $this->response->getStatus() >= 200)
+ ) {
+ $this->request->setLastEvent('sentBody', $upload);
+ }
+ $this->eventSentHeaders = true;
+ // we'll need a new response object
+ if ($this->eventReceivedHeaders) {
+ $this->eventReceivedHeaders = false;
+ $this->response = null;
+ }
+ }
+ if (empty($this->response)) {
+ $this->response = new HTTP_Request2_Response(
+ $string, false, curl_getinfo($ch, CURLINFO_EFFECTIVE_URL)
+ );
+ } else {
+ $this->response->parseHeaderLine($string);
+ if ('' == trim($string)) {
+ // don't bother with 100-Continue responses (bug #15785)
+ if (200 <= $this->response->getStatus()) {
+ $this->request->setLastEvent('receivedHeaders', $this->response);
+ }
+
+ if ($this->request->getConfig('follow_redirects') && $this->response->isRedirect()) {
+ $redirectUrl = new Net_URL2($this->response->getHeader('location'));
+
+ // for versions lower than 5.2.10, check the redirection URL protocol
+ if (!defined('CURLOPT_REDIR_PROTOCOLS') && $redirectUrl->isAbsolute()
+ && !in_array($redirectUrl->getScheme(), array('http', 'https'))
+ ) {
+ return -1;
+ }
+
+ if ($jar = $this->request->getCookieJar()) {
+ $jar->addCookiesFromResponse($this->response, $this->request->getUrl());
+ if (!$redirectUrl->isAbsolute()) {
+ $redirectUrl = $this->request->getUrl()->resolve($redirectUrl);
+ }
+ if ($cookies = $jar->getMatching($redirectUrl, true)) {
+ curl_setopt($ch, CURLOPT_COOKIE, $cookies);
+ }
+ }
+ }
+ $this->eventReceivedHeaders = true;
+ }
+ }
+ return strlen($string);
+ }
+
+ /**
+ * Callback function called by cURL for saving the response body
+ *
+ * @param resource cURL handle (not used)
+ * @param string part of the response body
+ * @return integer number of bytes saved
+ * @see HTTP_Request2_Response::appendBody()
+ */
+ protected function callbackWriteBody($ch, $string)
+ {
+ // cURL calls WRITEFUNCTION callback without calling HEADERFUNCTION if
+ // response doesn't start with proper HTTP status line (see bug #15716)
+ if (empty($this->response)) {
+ throw new HTTP_Request2_MessageException(
+ "Malformed response: {$string}",
+ HTTP_Request2_Exception::MALFORMED_RESPONSE
+ );
+ }
+ if ($this->request->getConfig('store_body')) {
+ $this->response->appendBody($string);
+ }
+ $this->request->setLastEvent('receivedBodyPart', $string);
+ return strlen($string);
+ }
+}
+?>
diff --git a/lib/ext/HTTP/Request2/Adapter/Mock.php b/lib/ext/HTTP/Request2/Adapter/Mock.php
new file mode 100644
index 0000000..6e9f827
--- /dev/null
+++ b/lib/ext/HTTP/Request2/Adapter/Mock.php
@@ -0,0 +1,171 @@
+<?php
+/**
+ * Mock adapter intended for testing
+ *
+ * PHP version 5
+ *
+ * LICENSE:
+ *
+ * Copyright (c) 2008-2011, Alexey Borzov <avb@php.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.
+ * * The names of the authors may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @category HTTP
+ * @package HTTP_Request2
+ * @author Alexey Borzov <avb@php.net>
+ * @license http://opensource.org/licenses/bsd-license.php New BSD License
+ * @version SVN: $Id: Mock.php 308322 2011-02-14 13:58:03Z avb $
+ * @link http://pear.php.net/package/HTTP_Request2
+ */
+
+/**
+ * Base class for HTTP_Request2 adapters
+ */
+require_once 'HTTP/Request2/Adapter.php';
+
+/**
+ * Mock adapter intended for testing
+ *
+ * Can be used to test applications depending on HTTP_Request2 package without
+ * actually performing any HTTP requests. This adapter will return responses
+ * previously added via addResponse()
+ * <code>
+ * $mock = new HTTP_Request2_Adapter_Mock();
+ * $mock->addResponse("HTTP/1.1 ... ");
+ *
+ * $request = new HTTP_Request2();
+ * $request->setAdapter($mock);
+ *
+ * // This will return the response set above
+ * $response = $req->send();
+ * </code>
+ *
+ * @category HTTP
+ * @package HTTP_Request2
+ * @author Alexey Borzov <avb@php.net>
+ * @version Release: 2.0.0
+ */
+class HTTP_Request2_Adapter_Mock extends HTTP_Request2_Adapter
+{
+ /**
+ * A queue of responses to be returned by sendRequest()
+ * @var array
+ */
+ protected $responses = array();
+
+ /**
+ * Returns the next response from the queue built by addResponse()
+ *
+ * If the queue is empty it will return default empty response with status 400,
+ * if an Exception object was added to the queue it will be thrown.
+ *
+ * @param HTTP_Request2
+ * @return HTTP_Request2_Response
+ * @throws Exception
+ */
+ public function sendRequest(HTTP_Request2 $request)
+ {
+ if (count($this->responses) > 0) {
+ $response = array_shift($this->responses);
+ if ($response instanceof HTTP_Request2_Response) {
+ return $response;
+ } else {
+ // rethrow the exception
+ $class = get_class($response);
+ $message = $response->getMessage();
+ $code = $response->getCode();
+ throw new $class($message, $code);
+ }
+ } else {
+ return self::createResponseFromString("HTTP/1.1 400 Bad Request\r\n\r\n");
+ }
+ }
+
+ /**
+ * Adds response to the queue
+ *
+ * @param mixed either a string, a pointer to an open file,
+ * an instance of HTTP_Request2_Response or Exception
+ * @throws HTTP_Request2_Exception
+ */
+ public function addResponse($response)
+ {
+ if (is_string($response)) {
+ $response = self::createResponseFromString($response);
+ } elseif (is_resource($response)) {
+ $response = self::createResponseFromFile($response);
+ } elseif (!$response instanceof HTTP_Request2_Response &&
+ !$response instanceof Exception
+ ) {
+ throw new HTTP_Request2_Exception('Parameter is not a valid response');
+ }
+ $this->responses[] = $response;
+ }
+
+ /**
+ * Creates a new HTTP_Request2_Response object from a string
+ *
+ * @param string
+ * @return HTTP_Request2_Response
+ * @throws HTTP_Request2_Exception
+ */
+ public static function createResponseFromString($str)
+ {
+ $parts = preg_split('!(\r?\n){2}!m', $str, 2);
+ $headerLines = explode("\n", $parts[0]);
+ $response = new HTTP_Request2_Response(array_shift($headerLines));
+ foreach ($headerLines as $headerLine) {
+ $response->parseHeaderLine($headerLine);
+ }
+ $response->parseHeaderLine('');
+ if (isset($parts[1])) {
+ $response->appendBody($parts[1]);
+ }
+ return $response;
+ }
+
+ /**
+ * Creates a new HTTP_Request2_Response object from a file
+ *
+ * @param resource file pointer returned by fopen()
+ * @return HTTP_Request2_Response
+ * @throws HTTP_Request2_Exception
+ */
+ public static function createResponseFromFile($fp)
+ {
+ $response = new HTTP_Request2_Response(fgets($fp));
+ do {
+ $headerLine = fgets($fp);
+ $response->parseHeaderLine($headerLine);
+ } while ('' != trim($headerLine));
+
+ while (!feof($fp)) {
+ $response->appendBody(fread($fp, 8192));
+ }
+ return $response;
+ }
+}
+?> \ No newline at end of file
diff --git a/lib/ext/HTTP/Request2/Adapter/Socket.php b/lib/ext/HTTP/Request2/Adapter/Socket.php
new file mode 100644
index 0000000..05cac6e
--- /dev/null
+++ b/lib/ext/HTTP/Request2/Adapter/Socket.php
@@ -0,0 +1,1084 @@
+<?php
+/**
+ * Socket-based adapter for HTTP_Request2
+ *
+ * PHP version 5
+ *
+ * LICENSE:
+ *
+ * Copyright (c) 2008-2011, Alexey Borzov <avb@php.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.
+ * * The names of the authors may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @category HTTP
+ * @package HTTP_Request2
+ * @author Alexey Borzov <avb@php.net>
+ * @license http://opensource.org/licenses/bsd-license.php New BSD License
+ * @version SVN: $Id: Socket.php 309921 2011-04-03 16:43:02Z avb $
+ * @link http://pear.php.net/package/HTTP_Request2
+ */
+
+/**
+ * Base class for HTTP_Request2 adapters
+ */
+require_once 'HTTP/Request2/Adapter.php';
+
+/**
+ * Socket-based adapter for HTTP_Request2
+ *
+ * This adapter uses only PHP sockets and will work on almost any PHP
+ * environment. Code is based on original HTTP_Request PEAR package.
+ *
+ * @category HTTP
+ * @package HTTP_Request2
+ * @author Alexey Borzov <avb@php.net>
+ * @version Release: 2.0.0
+ */
+class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter
+{
+ /**
+ * Regular expression for 'token' rule from RFC 2616
+ */
+ const REGEXP_TOKEN = '[^\x00-\x1f\x7f-\xff()<>@,;:\\\\"/\[\]?={}\s]+';
+
+ /**
+ * Regular expression for 'quoted-string' rule from RFC 2616
+ */
+ const REGEXP_QUOTED_STRING = '"(?:\\\\.|[^\\\\"])*"';
+
+ /**
+ * Connected sockets, needed for Keep-Alive support
+ * @var array
+ * @see connect()
+ */
+ protected static $sockets = array();
+
+ /**
+ * Data for digest authentication scheme
+ *
+ * The keys for the array are URL prefixes.
+ *
+ * The values are associative arrays with data (realm, nonce, nonce-count,
+ * opaque...) needed for digest authentication. Stored here to prevent making
+ * duplicate requests to digest-protected resources after we have already
+ * received the challenge.
+ *
+ * @var array
+ */
+ protected static $challenges = array();
+
+ /**
+ * Connected socket
+ * @var resource
+ * @see connect()
+ */
+ protected $socket;
+
+ /**
+ * Challenge used for server digest authentication
+ * @var array
+ */
+ protected $serverChallenge;
+
+ /**
+ * Challenge used for proxy digest authentication
+ * @var array
+ */
+ protected $proxyChallenge;
+
+ /**
+ * Sum of start time and global timeout, exception will be thrown if request continues past this time
+ * @var integer
+ */
+ protected $deadline = null;
+
+ /**
+ * Remaining length of the current chunk, when reading chunked response
+ * @var integer
+ * @see readChunked()
+ */
+ protected $chunkLength = 0;
+
+ /**
+ * Remaining amount of redirections to follow
+ *
+ * Starts at 'max_redirects' configuration parameter and is reduced on each
+ * subsequent redirect. An Exception will be thrown once it reaches zero.
+ *
+ * @var integer
+ */
+ protected $redirectCountdown = null;
+
+ /**
+ * Sends request to the remote server and returns its response
+ *
+ * @param HTTP_Request2
+ * @return HTTP_Request2_Response
+ * @throws HTTP_Request2_Exception
+ */
+ public function sendRequest(HTTP_Request2 $request)
+ {
+ $this->request = $request;
+
+ // Use global request timeout if given, see feature requests #5735, #8964
+ if ($timeout = $request->getConfig('timeout')) {
+ $this->deadline = time() + $timeout;
+ } else {
+ $this->deadline = null;
+ }
+
+ try {
+ $keepAlive = $this->connect();
+ $headers = $this->prepareHeaders();
+ if (false === @fwrite($this->socket, $headers, strlen($headers))) {
+ throw new HTTP_Request2_MessageException('Error writing request');
+ }
+ // provide request headers to the observer, see request #7633
+ $this->request->setLastEvent('sentHeaders', $headers);
+ $this->writeBody();
+
+ if ($this->deadline && time() > $this->deadline) {
+ throw new HTTP_Request2_MessageException(
+ 'Request timed out after ' .
+ $request->getConfig('timeout') . ' second(s)',
+ HTTP_Request2_Exception::TIMEOUT
+ );
+ }
+
+ $response = $this->readResponse();
+
+ if ($jar = $request->getCookieJar()) {
+ $jar->addCookiesFromResponse($response, $request->getUrl());
+ }
+
+ if (!$this->canKeepAlive($keepAlive, $response)) {
+ $this->disconnect();
+ }
+
+ if ($this->shouldUseProxyDigestAuth($response)) {
+ return $this->sendRequest($request);
+ }
+ if ($this->shouldUseServerDigestAuth($response)) {
+ return $this->sendRequest($request);
+ }
+ if ($authInfo = $response->getHeader('authentication-info')) {
+ $this->updateChallenge($this->serverChallenge, $authInfo);
+ }
+ if ($proxyInfo = $response->getHeader('proxy-authentication-info')) {
+ $this->updateChallenge($this->proxyChallenge, $proxyInfo);
+ }
+
+ } catch (Exception $e) {
+ $this->disconnect();
+ }
+
+ unset($this->request, $this->requestBody);
+
+ if (!empty($e)) {
+ $this->redirectCountdown = null;
+ throw $e;
+ }
+
+ if (!$request->getConfig('follow_redirects') || !$response->isRedirect()) {
+ $this->redirectCountdown = null;
+ return $response;
+ } else {
+ return $this->handleRedirect($request, $response);
+ }
+ }
+
+ /**
+ * Connects to the remote server
+ *
+ * @return bool whether the connection can be persistent
+ * @throws HTTP_Request2_Exception
+ */
+ protected function connect()
+ {
+ $secure = 0 == strcasecmp($this->request->getUrl()->getScheme(), 'https');
+ $tunnel = HTTP_Request2::METHOD_CONNECT == $this->request->getMethod();
+ $headers = $this->request->getHeaders();
+ $reqHost = $this->request->getUrl()->getHost();
+ if (!($reqPort = $this->request->getUrl()->getPort())) {
+ $reqPort = $secure? 443: 80;
+ }
+
+ if ($host = $this->request->getConfig('proxy_host')) {
+ if (!($port = $this->request->getConfig('proxy_port'))) {
+ throw new HTTP_Request2_LogicException(
+ 'Proxy port not provided',
+ HTTP_Request2_Exception::MISSING_VALUE
+ );
+ }
+ $proxy = true;
+ } else {
+ $host = $reqHost;
+ $port = $reqPort;
+ $proxy = false;
+ }
+
+ if ($tunnel && !$proxy) {
+ throw new HTTP_Request2_LogicException(
+ "Trying to perform CONNECT request without proxy",
+ HTTP_Request2_Exception::MISSING_VALUE
+ );
+ }
+ if ($secure && !in_array('ssl', stream_get_transports())) {
+ throw new HTTP_Request2_LogicException(
+ 'Need OpenSSL support for https:// requests',
+ HTTP_Request2_Exception::MISCONFIGURATION
+ );
+ }
+
+ // RFC 2068, section 19.7.1: A client MUST NOT send the Keep-Alive
+ // connection token to a proxy server...
+ if ($proxy && !$secure &&
+ !empty($headers['connection']) && 'Keep-Alive' == $headers['connection']
+ ) {
+ $this->request->setHeader('connection');
+ }
+
+ $keepAlive = ('1.1' == $this->request->getConfig('protocol_version') &&
+ empty($headers['connection'])) ||
+ (!empty($headers['connection']) &&
+ 'Keep-Alive' == $headers['connection']);
+ $host = ((!$secure || $proxy)? 'tcp://': 'ssl://') . $host;
+
+ $options = array();
+ if ($secure || $tunnel) {
+ foreach ($this->request->getConfig() as $name => $value) {
+ if ('ssl_' == substr($name, 0, 4) && null !== $value) {
+ if ('ssl_verify_host' == $name) {
+ if ($value) {
+ $options['CN_match'] = $reqHost;
+ }
+ } else {
+ $options[substr($name, 4)] = $value;
+ }
+ }
+ }
+ ksort($options);
+ }
+
+ // Changing SSL context options after connection is established does *not*
+ // work, we need a new connection if options change
+ $remote = $host . ':' . $port;
+ $socketKey = $remote . (($secure && $proxy)? "->{$reqHost}:{$reqPort}": '') .
+ (empty($options)? '': ':' . serialize($options));
+ unset($this->socket);
+
+ // We use persistent connections and have a connected socket?
+ // Ensure that the socket is still connected, see bug #16149
+ if ($keepAlive && !empty(self::$sockets[$socketKey]) &&
+ !feof(self::$sockets[$socketKey])
+ ) {
+ $this->socket =& self::$sockets[$socketKey];
+
+ } elseif ($secure && $proxy && !$tunnel) {
+ $this->establishTunnel();
+ $this->request->setLastEvent(
+ 'connect', "ssl://{$reqHost}:{$reqPort} via {$host}:{$port}"
+ );
+ self::$sockets[$socketKey] =& $this->socket;
+
+ } else {
+ // Set SSL context options if doing HTTPS request or creating a tunnel
+ $context = stream_context_create();
+ foreach ($options as $name => $value) {
+ if (!stream_context_set_option($context, 'ssl', $name, $value)) {
+ throw new HTTP_Request2_LogicException(
+ "Error setting SSL context option '{$name}'"
+ );
+ }
+ }
+ $track = @ini_set('track_errors', 1);
+ $this->socket = @stream_socket_client(
+ $remote, $errno, $errstr,
+ $this->request->getConfig('connect_timeout'),
+ STREAM_CLIENT_CONNECT, $context
+ );
+ if (!$this->socket) {
+ $e = new HTTP_Request2_ConnectionException(
+ "Unable to connect to {$remote}. Error: "
+ . (empty($errstr)? $php_errormsg: $errstr), 0, $errno
+ );
+ }
+ @ini_set('track_errors', $track);
+ if (isset($e)) {
+ throw $e;
+ }
+ $this->request->setLastEvent('connect', $remote);
+ self::$sockets[$socketKey] =& $this->socket;
+ }
+ return $keepAlive;
+ }
+
+ /**
+ * Establishes a tunnel to a secure remote server via HTTP CONNECT request
+ *
+ * This method will fail if 'ssl_verify_peer' is enabled. Probably because PHP
+ * sees that we are connected to a proxy server (duh!) rather than the server
+ * that presents its certificate.
+ *
+ * @link http://tools.ietf.org/html/rfc2817#section-5.2
+ * @throws HTTP_Request2_Exception
+ */
+ protected function establishTunnel()
+ {
+ $donor = new self;
+ $connect = new HTTP_Request2(
+ $this->request->getUrl(), HTTP_Request2::METHOD_CONNECT,
+ array_merge($this->request->getConfig(),
+ array('adapter' => $donor))
+ );
+ $response = $connect->send();
+ // Need any successful (2XX) response
+ if (200 > $response->getStatus() || 300 <= $response->getStatus()) {
+ throw new HTTP_Request2_ConnectionException(
+ 'Failed to connect via HTTPS proxy. Proxy response: ' .
+ $response->getStatus() . ' ' . $response->getReasonPhrase()
+ );
+ }
+ $this->socket = $donor->socket;
+
+ $modes = array(
+ STREAM_CRYPTO_METHOD_TLS_CLIENT,
+ STREAM_CRYPTO_METHOD_SSLv3_CLIENT,
+ STREAM_CRYPTO_METHOD_SSLv23_CLIENT,
+ STREAM_CRYPTO_METHOD_SSLv2_CLIENT
+ );
+
+ foreach ($modes as $mode) {
+ if (stream_socket_enable_crypto($this->socket, true, $mode)) {
+ return;
+ }
+ }
+ throw new HTTP_Request2_ConnectionException(
+ 'Failed to enable secure connection when connecting through proxy'
+ );
+ }
+
+ /**
+ * Checks whether current connection may be reused or should be closed
+ *
+ * @param boolean whether connection could be persistent
+ * in the first place
+ * @param HTTP_Request2_Response response object to check
+ * @return boolean
+ */
+ protected function canKeepAlive($requestKeepAlive, HTTP_Request2_Response $response)
+ {
+ // Do not close socket on successful CONNECT request
+ if (HTTP_Request2::METHOD_CONNECT == $this->request->getMethod() &&
+ 200 <= $response->getStatus() && 300 > $response->getStatus()
+ ) {
+ return true;
+ }
+
+ $lengthKnown = 'chunked' == strtolower($response->getHeader('transfer-encoding'))
+ || null !== $response->getHeader('content-length')
+ // no body possible for such responses, see also request #17031
+ || HTTP_Request2::METHOD_HEAD == $this->request->getMethod()
+ || in_array($response->getStatus(), array(204, 304));
+ $persistent = 'keep-alive' == strtolower($response->getHeader('connection')) ||
+ (null === $response->getHeader('connection') &&
+ '1.1' == $response->getVersion());
+ return $requestKeepAlive && $lengthKnown && $persistent;
+ }
+
+ /**
+ * Disconnects from the remote server
+ */
+ protected function disconnect()
+ {
+ if (is_resource($this->socket)) {
+ fclose($this->socket);
+ $this->socket = null;
+ $this->request->setLastEvent('disconnect');
+ }
+ }
+
+ /**
+ * Handles HTTP redirection
+ *
+ * This method will throw an Exception if redirect to a non-HTTP(S) location
+ * is attempted, also if number of redirects performed already is equal to
+ * 'max_redirects' configuration parameter.
+ *
+ * @param HTTP_Request2 Original request
+ * @param HTTP_Request2_Response Response containing redirect
+ * @return HTTP_Request2_Response Response from a new location
+ * @throws HTTP_Request2_Exception
+ */
+ protected function handleRedirect(HTTP_Request2 $request,
+ HTTP_Request2_Response $response)
+ {
+ if (is_null($this->redirectCountdown)) {
+ $this->redirectCountdown = $request->getConfig('max_redirects');
+ }
+ if (0 == $this->redirectCountdown) {
+ $this->redirectCountdown = null;
+ // Copying cURL behaviour
+ throw new HTTP_Request2_MessageException (
+ 'Maximum (' . $request->getConfig('max_redirects') . ') redirects followed',
+ HTTP_Request2_Exception::TOO_MANY_REDIRECTS
+ );
+ }
+ $redirectUrl = new Net_URL2(
+ $response->getHeader('location'),
+ array(Net_URL2::OPTION_USE_BRACKETS => $request->getConfig('use_brackets'))
+ );
+ // refuse non-HTTP redirect
+ if ($redirectUrl->isAbsolute()
+ && !in_array($redirectUrl->getScheme(), array('http', 'https'))
+ ) {
+ $this->redirectCountdown = null;
+ throw new HTTP_Request2_MessageException(
+ 'Refusing to redirect to a non-HTTP URL ' . $redirectUrl->__toString(),
+ HTTP_Request2_Exception::NON_HTTP_REDIRECT
+ );
+ }
+ // Theoretically URL should be absolute (see http://tools.ietf.org/html/rfc2616#section-14.30),
+ // but in practice it is often not
+ if (!$redirectUrl->isAbsolute()) {
+ $redirectUrl = $request->getUrl()->resolve($redirectUrl);
+ }
+ $redirect = clone $request;
+ $redirect->setUrl($redirectUrl);
+ if (303 == $response->getStatus() || (!$request->getConfig('strict_redirects')
+ && in_array($response->getStatus(), array(301, 302)))
+ ) {
+ $redirect->setMethod(HTTP_Request2::METHOD_GET);
+ $redirect->setBody('');
+ }
+
+ if (0 < $this->redirectCountdown) {
+ $this->redirectCountdown--;
+ }
+ return $this->sendRequest($redirect);
+ }
+
+ /**
+ * Checks whether another request should be performed with server digest auth
+ *
+ * Several conditions should be satisfied for it to return true:
+ * - response status should be 401
+ * - auth credentials should be set in the request object
+ * - response should contain WWW-Authenticate header with digest challenge
+ * - there is either no challenge stored for this URL or new challenge
+ * contains stale=true parameter (in other case we probably just failed
+ * due to invalid username / password)
+ *
+ * The method stores challenge values in $challenges static property
+ *
+ * @param HTTP_Request2_Response response to check
+ * @return boolean whether another request should be performed
+ * @throws HTTP_Request2_Exception in case of unsupported challenge parameters
+ */
+ protected function shouldUseServerDigestAuth(HTTP_Request2_Response $response)
+ {
+ // no sense repeating a request if we don't have credentials
+ if (401 != $response->getStatus() || !$this->request->getAuth()) {
+ return false;
+ }
+ if (!$challenge = $this->parseDigestChallenge($response->getHeader('www-authenticate'))) {
+ return false;
+ }
+
+ $url = $this->request->getUrl();
+ $scheme = $url->getScheme();
+ $host = $scheme . '://' . $url->getHost();
+ if ($port = $url->getPort()) {
+ if ((0 == strcasecmp($scheme, 'http') && 80 != $port) ||
+ (0 == strcasecmp($scheme, 'https') && 443 != $port)
+ ) {
+ $host .= ':' . $port;
+ }
+ }
+
+ if (!empty($challenge['domain'])) {
+ $prefixes = array();
+ foreach (preg_split('/\\s+/', $challenge['domain']) as $prefix) {
+ // don't bother with different servers
+ if ('/' == substr($prefix, 0, 1)) {
+ $prefixes[] = $host . $prefix;
+ }
+ }
+ }
+ if (empty($prefixes)) {
+ $prefixes = array($host . '/');
+ }
+
+ $ret = true;
+ foreach ($prefixes as $prefix) {
+ if (!empty(self::$challenges[$prefix]) &&
+ (empty($challenge['stale']) || strcasecmp('true', $challenge['stale']))
+ ) {
+ // probably credentials are invalid
+ $ret = false;
+ }
+ self::$challenges[$prefix] =& $challenge;
+ }
+ return $ret;
+ }
+
+ /**
+ * Checks whether another request should be performed with proxy digest auth
+ *
+ * Several conditions should be satisfied for it to return true:
+ * - response status should be 407
+ * - proxy auth credentials should be set in the request object
+ * - response should contain Proxy-Authenticate header with digest challenge
+ * - there is either no challenge stored for this proxy or new challenge
+ * contains stale=true parameter (in other case we probably just failed
+ * due to invalid username / password)
+ *
+ * The method stores challenge values in $challenges static property
+ *
+ * @param HTTP_Request2_Response response to check
+ * @return boolean whether another request should be performed
+ * @throws HTTP_Request2_Exception in case of unsupported challenge parameters
+ */
+ protected function shouldUseProxyDigestAuth(HTTP_Request2_Response $response)
+ {
+ if (407 != $response->getStatus() || !$this->request->getConfig('proxy_user')) {
+ return false;
+ }
+ if (!($challenge = $this->parseDigestChallenge($response->getHeader('proxy-authenticate')))) {
+ return false;
+ }
+
+ $key = 'proxy://' . $this->request->getConfig('proxy_host') .
+ ':' . $this->request->getConfig('proxy_port');
+
+ if (!empty(self::$challenges[$key]) &&
+ (empty($challenge['stale']) || strcasecmp('true', $challenge['stale']))
+ ) {
+ $ret = false;
+ } else {
+ $ret = true;
+ }
+ self::$challenges[$key] = $challenge;
+ return $ret;
+ }
+
+ /**
+ * Extracts digest method challenge from (WWW|Proxy)-Authenticate header value
+ *
+ * There is a problem with implementation of RFC 2617: several of the parameters
+ * are defined as quoted-string there and thus may contain backslash escaped
+ * double quotes (RFC 2616, section 2.2). However, RFC 2617 defines unq(X) as
+ * just value of quoted-string X without surrounding quotes, it doesn't speak
+ * about removing backslash escaping.
+ *
+ * Now realm parameter is user-defined and human-readable, strange things
+ * happen when it contains quotes:
+ * - Apache allows quotes in realm, but apparently uses realm value without
+ * backslashes for digest computation
+ * - Squid allows (manually escaped) quotes there, but it is impossible to
+ * authorize with either escaped or unescaped quotes used in digest,
+ * probably it can't parse the response (?)
+ * - Both IE and Firefox display realm value with backslashes in
+ * the password popup and apparently use the same value for digest
+ *
+ * HTTP_Request2 follows IE and Firefox (and hopefully RFC 2617) in
+ * quoted-string handling, unfortunately that means failure to authorize
+ * sometimes
+ *
+ * @param string value of WWW-Authenticate or Proxy-Authenticate header
+ * @return mixed associative array with challenge parameters, false if
+ * no challenge is present in header value
+ * @throws HTTP_Request2_NotImplementedException in case of unsupported challenge parameters
+ */
+ protected function parseDigestChallenge($headerValue)
+ {
+ $authParam = '(' . self::REGEXP_TOKEN . ')\\s*=\\s*(' .
+ self::REGEXP_TOKEN . '|' . self::REGEXP_QUOTED_STRING . ')';
+ $challenge = "!(?<=^|\\s|,)Digest ({$authParam}\\s*(,\\s*|$))+!";
+ if (!preg_match($challenge, $headerValue, $matches)) {
+ return false;
+ }
+
+ preg_match_all('!' . $authParam . '!', $matches[0], $params);
+ $paramsAry = array();
+ $knownParams = array('realm', 'domain', 'nonce', 'opaque', 'stale',
+ 'algorithm', 'qop');
+ for ($i = 0; $i < count($params[0]); $i++) {
+ // section 3.2.1: Any unrecognized directive MUST be ignored.
+ if (in_array($params[1][$i], $knownParams)) {
+ if ('"' == substr($params[2][$i], 0, 1)) {
+ $paramsAry[$params[1][$i]] = substr($params[2][$i], 1, -1);
+ } else {
+ $paramsAry[$params[1][$i]] = $params[2][$i];
+ }
+ }
+ }
+ // we only support qop=auth
+ if (!empty($paramsAry['qop']) &&
+ !in_array('auth', array_map('trim', explode(',', $paramsAry['qop'])))
+ ) {
+ throw new HTTP_Request2_NotImplementedException(
+ "Only 'auth' qop is currently supported in digest authentication, " .
+ "server requested '{$paramsAry['qop']}'"
+ );
+ }
+ // we only support algorithm=MD5
+ if (!empty($paramsAry['algorithm']) && 'MD5' != $paramsAry['algorithm']) {
+ throw new HTTP_Request2_NotImplementedException(
+ "Only 'MD5' algorithm is currently supported in digest authentication, " .
+ "server requested '{$paramsAry['algorithm']}'"
+ );
+ }
+
+ return $paramsAry;
+ }
+
+ /**
+ * Parses [Proxy-]Authentication-Info header value and updates challenge
+ *
+ * @param array challenge to update
+ * @param string value of [Proxy-]Authentication-Info header
+ * @todo validate server rspauth response
+ */
+ protected function updateChallenge(&$challenge, $headerValue)
+ {
+ $authParam = '!(' . self::REGEXP_TOKEN . ')\\s*=\\s*(' .
+ self::REGEXP_TOKEN . '|' . self::REGEXP_QUOTED_STRING . ')!';
+ $paramsAry = array();
+
+ preg_match_all($authParam, $headerValue, $params);
+ for ($i = 0; $i < count($params[0]); $i++) {
+ if ('"' == substr($params[2][$i], 0, 1)) {
+ $paramsAry[$params[1][$i]] = substr($params[2][$i], 1, -1);
+ } else {
+ $paramsAry[$params[1][$i]] = $params[2][$i];
+ }
+ }
+ // for now, just update the nonce value
+ if (!empty($paramsAry['nextnonce'])) {
+ $challenge['nonce'] = $paramsAry['nextnonce'];
+ $challenge['nc'] = 1;
+ }
+ }
+
+ /**
+ * Creates a value for [Proxy-]Authorization header when using digest authentication
+ *
+ * @param string user name
+ * @param string password
+ * @param string request URL
+ * @param array digest challenge parameters
+ * @return string value of [Proxy-]Authorization request header
+ * @link http://tools.ietf.org/html/rfc2617#section-3.2.2
+ */
+ protected function createDigestResponse($user, $password, $url, &$challenge)
+ {
+ if (false !== ($q = strpos($url, '?')) &&
+ $this->request->getConfig('digest_compat_ie')
+ ) {
+ $url = substr($url, 0, $q);
+ }
+
+ $a1 = md5($user . ':' . $challenge['realm'] . ':' . $password);
+ $a2 = md5($this->request->getMethod() . ':' . $url);
+
+ if (empty($challenge['qop'])) {
+ $digest = md5($a1 . ':' . $challenge['nonce'] . ':' . $a2);
+ } else {
+ $challenge['cnonce'] = 'Req2.' . rand();
+ if (empty($challenge['nc'])) {
+ $challenge['nc'] = 1;
+ }
+ $nc = sprintf('%08x', $challenge['nc']++);
+ $digest = md5($a1 . ':' . $challenge['nonce'] . ':' . $nc . ':' .
+ $challenge['cnonce'] . ':auth:' . $a2);
+ }
+ return 'Digest username="' . str_replace(array('\\', '"'), array('\\\\', '\\"'), $user) . '", ' .
+ 'realm="' . $challenge['realm'] . '", ' .
+ 'nonce="' . $challenge['nonce'] . '", ' .
+ 'uri="' . $url . '", ' .
+ 'response="' . $digest . '"' .
+ (!empty($challenge['opaque'])?
+ ', opaque="' . $challenge['opaque'] . '"':
+ '') .
+ (!empty($challenge['qop'])?
+ ', qop="auth", nc=' . $nc . ', cnonce="' . $challenge['cnonce'] . '"':
+ '');
+ }
+
+ /**
+ * Adds 'Authorization' header (if needed) to request headers array
+ *
+ * @param array request headers
+ * @param string request host (needed for digest authentication)
+ * @param string request URL (needed for digest authentication)
+ * @throws HTTP_Request2_NotImplementedException
+ */
+ protected function addAuthorizationHeader(&$headers, $requestHost, $requestUrl)
+ {
+ if (!($auth = $this->request->getAuth())) {
+ return;
+ }
+ switch ($auth['scheme']) {
+ case HTTP_Request2::AUTH_BASIC:
+ $headers['authorization'] =
+ 'Basic ' . base64_encode($auth['user'] . ':' . $auth['password']);
+ break;
+
+ case HTTP_Request2::AUTH_DIGEST:
+ unset($this->serverChallenge);
+ $fullUrl = ('/' == $requestUrl[0])?
+ $this->request->getUrl()->getScheme() . '://' .
+ $requestHost . $requestUrl:
+ $requestUrl;
+ foreach (array_keys(self::$challenges) as $key) {
+ if ($key == substr($fullUrl, 0, strlen($key))) {
+ $headers['authorization'] = $this->createDigestResponse(
+ $auth['user'], $auth['password'],
+ $requestUrl, self::$challenges[$key]
+ );
+ $this->serverChallenge =& self::$challenges[$key];
+ break;
+ }
+ }
+ break;
+
+ default:
+ throw new HTTP_Request2_NotImplementedException(
+ "Unknown HTTP authentication scheme '{$auth['scheme']}'"
+ );
+ }
+ }
+
+ /**
+ * Adds 'Proxy-Authorization' header (if needed) to request headers array
+ *
+ * @param array request headers
+ * @param string request URL (needed for digest authentication)
+ * @throws HTTP_Request2_NotImplementedException
+ */
+ protected function addProxyAuthorizationHeader(&$headers, $requestUrl)
+ {
+ if (!$this->request->getConfig('proxy_host') ||
+ !($user = $this->request->getConfig('proxy_user')) ||
+ (0 == strcasecmp('https', $this->request->getUrl()->getScheme()) &&
+ HTTP_Request2::METHOD_CONNECT != $this->request->getMethod())
+ ) {
+ return;
+ }
+
+ $password = $this->request->getConfig('proxy_password');
+ switch ($this->request->getConfig('proxy_auth_scheme')) {
+ case HTTP_Request2::AUTH_BASIC:
+ $headers['proxy-authorization'] =
+ 'Basic ' . base64_encode($user . ':' . $password);
+ break;
+
+ case HTTP_Request2::AUTH_DIGEST:
+ unset($this->proxyChallenge);
+ $proxyUrl = 'proxy://' . $this->request->getConfig('proxy_host') .
+ ':' . $this->request->getConfig('proxy_port');
+ if (!empty(self::$challenges[$proxyUrl])) {
+ $headers['proxy-authorization'] = $this->createDigestResponse(
+ $user, $password,
+ $requestUrl, self::$challenges[$proxyUrl]
+ );
+ $this->proxyChallenge =& self::$challenges[$proxyUrl];
+ }
+ break;
+
+ default:
+ throw new HTTP_Request2_NotImplementedException(
+ "Unknown HTTP authentication scheme '" .
+ $this->request->getConfig('proxy_auth_scheme') . "'"
+ );
+ }
+ }
+
+
+ /**
+ * Creates the string with the Request-Line and request headers
+ *
+ * @return string
+ * @throws HTTP_Request2_Exception
+ */
+ protected function prepareHeaders()
+ {
+ $headers = $this->request->getHeaders();
+ $url = $this->request->getUrl();
+ $connect = HTTP_Request2::METHOD_CONNECT == $this->request->getMethod();
+ $host = $url->getHost();
+
+ $defaultPort = 0 == strcasecmp($url->getScheme(), 'https')? 443: 80;
+ if (($port = $url->getPort()) && $port != $defaultPort || $connect) {
+ $host .= ':' . (empty($port)? $defaultPort: $port);
+ }
+ // Do not overwrite explicitly set 'Host' header, see bug #16146
+ if (!isset($headers['host'])) {
+ $headers['host'] = $host;
+ }
+
+ if ($connect) {
+ $requestUrl = $host;
+
+ } else {
+ if (!$this->request->getConfig('proxy_host') ||
+ 0 == strcasecmp($url->getScheme(), 'https')
+ ) {
+ $requestUrl = '';
+ } else {
+ $requestUrl = $url->getScheme() . '://' . $host;
+ }
+ $path = $url->getPath();
+ $query = $url->getQuery();
+ $requestUrl .= (empty($path)? '/': $path) . (empty($query)? '': '?' . $query);
+ }
+
+ if ('1.1' == $this->request->getConfig('protocol_version') &&
+ extension_loaded('zlib') && !isset($headers['accept-encoding'])
+ ) {
+ $headers['accept-encoding'] = 'gzip, deflate';
+ }
+ if (($jar = $this->request->getCookieJar())
+ && ($cookies = $jar->getMatching($this->request->getUrl(), true))
+ ) {
+ $headers['cookie'] = (empty($headers['cookie'])? '': $headers['cookie'] . '; ') . $cookies;
+ }
+
+ $this->addAuthorizationHeader($headers, $host, $requestUrl);
+ $this->addProxyAuthorizationHeader($headers, $requestUrl);
+ $this->calculateRequestLength($headers);
+
+ $headersStr = $this->request->getMethod() . ' ' . $requestUrl . ' HTTP/' .
+ $this->request->getConfig('protocol_version') . "\r\n";
+ foreach ($headers as $name => $value) {
+ $canonicalName = implode('-', array_map('ucfirst', explode('-', $name)));
+ $headersStr .= $canonicalName . ': ' . $value . "\r\n";
+ }
+ return $headersStr . "\r\n";
+ }
+
+ /**
+ * Sends the request body
+ *
+ * @throws HTTP_Request2_MessageException
+ */
+ protected function writeBody()
+ {
+ if (in_array($this->request->getMethod(), self::$bodyDisallowed) ||
+ 0 == $this->contentLength
+ ) {
+ return;
+ }
+
+ $position = 0;
+ $bufferSize = $this->request->getConfig('buffer_size');
+ while ($position < $this->contentLength) {
+ if (is_string($this->requestBody)) {
+ $str = substr($this->requestBody, $position, $bufferSize);
+ } elseif (is_resource($this->requestBody)) {
+ $str = fread($this->requestBody, $bufferSize);
+ } else {
+ $str = $this->requestBody->read($bufferSize);
+ }
+ if (false === @fwrite($this->socket, $str, strlen($str))) {
+ throw new HTTP_Request2_MessageException('Error writing request');
+ }
+ // Provide the length of written string to the observer, request #7630
+ $this->request->setLastEvent('sentBodyPart', strlen($str));
+ $position += strlen($str);
+ }
+ $this->request->setLastEvent('sentBody', $this->contentLength);
+ }
+
+ /**
+ * Reads the remote server's response
+ *
+ * @return HTTP_Request2_Response
+ * @throws HTTP_Request2_Exception
+ */
+ protected function readResponse()
+ {
+ $bufferSize = $this->request->getConfig('buffer_size');
+
+ do {
+ $response = new HTTP_Request2_Response(
+ $this->readLine($bufferSize), true, $this->request->getUrl()
+ );
+ do {
+ $headerLine = $this->readLine($bufferSize);
+ $response->parseHeaderLine($headerLine);
+ } while ('' != $headerLine);
+ } while (in_array($response->getStatus(), array(100, 101)));
+
+ $this->request->setLastEvent('receivedHeaders', $response);
+
+ // No body possible in such responses
+ if (HTTP_Request2::METHOD_HEAD == $this->request->getMethod() ||
+ (HTTP_Request2::METHOD_CONNECT == $this->request->getMethod() &&
+ 200 <= $response->getStatus() && 300 > $response->getStatus()) ||
+ in_array($response->getStatus(), array(204, 304))
+ ) {
+ return $response;
+ }
+
+ $chunked = 'chunked' == $response->getHeader('transfer-encoding');
+ $length = $response->getHeader('content-length');
+ $hasBody = false;
+ if ($chunked || null === $length || 0 < intval($length)) {
+ // RFC 2616, section 4.4:
+ // 3. ... If a message is received with both a
+ // Transfer-Encoding header field and a Content-Length header field,
+ // the latter MUST be ignored.
+ $toRead = ($chunked || null === $length)? null: $length;
+ $this->chunkLength = 0;
+
+ while (!feof($this->socket) && (is_null($toRead) || 0 < $toRead)) {
+ if ($chunked) {
+ $data = $this->readChunked($bufferSize);
+ } elseif (is_null($toRead)) {
+ $data = $this->fread($bufferSize);
+ } else {
+ $data = $this->fread(min($toRead, $bufferSize));
+ $toRead -= strlen($data);
+ }
+ if ('' == $data && (!$this->chunkLength || feof($this->socket))) {
+ break;
+ }
+
+ $hasBody = true;
+ if ($this->request->getConfig('store_body')) {
+ $response->appendBody($data);
+ }
+ if (!in_array($response->getHeader('content-encoding'), array('identity', null))) {
+ $this->request->setLastEvent('receivedEncodedBodyPart', $data);
+ } else {
+ $this->request->setLastEvent('receivedBodyPart', $data);
+ }
+ }
+ }
+
+ if ($hasBody) {
+ $this->request->setLastEvent('receivedBody', $response);
+ }
+ return $response;
+ }
+
+ /**
+ * Reads until either the end of the socket or a newline, whichever comes first
+ *
+ * Strips the trailing newline from the returned data, handles global
+ * request timeout. Method idea borrowed from Net_Socket PEAR package.
+ *
+ * @param int buffer size to use for reading
+ * @return Available data up to the newline (not including newline)
+ * @throws HTTP_Request2_MessageException In case of timeout
+ */
+ protected function readLine($bufferSize)
+ {
+ $line = '';
+ while (!feof($this->socket)) {
+ if ($this->deadline) {
+ stream_set_timeout($this->socket, max($this->deadline - time(), 1));
+ }
+ $line .= @fgets($this->socket, $bufferSize);
+ $info = stream_get_meta_data($this->socket);
+ if ($info['timed_out'] || $this->deadline && time() > $this->deadline) {
+ $reason = $this->deadline
+ ? 'after ' . $this->request->getConfig('timeout') . ' second(s)'
+ : 'due to default_socket_timeout php.ini setting';
+ throw new HTTP_Request2_MessageException(
+ "Request timed out {$reason}", HTTP_Request2_Exception::TIMEOUT
+ );
+ }
+ if (substr($line, -1) == "\n") {
+ return rtrim($line, "\r\n");
+ }
+ }
+ return $line;
+ }
+
+ /**
+ * Wrapper around fread(), handles global request timeout
+ *
+ * @param int Reads up to this number of bytes
+ * @return Data read from socket
+ * @throws HTTP_Request2_MessageException In case of timeout
+ */
+ protected function fread($length)
+ {
+ if ($this->deadline) {
+ stream_set_timeout($this->socket, max($this->deadline - time(), 1));
+ }
+ $data = fread($this->socket, $length);
+ $info = stream_get_meta_data($this->socket);
+ if ($info['timed_out'] || $this->deadline && time() > $this->deadline) {
+ $reason = $this->deadline
+ ? 'after ' . $this->request->getConfig('timeout') . ' second(s)'
+ : 'due to default_socket_timeout php.ini setting';
+ throw new HTTP_Request2_MessageException(
+ "Request timed out {$reason}", HTTP_Request2_Exception::TIMEOUT
+ );
+ }
+ return $data;
+ }
+
+ /**
+ * Reads a part of response body encoded with chunked Transfer-Encoding
+ *
+ * @param int buffer size to use for reading
+ * @return string
+ * @throws HTTP_Request2_MessageException
+ */
+ protected function readChunked($bufferSize)
+ {
+ // at start of the next chunk?
+ if (0 == $this->chunkLength) {
+ $line = $this->readLine($bufferSize);
+ if (!preg_match('/^([0-9a-f]+)/i', $line, $matches)) {
+ throw new HTTP_Request2_MessageException(
+ "Cannot decode chunked response, invalid chunk length '{$line}'",
+ HTTP_Request2_Exception::DECODE_ERROR
+ );
+ } else {
+ $this->chunkLength = hexdec($matches[1]);
+ // Chunk with zero length indicates the end
+ if (0 == $this->chunkLength) {
+ $this->readLine($bufferSize);
+ return '';
+ }
+ }
+ }
+ $data = $this->fread(min($this->chunkLength, $bufferSize));
+ $this->chunkLength -= strlen($data);
+ if (0 == $this->chunkLength) {
+ $this->readLine($bufferSize); // Trailing CRLF
+ }
+ return $data;
+ }
+}
+
+?> \ No newline at end of file
diff --git a/lib/ext/HTTP/Request2/CookieJar.php b/lib/ext/HTTP/Request2/CookieJar.php
new file mode 100644
index 0000000..af7534f
--- /dev/null
+++ b/lib/ext/HTTP/Request2/CookieJar.php
@@ -0,0 +1,499 @@
+<?php
+/**
+ * Stores cookies and passes them between HTTP requests
+ *
+ * PHP version 5
+ *
+ * LICENSE:
+ *
+ * Copyright (c) 2008-2011, Alexey Borzov <avb@php.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.
+ * * The names of the authors may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @category HTTP
+ * @package HTTP_Request2
+ * @author Alexey Borzov <avb@php.net>
+ * @license http://opensource.org/licenses/bsd-license.php New BSD License
+ * @version SVN: $Id: CookieJar.php 308629 2011-02-24 17:34:24Z avb $
+ * @link http://pear.php.net/package/HTTP_Request2
+ */
+
+/** Class representing a HTTP request message */
+require_once 'HTTP/Request2.php';
+
+/**
+ * Stores cookies and passes them between HTTP requests
+ *
+ * @category HTTP
+ * @package HTTP_Request2
+ * @author Alexey Borzov <avb@php.net>
+ * @version Release: @package_version@
+ */
+class HTTP_Request2_CookieJar implements Serializable
+{
+ /**
+ * Array of stored cookies
+ *
+ * The array is indexed by domain, path and cookie name
+ * .example.com
+ * /
+ * some_cookie => cookie data
+ * /subdir
+ * other_cookie => cookie data
+ * .example.org
+ * ...
+ *
+ * @var array
+ */
+ protected $cookies = array();
+
+ /**
+ * Whether session cookies should be serialized when serializing the jar
+ * @var bool
+ */
+ protected $serializeSession = false;
+
+ /**
+ * Whether Public Suffix List should be used for domain matching
+ * @var bool
+ */
+ protected $useList = true;
+
+ /**
+ * Array with Public Suffix List data
+ * @var array
+ * @link http://publicsuffix.org/
+ */
+ protected static $psl = array();
+
+ /**
+ * Class constructor, sets various options
+ *
+ * @param bool Controls serializing session cookies, see {@link serializeSessionCookies()}
+ * @param bool Controls using Public Suffix List, see {@link usePublicSuffixList()}
+ */
+ public function __construct($serializeSessionCookies = false, $usePublicSuffixList = true)
+ {
+ $this->serializeSessionCookies($serializeSessionCookies);
+ $this->usePublicSuffixList($usePublicSuffixList);
+ }
+
+ /**
+ * Returns current time formatted in ISO-8601 at UTC timezone
+ *
+ * @return string
+ */
+ protected function now()
+ {
+ $dt = new DateTime();
+ $dt->setTimezone(new DateTimeZone('UTC'));
+ return $dt->format(DateTime::ISO8601);
+ }
+
+ /**
+ * Checks cookie array for correctness, possibly updating its 'domain', 'path' and 'expires' fields
+ *
+ * The checks are as follows:
+ * - cookie array should contain 'name' and 'value' fields;
+ * - name and value should not contain disallowed symbols;
+ * - 'expires' should be either empty parseable by DateTime;
+ * - 'domain' and 'path' should be either not empty or an URL where
+ * cookie was set should be provided.
+ * - if $setter is provided, then document at that URL should be allowed
+ * to set a cookie for that 'domain'. If $setter is not provided,
+ * then no domain checks will be made.
+ *
+ * 'expires' field will be converted to ISO8601 format from COOKIE format,
+ * 'domain' and 'path' will be set from setter URL if empty.
+ *
+ * @param array cookie data, as returned by {@link HTTP_Request2_Response::getCookies()}
+ * @param Net_URL2 URL of the document that sent Set-Cookie header
+ * @return array Updated cookie array
+ * @throws HTTP_Request2_LogicException
+ * @throws HTTP_Request2_MessageException
+ */
+ protected function checkAndUpdateFields(array $cookie, Net_URL2 $setter = null)
+ {
+ if ($missing = array_diff(array('name', 'value'), array_keys($cookie))) {
+ throw new HTTP_Request2_LogicException(
+ "Cookie array should contain 'name' and 'value' fields",
+ HTTP_Request2_Exception::MISSING_VALUE
+ );
+ }
+ if (preg_match(HTTP_Request2::REGEXP_INVALID_COOKIE, $cookie['name'])) {
+ throw new HTTP_Request2_LogicException(
+ "Invalid cookie name: '{$cookie['name']}'",
+ HTTP_Request2_Exception::INVALID_ARGUMENT
+ );
+ }
+ if (preg_match(HTTP_Request2::REGEXP_INVALID_COOKIE, $cookie['value'])) {
+ throw new HTTP_Request2_LogicException(
+ "Invalid cookie value: '{$cookie['value']}'",
+ HTTP_Request2_Exception::INVALID_ARGUMENT
+ );
+ }
+ $cookie += array('domain' => '', 'path' => '', 'expires' => null, 'secure' => false);
+
+ // Need ISO-8601 date @ UTC timezone
+ if (!empty($cookie['expires'])
+ && !preg_match('/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\+0000$/', $cookie['expires'])
+ ) {
+ try {
+ $dt = new DateTime($cookie['expires']);
+ $dt->setTimezone(new DateTimeZone('UTC'));
+ $cookie['expires'] = $dt->format(DateTime::ISO8601);
+ } catch (Exception $e) {
+ throw new HTTP_Request2_LogicException($e->getMessage());
+ }
+ }
+
+ if (empty($cookie['domain']) || empty($cookie['path'])) {
+ if (!$setter) {
+ throw new HTTP_Request2_LogicException(
+ 'Cookie misses domain and/or path component, cookie setter URL needed',
+ HTTP_Request2_Exception::MISSING_VALUE
+ );
+ }
+ if (empty($cookie['domain'])) {
+ if ($host = $setter->getHost()) {
+ $cookie['domain'] = $host;
+ } else {
+ throw new HTTP_Request2_LogicException(
+ 'Setter URL does not contain host part, can\'t set cookie domain',
+ HTTP_Request2_Exception::MISSING_VALUE
+ );
+ }
+ }
+ if (empty($cookie['path'])) {
+ $path = $setter->getPath();
+ $cookie['path'] = empty($path)? '/': substr($path, 0, strrpos($path, '/') + 1);
+ }
+ }
+
+ if ($setter && !$this->domainMatch($setter->getHost(), $cookie['domain'])) {
+ throw new HTTP_Request2_MessageException(
+ "Domain " . $setter->getHost() . " cannot set cookies for "
+ . $cookie['domain']
+ );
+ }
+
+ return $cookie;
+ }
+
+ /**
+ * Stores a cookie in the jar
+ *
+ * @param array cookie data, as returned by {@link HTTP_Request2_Response::getCookies()}
+ * @param Net_URL2 URL of the document that sent Set-Cookie header
+ * @throws HTTP_Request2_Exception
+ */
+ public function store(array $cookie, Net_URL2 $setter = null)
+ {
+ $cookie = $this->checkAndUpdateFields($cookie, $setter);
+
+ if (strlen($cookie['value'])
+ && (is_null($cookie['expires']) || $cookie['expires'] > $this->now())
+ ) {
+ if (!isset($this->cookies[$cookie['domain']])) {
+ $this->cookies[$cookie['domain']] = array();
+ }
+ if (!isset($this->cookies[$cookie['domain']][$cookie['path']])) {
+ $this->cookies[$cookie['domain']][$cookie['path']] = array();
+ }
+ $this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']] = $cookie;
+
+ } elseif (isset($this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']])) {
+ unset($this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']]);
+ }
+ }
+
+ /**
+ * Adds cookies set in HTTP response to the jar
+ *
+ * @param HTTP_Request2_Response response
+ * @param Net_URL2 original request URL, needed for setting
+ * default domain/path
+ */
+ public function addCookiesFromResponse(HTTP_Request2_Response $response, Net_URL2 $setter)
+ {
+ foreach ($response->getCookies() as $cookie) {
+ $this->store($cookie, $setter);
+ }
+ }
+
+ /**
+ * Returns all cookies matching a given request URL
+ *
+ * The following checks are made:
+ * - cookie domain should match request host
+ * - cookie path should be a prefix for request path
+ * - 'secure' cookies will only be sent for HTTPS requests
+ *
+ * @param Net_URL2
+ * @param bool Whether to return cookies as string for "Cookie: " header
+ * @return array
+ */
+ public function getMatching(Net_URL2 $url, $asString = false)
+ {
+ $host = $url->getHost();
+ $path = $url->getPath();
+ $secure = 0 == strcasecmp($url->getScheme(), 'https');
+
+ $matched = $ret = array();
+ foreach (array_keys($this->cookies) as $domain) {
+ if ($this->domainMatch($host, $domain)) {
+ foreach (array_keys($this->cookies[$domain]) as $cPath) {
+ if (0 === strpos($path, $cPath)) {
+ foreach ($this->cookies[$domain][$cPath] as $name => $cookie) {
+ if (!$cookie['secure'] || $secure) {
+ $matched[$name][strlen($cookie['path'])] = $cookie;
+ }
+ }
+ }
+ }
+ }
+ }
+ foreach ($matched as $cookies) {
+ krsort($cookies);
+ $ret = array_merge($ret, $cookies);
+ }
+ if (!$asString) {
+ return $ret;
+ } else {
+ $str = '';
+ foreach ($ret as $c) {
+ $str .= (empty($str)? '': '; ') . $c['name'] . '=' . $c['value'];
+ }
+ return $str;
+ }
+ }
+
+ /**
+ * Returns all cookies stored in a jar
+ *
+ * @return array
+ */
+ public function getAll()
+ {
+ $cookies = array();
+ foreach (array_keys($this->cookies) as $domain) {
+ foreach (array_keys($this->cookies[$domain]) as $path) {
+ foreach ($this->cookies[$domain][$path] as $name => $cookie) {
+ $cookies[] = $cookie;
+ }
+ }
+ }
+ return $cookies;
+ }
+
+ /**
+ * Sets whether session cookies should be serialized when serializing the jar
+ *
+ * @param boolean
+ */
+ public function serializeSessionCookies($serialize)
+ {
+ $this->serializeSession = (bool)$serialize;
+ }
+
+ /**
+ * Sets whether Public Suffix List should be used for restricting cookie-setting
+ *
+ * Without PSL {@link domainMatch()} will only prevent setting cookies for
+ * top-level domains like '.com' or '.org'. However, it will not prevent
+ * setting a cookie for '.co.uk' even though only third-level registrations
+ * are possible in .uk domain.
+ *
+ * With the List it is possible to find the highest level at which a domain
+ * may be registered for a particular top-level domain and consequently
+ * prevent cookies set for '.co.uk' or '.msk.ru'. The same list is used by
+ * Firefox, Chrome and Opera browsers to restrict cookie setting.
+ *
+ * Note that PSL is licensed differently to HTTP_Request2 package (refer to
+ * the license information in public-suffix-list.php), so you can disable
+ * its use if this is an issue for you.
+ *
+ * @param boolean
+ * @link http://publicsuffix.org/learn/
+ */
+ public function usePublicSuffixList($useList)
+ {
+ $this->useList = (bool)$useList;
+ }
+
+ /**
+ * Returns string representation of object
+ *
+ * @return string
+ * @see Serializable::serialize()
+ */
+ public function serialize()
+ {
+ $cookies = $this->getAll();
+ if (!$this->serializeSession) {
+ for ($i = count($cookies) - 1; $i >= 0; $i--) {
+ if (empty($cookies[$i]['expires'])) {
+ unset($cookies[$i]);
+ }
+ }
+ }
+ return serialize(array(
+ 'cookies' => $cookies,
+ 'serializeSession' => $this->serializeSession,
+ 'useList' => $this->useList
+ ));
+ }
+
+ /**
+ * Constructs the object from serialized string
+ *
+ * @param string string representation
+ * @see Serializable::unserialize()
+ */
+ public function unserialize($serialized)
+ {
+ $data = unserialize($serialized);
+ $now = $this->now();
+ $this->serializeSessionCookies($data['serializeSession']);
+ $this->usePublicSuffixList($data['useList']);
+ foreach ($data['cookies'] as $cookie) {
+ if (!empty($cookie['expires']) && $cookie['expires'] <= $now) {
+ continue;
+ }
+ if (!isset($this->cookies[$cookie['domain']])) {
+ $this->cookies[$cookie['domain']] = array();
+ }
+ if (!isset($this->cookies[$cookie['domain']][$cookie['path']])) {
+ $this->cookies[$cookie['domain']][$cookie['path']] = array();
+ }
+ $this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']] = $cookie;
+ }
+ }
+
+ /**
+ * Checks whether a cookie domain matches a request host.
+ *
+ * The method is used by {@link store()} to check for whether a document
+ * at given URL can set a cookie with a given domain attribute and by
+ * {@link getMatching()} to find cookies matching the request URL.
+ *
+ * @param string request host
+ * @param string cookie domain
+ * @return bool match success
+ */
+ public function domainMatch($requestHost, $cookieDomain)
+ {
+ if ($requestHost == $cookieDomain) {
+ return true;
+ }
+ // IP address, we require exact match
+ if (preg_match('/^(?:\d{1,3}\.){3}\d{1,3}$/', $requestHost)) {
+ return false;
+ }
+ if ('.' != $cookieDomain[0]) {
+ $cookieDomain = '.' . $cookieDomain;
+ }
+ // prevents setting cookies for '.com' and similar domains
+ if (!$this->useList && substr_count($cookieDomain, '.') < 2
+ || $this->useList && !self::getRegisteredDomain($cookieDomain)
+ ) {
+ return false;
+ }
+ return substr('.' . $requestHost, -strlen($cookieDomain)) == $cookieDomain;
+ }
+
+ /**
+ * Removes subdomains to get the registered domain (the first after top-level)
+ *
+ * The method will check Public Suffix List to find out where top-level
+ * domain ends and registered domain starts. It will remove domain parts
+ * to the left of registered one.
+ *
+ * @param string domain name
+ * @return string|bool registered domain, will return false if $domain is
+ * either invalid or a TLD itself
+ */
+ public static function getRegisteredDomain($domain)
+ {
+ $domainParts = explode('.', ltrim($domain, '.'));
+
+ // load the list if needed
+ if (empty(self::$psl)) {
+ $path = '@data_dir@' . DIRECTORY_SEPARATOR . 'HTTP_Request2';
+ if (0 === strpos($path, '@' . 'data_dir@')) {
+ $path = realpath(dirname(__FILE__) . DIRECTORY_SEPARATOR . '..'
+ . DIRECTORY_SEPARATOR . 'data');
+ }
+ self::$psl = include_once $path . DIRECTORY_SEPARATOR . 'public-suffix-list.php';
+ }
+
+ if (!($result = self::checkDomainsList($domainParts, self::$psl))) {
+ // known TLD, invalid domain name
+ return false;
+ }
+
+ // unknown TLD
+ if (!strpos($result, '.')) {
+ // fallback to checking that domain "has at least two dots"
+ if (2 > ($count = count($domainParts))) {
+ return false;
+ }
+ return $domainParts[$count - 2] . '.' . $domainParts[$count - 1];
+ }
+ return $result;
+ }
+
+ /**
+ * Recursive helper method for {@link getRegisteredDomain()}
+ *
+ * @param array remaining domain parts
+ * @param mixed node in {@link HTTP_Request2_CookieJar::$psl} to check
+ * @return string|null concatenated domain parts, null in case of error
+ */
+ protected static function checkDomainsList(array $domainParts, $listNode)
+ {
+ $sub = array_pop($domainParts);
+ $result = null;
+
+ if (!is_array($listNode) || is_null($sub)
+ || array_key_exists('!' . $sub, $listNode)
+ ) {
+ return $sub;
+
+ } elseif (array_key_exists($sub, $listNode)) {
+ $result = self::checkDomainsList($domainParts, $listNode[$sub]);
+
+ } elseif (array_key_exists('*', $listNode)) {
+ $result = self::checkDomainsList($domainParts, $listNode['*']);
+
+ } else {
+ return $sub;
+ }
+
+ return (strlen($result) > 0) ? ($result . '.' . $sub) : null;
+ }
+}
+?> \ No newline at end of file
diff --git a/lib/ext/HTTP/Request2/Exception.php b/lib/ext/HTTP/Request2/Exception.php
new file mode 100644
index 0000000..34256c2
--- /dev/null
+++ b/lib/ext/HTTP/Request2/Exception.php
@@ -0,0 +1,160 @@
+<?php
+/**
+ * Exception classes for HTTP_Request2 package
+ *
+ * PHP version 5
+ *
+ * LICENSE:
+ *
+ * Copyright (c) 2008-2011, Alexey Borzov <avb@php.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.
+ * * The names of the authors may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @category HTTP
+ * @package HTTP_Request2
+ * @author Alexey Borzov <avb@php.net>
+ * @license http://opensource.org/licenses/bsd-license.php New BSD License
+ * @version SVN: $Id: Exception.php 308629 2011-02-24 17:34:24Z avb $
+ * @link http://pear.php.net/package/HTTP_Request2
+ */
+
+/**
+ * Base class for exceptions in PEAR
+ */
+require_once 'PEAR/Exception.php';
+
+/**
+ * Base exception class for HTTP_Request2 package
+ *
+ * @category HTTP
+ * @package HTTP_Request2
+ * @version Release: 2.0.0
+ * @link http://pear.php.net/pepr/pepr-proposal-show.php?id=132
+ */
+class HTTP_Request2_Exception extends PEAR_Exception
+{
+ /** An invalid argument was passed to a method */
+ const INVALID_ARGUMENT = 1;
+ /** Some required value was not available */
+ const MISSING_VALUE = 2;
+ /** Request cannot be processed due to errors in PHP configuration */
+ const MISCONFIGURATION = 3;
+ /** Error reading the local file */
+ const READ_ERROR = 4;
+
+ /** Server returned a response that does not conform to HTTP protocol */
+ const MALFORMED_RESPONSE = 10;
+ /** Failure decoding Content-Encoding or Transfer-Encoding of response */
+ const DECODE_ERROR = 20;
+ /** Operation timed out */
+ const TIMEOUT = 30;
+ /** Number of redirects exceeded 'max_redirects' configuration parameter */
+ const TOO_MANY_REDIRECTS = 40;
+ /** Redirect to a protocol other than http(s):// */
+ const NON_HTTP_REDIRECT = 50;
+
+ /**
+ * Native error code
+ * @var int
+ */
+ private $_nativeCode;
+
+ /**
+ * Constructor, can set package error code and native error code
+ *
+ * @param string exception message
+ * @param int package error code, one of class constants
+ * @param int error code from underlying PHP extension
+ */
+ public function __construct($message = null, $code = null, $nativeCode = null)
+ {
+ parent::__construct($message, $code);
+ $this->_nativeCode = $nativeCode;
+ }
+
+ /**
+ * Returns error code produced by underlying PHP extension
+ *
+ * For Socket Adapter this may contain error number returned by
+ * stream_socket_client(), for Curl Adapter this will contain error number
+ * returned by curl_errno()
+ *
+ * @return integer
+ */
+ public function getNativeCode()
+ {
+ return $this->_nativeCode;
+ }
+}
+
+/**
+ * Exception thrown in case of missing features
+ *
+ * @category HTTP
+ * @package HTTP_Request2
+ * @version Release: 2.0.0
+ */
+class HTTP_Request2_NotImplementedException extends HTTP_Request2_Exception {}
+
+/**
+ * Exception that represents error in the program logic
+ *
+ * This exception usually implies a programmer's error, like passing invalid
+ * data to methods or trying to use PHP extensions that weren't installed or
+ * enabled. Usually exceptions of this kind will be thrown before request even
+ * starts.
+ *
+ * The exception will usually contain a package error code.
+ *
+ * @category HTTP
+ * @package HTTP_Request2
+ * @version Release: 2.0.0
+ */
+class HTTP_Request2_LogicException extends HTTP_Request2_Exception {}
+
+/**
+ * Exception thrown when connection to a web or proxy server fails
+ *
+ * The exception will not contain a package error code, but will contain
+ * native error code, as returned by stream_socket_client() or curl_errno().
+ *
+ * @category HTTP
+ * @package HTTP_Request2
+ * @version Release: 2.0.0
+ */
+class HTTP_Request2_ConnectionException extends HTTP_Request2_Exception {}
+
+/**
+ * Exception thrown when sending or receiving HTTP message fails
+ *
+ * The exception may contain both package error code and native error code.
+ *
+ * @category HTTP
+ * @package HTTP_Request2
+ * @version Release: 2.0.0
+ */
+class HTTP_Request2_MessageException extends HTTP_Request2_Exception {}
+?> \ No newline at end of file
diff --git a/lib/ext/HTTP/Request2/MultipartBody.php b/lib/ext/HTTP/Request2/MultipartBody.php
new file mode 100644
index 0000000..021a199
--- /dev/null
+++ b/lib/ext/HTTP/Request2/MultipartBody.php
@@ -0,0 +1,274 @@
+<?php
+/**
+ * Helper class for building multipart/form-data request body
+ *
+ * PHP version 5
+ *
+ * LICENSE:
+ *
+ * Copyright (c) 2008-2011, Alexey Borzov <avb@php.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.
+ * * The names of the authors may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @category HTTP
+ * @package HTTP_Request2
+ * @author Alexey Borzov <avb@php.net>
+ * @license http://opensource.org/licenses/bsd-license.php New BSD License
+ * @version SVN: $Id: MultipartBody.php 308322 2011-02-14 13:58:03Z avb $
+ * @link http://pear.php.net/package/HTTP_Request2
+ */
+
+/**
+ * Class for building multipart/form-data request body
+ *
+ * The class helps to reduce memory consumption by streaming large file uploads
+ * from disk, it also allows monitoring of upload progress (see request #7630)
+ *
+ * @category HTTP
+ * @package HTTP_Request2
+ * @author Alexey Borzov <avb@php.net>
+ * @version Release: 2.0.0
+ * @link http://tools.ietf.org/html/rfc1867
+ */
+class HTTP_Request2_MultipartBody
+{
+ /**
+ * MIME boundary
+ * @var string
+ */
+ private $_boundary;
+
+ /**
+ * Form parameters added via {@link HTTP_Request2::addPostParameter()}
+ * @var array
+ */
+ private $_params = array();
+
+ /**
+ * File uploads added via {@link HTTP_Request2::addUpload()}
+ * @var array
+ */
+ private $_uploads = array();
+
+ /**
+ * Header for parts with parameters
+ * @var string
+ */
+ private $_headerParam = "--%s\r\nContent-Disposition: form-data; name=\"%s\"\r\n\r\n";
+
+ /**
+ * Header for parts with uploads
+ * @var string
+ */
+ private $_headerUpload = "--%s\r\nContent-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\nContent-Type: %s\r\n\r\n";
+
+ /**
+ * Current position in parameter and upload arrays
+ *
+ * First number is index of "current" part, second number is position within
+ * "current" part
+ *
+ * @var array
+ */
+ private $_pos = array(0, 0);
+
+
+ /**
+ * Constructor. Sets the arrays with POST data.
+ *
+ * @param array values of form fields set via {@link HTTP_Request2::addPostParameter()}
+ * @param array file uploads set via {@link HTTP_Request2::addUpload()}
+ * @param bool whether to append brackets to array variable names
+ */
+ public function __construct(array $params, array $uploads, $useBrackets = true)
+ {
+ $this->_params = self::_flattenArray('', $params, $useBrackets);
+ foreach ($uploads as $fieldName => $f) {
+ if (!is_array($f['fp'])) {
+ $this->_uploads[] = $f + array('name' => $fieldName);
+ } else {
+ for ($i = 0; $i < count($f['fp']); $i++) {
+ $upload = array(
+ 'name' => ($useBrackets? $fieldName . '[' . $i . ']': $fieldName)
+ );
+ foreach (array('fp', 'filename', 'size', 'type') as $key) {
+ $upload[$key] = $f[$key][$i];
+ }
+ $this->_uploads[] = $upload;
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the length of the body to use in Content-Length header
+ *
+ * @return integer
+ */
+ public function getLength()
+ {
+ $boundaryLength = strlen($this->getBoundary());
+ $headerParamLength = strlen($this->_headerParam) - 4 + $boundaryLength;
+ $headerUploadLength = strlen($this->_headerUpload) - 8 + $boundaryLength;
+ $length = $boundaryLength + 6;
+ foreach ($this->_params as $p) {
+ $length += $headerParamLength + strlen($p[0]) + strlen($p[1]) + 2;
+ }
+ foreach ($this->_uploads as $u) {
+ $length += $headerUploadLength + strlen($u['name']) + strlen($u['type']) +
+ strlen($u['filename']) + $u['size'] + 2;
+ }
+ return $length;
+ }
+
+ /**
+ * Returns the boundary to use in Content-Type header
+ *
+ * @return string
+ */
+ public function getBoundary()
+ {
+ if (empty($this->_boundary)) {
+ $this->_boundary = '--' . md5('PEAR-HTTP_Request2-' . microtime());
+ }
+ return $this->_boundary;
+ }
+
+ /**
+ * Returns next chunk of request body
+ *
+ * @param integer Amount of bytes to read
+ * @return string Up to $length bytes of data, empty string if at end
+ */
+ public function read($length)
+ {
+ $ret = '';
+ $boundary = $this->getBoundary();
+ $paramCount = count($this->_params);
+ $uploadCount = count($this->_uploads);
+ while ($length > 0 && $this->_pos[0] <= $paramCount + $uploadCount) {
+ $oldLength = $length;
+ if ($this->_pos[0] < $paramCount) {
+ $param = sprintf($this->_headerParam, $boundary,
+ $this->_params[$this->_pos[0]][0]) .
+ $this->_params[$this->_pos[0]][1] . "\r\n";
+ $ret .= substr($param, $this->_pos[1], $length);
+ $length -= min(strlen($param) - $this->_pos[1], $length);
+
+ } elseif ($this->_pos[0] < $paramCount + $uploadCount) {
+ $pos = $this->_pos[0] - $paramCount;
+ $header = sprintf($this->_headerUpload, $boundary,
+ $this->_uploads[$pos]['name'],
+ $this->_uploads[$pos]['filename'],
+ $this->_uploads[$pos]['type']);
+ if ($this->_pos[1] < strlen($header)) {
+ $ret .= substr($header, $this->_pos[1], $length);
+ $length -= min(strlen($header) - $this->_pos[1], $length);
+ }
+ $filePos = max(0, $this->_pos[1] - strlen($header));
+ if ($length > 0 && $filePos < $this->_uploads[$pos]['size']) {
+ $ret .= fread($this->_uploads[$pos]['fp'], $length);
+ $length -= min($length, $this->_uploads[$pos]['size'] - $filePos);
+ }
+ if ($length > 0) {
+ $start = $this->_pos[1] + ($oldLength - $length) -
+ strlen($header) - $this->_uploads[$pos]['size'];
+ $ret .= substr("\r\n", $start, $length);
+ $length -= min(2 - $start, $length);
+ }
+
+ } else {
+ $closing = '--' . $boundary . "--\r\n";
+ $ret .= substr($closing, $this->_pos[1], $length);
+ $length -= min(strlen($closing) - $this->_pos[1], $length);
+ }
+ if ($length > 0) {
+ $this->_pos = array($this->_pos[0] + 1, 0);
+ } else {
+ $this->_pos[1] += $oldLength;
+ }
+ }
+ return $ret;
+ }
+
+ /**
+ * Sets the current position to the start of the body
+ *
+ * This allows reusing the same body in another request
+ */
+ public function rewind()
+ {
+ $this->_pos = array(0, 0);
+ foreach ($this->_uploads as $u) {
+ rewind($u['fp']);
+ }
+ }
+
+ /**
+ * Returns the body as string
+ *
+ * Note that it reads all file uploads into memory so it is a good idea not
+ * to use this method with large file uploads and rely on read() instead.
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ $this->rewind();
+ return $this->read($this->getLength());
+ }
+
+
+ /**
+ * Helper function to change the (probably multidimensional) associative array
+ * into the simple one.
+ *
+ * @param string name for item
+ * @param mixed item's values
+ * @param bool whether to append [] to array variables' names
+ * @return array array with the following items: array('item name', 'item value');
+ */
+ private static function _flattenArray($name, $values, $useBrackets)
+ {
+ if (!is_array($values)) {
+ return array(array($name, $values));
+ } else {
+ $ret = array();
+ foreach ($values as $k => $v) {
+ if (empty($name)) {
+ $newName = $k;
+ } elseif ($useBrackets) {
+ $newName = $name . '[' . $k . ']';
+ } else {
+ $newName = $name;
+ }
+ $ret = array_merge($ret, self::_flattenArray($newName, $v, $useBrackets));
+ }
+ return $ret;
+ }
+ }
+}
+?>
diff --git a/lib/ext/HTTP/Request2/Observer/Log.php b/lib/ext/HTTP/Request2/Observer/Log.php
new file mode 100644
index 0000000..dd59859
--- /dev/null
+++ b/lib/ext/HTTP/Request2/Observer/Log.php
@@ -0,0 +1,215 @@
+<?php
+/**
+ * An observer useful for debugging / testing.
+ *
+ * PHP version 5
+ *
+ * LICENSE:
+ *
+ * Copyright (c) 2008-2011, Alexey Borzov <avb@php.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.
+ * * The names of the authors may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @category HTTP
+ * @package HTTP_Request2
+ * @author David Jean Louis <izi@php.net>
+ * @author Alexey Borzov <avb@php.net>
+ * @license http://opensource.org/licenses/bsd-license.php New BSD License
+ * @version SVN: $Id: Log.php 308680 2011-02-25 17:40:17Z avb $
+ * @link http://pear.php.net/package/HTTP_Request2
+ */
+
+/**
+ * Exception class for HTTP_Request2 package
+ */
+require_once 'HTTP/Request2/Exception.php';
+
+/**
+ * A debug observer useful for debugging / testing.
+ *
+ * This observer logs to a log target data corresponding to the various request
+ * and response events, it logs by default to php://output but can be configured
+ * to log to a file or via the PEAR Log package.
+ *
+ * A simple example:
+ * <code>
+ * require_once 'HTTP/Request2.php';
+ * require_once 'HTTP/Request2/Observer/Log.php';
+ *
+ * $request = new HTTP_Request2('http://www.example.com');
+ * $observer = new HTTP_Request2_Observer_Log();
+ * $request->attach($observer);
+ * $request->send();
+ * </code>
+ *
+ * A more complex example with PEAR Log:
+ * <code>
+ * require_once 'HTTP/Request2.php';
+ * require_once 'HTTP/Request2/Observer/Log.php';
+ * require_once 'Log.php';
+ *
+ * $request = new HTTP_Request2('http://www.example.com');
+ * // we want to log with PEAR log
+ * $observer = new HTTP_Request2_Observer_Log(Log::factory('console'));
+ *
+ * // we only want to log received headers
+ * $observer->events = array('receivedHeaders');
+ *
+ * $request->attach($observer);
+ * $request->send();
+ * </code>
+ *
+ * @category HTTP
+ * @package HTTP_Request2
+ * @author David Jean Louis <izi@php.net>
+ * @author Alexey Borzov <avb@php.net>
+ * @license http://opensource.org/licenses/bsd-license.php New BSD License
+ * @version Release: 2.0.0
+ * @link http://pear.php.net/package/HTTP_Request2
+ */
+class HTTP_Request2_Observer_Log implements SplObserver
+{
+ // properties {{{
+
+ /**
+ * The log target, it can be a a resource or a PEAR Log instance.
+ *
+ * @var resource|Log $target
+ */
+ protected $target = null;
+
+ /**
+ * The events to log.
+ *
+ * @var array $events
+ */
+ public $events = array(
+ 'connect',
+ 'sentHeaders',
+ 'sentBody',
+ 'receivedHeaders',
+ 'receivedBody',
+ 'disconnect',
+ );
+
+ // }}}
+ // __construct() {{{
+
+ /**
+ * Constructor.
+ *
+ * @param mixed $target Can be a file path (default: php://output), a resource,
+ * or an instance of the PEAR Log class.
+ * @param array $events Array of events to listen to (default: all events)
+ *
+ * @return void
+ */
+ public function __construct($target = 'php://output', array $events = array())
+ {
+ if (!empty($events)) {
+ $this->events = $events;
+ }
+ if (is_resource($target) || $target instanceof Log) {
+ $this->target = $target;
+ } elseif (false === ($this->target = @fopen($target, 'ab'))) {
+ throw new HTTP_Request2_Exception("Unable to open '{$target}'");
+ }
+ }
+
+ // }}}
+ // update() {{{
+
+ /**
+ * Called when the request notifies us of an event.
+ *
+ * @param HTTP_Request2 $subject The HTTP_Request2 instance
+ *
+ * @return void
+ */
+ public function update(SplSubject $subject)
+ {
+ $event = $subject->getLastEvent();
+ if (!in_array($event['name'], $this->events)) {
+ return;
+ }
+
+ switch ($event['name']) {
+ case 'connect':
+ $this->log('* Connected to ' . $event['data']);
+ break;
+ case 'sentHeaders':
+ $headers = explode("\r\n", $event['data']);
+ array_pop($headers);
+ foreach ($headers as $header) {
+ $this->log('> ' . $header);
+ }
+ break;
+ case 'sentBody':
+ $this->log('> ' . $event['data'] . ' byte(s) sent');
+ break;
+ case 'receivedHeaders':
+ $this->log(sprintf('< HTTP/%s %s %s',
+ $event['data']->getVersion(),
+ $event['data']->getStatus(),
+ $event['data']->getReasonPhrase()));
+ $headers = $event['data']->getHeader();
+ foreach ($headers as $key => $val) {
+ $this->log('< ' . $key . ': ' . $val);
+ }
+ $this->log('< ');
+ break;
+ case 'receivedBody':
+ $this->log($event['data']->getBody());
+ break;
+ case 'disconnect':
+ $this->log('* Disconnected');
+ break;
+ }
+ }
+
+ // }}}
+ // log() {{{
+
+ /**
+ * Logs the given message to the configured target.
+ *
+ * @param string $message Message to display
+ *
+ * @return void
+ */
+ protected function log($message)
+ {
+ if ($this->target instanceof Log) {
+ $this->target->debug($message);
+ } elseif (is_resource($this->target)) {
+ fwrite($this->target, $message . "\r\n");
+ }
+ }
+
+ // }}}
+}
+
+?> \ No newline at end of file
diff --git a/lib/ext/HTTP/Request2/Response.php b/lib/ext/HTTP/Request2/Response.php
new file mode 100644
index 0000000..6e0f659
--- /dev/null
+++ b/lib/ext/HTTP/Request2/Response.php
@@ -0,0 +1,643 @@
+<?php
+/**
+ * Class representing a HTTP response
+ *
+ * PHP version 5
+ *
+ * LICENSE:
+ *
+ * Copyright (c) 2008-2011, Alexey Borzov <avb@php.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.
+ * * The names of the authors may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @category HTTP
+ * @package HTTP_Request2
+ * @author Alexey Borzov <avb@php.net>
+ * @license http://opensource.org/licenses/bsd-license.php New BSD License
+ * @version SVN: $Id: Response.php 317591 2011-10-01 08:37:49Z avb $
+ * @link http://pear.php.net/package/HTTP_Request2
+ */
+
+/**
+ * Exception class for HTTP_Request2 package
+ */
+require_once 'HTTP/Request2/Exception.php';
+
+/**
+ * Class representing a HTTP response
+ *
+ * The class is designed to be used in "streaming" scenario, building the
+ * response as it is being received:
+ * <code>
+ * $statusLine = read_status_line();
+ * $response = new HTTP_Request2_Response($statusLine);
+ * do {
+ * $headerLine = read_header_line();
+ * $response->parseHeaderLine($headerLine);
+ * } while ($headerLine != '');
+ *
+ * while ($chunk = read_body()) {
+ * $response->appendBody($chunk);
+ * }
+ *
+ * var_dump($response->getHeader(), $response->getCookies(), $response->getBody());
+ * </code>
+ *
+ *
+ * @category HTTP
+ * @package HTTP_Request2
+ * @author Alexey Borzov <avb@php.net>
+ * @version Release: 2.0.0
+ * @link http://tools.ietf.org/html/rfc2616#section-6
+ */
+class HTTP_Request2_Response
+{
+ /**
+ * HTTP protocol version (e.g. 1.0, 1.1)
+ * @var string
+ */
+ protected $version;
+
+ /**
+ * Status code
+ * @var integer
+ * @link http://tools.ietf.org/html/rfc2616#section-6.1.1
+ */
+ protected $code;
+
+ /**
+ * Reason phrase
+ * @var string
+ * @link http://tools.ietf.org/html/rfc2616#section-6.1.1
+ */
+ protected $reasonPhrase;
+
+ /**
+ * Effective URL (may be different from original request URL in case of redirects)
+ * @var string
+ */
+ protected $effectiveUrl;
+
+ /**
+ * Associative array of response headers
+ * @var array
+ */
+ protected $headers = array();
+
+ /**
+ * Cookies set in the response
+ * @var array
+ */
+ protected $cookies = array();
+
+ /**
+ * Name of last header processed by parseHederLine()
+ *
+ * Used to handle the headers that span multiple lines
+ *
+ * @var string
+ */
+ protected $lastHeader = null;
+
+ /**
+ * Response body
+ * @var string
+ */
+ protected $body = '';
+
+ /**
+ * Whether the body is still encoded by Content-Encoding
+ *
+ * cURL provides the decoded body to the callback; if we are reading from
+ * socket the body is still gzipped / deflated
+ *
+ * @var bool
+ */
+ protected $bodyEncoded;
+
+ /**
+ * Associative array of HTTP status code / reason phrase.
+ *
+ * @var array
+ * @link http://tools.ietf.org/html/rfc2616#section-10
+ */
+ protected static $phrases = array(
+
+ // 1xx: Informational - Request received, continuing process
+ 100 => 'Continue',
+ 101 => 'Switching Protocols',
+
+ // 2xx: Success - The action was successfully received, understood and
+ // accepted
+ 200 => 'OK',
+ 201 => 'Created',
+ 202 => 'Accepted',
+ 203 => 'Non-Authoritative Information',
+ 204 => 'No Content',
+ 205 => 'Reset Content',
+ 206 => 'Partial Content',
+
+ // 3xx: Redirection - Further action must be taken in order to complete
+ // the request
+ 300 => 'Multiple Choices',
+ 301 => 'Moved Permanently',
+ 302 => 'Found', // 1.1
+ 303 => 'See Other',
+ 304 => 'Not Modified',
+ 305 => 'Use Proxy',
+ 307 => 'Temporary Redirect',
+
+ // 4xx: Client Error - The request contains bad syntax or cannot be
+ // fulfilled
+ 400 => 'Bad Request',
+ 401 => 'Unauthorized',
+ 402 => 'Payment Required',
+ 403 => 'Forbidden',
+ 404 => 'Not Found',
+ 405 => 'Method Not Allowed',
+ 406 => 'Not Acceptable',
+ 407 => 'Proxy Authentication Required',
+ 408 => 'Request Timeout',
+ 409 => 'Conflict',
+ 410 => 'Gone',
+ 411 => 'Length Required',
+ 412 => 'Precondition Failed',
+ 413 => 'Request Entity Too Large',
+ 414 => 'Request-URI Too Long',
+ 415 => 'Unsupported Media Type',
+ 416 => 'Requested Range Not Satisfiable',
+ 417 => 'Expectation Failed',
+
+ // 5xx: Server Error - The server failed to fulfill an apparently
+ // valid request
+ 500 => 'Internal Server Error',
+ 501 => 'Not Implemented',
+ 502 => 'Bad Gateway',
+ 503 => 'Service Unavailable',
+ 504 => 'Gateway Timeout',
+ 505 => 'HTTP Version Not Supported',
+ 509 => 'Bandwidth Limit Exceeded',
+
+ );
+
+ /**
+ * Returns the default reason phrase for the given code or all reason phrases
+ *
+ * @param int $code Response code
+ * @return string|array|null Default reason phrase for $code if $code is given
+ * (null if no phrase is available), array of all
+ * reason phrases if $code is null
+ * @link http://pear.php.net/bugs/18716
+ */
+ public static function getDefaultReasonPhrase($code = null)
+ {
+ if (null === $code) {
+ return self::$phrases;
+ } else {
+ return isset(self::$phrases[$code]) ? self::$phrases[$code] : null;
+ }
+ }
+
+ /**
+ * Constructor, parses the response status line
+ *
+ * @param string Response status line (e.g. "HTTP/1.1 200 OK")
+ * @param bool Whether body is still encoded by Content-Encoding
+ * @param string Effective URL of the response
+ * @throws HTTP_Request2_MessageException if status line is invalid according to spec
+ */
+ public function __construct($statusLine, $bodyEncoded = true, $effectiveUrl = null)
+ {
+ if (!preg_match('!^HTTP/(\d\.\d) (\d{3})(?: (.+))?!', $statusLine, $m)) {
+ throw new HTTP_Request2_MessageException(
+ "Malformed response: {$statusLine}",
+ HTTP_Request2_Exception::MALFORMED_RESPONSE
+ );
+ }
+ $this->version = $m[1];
+ $this->code = intval($m[2]);
+ $this->reasonPhrase = !empty($m[3]) ? trim($m[3]) : self::getDefaultReasonPhrase($this->code);
+ $this->bodyEncoded = (bool)$bodyEncoded;
+ $this->effectiveUrl = (string)$effectiveUrl;
+ }
+
+ /**
+ * Parses the line from HTTP response filling $headers array
+ *
+ * The method should be called after reading the line from socket or receiving
+ * it into cURL callback. Passing an empty string here indicates the end of
+ * response headers and triggers additional processing, so be sure to pass an
+ * empty string in the end.
+ *
+ * @param string Line from HTTP response
+ */
+ public function parseHeaderLine($headerLine)
+ {
+ $headerLine = trim($headerLine, "\r\n");
+
+ // empty string signals the end of headers, process the received ones
+ if ('' == $headerLine) {
+ if (!empty($this->headers['set-cookie'])) {
+ $cookies = is_array($this->headers['set-cookie'])?
+ $this->headers['set-cookie']:
+ array($this->headers['set-cookie']);
+ foreach ($cookies as $cookieString) {
+ $this->parseCookie($cookieString);
+ }
+ unset($this->headers['set-cookie']);
+ }
+ foreach (array_keys($this->headers) as $k) {
+ if (is_array($this->headers[$k])) {
+ $this->headers[$k] = implode(', ', $this->headers[$k]);
+ }
+ }
+
+ // string of the form header-name: header value
+ } elseif (preg_match('!^([^\x00-\x1f\x7f-\xff()<>@,;:\\\\"/\[\]?={}\s]+):(.+)$!', $headerLine, $m)) {
+ $name = strtolower($m[1]);
+ $value = trim($m[2]);
+ if (empty($this->headers[$name])) {
+ $this->headers[$name] = $value;
+ } else {
+ if (!is_array($this->headers[$name])) {
+ $this->headers[$name] = array($this->headers[$name]);
+ }
+ $this->headers[$name][] = $value;
+ }
+ $this->lastHeader = $name;
+
+ // continuation of a previous header
+ } elseif (preg_match('!^\s+(.+)$!', $headerLine, $m) && $this->lastHeader) {
+ if (!is_array($this->headers[$this->lastHeader])) {
+ $this->headers[$this->lastHeader] .= ' ' . trim($m[1]);
+ } else {
+ $key = count($this->headers[$this->lastHeader]) - 1;
+ $this->headers[$this->lastHeader][$key] .= ' ' . trim($m[1]);
+ }
+ }
+ }
+
+ /**
+ * Parses a Set-Cookie header to fill $cookies array
+ *
+ * @param string value of Set-Cookie header
+ * @link http://web.archive.org/web/20080331104521/http://cgi.netscape.com/newsref/std/cookie_spec.html
+ */
+ protected function parseCookie($cookieString)
+ {
+ $cookie = array(
+ 'expires' => null,
+ 'domain' => null,
+ 'path' => null,
+ 'secure' => false
+ );
+
+ // Only a name=value pair
+ if (!strpos($cookieString, ';')) {
+ $pos = strpos($cookieString, '=');
+ $cookie['name'] = trim(substr($cookieString, 0, $pos));
+ $cookie['value'] = trim(substr($cookieString, $pos + 1));
+
+ // Some optional parameters are supplied
+ } else {
+ $elements = explode(';', $cookieString);
+ $pos = strpos($elements[0], '=');
+ $cookie['name'] = trim(substr($elements[0], 0, $pos));
+ $cookie['value'] = trim(substr($elements[0], $pos + 1));
+
+ for ($i = 1; $i < count($elements); $i++) {
+ if (false === strpos($elements[$i], '=')) {
+ $elName = trim($elements[$i]);
+ $elValue = null;
+ } else {
+ list ($elName, $elValue) = array_map('trim', explode('=', $elements[$i]));
+ }
+ $elName = strtolower($elName);
+ if ('secure' == $elName) {
+ $cookie['secure'] = true;
+ } elseif ('expires' == $elName) {
+ $cookie['expires'] = str_replace('"', '', $elValue);
+ } elseif ('path' == $elName || 'domain' == $elName) {
+ $cookie[$elName] = urldecode($elValue);
+ } else {
+ $cookie[$elName] = $elValue;
+ }
+ }
+ }
+ $this->cookies[] = $cookie;
+ }
+
+ /**
+ * Appends a string to the response body
+ * @param string
+ */
+ public function appendBody($bodyChunk)
+ {
+ $this->body .= $bodyChunk;
+ }
+
+ /**
+ * Returns the effective URL of the response
+ *
+ * This may be different from the request URL if redirects were followed.
+ *
+ * @return string
+ * @link http://pear.php.net/bugs/bug.php?id=18412
+ */
+ public function getEffectiveUrl()
+ {
+ return $this->effectiveUrl;
+ }
+
+ /**
+ * Returns the status code
+ * @return integer
+ */
+ public function getStatus()
+ {
+ return $this->code;
+ }
+
+ /**
+ * Returns the reason phrase
+ * @return string
+ */
+ public function getReasonPhrase()
+ {
+ return $this->reasonPhrase;
+ }
+
+ /**
+ * Whether response is a redirect that can be automatically handled by HTTP_Request2
+ * @return bool
+ */
+ public function isRedirect()
+ {
+ return in_array($this->code, array(300, 301, 302, 303, 307))
+ && isset($this->headers['location']);
+ }
+
+ /**
+ * Returns either the named header or all response headers
+ *
+ * @param string Name of header to return
+ * @return string|array Value of $headerName header (null if header is
+ * not present), array of all response headers if
+ * $headerName is null
+ */
+ public function getHeader($headerName = null)
+ {
+ if (null === $headerName) {
+ return $this->headers;
+ } else {
+ $headerName = strtolower($headerName);
+ return isset($this->headers[$headerName])? $this->headers[$headerName]: null;
+ }
+ }
+
+ /**
+ * Returns cookies set in response
+ *
+ * @return array
+ */
+ public function getCookies()
+ {
+ return $this->cookies;
+ }
+
+ /**
+ * Returns the body of the response
+ *
+ * @return string
+ * @throws HTTP_Request2_Exception if body cannot be decoded
+ */
+ public function getBody()
+ {
+ if (0 == strlen($this->body) || !$this->bodyEncoded ||
+ !in_array(strtolower($this->getHeader('content-encoding')), array('gzip', 'deflate'))
+ ) {
+ return $this->body;
+
+ } else {
+ if (extension_loaded('mbstring') && (2 & ini_get('mbstring.func_overload'))) {
+ $oldEncoding = mb_internal_encoding();
+ mb_internal_encoding('iso-8859-1');
+ }
+
+ try {
+ switch (strtolower($this->getHeader('content-encoding'))) {
+ case 'gzip':
+ $decoded = self::decodeGzip($this->body);
+ break;
+ case 'deflate':
+ $decoded = self::decodeDeflate($this->body);
+ }
+ } catch (Exception $e) {
+ }
+
+ if (!empty($oldEncoding)) {
+ mb_internal_encoding($oldEncoding);
+ }
+ if (!empty($e)) {
+ throw $e;
+ }
+ return $decoded;
+ }
+ }
+
+ /**
+ * Get the HTTP version of the response
+ *
+ * @return string
+ */
+ public function getVersion()
+ {
+ return $this->version;
+ }
+
+ /**
+ * Decodes the message-body encoded by gzip
+ *
+ * The real decoding work is done by gzinflate() built-in function, this
+ * method only parses the header and checks data for compliance with
+ * RFC 1952
+ *
+ * @param string gzip-encoded data
+ * @return string decoded data
+ * @throws HTTP_Request2_LogicException
+ * @throws HTTP_Request2_MessageException
+ * @link http://tools.ietf.org/html/rfc1952
+ */
+ public static function decodeGzip($data)
+ {
+ $length = strlen($data);
+ // If it doesn't look like gzip-encoded data, don't bother
+ if (18 > $length || strcmp(substr($data, 0, 2), "\x1f\x8b")) {
+ return $data;
+ }
+ if (!function_exists('gzinflate')) {
+ throw new HTTP_Request2_LogicException(
+ 'Unable to decode body: gzip extension not available',
+ HTTP_Request2_Exception::MISCONFIGURATION
+ );
+ }
+ $method = ord(substr($data, 2, 1));
+ if (8 != $method) {
+ throw new HTTP_Request2_MessageException(
+ 'Error parsing gzip header: unknown compression method',
+ HTTP_Request2_Exception::DECODE_ERROR
+ );
+ }
+ $flags = ord(substr($data, 3, 1));
+ if ($flags & 224) {
+ throw new HTTP_Request2_MessageException(
+ 'Error parsing gzip header: reserved bits are set',
+ HTTP_Request2_Exception::DECODE_ERROR
+ );
+ }
+
+ // header is 10 bytes minimum. may be longer, though.
+ $headerLength = 10;
+ // extra fields, need to skip 'em
+ if ($flags & 4) {
+ if ($length - $headerLength - 2 < 8) {
+ throw new HTTP_Request2_MessageException(
+ 'Error parsing gzip header: data too short',
+ HTTP_Request2_Exception::DECODE_ERROR
+ );
+ }
+ $extraLength = unpack('v', substr($data, 10, 2));
+ if ($length - $headerLength - 2 - $extraLength[1] < 8) {
+ throw new HTTP_Request2_MessageException(
+ 'Error parsing gzip header: data too short',
+ HTTP_Request2_Exception::DECODE_ERROR
+ );
+ }
+ $headerLength += $extraLength[1] + 2;
+ }
+ // file name, need to skip that
+ if ($flags & 8) {
+ if ($length - $headerLength - 1 < 8) {
+ throw new HTTP_Request2_MessageException(
+ 'Error parsing gzip header: data too short',
+ HTTP_Request2_Exception::DECODE_ERROR
+ );
+ }
+ $filenameLength = strpos(substr($data, $headerLength), chr(0));
+ if (false === $filenameLength || $length - $headerLength - $filenameLength - 1 < 8) {
+ throw new HTTP_Request2_MessageException(
+ 'Error parsing gzip header: data too short',
+ HTTP_Request2_Exception::DECODE_ERROR
+ );
+ }
+ $headerLength += $filenameLength + 1;
+ }
+ // comment, need to skip that also
+ if ($flags & 16) {
+ if ($length - $headerLength - 1 < 8) {
+ throw new HTTP_Request2_MessageException(
+ 'Error parsing gzip header: data too short',
+ HTTP_Request2_Exception::DECODE_ERROR
+ );
+ }
+ $commentLength = strpos(substr($data, $headerLength), chr(0));
+ if (false === $commentLength || $length - $headerLength - $commentLength - 1 < 8) {
+ throw new HTTP_Request2_MessageException(
+ 'Error parsing gzip header: data too short',
+ HTTP_Request2_Exception::DECODE_ERROR
+ );
+ }
+ $headerLength += $commentLength + 1;
+ }
+ // have a CRC for header. let's check
+ if ($flags & 2) {
+ if ($length - $headerLength - 2 < 8) {
+ throw new HTTP_Request2_MessageException(
+ 'Error parsing gzip header: data too short',
+ HTTP_Request2_Exception::DECODE_ERROR
+ );
+ }
+ $crcReal = 0xffff & crc32(substr($data, 0, $headerLength));
+ $crcStored = unpack('v', substr($data, $headerLength, 2));
+ if ($crcReal != $crcStored[1]) {
+ throw new HTTP_Request2_MessageException(
+ 'Header CRC check failed',
+ HTTP_Request2_Exception::DECODE_ERROR
+ );
+ }
+ $headerLength += 2;
+ }
+ // unpacked data CRC and size at the end of encoded data
+ $tmp = unpack('V2', substr($data, -8));
+ $dataCrc = $tmp[1];
+ $dataSize = $tmp[2];
+
+ // finally, call the gzinflate() function
+ // don't pass $dataSize to gzinflate, see bugs #13135, #14370
+ $unpacked = gzinflate(substr($data, $headerLength, -8));
+ if (false === $unpacked) {
+ throw new HTTP_Request2_MessageException(
+ 'gzinflate() call failed',
+ HTTP_Request2_Exception::DECODE_ERROR
+ );
+ } elseif ($dataSize != strlen($unpacked)) {
+ throw new HTTP_Request2_MessageException(
+ 'Data size check failed',
+ HTTP_Request2_Exception::DECODE_ERROR
+ );
+ } elseif ((0xffffffff & $dataCrc) != (0xffffffff & crc32($unpacked))) {
+ throw new HTTP_Request2_Exception(
+ 'Data CRC check failed',
+ HTTP_Request2_Exception::DECODE_ERROR
+ );
+ }
+ return $unpacked;
+ }
+
+ /**
+ * Decodes the message-body encoded by deflate
+ *
+ * @param string deflate-encoded data
+ * @return string decoded data
+ * @throws HTTP_Request2_LogicException
+ */
+ public static function decodeDeflate($data)
+ {
+ if (!function_exists('gzuncompress')) {
+ throw new HTTP_Request2_LogicException(
+ 'Unable to decode body: gzip extension not available',
+ HTTP_Request2_Exception::MISCONFIGURATION
+ );
+ }
+ // RFC 2616 defines 'deflate' encoding as zlib format from RFC 1950,
+ // while many applications send raw deflate stream from RFC 1951.
+ // We should check for presence of zlib header and use gzuncompress() or
+ // gzinflate() as needed. See bug #15305
+ $header = unpack('n', substr($data, 0, 2));
+ return (0 == $header[1] % 31)? gzuncompress($data): gzinflate($data);
+ }
+}
+?> \ No newline at end of file
diff --git a/lib/ext/Net/URL2.php b/lib/ext/Net/URL2.php
new file mode 100755
index 0000000..9989404
--- /dev/null
+++ b/lib/ext/Net/URL2.php
@@ -0,0 +1,942 @@
+<?php
+/**
+ * Net_URL2, a class representing a URL as per RFC 3986.
+ *
+ * PHP version 5
+ *
+ * LICENSE:
+ *
+ * Copyright (c) 2007-2009, Peytz & Co. A/S
+ * 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 of the Net_URL2 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.
+ *
+ * @category Networking
+ * @package Net_URL2
+ * @author Christian Schmidt <schmidt@php.net>
+ * @copyright 2007-2009 Peytz & Co. A/S
+ * @license http://www.opensource.org/licenses/bsd-license.php New BSD License
+ * @version CVS: $Id: URL2.php 309223 2011-03-14 14:26:32Z till $
+ * @link http://www.rfc-editor.org/rfc/rfc3986.txt
+ */
+
+/**
+ * Represents a URL as per RFC 3986.
+ *
+ * @category Networking
+ * @package Net_URL2
+ * @author Christian Schmidt <schmidt@php.net>
+ * @copyright 2007-2009 Peytz & Co. A/S
+ * @license http://www.opensource.org/licenses/bsd-license.php New BSD License
+ * @version Release: @package_version@
+ * @link http://pear.php.net/package/Net_URL2
+ */
+class Net_URL2
+{
+ /**
+ * Do strict parsing in resolve() (see RFC 3986, section 5.2.2). Default
+ * is true.
+ */
+ const OPTION_STRICT = 'strict';
+
+ /**
+ * Represent arrays in query using PHP's [] notation. Default is true.
+ */
+ const OPTION_USE_BRACKETS = 'use_brackets';
+
+ /**
+ * URL-encode query variable keys. Default is true.
+ */
+ const OPTION_ENCODE_KEYS = 'encode_keys';
+
+ /**
+ * Query variable separators when parsing the query string. Every character
+ * is considered a separator. Default is "&".
+ */
+ const OPTION_SEPARATOR_INPUT = 'input_separator';
+
+ /**
+ * Query variable separator used when generating the query string. Default
+ * is "&".
+ */
+ const OPTION_SEPARATOR_OUTPUT = 'output_separator';
+
+ /**
+ * Default options corresponds to how PHP handles $_GET.
+ */
+ private $_options = array(
+ self::OPTION_STRICT => true,
+ self::OPTION_USE_BRACKETS => true,
+ self::OPTION_ENCODE_KEYS => true,
+ self::OPTION_SEPARATOR_INPUT => '&',
+ self::OPTION_SEPARATOR_OUTPUT => '&',
+ );
+
+ /**
+ * @var string|bool
+ */
+ private $_scheme = false;
+
+ /**
+ * @var string|bool
+ */
+ private $_userinfo = false;
+
+ /**
+ * @var string|bool
+ */
+ private $_host = false;
+
+ /**
+ * @var string|bool
+ */
+ private $_port = false;
+
+ /**
+ * @var string
+ */
+ private $_path = '';
+
+ /**
+ * @var string|bool
+ */
+ private $_query = false;
+
+ /**
+ * @var string|bool
+ */
+ private $_fragment = false;
+
+ /**
+ * Constructor.
+ *
+ * @param string $url an absolute or relative URL
+ * @param array $options an array of OPTION_xxx constants
+ *
+ * @return $this
+ * @uses self::parseUrl()
+ */
+ public function __construct($url, array $options = array())
+ {
+ foreach ($options as $optionName => $value) {
+ if (array_key_exists($optionName, $this->_options)) {
+ $this->_options[$optionName] = $value;
+ }
+ }
+
+ $this->parseUrl($url);
+ }
+
+ /**
+ * Magic Setter.
+ *
+ * This method will magically set the value of a private variable ($var)
+ * with the value passed as the args
+ *
+ * @param string $var The private variable to set.
+ * @param mixed $arg An argument of any type.
+ * @return void
+ */
+ public function __set($var, $arg)
+ {
+ $method = 'set' . $var;
+ if (method_exists($this, $method)) {
+ $this->$method($arg);
+ }
+ }
+
+ /**
+ * Magic Getter.
+ *
+ * This is the magic get method to retrieve the private variable
+ * that was set by either __set() or it's setter...
+ *
+ * @param string $var The property name to retrieve.
+ * @return mixed $this->$var Either a boolean false if the
+ * property is not set or the value
+ * of the private property.
+ */
+ public function __get($var)
+ {
+ $method = 'get' . $var;
+ if (method_exists($this, $method)) {
+ return $this->$method();
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the scheme, e.g. "http" or "urn", or false if there is no
+ * scheme specified, i.e. if this is a relative URL.
+ *
+ * @return string|bool
+ */
+ public function getScheme()
+ {
+ return $this->_scheme;
+ }
+
+ /**
+ * Sets the scheme, e.g. "http" or "urn". Specify false if there is no
+ * scheme specified, i.e. if this is a relative URL.
+ *
+ * @param string|bool $scheme e.g. "http" or "urn", or false if there is no
+ * scheme specified, i.e. if this is a relative
+ * URL
+ *
+ * @return $this
+ * @see getScheme()
+ */
+ public function setScheme($scheme)
+ {
+ $this->_scheme = $scheme;
+ return $this;
+ }
+
+ /**
+ * Returns the user part of the userinfo part (the part preceding the first
+ * ":"), or false if there is no userinfo part.
+ *
+ * @return string|bool
+ */
+ public function getUser()
+ {
+ return $this->_userinfo !== false
+ ? preg_replace('@:.*$@', '', $this->_userinfo)
+ : false;
+ }
+
+ /**
+ * Returns the password part of the userinfo part (the part after the first
+ * ":"), or false if there is no userinfo part (i.e. the URL does not
+ * contain "@" in front of the hostname) or the userinfo part does not
+ * contain ":".
+ *
+ * @return string|bool
+ */
+ public function getPassword()
+ {
+ return $this->_userinfo !== false
+ ? substr(strstr($this->_userinfo, ':'), 1)
+ : false;
+ }
+
+ /**
+ * Returns the userinfo part, or false if there is none, i.e. if the
+ * authority part does not contain "@".
+ *
+ * @return string|bool
+ */
+ public function getUserinfo()
+ {
+ return $this->_userinfo;
+ }
+
+ /**
+ * Sets the userinfo part. If two arguments are passed, they are combined
+ * in the userinfo part as username ":" password.
+ *
+ * @param string|bool $userinfo userinfo or username
+ * @param string|bool $password optional password, or false
+ *
+ * @return $this
+ */
+ public function setUserinfo($userinfo, $password = false)
+ {
+ $this->_userinfo = $userinfo;
+ if ($password !== false) {
+ $this->_userinfo .= ':' . $password;
+ }
+ return $this;
+ }
+
+ /**
+ * Returns the host part, or false if there is no authority part, e.g.
+ * relative URLs.
+ *
+ * @return string|bool a hostname, an IP address, or false
+ */
+ public function getHost()
+ {
+ return $this->_host;
+ }
+
+ /**
+ * Sets the host part. Specify false if there is no authority part, e.g.
+ * relative URLs.
+ *
+ * @param string|bool $host a hostname, an IP address, or false
+ *
+ * @return $this
+ */
+ public function setHost($host)
+ {
+ $this->_host = $host;
+ return $this;
+ }
+
+ /**
+ * Returns the port number, or false if there is no port number specified,
+ * i.e. if the default port is to be used.
+ *
+ * @return string|bool
+ */
+ public function getPort()
+ {
+ return $this->_port;
+ }
+
+ /**
+ * Sets the port number. Specify false if there is no port number specified,
+ * i.e. if the default port is to be used.
+ *
+ * @param string|bool $port a port number, or false
+ *
+ * @return $this
+ */
+ public function setPort($port)
+ {
+ $this->_port = $port;
+ return $this;
+ }
+
+ /**
+ * Returns the authority part, i.e. [ userinfo "@" ] host [ ":" port ], or
+ * false if there is no authority.
+ *
+ * @return string|bool
+ */
+ public function getAuthority()
+ {
+ if (!$this->_host) {
+ return false;
+ }
+
+ $authority = '';
+
+ if ($this->_userinfo !== false) {
+ $authority .= $this->_userinfo . '@';
+ }
+
+ $authority .= $this->_host;
+
+ if ($this->_port !== false) {
+ $authority .= ':' . $this->_port;
+ }
+
+ return $authority;
+ }
+
+ /**
+ * Sets the authority part, i.e. [ userinfo "@" ] host [ ":" port ]. Specify
+ * false if there is no authority.
+ *
+ * @param string|false $authority a hostname or an IP addresse, possibly
+ * with userinfo prefixed and port number
+ * appended, e.g. "foo:bar@example.org:81".
+ *
+ * @return $this
+ */
+ public function setAuthority($authority)
+ {
+ $this->_userinfo = false;
+ $this->_host = false;
+ $this->_port = false;
+ if (preg_match('@^(([^\@]*)\@)?([^:]+)(:(\d*))?$@', $authority, $reg)) {
+ if ($reg[1]) {
+ $this->_userinfo = $reg[2];
+ }
+
+ $this->_host = $reg[3];
+ if (isset($reg[5])) {
+ $this->_port = $reg[5];
+ }
+ }
+ return $this;
+ }
+
+ /**
+ * Returns the path part (possibly an empty string).
+ *
+ * @return string
+ */
+ public function getPath()
+ {
+ return $this->_path;
+ }
+
+ /**
+ * Sets the path part (possibly an empty string).
+ *
+ * @param string $path a path
+ *
+ * @return $this
+ */
+ public function setPath($path)
+ {
+ $this->_path = $path;
+ return $this;
+ }
+
+ /**
+ * Returns the query string (excluding the leading "?"), or false if "?"
+ * is not present in the URL.
+ *
+ * @return string|bool
+ * @see self::getQueryVariables()
+ */
+ public function getQuery()
+ {
+ return $this->_query;
+ }
+
+ /**
+ * Sets the query string (excluding the leading "?"). Specify false if "?"
+ * is not present in the URL.
+ *
+ * @param string|bool $query a query string, e.g. "foo=1&bar=2"
+ *
+ * @return $this
+ * @see self::setQueryVariables()
+ */
+ public function setQuery($query)
+ {
+ $this->_query = $query;
+ return $this;
+ }
+
+ /**
+ * Returns the fragment name, or false if "#" is not present in the URL.
+ *
+ * @return string|bool
+ */
+ public function getFragment()
+ {
+ return $this->_fragment;
+ }
+
+ /**
+ * Sets the fragment name. Specify false if "#" is not present in the URL.
+ *
+ * @param string|bool $fragment a fragment excluding the leading "#", or
+ * false
+ *
+ * @return $this
+ */
+ public function setFragment($fragment)
+ {
+ $this->_fragment = $fragment;
+ return $this;
+ }
+
+ /**
+ * Returns the query string like an array as the variables would appear in
+ * $_GET in a PHP script. If the URL does not contain a "?", an empty array
+ * is returned.
+ *
+ * @return array
+ */
+ public function getQueryVariables()
+ {
+ $pattern = '/[' .
+ preg_quote($this->getOption(self::OPTION_SEPARATOR_INPUT), '/') .
+ ']/';
+ $parts = preg_split($pattern, $this->_query, -1, PREG_SPLIT_NO_EMPTY);
+ $return = array();
+
+ foreach ($parts as $part) {
+ if (strpos($part, '=') !== false) {
+ list($key, $value) = explode('=', $part, 2);
+ } else {
+ $key = $part;
+ $value = null;
+ }
+
+ if ($this->getOption(self::OPTION_ENCODE_KEYS)) {
+ $key = rawurldecode($key);
+ }
+ $value = rawurldecode($value);
+
+ if ($this->getOption(self::OPTION_USE_BRACKETS) &&
+ preg_match('#^(.*)\[([0-9a-z_-]*)\]#i', $key, $matches)) {
+
+ $key = $matches[1];
+ $idx = $matches[2];
+
+ // Ensure is an array
+ if (empty($return[$key]) || !is_array($return[$key])) {
+ $return[$key] = array();
+ }
+
+ // Add data
+ if ($idx === '') {
+ $return[$key][] = $value;
+ } else {
+ $return[$key][$idx] = $value;
+ }
+ } elseif (!$this->getOption(self::OPTION_USE_BRACKETS)
+ && !empty($return[$key])
+ ) {
+ $return[$key] = (array) $return[$key];
+ $return[$key][] = $value;
+ } else {
+ $return[$key] = $value;
+ }
+ }
+
+ return $return;
+ }
+
+ /**
+ * Sets the query string to the specified variable in the query string.
+ *
+ * @param array $array (name => value) array
+ *
+ * @return $this
+ */
+ public function setQueryVariables(array $array)
+ {
+ if (!$array) {
+ $this->_query = false;
+ } else {
+ $this->_query = $this->buildQuery(
+ $array,
+ $this->getOption(self::OPTION_SEPARATOR_OUTPUT)
+ );
+ }
+ return $this;
+ }
+
+ /**
+ * Sets the specified variable in the query string.
+ *
+ * @param string $name variable name
+ * @param mixed $value variable value
+ *
+ * @return $this
+ */
+ public function setQueryVariable($name, $value)
+ {
+ $array = $this->getQueryVariables();
+ $array[$name] = $value;
+ $this->setQueryVariables($array);
+ return $this;
+ }
+
+ /**
+ * Removes the specifed variable from the query string.
+ *
+ * @param string $name a query string variable, e.g. "foo" in "?foo=1"
+ *
+ * @return void
+ */
+ public function unsetQueryVariable($name)
+ {
+ $array = $this->getQueryVariables();
+ unset($array[$name]);
+ $this->setQueryVariables($array);
+ }
+
+ /**
+ * Returns a string representation of this URL.
+ *
+ * @return string
+ */
+ public function getURL()
+ {
+ // See RFC 3986, section 5.3
+ $url = "";
+
+ if ($this->_scheme !== false) {
+ $url .= $this->_scheme . ':';
+ }
+
+ $authority = $this->getAuthority();
+ if ($authority !== false) {
+ $url .= '//' . $authority;
+ }
+ $url .= $this->_path;
+
+ if ($this->_query !== false) {
+ $url .= '?' . $this->_query;
+ }
+
+ if ($this->_fragment !== false) {
+ $url .= '#' . $this->_fragment;
+ }
+
+ return $url;
+ }
+
+ /**
+ * Returns a string representation of this URL.
+ *
+ * @return string
+ * @see toString()
+ */
+ public function __toString()
+ {
+ return $this->getURL();
+ }
+
+ /**
+ * Returns a normalized string representation of this URL. This is useful
+ * for comparison of URLs.
+ *
+ * @return string
+ */
+ public function getNormalizedURL()
+ {
+ $url = clone $this;
+ $url->normalize();
+ return $url->getUrl();
+ }
+
+ /**
+ * Returns a normalized Net_URL2 instance.
+ *
+ * @return Net_URL2
+ */
+ public function normalize()
+ {
+ // See RFC 3886, section 6
+
+ // Schemes are case-insensitive
+ if ($this->_scheme) {
+ $this->_scheme = strtolower($this->_scheme);
+ }
+
+ // Hostnames are case-insensitive
+ if ($this->_host) {
+ $this->_host = strtolower($this->_host);
+ }
+
+ // Remove default port number for known schemes (RFC 3986, section 6.2.3)
+ if ($this->_port &&
+ $this->_scheme &&
+ $this->_port == getservbyname($this->_scheme, 'tcp')) {
+
+ $this->_port = false;
+ }
+
+ // Normalize case of %XX percentage-encodings (RFC 3986, section 6.2.2.1)
+ foreach (array('_userinfo', '_host', '_path') as $part) {
+ if ($this->$part) {
+ $this->$part = preg_replace('/%[0-9a-f]{2}/ie',
+ 'strtoupper("\0")',
+ $this->$part);
+ }
+ }
+
+ // Path segment normalization (RFC 3986, section 6.2.2.3)
+ $this->_path = self::removeDotSegments($this->_path);
+
+ // Scheme based normalization (RFC 3986, section 6.2.3)
+ if ($this->_host && !$this->_path) {
+ $this->_path = '/';
+ }
+ }
+
+ /**
+ * Returns whether this instance represents an absolute URL.
+ *
+ * @return bool
+ */
+ public function isAbsolute()
+ {
+ return (bool) $this->_scheme;
+ }
+
+ /**
+ * Returns an Net_URL2 instance representing an absolute URL relative to
+ * this URL.
+ *
+ * @param Net_URL2|string $reference relative URL
+ *
+ * @return Net_URL2
+ */
+ public function resolve($reference)
+ {
+ if (!$reference instanceof Net_URL2) {
+ $reference = new self($reference);
+ }
+ if (!$this->isAbsolute()) {
+ throw new Exception('Base-URL must be absolute');
+ }
+
+ // A non-strict parser may ignore a scheme in the reference if it is
+ // identical to the base URI's scheme.
+ if (!$this->getOption(self::OPTION_STRICT) && $reference->_scheme == $this->_scheme) {
+ $reference->_scheme = false;
+ }
+
+ $target = new self('');
+ if ($reference->_scheme !== false) {
+ $target->_scheme = $reference->_scheme;
+ $target->setAuthority($reference->getAuthority());
+ $target->_path = self::removeDotSegments($reference->_path);
+ $target->_query = $reference->_query;
+ } else {
+ $authority = $reference->getAuthority();
+ if ($authority !== false) {
+ $target->setAuthority($authority);
+ $target->_path = self::removeDotSegments($reference->_path);
+ $target->_query = $reference->_query;
+ } else {
+ if ($reference->_path == '') {
+ $target->_path = $this->_path;
+ if ($reference->_query !== false) {
+ $target->_query = $reference->_query;
+ } else {
+ $target->_query = $this->_query;
+ }
+ } else {
+ if (substr($reference->_path, 0, 1) == '/') {
+ $target->_path = self::removeDotSegments($reference->_path);
+ } else {
+ // Merge paths (RFC 3986, section 5.2.3)
+ if ($this->_host !== false && $this->_path == '') {
+ $target->_path = '/' . $this->_path;
+ } else {
+ $i = strrpos($this->_path, '/');
+ if ($i !== false) {
+ $target->_path = substr($this->_path, 0, $i + 1);
+ }
+ $target->_path .= $reference->_path;
+ }
+ $target->_path = self::removeDotSegments($target->_path);
+ }
+ $target->_query = $reference->_query;
+ }
+ $target->setAuthority($this->getAuthority());
+ }
+ $target->_scheme = $this->_scheme;
+ }
+
+ $target->_fragment = $reference->_fragment;
+
+ return $target;
+ }
+
+ /**
+ * Removes dots as described in RFC 3986, section 5.2.4, e.g.
+ * "/foo/../bar/baz" => "/bar/baz"
+ *
+ * @param string $path a path
+ *
+ * @return string a path
+ */
+ public static function removeDotSegments($path)
+ {
+ $output = '';
+
+ // Make sure not to be trapped in an infinite loop due to a bug in this
+ // method
+ $j = 0;
+ while ($path && $j++ < 100) {
+ if (substr($path, 0, 2) == './') {
+ // Step 2.A
+ $path = substr($path, 2);
+ } elseif (substr($path, 0, 3) == '../') {
+ // Step 2.A
+ $path = substr($path, 3);
+ } elseif (substr($path, 0, 3) == '/./' || $path == '/.') {
+ // Step 2.B
+ $path = '/' . substr($path, 3);
+ } elseif (substr($path, 0, 4) == '/../' || $path == '/..') {
+ // Step 2.C
+ $path = '/' . substr($path, 4);
+ $i = strrpos($output, '/');
+ $output = $i === false ? '' : substr($output, 0, $i);
+ } elseif ($path == '.' || $path == '..') {
+ // Step 2.D
+ $path = '';
+ } else {
+ // Step 2.E
+ $i = strpos($path, '/');
+ if ($i === 0) {
+ $i = strpos($path, '/', 1);
+ }
+ if ($i === false) {
+ $i = strlen($path);
+ }
+ $output .= substr($path, 0, $i);
+ $path = substr($path, $i);
+ }
+ }
+
+ return $output;
+ }
+
+ /**
+ * Percent-encodes all non-alphanumeric characters except these: _ . - ~
+ * Similar to PHP's rawurlencode(), except that it also encodes ~ in PHP
+ * 5.2.x and earlier.
+ *
+ * @param $raw the string to encode
+ * @return string
+ */
+ public static function urlencode($string)
+ {
+ $encoded = rawurlencode($string);
+
+ // This is only necessary in PHP < 5.3.
+ $encoded = str_replace('%7E', '~', $encoded);
+ return $encoded;
+ }
+
+ /**
+ * Returns a Net_URL2 instance representing the canonical URL of the
+ * currently executing PHP script.
+ *
+ * @return string
+ */
+ public static function getCanonical()
+ {
+ if (!isset($_SERVER['REQUEST_METHOD'])) {
+ // ALERT - no current URL
+ throw new Exception('Script was not called through a webserver');
+ }
+
+ // Begin with a relative URL
+ $url = new self($_SERVER['PHP_SELF']);
+ $url->_scheme = isset($_SERVER['HTTPS']) ? 'https' : 'http';
+ $url->_host = $_SERVER['SERVER_NAME'];
+ $port = $_SERVER['SERVER_PORT'];
+ if ($url->_scheme == 'http' && $port != 80 ||
+ $url->_scheme == 'https' && $port != 443) {
+
+ $url->_port = $port;
+ }
+ return $url;
+ }
+
+ /**
+ * Returns the URL used to retrieve the current request.
+ *
+ * @return string
+ */
+ public static function getRequestedURL()
+ {
+ return self::getRequested()->getUrl();
+ }
+
+ /**
+ * Returns a Net_URL2 instance representing the URL used to retrieve the
+ * current request.
+ *
+ * @return Net_URL2
+ */
+ public static function getRequested()
+ {
+ if (!isset($_SERVER['REQUEST_METHOD'])) {
+ // ALERT - no current URL
+ throw new Exception('Script was not called through a webserver');
+ }
+
+ // Begin with a relative URL
+ $url = new self($_SERVER['REQUEST_URI']);
+ $url->_scheme = isset($_SERVER['HTTPS']) ? 'https' : 'http';
+ // Set host and possibly port
+ $url->setAuthority($_SERVER['HTTP_HOST']);
+ return $url;
+ }
+
+ /**
+ * Returns the value of the specified option.
+ *
+ * @param string $optionName The name of the option to retrieve
+ *
+ * @return mixed
+ */
+ public function getOption($optionName)
+ {
+ return isset($this->_options[$optionName])
+ ? $this->_options[$optionName] : false;
+ }
+
+ /**
+ * A simple version of http_build_query in userland. The encoded string is
+ * percentage encoded according to RFC 3986.
+ *
+ * @param array $data An array, which has to be converted into
+ * QUERY_STRING. Anything is possible.
+ * @param string $seperator See {@link self::OPTION_SEPARATOR_OUTPUT}
+ * @param string $key For stacked values (arrays in an array).
+ *
+ * @return string
+ */
+ protected function buildQuery(array $data, $separator, $key = null)
+ {
+ $query = array();
+ foreach ($data as $name => $value) {
+ if ($this->getOption(self::OPTION_ENCODE_KEYS) === true) {
+ $name = rawurlencode($name);
+ }
+ if ($key !== null) {
+ if ($this->getOption(self::OPTION_USE_BRACKETS) === true) {
+ $name = $key . '[' . $name . ']';
+ } else {
+ $name = $key;
+ }
+ }
+ if (is_array($value)) {
+ $query[] = $this->buildQuery($value, $separator, $name);
+ } else {
+ $query[] = $name . '=' . rawurlencode($value);
+ }
+ }
+ return implode($separator, $query);
+ }
+
+ /**
+ * This method uses a funky regex to parse the url into the designated parts.
+ *
+ * @param string $url
+ *
+ * @return void
+ * @uses self::$_scheme, self::setAuthority(), self::$_path, self::$_query,
+ * self::$_fragment
+ * @see self::__construct()
+ */
+ protected function parseUrl($url)
+ {
+ // The regular expression is copied verbatim from RFC 3986, appendix B.
+ // The expression does not validate the URL but matches any string.
+ preg_match('!^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?!',
+ $url,
+ $matches);
+
+ // "path" is always present (possibly as an empty string); the rest
+ // are optional.
+ $this->_scheme = !empty($matches[1]) ? $matches[2] : false;
+ $this->setAuthority(!empty($matches[3]) ? $matches[4] : false);
+ $this->_path = $matches[5];
+ $this->_query = !empty($matches[6]) ? $matches[7] : false;
+ $this->_fragment = !empty($matches[8]) ? $matches[9] : false;
+ }
+}
diff --git a/lib/functions.php b/lib/functions.php
index 8dd2be9..e09669c 100644
--- a/lib/functions.php
+++ b/lib/functions.php
@@ -1,5 +1,6 @@
<?php
ini_set('include_path', dirname(__FILE__) . PATH_SEPARATOR . ini_get('include_path'));
+ ini_set('include_path', dirname(__FILE__) . "/ext/" . PATH_SEPARATOR . ini_get('include_path'));
// These are just here for some statistics.
list($usec, $sec) = explode(' ',microtime());
@@ -13,12 +14,20 @@
// register autoloader
function class_autoloader($classname) {
- $classname = preg_replace('/(Net|MDB2)_(.+)/', "\\1/\\2", $classname);
+ $classname = preg_replace('/(Net|MDB2|HTTP)_(.+)/', "\\1/\\2", $classname);
if ($fp = @fopen("$classname.php", 'r', true)) {
include_once("$classname.php");
fclose($fp);
return true;
+ } elseif ($fp = @fopen("api/$classname.php", 'r', true)) {
+ include_once("api/$classname.php");
+ fclose($fp);
+ return true;
+ } elseif ($fp = @fopen("client/$classname.php", 'r', true)) {
+ include_once("client/$classname.php");
+ fclose($fp);
+ return true;
}
return false;
@@ -51,4 +60,43 @@
return $_SESSION['user']->authenticated();
}
+ /**
+ * Prints debug info into the 'console' log
+ */
+ function console() {
+ $args = func_get_args();
+
+ $msg = array();
+ foreach ($args as $arg) {
+ $msg[] = !is_string($arg) ? var_export($arg, true) : $arg;
+ }
+
+ write_log('console', join(";\n", $msg));
+ }
+
+ /**
+ * Appends a line to a log file in the logs directory.
+ * Date will be added automatically to the line.
+ *
+ * @param string $name Name of the log file
+ * @param mixed $line Line to append
+ */
+ function write_log($name, $line) {
+ if (!is_string($line)) {
+ $line = var_export($line, true);
+ }
+
+ $log_dir = dirname(__FILE__) . '/../logs';
+ $logfile = $log_dir . '/' . $name;
+ $date = date('d-M-Y H:i:s O');
+ $line = sprintf("[%s](%s): %s\n", $date, session_id(), $line);
+
+ if ($fp = @fopen($logfile, 'a')) {
+ fwrite($fp, $line);
+ fflush($fp);
+ fclose($fp);
+ }
+ }
+
+
?>
diff --git a/lib/kolab_admin_api.php b/lib/kolab_admin_api.php
new file mode 100644
index 0000000..a58840d
--- /dev/null
+++ b/lib/kolab_admin_api.php
@@ -0,0 +1,157 @@
+<?php
+
+require_once("HTTP/Request2.php");
+
+class kolab_admin_api
+{
+ /**
+ * @var HTTP_Request2
+ */
+ private $request;
+
+ /**
+ * @var string
+ */
+ private $base_url;
+
+
+ const STATUS_OK = 0;
+ const STATUS_ERROR = 1;
+
+ const ERROR_INTERNAL = 500;
+
+ /**
+ * Class constructor.
+ *
+ * @param string $base_url Base URL of Kolab API
+ */
+ public function __construct($base_url)
+ {
+ $this->base_url = $base_url;
+ $this->request = new HTTP_Request2();
+ }
+
+ /**
+ *
+ * @return array Session user data (token, domain)
+ */
+ public function login($username, $password)
+ {
+ $query = array(
+ 'username' => $username,
+ 'password' => $password
+ );
+
+ $response = $this->post('system.authenticate', null, $query);
+
+ if ($token = $response->get('session_token')) {
+ return array(
+ 'token' => $token,
+ 'domain' => $response->get('domain'),
+ );
+ }
+ }
+
+ public function logout()
+ {
+ $response = $this->get('system.quit');
+
+ return $response->get_error_code() ? false : true;
+ }
+
+ public function set_session_token($token)
+ {
+ console("Setting X-Session-Token header to: " . $token);
+ $this->request->setHeader('X-Session-Token', $token);
+ }
+
+ public function get_capabilities()
+ {
+ $this->get('system.capabilities');
+ }
+
+ public function get($action, $args = array())
+ {
+ $url = $this->build_url($action, $args);
+
+ $this->request->setMethod(HTTP_Request2::METHOD_GET);
+
+ return $this->get_response($url);
+ }
+
+ public function post($action, $url_args = array(), $post = array())
+ {
+ $url = $this->build_url($action, $url_args);
+
+ $this->request->setMethod(HTTP_Request2::METHOD_POST);
+ $this->request->setBody(@json_encode($post));
+
+ return $this->get_response($url);
+ }
+
+ /**
+ * @param string $action Action GET parameter
+ * @param array $args GET parameters (hash array: name => value)
+ *
+ * @return Net_URL2 URL object
+ */
+ private function build_url($action, $args)
+ {
+ $url = $this->base_url;
+
+ if ($action) {
+ $url .= '/' . urlencode($action);
+ }
+
+ $url = new Net_URL2($url);
+
+ if (!empty($args)) {
+ $url->setQueryVariables($args);
+ }
+
+ return $url;
+ }
+
+ /**
+ * HTTP Response handler.
+ *
+ * @param Net_URL2 $url URL object
+ *
+ * @return kolab_admin_api_result Response object
+ */
+ private function get_response($url)
+ {
+ try {
+ $this->request->setUrl($url);
+ $response = $this->request->send();
+ }
+ catch (Exception $e) {
+ return new kolab_admin_api_result(null,
+ self::ERROR_INTERNAL, $e->getMessage());
+ }
+
+ try {
+ $body = $response->getBody();
+ }
+ catch (Exception $e) {
+ return new kolab_admin_api_result(null,
+ self::ERROR_INTERNAL, $e->getMessage());
+ }
+
+//print_r($body);
+ $body = @json_decode($body, true);
+ $err_code = null;
+ $err_str = null;
+
+ if (is_array($body) && (empty($body['status']) || $body['status'] != 'OK')) {
+ $err_code = !empty($data['code']) ? $data['code'] : self::ERROR_INTERNAL;
+ $err_str = !empty($data['reason']) ? $data['reason'] : 'Unknown error';
+ }
+ else if (!is_array($body)) {
+ $err_code = self::ERROR_INTERNAL;
+ $err_str = 'Unable to decode response';
+ }
+
+ return new kolab_admin_api_result($body, $err_code, $err_str);
+ }
+}
diff --git a/lib/kolab_admin_controller.php b/lib/kolab_admin_api_controller.php
index fdf6fc2..f3f957f 100644
--- a/lib/kolab_admin_controller.php
+++ b/lib/kolab_admin_api_controller.php
@@ -3,7 +3,7 @@
/**
* Main controller class to serve the Kolab Admin API
*/
- class kolab_admin_controller
+ class kolab_admin_api_controller
{
public $output;
@@ -12,7 +12,6 @@
private $services = Array();
private $domains = Array('localhost.localdomain');
-
public function __construct()
{
$this->output = new kolab_admin_json_output();
@@ -42,7 +41,6 @@
$this->add_service('domains', 'kolab_admin_domains_actions');
}
-
/**
* Register a class that serves a particular backend service
*/
@@ -56,12 +54,13 @@
$this->services[$service] = $handler;
}
-
/**
* Getter for a certain service object
*/
public function get_service($service)
{
+ error_log($service);
+
// we are the system!
if ($service == 'system')
return $this;
@@ -70,10 +69,12 @@
if (is_string($handler))
$handler = $this->services[$service] = new $handler($this);
- if (is_a($handler, 'kolab_admin_service'))
+ if (is_a($handler, 'kolab_admin_api_service'))
return $handler;
}
+ error_log("Unknown service $service");
+
throw new Exception("Unknown service", 400);
}
@@ -95,6 +96,7 @@
$service = $this->request['service'];
$method = $this->request['method'];
+ console("Calling method " . $method . " on service . " . $service);
// validate user session
if ($method != 'authenticate') {
if (!$this->session_validate($postdata)) {
@@ -127,7 +129,7 @@
*/
private function session_validate($postdata)
{
- $sess_id = !empty($postdata['session_token']) ? $postdata['session_token'] : self::get_request_header('X-Session-Token');
+ $sess_id = !empty($postdata['session_token']) ? $postdata['session_token'] : kolab_utils::get_request_header('X-Session-Token');
if (empty($sess_id))
return false;
@@ -242,43 +244,6 @@
/* ======== Utility functions ======== */
- /**
- * Make sure the string ends with a slash
- */
- public static function slashify($str)
- {
- return self::unslashify($str).'/';
- }
-
- /**
- * Remove slash at the end of the string
- */
- public static function unslashify($str)
- {
- return preg_replace('/\/$/', '', $str);
- }
-
- /**
- * Read a specific HTTP request header
- *
- * @param string $name Header name
- * @return mixed Header value or null if not available
- */
- public static function get_request_header($name) {
- if (function_exists('getallheaders')) {
- $hdrs = array_change_key_case(getallheaders(), CASE_UPPER);
- $key = strtoupper($name);
- } else {
- $key = 'HTTP_' . strtoupper(strtr($name, '-', '_'));
- $hdrs = array_change_key_case($_SERVER, CASE_UPPER);
- }
-
- if (array_key_exists($key, $hdrs)) {
- return $hdrs[$key];
- } else {
- return null;
- }
- }
}
?>
diff --git a/lib/kolab_admin_api_result.php b/lib/kolab_admin_api_result.php
new file mode 100644
index 0000000..772030f
--- /dev/null
+++ b/lib/kolab_admin_api_result.php
@@ -0,0 +1,42 @@
+<?php
+
+class kolab_admin_api_result
+{
+ /**
+ * @var array
+ */
+ private $data = array();
+
+ private $error_code;
+ private $error_str;
+
+
+ public function __construct($data = array(), $error_code = null, $error_str = null)
+ {
+ if (is_array($data) && isset($data['result'])) {
+ $this->data = $data['result'];
+ }
+
+ $this->error_code = $error_code;
+ $this->error_str = $error_str;
+ }
+
+ public function get_error_code()
+ {
+ $this->error_code;
+ }
+
+ public function get_error_str()
+ {
+ $this->error_str;
+ }
+
+ public function get($name = null)
+ {
+ if ($name !== null) {
+ return isset($this->data[$name]) ? $this->data[$name] : null;
+ }
+
+ return $this->data;
+ }
+}
diff --git a/lib/kolab_admin_service.php b/lib/kolab_admin_api_service.php
index 2d0cedc..7b871cb 100644
--- a/lib/kolab_admin_service.php
+++ b/lib/kolab_admin_api_service.php
@@ -3,7 +3,7 @@
/**
* Interface class for Kolab Admin Services
*/
- abstract class kolab_admin_service
+ abstract class kolab_admin_api_service
{
protected $controller;
@@ -19,4 +19,4 @@
}
-?> \ No newline at end of file
+?>
diff --git a/lib/kolab_admin_client_output.php b/lib/kolab_admin_client_output.php
new file mode 100644
index 0000000..7d3f233
--- /dev/null
+++ b/lib/kolab_admin_client_output.php
@@ -0,0 +1,162 @@
+<?php
+
+class kolab_admin_client_output
+{
+ private $tpl_vars = array();
+ private $env = array();
+ private $objects = array();
+ private $commands = array();
+ private $labels = array();
+ private $skin;
+
+ public function __construct($skin = null)
+ {
+ $this->skin = $skin ? $skin : 'default';
+ $this->init();
+ }
+
+ public function init()
+ {
+ require_once 'Smarty/Smarty.class.php';
+
+ $SMARTY = new Smarty;
+ $SMARTY->template_dir = 'skins/' . $this->skin . '/templates';
+ $SMARTY->compile_dir = 'cache';
+ $SMARTY->plugins_dir = dirname(__FILE__) . '/Smarty/plugins/';
+ $SMARTY->debugging = false;
+
+ $this->tpl = $SMARTY;
+ }
+
+ public function send($template = null)
+ {
+ if ($this->is_ajax()) {
+ echo $this->send_json();
+ }
+ else {
+ $this->send_tpl($template);
+ }
+ }
+
+ private function send_json()
+ {
+ header('Content-Type: application/json');
+
+ $response = array(
+ 'objects' => $this->objects,
+ 'env' => array(),
+ );
+
+ foreach ($this->env as $name => $value) {
+ $response['env'][$name] = $value;
+ }
+
+ foreach ($this->commands as $command) {
+ $cname = array_shift($command);
+ $args = array();
+
+ foreach ($command as $arg) {
+ $args[] = json_encode($arg);
+ }
+
+ $commands[] = sprintf('kadm.%s(%s);', $cname, implode(',', $args));
+ }
+
+ if (!empty($commands)) {
+ $response['exec'] = implode("\n", $commands);
+ }
+
+ $this->labels = array_unique($this->labels);
+ foreach ($this->labels as $label) {
+ $response['labels'][$label] = kolab_admin_client_task::translate($label);
+ }
+
+ return json_encode($response);
+ }
+
+ private function send_tpl($template)
+ {
+ if (!$template) {
+ return;
+ }
+
+ foreach ($this->tpl_vars as $name => $value) {
+ $this->tpl->assign($name, $value);
+ }
+
+ $script = '';
+
+ if (!empty($this->env)) {
+ $script[] = 'kadm.env = ' . json_encode($this->env) . ';';
+ }
+
+ $this->labels = array_unique($this->labels);
+ if (!empty($this->labels)) {
+ foreach ($this->labels as $label) {
+ $labels[$label] = kolab_admin_client_task::translate($label);
+ }
+ $script[] = 'kadm.tdef(' . json_encode($labels) . ');';
+ }
+
+ foreach ($this->commands as $command) {
+ $cname = array_shift($command);
+ $args = array();
+
+ foreach ($command as $arg) {
+ $args[] = json_encode($arg);
+ }
+
+ $script[] = sprintf('kadm.%s(%s);', $cname, implode(',', $args));
+ }
+
+ $this->tpl->assign('skin_path', 'skins/' . $this->skin . '/');
+ if ($script) {
+ $script = "<script type=\"text/javascript\">\n" . implode("\n", $script) . "\n</script>";
+ $this->tpl->assign('script', $script);
+ }
+
+ $this->tpl->display($template . '.html');
+ }
+
+ public function is_ajax()
+ {
+ return !empty($_REQUEST['remote']);
+ }
+
+ public function assign($name, $value)
+ {
+ $this->tpl_vars[$name] = $value;
+ }
+
+ public function set_env($name, $value)
+ {
+ $this->env[$name] = $value;
+ }
+
+ public function set_object($name, $content, $is_template = false)
+ {
+ if ($is_template) {
+ ob_start();
+ $this->send_tpl($content);
+ $content = ob_get_contents();
+ ob_end_clean();
+ }
+
+ $this->objects[$name] = $content;
+ }
+
+ public function command()
+ {
+ $this->commands[] = func_get_args();
+ }
+
+ public function add_translation()
+ {
+ $this->labels = array_merge($this->labels, func_get_args());
+ }
+
+ public static function escape($str)
+ {
+
+ }
+}
diff --git a/lib/kolab_admin_client_task.php b/lib/kolab_admin_client_task.php
new file mode 100644
index 0000000..4e5e70d
--- /dev/null
+++ b/lib/kolab_admin_client_task.php
@@ -0,0 +1,283 @@
+<?php
+
+
+class kolab_admin_client_task
+{
+ /**
+ * @var kolab_admin_output
+ */
+ protected $output;
+
+ /**
+ * @var kolab_admin_api
+ */
+ protected $api;
+
+ protected $ajax_only = false;
+ protected $page_title = 'Kolab Admin Panel';
+ protected $menu = array();
+
+ static $translation = array();
+
+
+ public function __construct()
+ {
+ $this->conf = Conf::get_instance();
+ $this->output_init();
+ $this->api_init();
+
+ session_start();
+
+ $this->auth();
+ }
+
+ private function locale_init()
+ {
+ $aliases = array(
+ 'de' => 'de_DE',
+ 'en' => 'en_US',
+ 'pl' => 'pl_PL',
+ );
+
+ // UI language
+ $langs = !empty($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] : '';
+ $langs = explode(',', $langs);
+
+ if (!empty($_SESSION['user']) && !empty($_SESSION['user']['language'])) {
+ array_unshift($langs, $_SESSION['user']['language']);
+ }
+
+ while ($lang = array_shift($langs)) {
+ $lang = explode(';', $lang);
+ $lang = $lang[0];
+ $lang = str_replace('-', '_', $lang);
+
+ if (file_exists(dirname(__FILE__) . "/locale/$lang.php")) {
+ $language = $lang;
+ break;
+ }
+ if (isset($aliases[$lang]) && ($alias = $aliases[$lang])
+ && file_exists(dirname(__FILE__) . "/locale/$alias.php")
+ ) {
+ $language = $alias;
+ break;
+ }
+ }
+
+ $LANG = array();
+ @include dirname(__FILE__) . '/locale/en_US.php';
+
+ if (isset($language)) {
+ @include dirname(__FILE__) . "/locale/$language.php";
+ setlocale(LC_ALL, $language . '.utf8', 'en_US.utf8');
+ }
+ else {
+ setlocale(LC_ALL, 'en_US.utf8');
+ }
+
+ self::$translation = $LANG;
+ }
+
+ private function output_init()
+ {
+ $skin = $this->conf->get('kolab_wap', 'skin');
+
+ $this->output = new kolab_admin_client_output($skin);
+ }
+
+ private function api_init()
+ {
+ $url = $this->conf->get('kolab_wap', 'api_url');
+ $this->api = new kolab_admin_api($url);
+ }
+
+ private function auth()
+ {
+ if (isset($_POST['login'])) {
+ $login = $this->get_input('login', 'POST');
+
+ if ($login['username']) {
+ $result = $this->api->login($login['username'], $login['password']);
+
+ if ($result) {
+ $this->api->set_session_token($result['token']);
+ // find user settings
+ $res = $this->api->get('user.info', array('user' => $login['username']));
+ $res = $res->get();
+
+ if (is_array($res) && ($res = array_shift($res))) {
+ $result['language'] = $res['preferredlanguage'];
+ $result['fullname'] = $res['cn'];
+ }
+
+ $_SESSION['user'] = $result;
+ header('Location: ?');
+ die;
+ }
+ else {
+ $this->output->command('display_message', 'loginerror', 'error');
+ }
+ }
+ }
+ else if (!empty($_SESSION['user']) && !empty($_SESSION['user']['token'])) {
+ $this->api->set_session_token($_SESSION['user']['token']);
+ return;
+ }
+
+ }
+
+ private function action_logout()
+ {
+ if (!empty($_SESSION['user']) && !empty($_SESSION['user']['token'])) {
+ $this->api->logout();
+ }
+ unset($_SESSION['user']);
+
+ if ($this->output->is_ajax()) {
+ $this->output->command('main_logout');
+ }
+ else {
+ $this->output->assign('login', $this->get_input('login', 'POST'));
+ $this->output->add_translation('loginerror');
+ $this->output->send('login');
+ }
+ exit;
+ }
+
+ public function run()
+ {
+ // Initialize locales
+ $this->locale_init();
+
+ // Run security checks
+ $this->input_checks();
+
+ if (empty($_SESSION['user']) || empty($_SESSION['user']['token'])) {
+ $this->action_logout();
+ }
+
+ $action = $this->get_input('action', 'GET');
+
+ if ($action) {
+ $method = 'action_' . $action;
+ if (method_exists($this, $method)) {
+ $this->$method();
+ }
+ }
+ else if (method_exists($this, 'action_default')) {
+ $this->action_default();
+ }
+ }
+
+ public function input_checks()
+ {
+ $ajax = $this->output->is_ajax();
+ // Check AJAX-only tasks
+ if ($this->ajax_only && !$ajax) {
+ $this->raise_error(500, 'Invalid request type!');
+ }
+
+ // CSRF prevention
+ $token = $ajax ? kolab_utils::get_request_header('X-KAP-Request') : $this->get_input('token');
+ $task = $this->get_task();
+
+ console("Ajax:", $ajax, "Token:", $token, "X-KAP-Request:", kolab_utils::get_request_header('X-KAP-Request'));
+ console("User session token:", $_SESSION['user']['token'], "Task:", $task);
+
+ if ($task != 'main' && $token != $_SESSION['user']['token']) {
+ $this->raise_error(403, 'Invalid request data!');
+ }
+ }
+
+ public function raise_error($code, $msg)
+ {
+ // @TODO: log
+
+ if ($this->output->is_ajax()) {
+ header("HTTP/1.0 $code $msg");
+ die;
+ }
+
+ $this->output->assign('error_code', $code);
+ $this->output->assign('error_message', $msg);
+ $this->output->send('error');
+ exit;
+ }
+
+ public function send()
+ {
+ $template = $this->get_task();
+
+ if ($this->page_title) {
+ $this->output->assign('pagetitle', $this->page_title);
+ }
+
+ $this->output->send($template);
+ exit;
+ }
+
+ public function get_task()
+ {
+ $class_name = get_class($this);
+
+ if (preg_match('/^kolab_admin_client_task_([a-z]+)$/', $class_name, $m)) {
+ return $m[1];
+ }
+ }
+
+ public static function translate()
+ {
+ $args = func_get_args();
+
+ if (is_array($args[0])) {
+ $args = $args[0];
+ }
+
+ $label = $args[0];
+
+ if (isset(self::$translation[$label])) {
+ $content = trim(self::$translation[$label]);
+ }
+ else {
+ $content = $label;
+ }
+
+ for ($i = 1, $len = count($args); $i < $len; $i++) {
+ $content = str_replace('$'.$i, $args[$i], $content);
+ }
+
+ return $content;
+ }
+
+ public static function get_input($name, $type = null, $allow_html = false)
+ {
+ return kolab_utils::get_input($name, $type = null, $allow_html = false);
+ }
+
+ public function menu()
+ {
+ if (empty($this->menu)) {
+ return '';
+ }
+
+ $task = $this->get_task();
+
+ foreach ($this->menu as $idx => $label) {
+ if (strpos($idx, '.')) {
+ $action = $idx;
+ $class = preg_replace('/\.[a-z_-]+$/', '', $idx);
+ }
+ else {
+ $action = $task . '.' . $idx;
+ $class = $idx;
+ }
+
+ $menu[$idx] = sprintf('<li class="%s"><a href="#%s" '
+ .'onclick="return kadm.command(\'%s\', \'\', this)">%s</a></li>',
+ $class, $idx, $action, $this->translate($label));
+ }
+
+ return '<ul>' . implode("\n", $menu) . '</ul>';
+ }
+
+}
diff --git a/lib/kolab_html.php b/lib/kolab_html.php
new file mode 100644
index 0000000..333a3f8
--- /dev/null
+++ b/lib/kolab_html.php
@@ -0,0 +1,330 @@
+<?php
+
+
+class kolab_html
+{
+ const INPUT_NONE = 0;
+ const INPUT_TEXT = 1;
+ const INPUT_PASSWORD = 2;
+ const INPUT_TEXTAREA = 3;
+ const INPUT_CHECKBOX = 4;
+ const INPUT_RADIO = 5;
+ const INPUT_BUTTON = 6;
+ const INPUT_SUBMIT = 7;
+
+ public static $common_attribs = array('id', 'class', 'style', 'title', 'align', 'dir');
+ public static $event_attribs = array('onclick', 'ondblclick', 'onmousedown', 'onmouseup',
+ 'onmouseover', 'onmousemove', 'onmouseout');
+ public static $input_event_attribs = array('onfocus', 'onblur', 'onkeypress', 'onkeydown', 'onkeyup',
+ 'onsubmit', 'onreset', 'onselect', 'onchange');
+ public static $table_attribs = array('summary');
+ public static $tr_attribs = array();
+ public static $td_attribs = array('colspan', 'rowspan');
+ public static $textarea_attribs = array('cols', 'rows', 'disabled', 'name', 'readonly', 'tabindex');
+ public static $input_attribs = array('checked', 'disabled', 'name', 'readonly', 'tabindex',
+ 'type', 'size', 'maxlength', 'value');
+ public static $select_attribs = array('multiple', 'name', 'size', 'disabled');
+ public static $option_attribs = array('selected', 'value', 'disabled');
+ public static $a_attribs = array('href', 'name', 'rel', 'tabindex', 'target');
+ public static $form_attribs = array('action', 'enctype', 'method', 'name', 'target');
+ public static $label_attribs = array('for');
+
+
+ public static function form_table($attribs = array(), $definition = array(), $data = array())
+ {
+ $content = '';
+ if (!empty($definition) && is_array($definition)) {
+ foreach ($definition as $set) {
+ $set_content = '';
+ if (isset($set['fields'])) {
+ $fields = $set['fields'];
+ }
+ else {
+ $fields = array($set);
+ }
+
+ $rows = array();
+
+ foreach ($fields as $fieldname => $field) {
+ $cells = array(
+ 0 => array(
+ 'class' => 'label',
+ 'body' => self::escape($field['label']),
+ ),
+ 1 => array(
+ 'class' => 'value',
+ 'body' => self::form_element($field),
+ ),
+ 2 => array(
+ 'class' => 'description',
+ 'body' => $field['description'],
+ ),
+ );
+ $rows[] = array('cells' => $cells);
+ }
+
+ if (!empty($rows)) {
+ $set_content = self::table(array('body' => $rows, 'class' => 'form'));
+ }
+
+ if ($set_content) {
+ if (isset($set['fields'])) {
+ $content .= "\n" . self::fieldset($set, $set_content);
+ }
+ else {
+ $content .= "\n" . $set_content;
+ }
+ }
+ }
+ }
+
+// foreach ($definition as $
+ return self::form($attribs, $content);
+ }
+
+
+ public static function table($attribs = array(), $content = null)
+ {
+ $table_attribs = array_merge(self::$table_attribs, self::$common_attribs, self::$event_attribs);
+ $table = '<table' . self::attrib_string($attribs, $table_attribs) . '>';
+
+ if ($content) {
+ $table .= $content;
+ }
+ else {
+ if (!empty($attribs['head']) && is_array($attribs['head'])) {
+ $table .= '<thead>';
+ foreach ($attribs['head'] as $row) {
+ $table .= "\n" . self::tr($row, null, true);
+ }
+ $table .= '</thead>';
+ }
+ if (!empty($attribs['body']) && is_array($attribs['body'])) {
+ $table .= '<tbody>';
+ foreach ($attribs['body'] as $row) {
+ $table .= "\n" . self::tr($row);
+ }
+ $table .= '</tbody>';
+ }
+ if (!empty($attribs['foot']) && is_array($attribs['foot'])) {
+ $table .= '<tfoot>';
+ foreach ($attribs['foot'] as $row) {
+ $table .= "\n" . self::tr($row);
+ }
+ $table .= '</tfoot>';
+ }
+ }
+
+ $table .= "\n</table>";
+
+ return $table;
+ }
+
+ public static function tr($attribs = array(), $is_head = false)
+ {
+ $row_attribs = array_merge(self::$tr_attribs, self::$common_attribs, self::$event_attribs);
+ $row = '<tr' . self::attrib_string($attribs, $row_attribs) . '>';
+
+ if (!empty($attribs['cells']) && is_array($attribs['cells'])) {
+ foreach ($attribs['cells'] as $cell) {
+ $row .= "\n" . self::td($cell, $is_head);
+ }
+ }
+
+ $row .= "\n</tr>";
+
+ return $row;
+ }
+
+ public static function td($attribs = array(), $is_head = false)
+ {
+ $cell_attribs = array_merge(self::$td_attribs, self::$common_attribs, self::$event_attribs);
+ $tag = $is_head ? 'th' : 'td';
+ $cell .= '<' . $tag . self::attrib_string($attribs, $cell_attribs) . '>';
+
+ if (isset($attribs['body'])) {
+ $cell .= $attribs['body'];
+ }
+ else if (!empty($attribs['element'])) {
+ $cell .= self::form_element($attribs['element']);
+ }
+
+ $cell .= "</$tag>";
+
+ return $cell;
+ }
+
+ public static function form_element($attribs = array())
+ {
+ $type = isset($attribs['type']) ? $attribs['type'] : 0;
+
+ switch ($type) {
+ case self::INPUT_TEXT:
+ case self::INPUT_PASSWORD:
+ $attribs['type'] = $type == self::INPUT_PASSWORD ? 'password' : 'text';
+ $content = self::input($attribs);
+ break;
+ case self::INPUT_TEXTAREA:
+ $content = self::textarea($attribs);
+ break;
+ case INPUT_NONE:
+ default:
+ if (is_array($attribs)) {
+ $content = isset($attribs['value']) ? $attribs['value'] : '';
+ }
+ else {
+ $content = $attribs;
+ }
+ $content = self::escape($content);
+ }
+
+ return $content;
+ }
+
+ public static function input($attribs = array())
+ {
+ $elem_attribs = array_merge(self::$input_attribs, self::$input_event_attribs,
+ self::$common_attribs, self::$event_attribs);
+
+ return sprintf('<input%s />', self::attrib_string($attribs, $elem_attribs));
+ }
+
+ public static function textarea($attribs = array())
+ {
+ $elem_attribs = array_merge(self::$textarea_attribs, self::$input_event_attribs,
+ self::$common_attribs, self::$event_attribs);
+
+ $value = isset($attribs['value']) ? self::escape($attribs['value']) : '';
+
+ return sprintf('<textarea%s>%s</textarea>',
+ self::attrib_string($attribs, $elem_attribs), $value);
+ }
+
+ public static function select($attribs = array())
+ {
+ $elem_attribs = array_merge(self::$select_attribs, self::$input_event_attribs,
+ self::$common_attribs, self::$event_attribs);
+
+ $content = array();
+ if (!empty($attribs['options']) && is_array($attribs['options'])) {
+ foreach ($attribs['options'] as $option) {
+ $content[] = self::option($option);
+ }
+ }
+
+ return sprintf('<select%s>%s</select>',
+ self::attrib_string($attribs, $elem_attribs), implode("\n", $content));
+ }
+
+ public static function option($attribs = array())
+ {
+ $elem_attribs = array_merge(self::$option_attribs, self::$common_attribs);
+
+ $content = isset($attribs['content']) ? self::escape($attribs['content']) : '';
+
+ return sprintf('<textarea%s>%s</textarea>',
+ self::attrib_string($attribs, $elem_attribs), $content);
+ }
+
+ public static function fieldset($attribs = array(), $content = null)
+ {
+ $elem_attribs = array_merge(self::$common_attribs);
+ $legend = isset($attribs['legend']) ? $attribs['legend'] : $attribs['label'];
+
+ return sprintf('<fieldset%s><legend>%s</legend>%s</fieldset>',
+ self::attrib_string($attribs, $elem_attribs), $legend, $content);
+ }
+
+ public static function a($attribs = array(), $content = null)
+ {
+ $elem_attribs = array_merge(self::$a_attribs, self::$common_attribs, self::$event_attribs);
+
+ return sprintf('<a%s>%s</a>',
+ self::attrib_string($attribs, $elem_attribs), self::escape($content));
+ }
+
+ public static function label($attribs = array(), $content = null)
+ {
+ $elem_attribs = array_merge(self::$label_attribs, self::$common_attribs);
+
+ return sprintf('<label%s>%s</label>',
+ self::attrib_string($attribs, $elem_attribs), self::escape($content));
+ }
+
+ public static function div($attribs = array(), $content = null)
+ {
+ $elem_attribs = array_merge(self::$common_attribs, self::$event_attribs);
+
+ return sprintf('<div%s>%s</div>',
+ self::attrib_string($attribs, $elem_attribs), $content);
+ }
+
+ public static function span($attribs = array(), $content = null)
+ {
+ $elem_attribs = array_merge(self::$common_attribs, self::$event_attribs);
+
+ return sprintf('<span%s>%s</span>',
+ self::attrib_string($attribs, $elem_attribs), $content);
+ }
+
+ public static function form($attribs = array(), $content = null)
+ {
+ $elem_attribs = array_merge(self::$form_attribs, self::$common_attribs, self::$event_attribs);
+
+ return sprintf('<form%s>%s</form>',
+ self::attrib_string($attribs, $elem_attribs), $content);
+ }
+
+ /**
+ * Create string with attributes
+ *
+ * @param array $attrib Associative array with tag attributes
+ * @param array $allowed List of allowed attributes
+ *
+ * @return string Valid attribute string
+ */
+ public static function attrib_string($attrib = array(), $allowed = array())
+ {
+ if (empty($attrib)) {
+ return '';
+ }
+
+ $allowed = array_flip((array)$allowed);
+ $attrib_arr = array();
+
+ foreach ($attrib as $key => $value) {
+ // skip size if not numeric
+ if (($key == 'size' && !is_numeric($value))) {
+ continue;
+ }
+
+ // ignore "internal" or not allowed attributes
+ if ((!empty($allowed) && !isset($allowed[$key])) || $value === null) {
+ continue;
+ }
+
+ // skip empty eventhandlers
+ if (preg_match('/^on[a-z]+/', $key) && !$value) {
+ continue;
+ }
+
+ // boolean attributes
+ if (preg_match('/^(checked|multiple|disabled|selected|readonly)$/', $key)) {
+ if ($value) {
+ $attrib_arr[] = sprintf('%s="%s"', $key, $key);
+ }
+ }
+ // the rest
+ else {
+ $attrib_arr[] = sprintf('%s="%s"', $key, self::escape($value));
+ }
+ }
+
+ return count($attrib_arr) ? ' '.implode(' ', $attrib_arr) : '';
+ }
+
+ public static function escape($value)
+ {
+ return htmlspecialchars($value, null, 'UTF-8');
+ }
+}
diff --git a/lib/kolab_utils.php b/lib/kolab_utils.php
new file mode 100644
index 0000000..97b8bdf
--- /dev/null
+++ b/lib/kolab_utils.php
@@ -0,0 +1,78 @@
+<?php
+
+ class kolab_utils {
+ /**
+ * Read a specific HTTP request header
+ *
+ * @param string $name Header name
+ * @return mixed Header value or null if not available
+ */
+ public static function get_request_header($name) {
+ if (function_exists('getallheaders')) {
+ $hdrs = array_change_key_case(getallheaders(), CASE_UPPER);
+ $key = strtoupper($name);
+ } else {
+ $key = 'HTTP_' . strtoupper(strtr($name, '-', '_'));
+ $hdrs = array_change_key_case($_SERVER, CASE_UPPER);
+ }
+
+ if (array_key_exists($key, $hdrs)) {
+ return $hdrs[$key];
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Make sure the string ends with a slash
+ */
+ public static function slashify($str)
+ {
+ return self::unslashify($str).'/';
+ }
+
+ /**
+ * Remove slash at the end of the string
+ */
+ public static function unslashify($str)
+ {
+ return preg_replace('/\/$/', '', $str);
+ }
+
+ public static function get_input($name, $type = null, $allow_html = false)
+ {
+ if ($type == 'GET') {
+ $value = isset($_GET[$name]) ? $_GET[$name] : null;
+ }
+ else if ($type == 'POST') {
+ $value = isset($_POST[$name]) ? $_POST[$name] : null;
+ }
+ else {
+ $value = isset($_REQUEST[$name]) ? $_REQUEST[$name] : null;
+ }
+
+ return self::parse_input($value, $allow_html);
+ }
+
+ public static function parse_input($value, $allow_html = false)
+ {
+ if (empty($value)) {
+ return $value;
+ }
+
+ if (is_array($value)) {
+ foreach ($value as $idx => $val) {
+ $value[$idx] = self::parse_input($val, $allow_html);
+ }
+ }
+ // remove HTML tags if not allowed
+ else if (!$allow_html) {
+ $value = strip_tags($value);
+ }
+
+ return $value;
+ }
+
+ }
+
+?>
diff --git a/lib/locale/en_US.php b/lib/locale/en_US.php
new file mode 100644
index 0000000..a5a2d18
--- /dev/null
+++ b/lib/locale/en_US.php
@@ -0,0 +1,29 @@
+<?php
+
+$LANG['username'] = 'Username';
+$LANG['password'] = 'Password';
+$LANG['loading'] = 'Loading...';
+$LANG['error'] = 'Error';
+$LANG['servererror'] = 'Server Error!';
+$LANG['search'] = 'Search';
+
+$LANG['menu.users'] = 'Users';
+$LANG['menu.groups'] = 'Groups';
+$LANG['menu.about'] = 'About';
+$LANG['menu.kolab'] = 'Kolab';
+$LANG['menu.kolabsys'] = 'Kolab Systems';
+$LANG['menu.technology'] = 'Technology';
+
+$LANG['user.list'] = 'List';
+$LANG['user.add'] = 'Add';
+$LANG['user.name'] = 'Name';
+$LANG['user.norecords'] = 'No user records found!';
+$LANG['user.surname'] = 'Surname';
+$LANG['user.givenname'] = 'Given name';
+$LANG['user.email'] = 'Email';
+$LANG['user.personal'] = 'Personal Information';
+
+$LANG['group.list'] = 'List';
+$LANG['group.add'] = 'Add';
+
+$LANG['loginerror'] = 'Incorrect username or password!';