← Back to team overview

anewt-developers team mailing list archive

[Branch ~uws/anewt/anewt.uws] Rev 1794: [curl] New module wrapping curl

 

------------------------------------------------------------
revno: 1794
committer: Wouter Bolsterlee <uws@xxxxxxxxx>
branch nick: anewt
timestamp: Sat 2010-10-02 20:52:08 +0200
message:
  [curl] New module wrapping curl
  
  Implemented a new class, AnewtCurl, wrapping the curl API
  and providing a friendlier and more Anewt-like API.
  
  This module also implement multi-curl functionality
  (parallel requests) in the AnewtMultiCurl class, layered on
  top of AnewtCurl.
  
  Both AnewtCurl and AnewtMultiCurl provide various properties
  to influence its behaviour (request options, response data,
  auth, ssl). Look at the constructor of those classes for the
  available options and their defaults.
  
  The code tries to make sure to handle errors correctly, and
  to keep all relevant information available when after an
  error occurred.
  
  Fixes bug lp:514474.
added:
  curl/
  curl/curl.lib.php
  curl/curl.test.php
  curl/main.lib.php
  curl/multicurl.lib.php


--
lp:anewt
https://code.launchpad.net/~uws/anewt/anewt.uws

Your team Anewt developers is subscribed to branch lp:anewt.
To unsubscribe from this branch go to https://code.launchpad.net/~uws/anewt/anewt.uws/+edit-subscription
=== added directory 'curl'
=== added file 'curl/curl.lib.php'
--- curl/curl.lib.php	1970-01-01 00:00:00 +0000
+++ curl/curl.lib.php	2010-10-02 18:52:08 +0000
@@ -0,0 +1,252 @@
+<?php
+
+/*
+ * Anewt, Almost No Effort Web Toolkit, curl module
+ *
+ * This code is copyrighted and distributed under the terms of the GNU LGPL.
+ * See the README file for more information.
+ */
+
+
+/**
+ * Exception class for Curl errors.
+ */
+class AnewtCurlException extends AnewtException
+{
+	/** The HTTP status code of the error response (if any) */
+	public $response_status_code;
+
+	/** The content type of the HTTP error response (if any) */
+	public $response_content_type;
+
+	/** The body of the HTTP error response (if any) */
+	public $response_body;
+}
+
+
+/**
+ * Wrapper class for Curl functionality.
+ *
+ * \see AnewtMultiCurl
+ */
+class AnewtCurl extends AnewtContainer
+{
+	/** List of additional headers for this request. */
+	private $headers;
+
+	/**
+	 * \private Curl handle (for internal use only)
+	 */
+	public $curl_handle;
+
+	public function __construct($url=null)
+	{
+		$this->headers = array();
+
+		$this->_seed(array(
+			/* Urls */
+			'url' => $url,
+			'referer' => null,
+
+			/* Request method: GET or POST */
+			'method' => 'GET',
+
+			/* Request data (only for POST) */
+			'data' => null,
+
+			/* Credentials */
+			'username' => null,
+			'password' => null,
+
+			/* Follow redirects within sane limits */
+			'follow-redirects' => true,
+			'max-redirects' => 10,
+
+			/* Misc settings */
+			'strict-ssl-verification' => false,
+			'user-agent' => null,
+			'timeout' => null,
+			'prepared' => false,
+
+			/* Result state */
+			'response-body' => null,
+			'response-details' => null,
+			'error' => null,
+		));
+	}
+
+	/**
+	 * Add a custom header to the request.
+	 *
+	 * \param $name
+	 *   The header name
+	 * \param $value
+	 *   The header value
+	 */
+	public function add_header($name, $value)
+	{
+		assert('is_string($name);');
+		assert('is_string($value);');
+
+		$this->headers[] = sprintf('%s: %s', $name, $value);
+	}
+
+	/**
+	 * \private
+	 *
+	 * Prepare curl handle for execution.
+	 * This method is for internal use only.
+	 */
+	public function prepare_handle()
+	{
+		$handle = curl_init();
+
+		/* Set default CURL options */
+ 		curl_setopt_array($handle, array(
+			/* The url of the resource to retrieve */
+			CURLOPT_URL => $this->url,
+
+			/* Follow redirects within sane limits */
+			CURLOPT_FOLLOWLOCATION => $this->follow_redirects,
+			CURLOPT_MAXREDIRS => $this->max_redirects,
+
+			/* Sloppy SSL verificiation */
+			CURLOPT_SSL_VERIFYPEER => $this->strict_ssl_verification,
+
+			/* Don't fail immediately on errors, because errors are checked in
+			 * a different way (so that the error response contents are
+			 * not lost) */
+			CURLOPT_FAILONERROR => false,
+
+			/* Don't passthru received data to client directly */
+			CURLOPT_RETURNTRANSFER => true,
+		));
+
+
+		/* Authentication */
+
+		if (!is_null($this->username) && !is_null($this->password)) {
+			curl_setopt($handle, CURLOPT_USERPWD, sprintf('%s:%s', $this->username, $this->password));
+		}
+
+
+		/* Headers */
+
+		if ($this->headers)
+			curl_setopt($handle, CURLOPT_HTTPHEADER, $this->headers);
+
+
+		/* HTTP method */
+
+		$data = $this->data;
+		switch ($this->method)
+		{
+			case 'GET':
+				if ($data)
+					throw new AnewtCurlException('GET requests cannot have a $data parameter');
+
+				curl_setopt($handle, CURLOPT_HTTPGET, true);
+				break;
+
+			case 'POST':
+				/* The data array is manually serialised to a string because curl will
+				 * send it as multipart/form-data instead of
+				 * application/x-www-form-urlencoded if passed as an array. Note that
+				 * this is contrary to what the manual says about the PHP curl
+				 * extension. See PHP bug #16305 for more information. */
+
+				if (is_null($data))
+					$data = '';
+
+				assert('is_string($data) || is_assoc_array($data);');
+
+				if (is_assoc_array($data))
+					$data = http_build_query($data);
+
+				curl_setopt($handle, CURLOPT_POST, true);
+				curl_setopt($handle, CURLOPT_POSTFIELDS, $data);
+				break;
+
+			default:
+				throw new AnewtCurlException('Unknown HTTP method: %s', $this->method);
+				break;
+		}
+
+		$this->prepared = true;
+		$this->curl_handle = $handle;
+	}
+
+	/**
+	 * Execute the Curl request.
+	 *
+	 * \return
+	 *   The response body (for convenience). This value is also stored in the
+	 *   <code>$curl->response_body</code>. More information is available in
+	 *   <code>$curl->response_content_type</code> and
+	 *   <code>$curl->response_details</code>.
+	 */
+	public function execute()
+	{
+		$this->prepare_handle();
+
+		/* Execute the curl call */
+		$out = curl_exec($this->curl_handle);
+
+		/* Set response body */
+		$this->response_body = $out;
+		$this->response_content_type = curl_getinfo($this->curl_handle, CURLINFO_CONTENT_TYPE);
+
+		/* Check for errors */
+		$this->check_error($out);
+		if ($this->error)
+			throw $this->error;
+
+		/* Store any details about the curl operation */
+		$this->response_details = curl_getinfo($this->curl_handle);
+
+		/* Free up resources */
+		curl_close($this->curl_handle);
+
+		return $out;
+	}
+
+	/**
+	 * \private
+	 *
+	 * Check the executed request for errors.
+	 *
+	 * The error is saved in the \c error propery, but not thrown. This makes it
+	 * possible to ignore errors in AnewtMultiCurl requests.
+	 *
+	 * \param $response
+	 *   The Curl response
+	 */
+	public function check_error($response)
+	{
+		$error = null;
+
+		/* In case an error occurred:
+		 *   - curl_exec returns FALSE
+		 *   - curl_multi_getcontent returns null
+		 */
+		if (is_null($response) || $response === false)
+			$error = new AnewtCurlException('Curl error: %s', curl_error($this->curl_handle));
+
+		$response_status_code = curl_getinfo($this->curl_handle, CURLINFO_HTTP_CODE);
+		if ($response_status_code >= 400 && $response_status_code <= 599)
+		{
+			$error = new AnewtCurlException(
+				$response_status_code,
+				'Received error response HTTP %d (%s) while executing request for %s',
+				$response_status_code, http_status_to_string($response_status_code), $this->url);
+
+			$error->response_status_code = $response_status_code;
+			$error->response_content_type = curl_getinfo($this->curl_handle, CURLINFO_CONTENT_TYPE);
+			$error->response_body = $response;
+		}
+
+		$this->error = $error;
+	}
+}
+
+?>

=== added file 'curl/curl.test.php'
--- curl/curl.test.php	1970-01-01 00:00:00 +0000
+++ curl/curl.test.php	2010-10-02 18:52:08 +0000
@@ -0,0 +1,32 @@
+<?php
+
+error_reporting(E_ALL | E_STRICT);
+require_once dirname(__FILE__) . '/../anewt.lib.php';
+
+anewt_include('curl');
+
+echo 'Testing single request:', NL, NL;
+
+$c = new AnewtCurl('http://localhost/');
+$c->referer = 'http://localhost/this-is-the-referer';
+$c->username = '';
+$c->password = '';
+$c->follow_redirects = true;
+$c->max_redirects = 1;
+$output = $c->execute();
+echo str_truncate($output, 120), NL;
+
+echo NL, NL,NL, 'Testing multiple concurrent requests:', NL, NL;
+
+$c1 = new AnewtCurl('http://localhost/page-1');
+$c2 = new AnewtCurl('http://localhost/page-2');
+$m = new AnewtMultiCurl();
+$m->allow_errors = true;
+$m->add_curl($c1);
+$m->add_curl($c2);
+$results = $m->execute();
+
+echo str_truncate($c1->response_body, 120), NL;
+var_dump($results);
+
+?>

=== added file 'curl/main.lib.php'
--- curl/main.lib.php	1970-01-01 00:00:00 +0000
+++ curl/main.lib.php	2010-10-02 18:52:08 +0000
@@ -0,0 +1,14 @@
+<?php
+
+/*
+ * Anewt, Almost No Effort Web Toolkit, curl module
+ *
+ * This code is copyrighted and distributed under the terms of the GNU LGPL.
+ * See the README file for more information.
+ */
+
+
+anewt_include('curl/curl');
+anewt_include('curl/multicurl');
+
+?>

=== added file 'curl/multicurl.lib.php'
--- curl/multicurl.lib.php	1970-01-01 00:00:00 +0000
+++ curl/multicurl.lib.php	2010-10-02 18:52:08 +0000
@@ -0,0 +1,122 @@
+<?php
+
+/*
+ * Anewt, Almost No Effort Web Toolkit, curl module
+ *
+ * This code is copyrighted and distributed under the terms of the GNU LGPL.
+ * See the README file for more information.
+ */
+
+
+/**
+ * Wrapper class for parallel Curl functionality.
+ *
+ * The <code>allow-errors</code> property influences how AnewtMultiCurl handles
+ * errors while executing subrequests. This setting is \c false by default,
+ * which means AnewtMultiCurl propagates the error whenever one of the
+ * subrequests resulted in an error. If enabled, errors are ignored, but still
+ * accessible via the <code>$curl->error</code> property on the individual
+ * AnewtCurl instances.
+ *
+ * \see AnewtCurl
+ */
+class AnewtMultiCurl extends AnewtContainer
+{
+	/** List of AnewtCurl instances */
+	private $curls;
+
+	public function __construct()
+	{
+		$this->curls = array();
+
+		$this->_seed(array(
+
+			/* Whether to break on subrequest errors */
+			'allow-errors' => false,
+
+		));
+	}
+
+	/**
+	 * Add a pending curl request to this AnewtMultiCurl.
+	 *
+	 * \param $curl
+	 *   An AnewtCurl instance that has not been executed yet.
+	 */
+	public function add_curl($curl)
+	{
+		assert('$curl instanceof AnewtCurl;');
+
+		$this->curls[] = $curl;
+	}
+
+	/**
+	 * Execute multiple Curl sessions in parallel.
+	 */
+	public function execute()
+	{
+		/* Prepare handles */
+
+		$multi_handle = curl_multi_init();
+
+		foreach ($this->curls as $curl)
+		{
+			if (!$curl->prepared)
+				$curl->prepare_handle();
+
+			curl_multi_add_handle($multi_handle, $curl->curl_handle);
+		}
+
+
+		/* Start performing the request */
+
+		do {
+			$exec_result = curl_multi_exec($multi_handle, $still_running);
+		} while ($exec_result === CURLM_CALL_MULTI_PERFORM);
+
+
+		/* Process the request */
+
+		while ($still_running && $exec_result == CURLM_OK)
+		{
+			/* Wait for the network */
+			$n_ready = curl_multi_select($multi_handle);
+
+			/* Retrieve data */
+			if ($n_ready != -1) {
+				do {
+					$exec_result = curl_multi_exec($multi_handle, $still_running);
+				} while ($exec_result === CURLM_CALL_MULTI_PERFORM);
+			}
+		}
+
+		if ($exec_result != CURLM_OK)
+			throw new AnewtCurlException('Error while executing multiple Curl requests: %s', curl_error($multi_handle));
+
+
+		/* Obtain results */
+
+		foreach ($this->curls as $curl)
+		{
+			$curl->response_content_type = curl_getinfo($curl->curl_handle, CURLINFO_CONTENT_TYPE);
+			$curl->response_body = curl_multi_getcontent($curl->curl_handle);
+
+			$curl->check_error($curl->response_body);
+
+			if (!$this->allow_errors && $curl->error)
+				throw $curl->error;
+
+			$curl->response_details = curl_getinfo($curl->curl_handle);
+
+			/* Cleanup */
+			curl_multi_remove_handle($multi_handle, $curl->curl_handle);
+			curl_close($curl->curl_handle);
+		}
+
+		curl_multi_close($multi_handle);
+
+		return true;
+	}
+}
+
+?>