* @copyright Copyright (c) 2010 United Prototype GmbH (http://unitedprototype.com) */ namespace UnitedPrototype\GoogleAnalytics\Internals\Request; use UnitedPrototype\GoogleAnalytics\Config; use UnitedPrototype\GoogleAnalytics\Internals\Util; /** * @link http://code.google.com/p/gaforflash/source/browse/trunk/src/com/google/analytics/core/GIFRequest.as */ abstract class HttpRequest { /** * Indicates the type of request, will be mapped to "utmt" parameter * * @see ParameterHolder::$utmt * @var string */ protected $type; /** * @var \UnitedPrototype\GoogleAnalytics\Config */ protected $config; /** * @var string */ protected $xForwardedFor; /** * @var string */ protected $userAgent; /** * @param \UnitedPrototype\GoogleAnalytics\Config $config */ public function __construct(Config $config = null) { $this->setConfig($config ? $config : new Config()); } /** * @return \UnitedPrototype\GoogleAnalytics\Config */ public function getConfig() { return $this->config; } /** * @param \UnitedPrototype\GoogleAnalytics\Config $config */ public function setConfig(Config $config) { $this->config = $config; } /** * @param string $value */ protected function setXForwardedFor($value) { $this->xForwardedFor = $value; } /** * @param string $value */ protected function setUserAgent($value) { $this->userAgent = $value; } /** * @return string */ protected function buildHttpRequest() { $parameters = $this->buildParameters(); // This constant is supported as the 4th argument of http_build_query() // from PHP 5.3.6 on and will tell it to use rawurlencode() instead of urlencode() // internally, see http://code.google.com/p/php-ga/issues/detail?id=3 if(defined('PHP_QUERY_RFC3986')) { // http_build_query() does automatically skip all array entries // with null values, exactly what we want here $queryString = http_build_query($parameters->toArray(), '', '&', PHP_QUERY_RFC3986); } else { // Manually replace "+"s with "%20" for backwards-compatibility $queryString = str_replace('+', '%20', http_build_query($parameters->toArray(), '', '&')); } // Mimic Javascript's encodeURIComponent() encoding for the query // string just to be sure we are 100% consistent with GA's Javascript client $queryString = Util::convertToUriComponentEncoding($queryString); // Recent versions of ga.js use HTTP POST requests if the query string is too long $usePost = strlen($queryString) > 2036; if(!$usePost) { $r = 'GET ' . $this->config->getEndpointPath() . '?' . $queryString . ' HTTP/1.0' . "\r\n"; } else { // FIXME: The "/p" shouldn't be hardcoded here, instead we need a GET and a POST endpoint... $r = 'POST /p' . $this->config->getEndpointPath() . ' HTTP/1.0' . "\r\n"; } $r .= 'Host: ' . $this->config->getEndpointHost() . "\r\n"; if($this->userAgent) { $r .= 'User-Agent: ' . str_replace(array("\n", "\r"), '', $this->userAgent) . "\r\n"; } if($this->xForwardedFor) { // Sadly "X-Fowarded-For" is not supported by GA so far, // see e.g. http://www.google.com/support/forum/p/Google+Analytics/thread?tid=017691c9e71d4b24, // but we include it nonetheless for the pure sake of correctness (and hope) $r .= 'X-Forwarded-For: ' . str_replace(array("\n", "\r"), '', $this->xForwardedFor) . "\r\n"; } if($usePost) { // Don't ask me why "text/plain", but ga.js says so :) $r .= 'Content-Type: text/plain' . "\r\n"; $r .= 'Content-Length: ' . strlen($queryString) . "\r\n"; } $r .= 'Connection: close' . "\r\n"; $r .= "\r\n\r\n"; if($usePost) { $r .= $queryString; } return $r; } /** * @return \UnitedPrototype\GoogleAnalytics\Internals\ParameterHolder */ protected abstract function buildParameters(); /** * This method should only be called directly or indirectly by fire(), but must * remain public as it can be called by a closure function. * * Sends either a normal HTTP request with response or an asynchronous request * to Google Analytics without waiting for the response. Will always return * null in the latter case, or false if any connection problems arise. * * @see HttpRequest::fire() * @param string $request * @return null|string|bool */ public function _send() { $request = $this->buildHttpRequest(); $response = null; // Do not actually send the request if endpoint host is set to null if($this->config->getEndpointHost() !== null) { $timeout = $this->config->getRequestTimeout(); $socket = fsockopen($this->config->getEndpointHost(), 80, $errno, $errstr, $timeout); if(!$socket) return false; if($this->config->getFireAndForget()) { stream_set_blocking($socket, false); } $timeoutS = intval($timeout); $timeoutUs = ($timeout - $timeoutS) * 100000; stream_set_timeout($socket, $timeoutS, $timeoutUs); fwrite($socket, $request); if(!$this->config->getFireAndForget()) { while(!feof($socket)) { $response .= fgets($socket, 512); } } fclose($socket); } if($loggingCallback = $this->config->getLoggingCallback()) { $loggingCallback($request, $response); } return $response; } /** * Simply delegates to send() if config option "sendOnShutdown" is disabled * or enqueues the request by registering a PHP shutdown function. */ public function fire() { if($this->config->getSendOnShutdown()) { // This dumb variable assignment is needed as PHP prohibits using // $this in closure use statements $instance = $this; // We use a closure here to retain the current values/states of // this instance and $request (as the use statement will copy them // into its own scope) register_shutdown_function(function() use($instance) { $instance->_send(); }); } else { $this->_send(); } } } ?>