276 lines
		
	
	
		
			7.8 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			276 lines
		
	
	
		
			7.8 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| /**
 | |
|  * StatusNet, the distributed open-source microblogging tool
 | |
|  *
 | |
|  * PHP version 5
 | |
|  *
 | |
|  * LICENCE: This program is free software: you can redistribute it and/or modify
 | |
|  * it under the terms of the GNU Affero General Public License as published by
 | |
|  * the Free Software Foundation, either version 3 of the License, or
 | |
|  * (at your option) any later version.
 | |
|  *
 | |
|  * This program is distributed in the hope that it will be useful,
 | |
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | |
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | |
|  * GNU Affero General Public License for more details.
 | |
|  *
 | |
|  * You should have received a copy of the GNU Affero General Public License
 | |
|  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | |
|  *
 | |
|  * @category  Plugin
 | |
|  * @package   StatusNet
 | |
|  * @author    Brion Vibber <brion@status.net>
 | |
|  * @copyright 2010 StatusNet, Inc.
 | |
|  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
 | |
|  * @link      http://status.net/
 | |
|  */
 | |
| 
 | |
| class OAuthData
 | |
| {
 | |
|     public $consumer_key, $consumer_secret, $token, $token_secret;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  *
 | |
|  */
 | |
| abstract class JsonStreamReader
 | |
| {
 | |
|     const CRLF = "\r\n";
 | |
| 
 | |
|     public $id;
 | |
|     protected $socket = null;
 | |
|     protected $state = 'init'; // 'init', 'connecting', 'waiting', 'headers', 'active'
 | |
| 
 | |
|     public function __construct()
 | |
|     {
 | |
|         $this->id = get_class($this) . '.' . substr(md5(mt_rand()), 0, 8);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Starts asynchronous connect operation...
 | |
|      *
 | |
|      * @fixme Can we do the open-socket fully async to? (need write select infrastructure)
 | |
|      *
 | |
|      * @param string $url
 | |
|      */
 | |
|     public function connect($url)
 | |
|     {
 | |
|         common_log(LOG_DEBUG, "$this->id opening connection to $url");
 | |
| 
 | |
|         $scheme = parse_url($url, PHP_URL_SCHEME);
 | |
|         if ($scheme == 'http') {
 | |
|             $rawScheme = 'tcp';
 | |
|         } else if ($scheme == 'https') {
 | |
|             $rawScheme = 'ssl';
 | |
|         } else {
 | |
|             // TRANS: Server exception thrown when an invalid URL scheme is detected.
 | |
|             throw new ServerException(_m('Invalid URL scheme for HTTP stream reader.'));
 | |
|         }
 | |
| 
 | |
|         $host = parse_url($url, PHP_URL_HOST);
 | |
|         $port = parse_url($url, PHP_URL_PORT);
 | |
|         if (!$port) {
 | |
|             if ($scheme == 'https') {
 | |
|                 $port = 443;
 | |
|             } else {
 | |
|                 $port = 80;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         $path = parse_url($url, PHP_URL_PATH);
 | |
|         $query = parse_url($url, PHP_URL_QUERY);
 | |
|         if ($query) {
 | |
|             $path .= '?' . $query;
 | |
|         }
 | |
| 
 | |
|         $errno = $errstr = null;
 | |
|         $timeout = 5;
 | |
|         //$flags = STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT;
 | |
|         $flags = STREAM_CLIENT_CONNECT;
 | |
|         // @fixme add SSL params
 | |
|         $this->socket = stream_socket_client("$rawScheme://$host:$port", $errno, $errstr, $timeout, $flags);
 | |
| 
 | |
|         $this->send($this->httpOpen($host, $path));
 | |
| 
 | |
|         stream_set_blocking($this->socket, false);
 | |
|         $this->state = 'waiting';
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Send some fun data off to the server.
 | |
|      *
 | |
|      * @param string $buffer
 | |
|      */
 | |
|     function send($buffer)
 | |
|     {
 | |
|         fwrite($this->socket, $buffer);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Read next packet of data from the socket.
 | |
|      *
 | |
|      * @return string
 | |
|      */
 | |
|     function read()
 | |
|     {
 | |
|         $buffer = fread($this->socket, 65536);
 | |
|         return $buffer;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Build HTTP request headers.
 | |
|      *
 | |
|      * @param string $host
 | |
|      * @param string $path
 | |
|      * @return string
 | |
|      */
 | |
|     protected function httpOpen($host, $path)
 | |
|     {
 | |
|         $lines = array(
 | |
|             "GET $path HTTP/1.1",
 | |
|             "Host: $host",
 | |
|             'User-Agent: ' . HTTPClient::userAgent() . ' (TwitterBridgePlugin)',
 | |
|             "Connection: close",
 | |
|             "",
 | |
|             ""
 | |
|         );
 | |
|         return implode(self::CRLF, $lines);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Close the current connection, if open.
 | |
|      */
 | |
|     public function close()
 | |
|     {
 | |
|         if ($this->isConnected()) {
 | |
|             common_log(LOG_DEBUG, "$this->id closing connection.");
 | |
|             fclose($this->socket);
 | |
|             $this->socket = null;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Are we currently connected?
 | |
|      *
 | |
|      * @return boolean
 | |
|      */
 | |
|     public function isConnected()
 | |
|     {
 | |
|         return $this->socket !== null;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Send any sockets we're listening on to the IO manager
 | |
|      * to wait for input.
 | |
|      *
 | |
|      * @return array of resources
 | |
|      */
 | |
|     public function getSockets()
 | |
|     {
 | |
|         if ($this->isConnected()) {
 | |
|             return array($this->socket);
 | |
|         }
 | |
|         return array();
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Take a chunk of input over the horn and go go go! :D
 | |
|      *
 | |
|      * @param string $buffer
 | |
|      */
 | |
|     public function handleInput($socket)
 | |
|     {
 | |
|         if ($this->socket !== $socket) {
 | |
|             // TRANS: Exception thrown when input from an inexpected socket is encountered.
 | |
|             throw new Exception(_m('Got input from unexpected socket!'));
 | |
|         }
 | |
| 
 | |
|         try {
 | |
|             $buffer = $this->read();
 | |
|             $lines = explode(self::CRLF, $buffer);
 | |
|             foreach ($lines as $line) {
 | |
|                 $this->handleLine($line);
 | |
|             }
 | |
|         } catch (Exception $e) {
 | |
|             common_log(LOG_ERR, "$this->id aborting connection due to error: " . $e->getMessage());
 | |
|             fclose($this->socket);
 | |
|             throw $e;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     protected function handleLine($line)
 | |
|     {
 | |
|         switch ($this->state)
 | |
|         {
 | |
|             case 'waiting':
 | |
|                 $this->handleLineWaiting($line);
 | |
|                 break;
 | |
|             case 'headers':
 | |
|                 $this->handleLineHeaders($line);
 | |
|                 break;
 | |
|             case 'active':
 | |
|                 $this->handleLineActive($line);
 | |
|                 break;
 | |
|             default:
 | |
|                 // TRANS: Exception thrown when an invalid state is encountered in handleLine.
 | |
|                 // TRANS: %s is the invalid state.
 | |
|                 throw new Exception(sprintf(_m('Invalid state in handleLine: %s.'),$this->state));
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      *
 | |
|      * @param <type> $line
 | |
|      */
 | |
|     protected function handleLineWaiting($line)
 | |
|     {
 | |
|         $bits = explode(' ', $line, 3);
 | |
|         if (count($bits) != 3) {
 | |
|             // TRANS: Exception thrown when an invalid response line is encountered.
 | |
|             // TRANS: %s is the invalid line.
 | |
|             throw new Exception(sprintf(_m('Invalid HTTP response line: %s.'),$line));
 | |
|         }
 | |
| 
 | |
|         list($http, $status, $text) = $bits;
 | |
|         if (substr($http, 0, 5) != 'HTTP/') {
 | |
|             // TRANS: Exception thrown when an invalid response line part is encountered.
 | |
|             // TRANS: %1$s is the chunk, %2$s is the line.
 | |
|             throw new Exception(sprintf(_m('Invalid HTTP response line chunk "%1$s": %2$s.'),$http, $line));
 | |
|         }
 | |
|         if ($status != '200') {
 | |
|             // TRANS: Exception thrown when an invalid response code is encountered.
 | |
|             // TRANS: %1$s is the response code, %2$s is the line.
 | |
|             throw new Exception(sprintf(_m('Bad HTTP response code %1$s: %2$s.'),$status,$line));
 | |
|         }
 | |
|         common_log(LOG_DEBUG, "$this->id $line");
 | |
|         $this->state = 'headers';
 | |
|     }
 | |
| 
 | |
|     protected function handleLineHeaders($line)
 | |
|     {
 | |
|         if ($line == '') {
 | |
|             $this->state = 'active';
 | |
|             common_log(LOG_DEBUG, "$this->id connection is active!");
 | |
|         } else {
 | |
|             common_log(LOG_DEBUG, "$this->id read HTTP header: $line");
 | |
|             $this->responseHeaders[] = $line;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     protected function handleLineActive($line)
 | |
|     {
 | |
|         if ($line == "") {
 | |
|             // Server sends empty lines as keepalive.
 | |
|             return;
 | |
|         }
 | |
|         $data = json_decode($line);
 | |
|         if ($data) {
 | |
|             $this->handleJson($data);
 | |
|         } else {
 | |
|             common_log(LOG_ERR, "$this->id received bogus JSON data: " . var_export($line, true));
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     abstract protected function handleJson(stdClass $data);
 | |
| }
 |