Just Another Blog

Are you thinking what I'm thinking?

Tuesday, February 01, 2005

XMLHTTPRequest in PHP 5

In PHP 5, while there is domDocument, there is no XMLHTTPRequest. I knew there is HTTP_Request, but since I don't want to have unnecessary dependency, I've decided to create my own one. Fortunately there is fsockopen. So in a several hours of quick learning and googling, I've created this wonderful wrapper class.

<?php

/**
 * A wrapper class for handling HTTP request.
 * Only HTTP is supported. So don't try anything like HTTPS or FTP.
 * @author Ming Hong Ng <minghong@gmail.com>
 * @version 0.1, 2005/02/01
 * @since 0.1
 */
class XmlHttpRequest
{
    /**
     * Constructor.
     * @version 0.1, 2005/02/01
     * @since 0.1
     */
    function __construct()
    {
        $this->responseText = NULL;
        $this->responseXml = NULL;
        $this->status = NULL;
        $this->statusText = NULL;
        $this->error = NULL;
        $this->errorText = NULL;

        $this->components = array();
        $this->requestHeaders = array(
            "User-Agent" => $_SERVER["SERVER_SOFTWARE"],
            "Accept" => $_SERVER["HTTP_ACCEPT"],
            "Accept-Language" => $_SERVER["HTTP_ACCEPT_LANGUAGE"],
            "Accept-Charset" => $_SERVER["HTTP_ACCEPT_CHARSET"],
            "Connection" => "Close"
        );
        $this->responseHeaders = array();
    }

    /**
     * Destructor.
     * @version 0.1, 2005/02/01
     * @since 0.1
     */
    function __destruct()
    {
        if ( !is_null( $this->responseXml ) )
        {
            unset( $this->responseXml );
        }
    }

    /**
     * Assign method and URL to the pending request.
     * Upcoming transaction is always handled synchronously.
     * Please urlencode the URL before parsing into this method.
     * Please set method to "POST" if you want to post data.
     * @version 0.1, 2005/02/01
     * @param method The request method
     * @param url The URL
     * @since 0.1
     * @todo Supports method other than "GET" and "POST"
     */
    public function open( $method, $url )
    {
        $this->method = strtoupper( $method );
        $this->components = parse_url( $url );

        // Fill in the missing components, if any
        if ( is_null( $this->components["scheme"] ) )
        {
            $this->components["scheme"] = "http";
        }
        if ( is_null( $this->components["port"] ) )
        {
            $this->components["port"] =
                ( $this->components["scheme"] == "http" ) ? 80 : 443;
        }
        if ( is_null( $this->components["path"] ) )
        {
            $this->components["path"] = "/";
        }
    }

    /**
     * Transmits the request with postable string/DOM content.
     * Content is ignored unless the method is "POST".
     * Please urlencode the key/value pairs unless you are sending XML data.
     * Set headerOnly to TRUE if you just need to get the headers (save time).
     * @version 0.1, 2005/02/01
     * @param content An optional string/DOM content
     * @since 0.1
     */
    public function send( $content = NULL )
    {
        // Prepare the request headers
        $path = $this->components["path"];
        if ( !is_null( $this->components["query"] ) )
        {
            $path .= "?" . $this->components["query"];
        }
        if ( !is_null( $this->components["fragment"] ) )
        {
            $path .= "#" . $this->components["fragment"];
        }
        $request = sprintf(
            "%s %s " . $_SERVER["SERVER_PROTOCOL"] . "\r\nHost: %s\r\n",
            $this->method, $path, $this->components["host"] );
        foreach( $this->requestHeaders as $key => $value )
        {
            if ( $key != "Content-Type" && $key != "Content-Length" )
            {
                $request .= "$key: $value\r\n";
            }
        }
        if ( !is_null( $content ) )
        {
            if ( $this->method == "POST" )
            {
                $body = $content;
                $type = "application/x-www-form-urlencoded";
                if ( get_class( $body ) == "DOMDocument" )
                {
                    $body = $body->saveXML();
                    $type = "application/xml";
                }

                $lines = array(
                    "Content-Type: $type",
                    "Content-Length: " . strlen( $body ),
                    "",
                    $body
                );

                $request .= implode( "\r\n", $lines );
            }
        }
        $request .= "\r\n\r\n";

        // Open socket connection to host
        $handle = @fsockopen( $this->components["host"],
            $this->components["port"], $this->error, $this->errorText );

        // Cannot open socket connection to host
        if ( !$handle )
        {
            // Abort
            return;
        }

        // Send the request
        fwrite( $handle, $request );

        // Receive the response headers
        $line = "";
        while ( $line != "\r\n" )
        {
            $line = fgets( $handle, 1024 );

            // Extract status code/text
            preg_match( "/^HTTP\/\d.\d (\d+) (.+)\r\n/", $line, $matches );
            if ( count( $matches ) > 0 )
            {
                $this->status = $matches[1];
                $this->statusText = $matches[2];
            }
            unset( $matches );

            // Extract response name/value pairs
            preg_match( "/^([\w|\-]+): (.+)\r\n/", $line, $matches );
            if ( count( $matches ) > 0 )
            {
                $this->responseHeaders[ $matches[1] ] = $matches[2];
            }
            unset( $matches );
        }
        // Receive the response body, if needed
        if ( $this->method != "HEAD" )
        {
            while ( !feof( $handle ) )
            {
                $this->responseText .= fgets( $handle, 1024 );
            }
            $this->responseXml = new domDocument();
            if ( @!$this->responseXml->loadXML( $this->responseText ) )
            {
                @$this->responseXml->loadHTML( $this->responseText );
            }
        }
    }

    /**
     * Get the text of the specific response header.
     * @version 0.1, 2005/02/01
     * @param header The name of the response header
     * @return The response header value
     * @since 0.1
     */
    public function getResponseHeader( $header )
    {
        return $this->responseHeaders[$header];
    }

    /**
     * Get the response headers as a string for HTTP request.
     * @version 0.1, 2005/02/01
     * @return The response headers
     * @since 0.1
     */
    public function getAllResponseHeaders()
    {
        $headers = "";
        foreach( $this->responseHeaders as $key => $value )
        {
            $headers .= "$key: $value\r\n";
        }
        return $headers;
    }

    /**
     * Set a HTTP request header for HTTP request.
     * @version 0.1, 2005/02/01
     * @param header The name of the request header
     * @param The request header value
     * @since 0.1
     */
    public function setRequestHeader( $header, $value )
    {
        $this->requestHeaders[$header] = $value;
    }

    public $responseText;           // Response text
    public $responseXml;            // Response XML
    public $status;                 // Status code
    public $statusText;             // Status text
    public $error;                  // Error code
    public $errorText;              // Error text

    private $method;                // Request method
    private $components;            // URL components
    private $requestHeaders;        // Request headers
    private $responseHeaders;       // Response headers
}

// An example of using this class
header( "Content-Type: text/plain" );
$request = new XmlHttpRequest();
$request->open( "get", "http://www.example.com" );
$request->send();
echo "---------- Status ----------\r\n";
echo "$request->status $request->statusText\r\n";
echo "\r\n---------- Headers ----------\r\n";
echo $request->getAllResponseHeaders();
echo "\r\n---------- Body ----------\r\n";
echo $request->responseText;

?>

This should give you something like this:

---------- Status ----------
200 OK

---------- Headers ----------
Date: Tue, 01 Feb 2005 16:21:14 GMT
Server: Apache/1.3.27 (Unix)  (Red-Hat/Linux)
Last-Modified: Wed, 08 Jan 2003 23:11:55 GMT
ETag: "3f80f-1b6-3e1cb03b"
Accept-Ranges: bytes
Content-Length: 438
Connection: close
Content-Type: text/html

---------- Body ----------
<HTML>
<HEAD>
  <TITLE>Example Web Page</TITLE>
</HEAD> 
<body>  
<p>You have reached this web page by typing "example.com",
"example.net",
  or "example.org" into your web browser.</p>
<p>These domain names are reserved for use in documentation and are not available 
  for registration. See <a href="http://www.rfc-editor.org/rfc/rfc2606.txt">RFC 
  2606</a>, Section 3.</p>
</BODY>
</HTML>

Feel free to use it! You just need to give me credit by keeping the javadoc-like comment.

0 Comments:

Note that troll and spam comments will be deleted without any notification.

Post a Comment

<< Home