├── CHANGELOG.md ├── Gass ├── Adapter │ ├── AdapterInterface.php │ ├── Base.php │ └── Multi.php ├── Bootstrap.php ├── BotInfo │ ├── Base.php │ ├── BotInfo.php │ ├── BotInfoInterface.php │ ├── BrowsCap.php │ ├── Multi.php │ └── UserAgentStringInfo.php ├── Exception │ ├── BadFunctionCallException.php │ ├── BadMethodCallException.php │ ├── DomainException.php │ ├── ExceptionInterface.php │ ├── InvalidArgumentException.php │ ├── LengthException.php │ ├── LogicException.php │ ├── OutOfBoundsException.php │ ├── OutOfRangeException.php │ ├── OverflowException.php │ ├── RangeException.php │ ├── RuntimeException.php │ ├── UnderflowException.php │ └── UnexpectedValueException.php ├── GassInterface.php ├── GoogleAnalyticsServerSide.php ├── Http │ ├── Base.php │ ├── Curl.php │ ├── Http.php │ ├── HttpInterface.php │ └── Stream.php ├── Loader │ └── SplClassLoader.php ├── Proxy │ └── ProxyInterface.php └── Validate │ ├── Base.php │ ├── IpAddress.php │ ├── LanguageCode.php │ ├── Url.php │ └── ValidateInterface.php ├── LICENSE ├── README.md ├── bin └── gass-browscap-updater ├── composer.json └── composer.lock /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Google Analytics Server Side Changelog 2 | ====================================== 3 | 4 | Version 0.14.5 Beta 5 | ------------------- 6 | 7 | - Add in support for IPv6 addresses 8 | - Remove test support for DNT header removed in 0.14.0 9 | - Fix UnitTest broken in PHP 7.4 10 | 11 | Version 0.14.4 Beta 12 | ------------------- 13 | 14 | - Correct minor non-functional issues with new Browscap ini file update functionality 15 | - Remove the need for gh-pages branch 16 | 17 | Version 0.14.3 Beta 18 | ------------------- 19 | 20 | - Add in documentation for new Browscap ini file update functionality in previous release 21 | - Correct update script help docs 22 | 23 | Version 0.14.2 Beta 24 | ------------------- 25 | 26 | - Add in ability to update browscap ini file via cron job 27 | - Add in ability to disable auto browscap ini file update per request 28 | - Add in unofficial support for PHP 7.2 - 7.4 29 | - Switch from using $php-errormsg to the error_get_last function, in part for compatibility with more recent PHP versions 30 | - Correct ordering of parameters to implode function in line with requirements for more recent PHP versions 31 | - Switch to using constant keys for default option setup 32 | 33 | Version 0.14.1 Beta 34 | ------------------- 35 | 36 | - Remove deprecated FILTER_VALIDATE options [depricated in PHP 7.3](https://www.php.net/manual/en/migration73.deprecated.php#migration73.deprecated.filter) 37 | 38 | Version 0.14.0 Beta 39 | ------------------- 40 | 41 | - Remove DNT header support since the technology has been abandoned (https://github.com/w3c/dnt/commit/5d85d6c3d116b5eb29fddc69352a77d87dfd2310) 42 | 43 | Version 0.12.2 Beta 44 | ------------------- 45 | 46 | - Finalise unit test coverage 47 | - Fixed a few remaining bugs / issues found as part of unit testing 48 | - Remove HHVM CI support 49 | 50 | Version 0.12.1 Beta 51 | ------------------- 52 | 53 | - Increase unit test coverage 54 | - Slight re-write of BrowsCap BotInfo Adapter, abstracting common functionality 55 | - Increase configuration options of BrowsCap BotInfo Adapter 56 | - Add in constants for config option names with BrowsCap and UserAgentStringInfo BotInfo Adapters 57 | 58 | Version 0.12.0 Beta 59 | ------------------- 60 | 61 | - Remove Test HTTP Adapter. This was added for unit testing purposes but has been replaced by mocking 62 | - Increase unit test coverage 63 | - Fix various bugs / issues, including bugs in the Multi BotInfo adapter 64 | 65 | Version 0.11.0 Beta 66 | ------------------- 67 | 68 | - Change to BSD License 69 | - Stop using parent namespaces in use statements 70 | - Ensure works with PHP 7+ 71 | - Various code style & Docblock updates 72 | 73 | Version 0.10.0 Beta 74 | ------------------ 75 | 76 | - New Multi Adapter to allow multiple adapter checks per call 77 | - BotInfo interface method altered from getIsBot to isBot 78 | - New BotInfo Multi Apadter 79 | - BrowsCap updated to use full file rather than standard file due to required attributes 80 | - User Agent String Info marked as deprecated until udger.com implements csvs to replace user agent string info's csv, as user-agent-string.info has now shut down 81 | - Rename Validator interface to ValidatorInterface 82 | - New Url Validator 83 | - Update PHP CodeSniffer to latest version for testing 84 | - Increase PHPUnit memory limit for new larger BrowsCap file size 85 | - Temporarily limit phpunit/phpunit-mock-objects to version 2.3.0 due to PHPUnit/HHVM issue (https://github.com/sebastianbergmann/phpunit-mock-objects/issues/223) 86 | - PSR-2 & coding style updates 87 | 88 | Version 0.9.3 Beta 89 | ------------------ 90 | 91 | - Update to PSR-4 autoloading 92 | - Add in extra CI checks for PHP Coding Standards Fixer 93 | - Add in Coveralls support 94 | - Add PHP 5.6 support 95 | 96 | Version 0.9.2 Beta 97 | ------------------ 98 | 99 | - Added PHP CodeSniffer & PHP CS Fixer PSR-2 Check to Travis CI 100 | - Added provisional Travis CI testing for PHP 5.6 & HipHop 101 | - Install test suite via composer for Travis CI tests 102 | - Fix issue with Gass\Http\Curl library in relation to headers 103 | - Start transition to using PHPUnit Mock for class dependency testing 104 | - Add in vfsStream to composer for future unit testing of filesystem interaction 105 | 106 | Version 0.9.1 Beta 107 | ------------------ 108 | 109 | - Update BrowsCap URLs as current ones used will stop working on 30th March 2014 110 | - Add in PHP 5.5 Support 111 | - Update Composer support 112 | - Docblock Corrections 113 | - PSR-2 Corrections 114 | - Update default version of ga.js used 115 | - Deal with multiple Set-Cookie headers received 116 | 117 | Version 0.9.0 Beta 118 | ------------------ 119 | 120 | - Rename namespace from GASS to Gass inline with PSR-2 standard 121 | - Move main GoogleAnalyticsServerSide class inside Gass namespace 122 | - Implement SplClassLoader rather than proprietary one 123 | - Bring the code-base fully inline with the PSR-2 standard using PHP_CodeSniffer 124 | - Add in PHPUnit configuration values to test where superglobal values are used 125 | - Update Test HTTP adapter to store the response for one or more HTTP requests 126 | 127 | Version 0.8.6 Beta 128 | ------------------ 129 | 130 | - Ensure the autoloader function is requested first so any other implemented autoloaders 131 | don't throw Exceptions when trying to load files from this framework 132 | - Add Do Not Track header functionality enabled by default. This can be disabled by calling the setIgnoreDoNotTrack method. 133 | - Update to deal with new search providers containing numerical characters in their names 134 | - Ensure ga.js file only retrieved if the user hasn't manually set the version and search engines 135 | - Ensure HTTP curl adapter doesn't overwrite any user defined headers 136 | 137 | Version 0.8.5 Beta 138 | ------------------ 139 | 140 | - Code to PSR-2 compliant 141 | ( using @fabpot's php coding standards fixer: https://github.com/fabpot/PHP-CS-Fixer ) 142 | - Update URLs for the browscap project now Gary Keith has transfered ownership to a new project leader 143 | 144 | Version 0.8.4 Beta 145 | ------------------ 146 | 147 | - Extract re-used validation into re-usable validator classes 148 | - Convert readme to github flavoured markdown 149 | - Added in PHPUnit tests for a large majority of the code, remainder to come 150 | - Search Engine information used for organic campaign info changed format in 5.3.0, update to deal with that 151 | - Altered parameters to getEventString. Includes code to deal with Backwards Compatibility. 152 | - Default GASS\Http adapter is now GASS\Http\Curl, falls back to GASS\Http\Stream if php cURL extension is not available 153 | 154 | Version 0.8.3 Beta 155 | ------------------ 156 | 157 | - Fix a few issues existing in the code with un-defined / un-used variables 158 | 159 | Version 0.8.2 Beta 160 | ------------------ 161 | 162 | - Add in support for organic campaign parameters with __utmz cookies. 163 | - Only send utmip gif query parameter when using a mobile account, pointless otherwise. 164 | 165 | Version 0.8.1 Beta 166 | ------------------ 167 | 168 | - Add a __callStatic magic method to GASS\Http so adapter methods can be called statically 169 | - Ensure $php_errormsg has a value in GASS\Http\Stream so can be used when http request fails 170 | - Correct spelling mistakes (retreive -> retrieve) 171 | 172 | Version 0.8.0 Beta 173 | ------------------ 174 | 175 | - Converted to PHP 5.3 with namespaces instead of PHP 5.2 virtual namespaces 176 | ( using @ralphschindler's php-namespacer: https://github.com/ralphschindler/PHPTools ) 177 | - Removed deprecated methods from main class 178 | - Removed get/setEvent, not in GA code and not needed, set is done directly in trackEvent 179 | - Removed support for old csv cache in UserAgentStringInfo which just stored user agents without IPs 180 | - BrowserCap renamed BrowsCap inline with PHP 181 | 182 | Version 0.7.13 Beta 183 | ------------------ 184 | 185 | - Fix a few issues existing in the code with un-defined / un-used variables 186 | 187 | Version 0.7.12 Beta 188 | ------------------- 189 | 190 | - Ensure $php_errormsg has a value in GASS_Http_Stream so can be used when http request fails 191 | - Correct spelling mistakes (retreive -> retrieve) 192 | 193 | Version 0.7.11 Beta 194 | ------------------- 195 | 196 | - Remove all non-PHPDoc doc blocks and replace with PHPDoc blocks so all works when PHPDoc run on project 197 | - Add in package and subpackage tags to all files for use in PHPDoc 198 | - GASS_BotInfo_UserAgentStringInfo now blocks per IP address as well as user agent string to ensure is blocking all bots in list 199 | 200 | Version 0.7.10 Beta 201 | ------------------- 202 | 203 | - Sets the __utmv custom var cookie which stores scope 1 (visitor-level) variables 204 | - Correct the custom var string passed to GA, scope 3 (page-level) shouldn't be passed. 205 | - PHPDoc completion. 206 | - Only load data files in BotInfo adapters when data actually needed 207 | - setCustomVar uses the first available index when one not provided and returns $this for chaining 208 | 209 | Version 0.7.9 Beta 210 | ------------------ 211 | 212 | - 4: Ensure user can use the Mobile GA accounts starting in "MO-" aswell. Lets utmip provide the user's location to GA. 213 | (https://github.com/chappy84/google-analytics-server-side/issues/4) 214 | - Remove un-required method call. 215 | - Silence parse_url when an issue occurs, return value is checked. 216 | 217 | Version 0.7.8 Beta 218 | ------------------ 219 | 220 | - Silence all is_readable, is_writable, file_exists and filemtime calls as the result is being checked anyway 221 | so we don't really want a lot of E_WARNING php errors for no reason. 222 | - Minor code speed improvements 223 | - Some extra verification on event / custom variable strings 224 | - Remove issue where cookie contains one of the invalid raw cookie characters 225 | - convert createPageView / createEvent / setCustomVariable to trackPageView / trackEvent / setCustomVar as they 226 | are in the Google Analytics ECMAScript, left alias functions for old method names 227 | - Add getVisitorCustomVar, deleteCustomVar, setSessionCookieTimeout & setVisitorCookieTimeout methods 228 | 229 | Version 0.7.7 Beta 230 | ------------------ 231 | 232 | - Ensure the BrowserCap latest version date isn't retrieved more than once a day form the server. 233 | Store the version date in a one day cache file in same dir as php_browscap.ini. 234 | - Make createPageView compatible with Google's trackPageView. 235 | - 6: Fix issue with the event value being passed wrongly to Google. 236 | (https://github.com/chappy84/google-analytics-server-side/issues/6) 237 | 238 | Version 0.7.6 Beta 239 | ------------------ 240 | 241 | - BrowserCap ini file is now parsed and dealt with by the code rather than using php's built in get_browser. 242 | - BrowserCap ini file now updated whenever update available on server. 243 | 244 | Version 0.7.5 Beta 245 | ------------------ 246 | 247 | - Fix wrong name variable used in BrowserCap. 248 | - Ensure UserAgent has been sent to BotInfo. 249 | - Ensure AcceptedLanguage / RemoteAddress / UserAgent has been sent to Http. 250 | 251 | Version 0.7.4 Beta 252 | ------------------ 253 | 254 | - Fix autoloading issue in PHP 5.2 where GASS_Adapter_Base::__construct didn't match GASS_Adapter_Interface::__construct. 255 | 256 | Version 0.7.3 Beta 257 | ------------------ 258 | 259 | - Add in setting of Custom Variables. 260 | - Add extra nonInteraction parameter to setEvent / createEvent. 261 | - Correct issue with getEventString where value set in wrong place. 262 | 263 | Version 0.7.2 Beta 264 | ------------------ 265 | 266 | - Check event value is integer. 267 | - 5: Readme updates from @skl 268 | (https://github.com/chappy84/google-analytics-server-side/issues/5) 269 | 270 | Version 0.7.1 Beta 271 | ------------------ 272 | 273 | - PHPDoc and Readme updates. 274 | 275 | Version 0.7.0 Beta 276 | ------------------ 277 | 278 | - Convert into a small framework under GASS PHP 5.2 virtual namespace (for backwards compatability). 279 | - Add in autoloader for new GASS virtual namespace. 280 | - GASS_Http singleton so can be used anywhere in framework without passing options around. 281 | - GASS_Http uses adapters, defaults to Stream, can also use cURL, ability for developer to write own adapters. 282 | - GASS_BotInfo which detects if user-agent is a bot or not separated out as not required. 283 | - GASS_BotInfo uses adapters, php's BrowserCap is default, but original UserAgentString.info is also available, developer can write own adapters. 284 | 285 | Version 0.6.4 Beta 286 | ------------------ 287 | 288 | - 3: Ensure bots.csv is not empty when received from url and ensure lines in csv aren't empty before dealing with them. 289 | (https://github.com/chappy84/google-analytics-server-side/pull/3) 290 | 291 | Version 0.6.3 Beta 292 | ------------------ 293 | 294 | - 1: Ensure set cookie headers are only sent out once. 295 | (https://github.com/chappy84/google-analytics-server-side/issues/1) 296 | - 2: Readme Updates from @skl 297 | (https://github.com/chappy84/google-analytics-server-side/issues/2) 298 | 299 | Version 0.6.2 Beta 300 | ------------------ 301 | 302 | - Add in check to ensure cURL extension has been installed when the class is instantiated. 303 | 304 | Version 0.6.1 Beta 305 | ------------------ 306 | 307 | - Allow setting / getting of any class level variable with a publicly available get / set method via getOption / setOption methods. 308 | 309 | Version 0.6.0 Beta 310 | ------------------ 311 | 312 | - Ability to ignore logging statistics for web trawling bots and spiders and caching of the bots list. 313 | - Auto-setting of latest GA version number from ga.js. 314 | - Auto-setting of accepted language from headers. 315 | - Ability to pass options to cURL. 316 | - Remove auto-setting of charset from headers, default to UTF-8 (response should define this, not request headers). 317 | - Try manually reporting IP address to GA as done in the GA mobile code ( http://www.google.com/analytics/googleanalyticsformobile.zip ). 318 | - str_getcsv is not available in PHP before PHP 5.3 so added in a crude implementation so it works in lower than 5.3. 319 | 320 | Version 0.5.5 Beta 321 | ------------------ 322 | 323 | - Set Document-Referer by default and check url format. 324 | - Add extra Exceptions thrown in required circumstances. 325 | - Ensure explode on cookies only returns no of parts required. 326 | - Add Traffic source string with utmz cookie. 327 | - Update PHPDoc in code. 328 | - Update Readme with extra info & to correct markdown format. 329 | 330 | 331 | Version 0.5.0 Beta 332 | ------------------ 333 | 334 | - Initial Release. 335 | - Provides basic PageView and Event functionality, setting cookies in correct format. 336 | -------------------------------------------------------------------------------- /Gass/Adapter/AdapterInterface.php: -------------------------------------------------------------------------------- 1 | setOptions($options); 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | * 56 | * @param array $options 57 | * @return $this 58 | */ 59 | public function setOptions(array $options) 60 | { 61 | foreach ($options as $name => $value) { 62 | $this->setOption($name, $value); 63 | } 64 | return $this; 65 | } 66 | 67 | /** 68 | * Sets a specific option 69 | * 70 | * @param string $name 71 | * @param mixed $value 72 | * @return $this 73 | */ 74 | public function setOption($name, $value) 75 | { 76 | $this->options[$name] = $value; 77 | return $this; 78 | } 79 | 80 | /** 81 | * Returns all options set 82 | * 83 | * @return array 84 | */ 85 | public function getOptions() 86 | { 87 | return $this->options; 88 | } 89 | 90 | /** 91 | * Returns a specific option 92 | * 93 | * @param string $name 94 | * @return mixed 95 | */ 96 | public function getOption($name) 97 | { 98 | return (isset($this->options[$name])) 99 | ? $this->options[$name] 100 | : null; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Gass/Adapter/Multi.php: -------------------------------------------------------------------------------- 1 | requiredClass)) { 76 | $this->requiredClass = static::DEFAULT_INTERFACE; 77 | } 78 | if ($this->requiredClass != static::DEFAULT_INTERFACE 79 | && !is_subclass_of($this->requiredClass, static::DEFAULT_INTERFACE) 80 | ) { 81 | throw new DomainException($this->requiredClass . ' must implement ' . static::DEFAULT_INTERFACE); 82 | } 83 | $this->setAdapters($adapters); 84 | } 85 | 86 | /** 87 | * Returns the current adapters 88 | * 89 | * @return array 90 | */ 91 | public function getAdapters() 92 | { 93 | return $this->adapters; 94 | } 95 | 96 | /** 97 | * Returns the specified adapter 98 | * 99 | * @param string $name 100 | * @throws DomainException 101 | * @return AdapterInterface 102 | */ 103 | public function getAdapter($name) 104 | { 105 | if (isset($this->adapters[$name])) { 106 | return $this->adapters[$name]; 107 | } 108 | throw new DomainException($name . ' is not currently set as an adapter'); 109 | } 110 | 111 | /** 112 | * Set the current adapters 113 | * 114 | * @param array $adapters 115 | * @return $this 116 | */ 117 | public function setAdapters(array $adapters) 118 | { 119 | $this->adapters = array(); 120 | foreach ($adapters as $name => $adapter) { 121 | $this->addAdapter($adapter, (is_string($name)) ? $name : null); 122 | } 123 | return $this; 124 | } 125 | 126 | /** 127 | * Add a specified adapter 128 | * 129 | * @param array $adapter 130 | * @param string $name 131 | * @throws InvalidArgumentException 132 | * @return $this 133 | */ 134 | public function addAdapter($adapter, $name = null) 135 | { 136 | if (!$adapter instanceof $this->requiredClass) { 137 | throw new InvalidArgumentException( 138 | get_class($adapter) . ' does not implement ' . $this->requiredClass 139 | ); 140 | } 141 | if (!empty($name) && !is_string($name)) { 142 | throw new InvalidArgumentException('$name must be a string'); 143 | } 144 | if (empty($name)) { 145 | $name = get_class($adapter); 146 | } 147 | $this->adapters[$name] = $adapter; 148 | return $this; 149 | } 150 | 151 | /** 152 | * Reset the stored list of adapters 153 | * 154 | * @return $this 155 | */ 156 | public function resetAdapters() 157 | { 158 | return $this->setAdapters(array()); 159 | } 160 | 161 | /** 162 | * {@inheritdoc} 163 | * 164 | * @param array $options 165 | * @throws BadMethodCallException 166 | */ 167 | public function setOptions(array $options) 168 | { 169 | throw new BadMethodCallException(__METHOD__ . ' cannot be called on ' . get_class($this)); 170 | } 171 | 172 | /** 173 | * {@inheritdoc} 174 | * 175 | * @param string $name 176 | * @param mixed $value 177 | * @throws BadMethodCallException 178 | */ 179 | public function setOption($name, $value) 180 | { 181 | throw new BadMethodCallException(__METHOD__ . ' cannot be called on ' . get_class($this)); 182 | } 183 | 184 | /** 185 | * {@inheritdoc} 186 | * 187 | * @throws BadMethodCallException 188 | */ 189 | public function getOptions() 190 | { 191 | throw new BadMethodCallException(__METHOD__ . ' cannot be called on ' . get_class($this)); 192 | } 193 | 194 | /** 195 | * {@inheritdoc} 196 | * 197 | * @param string $name 198 | * @throws BadMethodCallException 199 | */ 200 | public function getOption($name) 201 | { 202 | throw new BadMethodCallException(__METHOD__ . ' cannot be called on ' . get_class($this)); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /Gass/Bootstrap.php: -------------------------------------------------------------------------------- 1 | register(); 34 | // @codeCoverageIgnoreEnd 35 | -------------------------------------------------------------------------------- /Gass/BotInfo/Base.php: -------------------------------------------------------------------------------- 1 | remoteAddress; 65 | } 66 | 67 | /** 68 | * {@inheritdoc} 69 | * 70 | * @return string 71 | */ 72 | public function getUserAgent() 73 | { 74 | return $this->userAgent; 75 | } 76 | 77 | /** 78 | * {@inheritdoc} 79 | * 80 | * @param string $remoteAddress 81 | * @throws InvalidArgumentException 82 | * @return $this 83 | */ 84 | public function setRemoteAddress($remoteAddress) 85 | { 86 | $ipValidator = new ValidateIpAddress; 87 | if (!$ipValidator->isValid($remoteAddress)) { 88 | throw new InvalidArgumentException( 89 | 'Remote Address validation errors: ' . 90 | implode(', ', $ipValidator->getMessages()) 91 | ); 92 | } 93 | $this->remoteAddress = $remoteAddress; 94 | return $this; 95 | } 96 | 97 | /** 98 | * {@inheritdoc} 99 | * 100 | * @param string $userAgent 101 | * @return $this 102 | */ 103 | public function setUserAgent($userAgent) 104 | { 105 | $this->userAgent = $userAgent; 106 | return $this; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Gass/BotInfo/BotInfo.php: -------------------------------------------------------------------------------- 1 | setAdapter($adapter); 65 | if (0 < func_num_args()) { 66 | $this->setOptions($options); 67 | } 68 | } 69 | 70 | /** 71 | * Call magic method 72 | * 73 | * @param string $name 74 | * @param array $arguments 75 | * @throws DomainException 76 | * @return mixed 77 | */ 78 | public function __call($name, $arguments) 79 | { 80 | if (method_exists($this->adapter, $name)) { 81 | return call_user_func_array(array($this->adapter, $name), $arguments); 82 | } 83 | throw new BadMethodCallException( 84 | 'Method ' . get_class($this->adapter) . '::' . $name . ' does not exist.' 85 | ); 86 | } 87 | 88 | /** 89 | * Sets the current adapter to use 90 | * 91 | * @param string|BotInfoInterface $adapter 92 | * @throws InvalidArgumentException 93 | * @return $this 94 | */ 95 | public function setAdapter($adapter) 96 | { 97 | if (is_string($adapter)) { 98 | $adapterName = 'Gass\BotInfo\\' . ucfirst($adapter); 99 | $adapter = new $adapterName(); 100 | } 101 | if ($adapter instanceof BotInfoInterface) { 102 | $this->adapter = $adapter; 103 | return $this; 104 | } 105 | throw new InvalidArgumentException( 106 | 'The Gass\BotInfo adapter must implement Gass\BotInfo\BotInfoInterface.' 107 | ); 108 | } 109 | 110 | /** 111 | * Returns the current Adapter 112 | * 113 | * @return BotInfoInterface 114 | */ 115 | public function getAdapter() 116 | { 117 | return $this->adapter; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Gass/BotInfo/BotInfoInterface.php: -------------------------------------------------------------------------------- 1 | null, 112 | self::OPT_SAVE_PATH => null, 113 | self::OPT_LATEST_VERSION_DATE_FILE => 'latestVersionDate.txt', 114 | self::OPT_DISABLE_AUTO_UPDATE => false, 115 | ); 116 | 117 | /** 118 | * {@inheritdoc} 119 | * 120 | * @param array $options 121 | */ 122 | public function __construct(array $options = array()) 123 | { 124 | if (!isset($options[static::OPT_INI_FILE]) 125 | && !isset($options[static::OPT_SAVE_PATH]) 126 | && !isset($options[static::OPT_BROWSCAP]) 127 | && false !== ($browsCapLocation = ini_get(static::OPT_BROWSCAP)) 128 | && '' != trim($browsCapLocation) 129 | ) { 130 | $options[static::OPT_BROWSCAP] = trim($browsCapLocation); 131 | } 132 | parent::__construct($options); 133 | } 134 | 135 | /** 136 | * {@inheritdoc} 137 | * 138 | * @param string $name 139 | * @param mixed $value 140 | * @return $this 141 | */ 142 | public function setOption($name, $value) 143 | { 144 | if (static::OPT_BROWSCAP === $name) { 145 | $currentFileName = $this->getOption(static::OPT_INI_FILE); 146 | $currentDirectory = $this->getOption(static::OPT_SAVE_PATH); 147 | if (empty($currentDirectory) && empty($currentFileName)) { 148 | parent::setOption(static::OPT_INI_FILE, basename($value)); 149 | parent::setOption(static::OPT_SAVE_PATH, dirname($value)); 150 | } 151 | return $this; 152 | } 153 | return parent::setOption($name, $value); 154 | } 155 | 156 | /** 157 | * {@inheritdoc} 158 | * 159 | * @param string $name 160 | * @return mixed 161 | */ 162 | public function getOption($name) 163 | { 164 | if (static::OPT_BROWSCAP === $name) { 165 | return $this->getFilePath(static::OPT_INI_FILE); 166 | } 167 | return parent::getOption($name); 168 | } 169 | 170 | /** 171 | * Returns the last date the ini file was updated (on remote webiste) 172 | * 173 | * @return int 174 | */ 175 | public function getLatestVersionDate() 176 | { 177 | if ($this->latestVersionDate === null) { 178 | $this->setLatestVersionDate(); 179 | } 180 | return $this->latestVersionDate; 181 | } 182 | 183 | /** 184 | * Gets the latest version date from the web 185 | * 186 | * @throws DomainException 187 | * @throws RuntimeException 188 | */ 189 | private function setLatestVersionDate() 190 | { 191 | if (null === ($latestVersionDateFile = $this->getFilePath(static::OPT_LATEST_VERSION_DATE_FILE))) { 192 | throw new DomainException( 193 | 'Cannot deduce latest version date file location. Please set the required options.' 194 | ); 195 | } 196 | if (!file_exists($latestVersionDateFile) 197 | || false === ($fileSaveTime = filemtime($latestVersionDateFile)) 198 | || $fileSaveTime < time() - 86400 199 | ) { 200 | $latestDateString = trim( 201 | Http::getInstance() 202 | ->request(static::VERSION_DATE_URL) 203 | ->getResponse() 204 | ); 205 | if (false === $this->checkValidDateTimeString($latestDateString)) { 206 | unset($latestDateString); 207 | } else { 208 | $this->saveToFile($latestVersionDateFile, trim($latestDateString)); 209 | } 210 | } elseif (false === ($latestDateString = @file_get_contents($latestVersionDateFile))) { 211 | $errorMsg = 'error message not available. You may have a custom error handler in place.'; 212 | $errorDet = error_get_last(); 213 | if (!empty($errorDet['message'])) { 214 | $errorMsg = $errorDet['message']; 215 | } 216 | throw new RuntimeException( 217 | 'Couldn\'t read latest version date file: ' . $latestVersionDateFile . ' due to: ' . $errorMsg 218 | ); 219 | } 220 | if (isset($latestDateString) 221 | && false !== ($latestVersionDate = $this->checkValidDateTimeString($latestDateString)) 222 | ) { 223 | $this->latestVersionDate = $latestVersionDate; 224 | } 225 | } 226 | 227 | /** 228 | * Checks a string is a valid date format, and if so returns it's associated unix timestamp 229 | * 230 | * @param string $dateString 231 | * @return int|bool 232 | */ 233 | private function checkValidDateTimeString($dateString) 234 | { 235 | if (false === ($timestamp = strtotime($dateString))) { 236 | return false; 237 | } 238 | return $timestamp; 239 | } 240 | 241 | /** 242 | * Checks whether the browscap file exists, is readable, and hasn't expired the cache lifetime 243 | * 244 | * @throws DomainException 245 | * @throws RuntimeException 246 | */ 247 | public function checkIniFile() 248 | { 249 | if (null === ($iniFilePath = $this->getFilePath(static::OPT_INI_FILE))) { 250 | throw new DomainException( 251 | 'Cannot deduce browscap ini file location. Please set the required options.' 252 | ); 253 | } 254 | if (!file_exists($iniFilePath) && !$this->getOption(self::OPT_DISABLE_AUTO_UPDATE)) { 255 | $this->updateIniFile(); 256 | } 257 | if (!is_readable($iniFilePath)) { 258 | throw new RuntimeException( 259 | 'The browscap ini file ' . 260 | $iniFilePath . 261 | ' is un-readable, please ensure the permissions are correct and try again.' 262 | ); 263 | } 264 | if (!$this->getOption(self::OPT_DISABLE_AUTO_UPDATE) 265 | && (false === ($fileSaveTime = filemtime($iniFilePath)) 266 | || (null !== ($latestVersionDate = $this->getLatestVersionDate()) 267 | && $fileSaveTime < $latestVersionDate) 268 | )) { 269 | $this->updateIniFile(); 270 | } 271 | $this->loadIniFile(); 272 | } 273 | 274 | /** 275 | * Updates the browscap ini file to the latest version 276 | * 277 | * @throws DomainException 278 | * @throws RuntimeException 279 | */ 280 | private function updateIniFile() 281 | { 282 | if (null === ($iniFilePath = $this->getFilePath(static::OPT_INI_FILE))) { 283 | throw new DomainException( 284 | 'Cannot deduce browscap ini file location. Please set the required options.' 285 | ); 286 | } 287 | 288 | $http = Http::getInstance(); 289 | $currentHttpUserAgent = $http->getUserAgent(); 290 | if ($currentHttpUserAgent === null || '' == trim($currentHttpUserAgent)) { 291 | throw new DomainException( 292 | 'A user-agent has not beeen set in the Gass\Http adapter.' . 293 | ' The remote server rejects requests without a user-agent.' 294 | ); 295 | } 296 | $browscapSource = $http->request(static::BROWSCAP_URL)->getResponse(); 297 | $browscapContents = trim($browscapSource); 298 | if (empty($browscapContents)) { 299 | throw new RuntimeException( 300 | 'browscap ini file retrieved from external source seems to be empty. ' . 301 | 'Please ensure the ini file file can be retrieved.' 302 | ); 303 | } 304 | 305 | $this->saveToFile($iniFilePath, $browscapContents); 306 | } 307 | 308 | /** 309 | * Loads the browscap ini file from the specified location 310 | * 311 | * @throws RuntimeException 312 | */ 313 | private function loadIniFile() 314 | { 315 | $browsers = parse_ini_file($this->getFilePath(static::OPT_INI_FILE), true, INI_SCANNER_RAW); 316 | if (empty($browsers)) { 317 | throw new RuntimeException('Browscap ini file could not be parsed.'); 318 | } 319 | $this->browsers = $browsers; 320 | } 321 | 322 | /** 323 | * Returns all the details related to a browser 324 | * 325 | * @param string $index 326 | * @return array|bool 327 | */ 328 | private function getBrowserDetails($index) 329 | { 330 | if (isset($this->browsers[$index])) { 331 | $browserDetails = $this->browsers[$index]; 332 | if (isset($browserDetails['Parent'])) { 333 | if (false === ($extraDetails = $this->getBrowserDetails($browserDetails['Parent']))) { 334 | return false; 335 | } 336 | $browserDetails = array_merge($extraDetails, $browserDetails); 337 | } 338 | return $browserDetails; 339 | } 340 | return false; 341 | } 342 | 343 | /** 344 | * Checks the parsed browscap ini file for the provided user-agent. 345 | * Return value is compatible with php's get_browser return value. 346 | * 347 | * @param string $userAgent 348 | * @param bool $returnArray 349 | * @return bool|object|array 350 | */ 351 | public function getBrowser($userAgent = null, $returnArray = false) 352 | { 353 | if (empty($this->browsers)) { 354 | $this->checkIniFile(); 355 | } 356 | 357 | if (0 < func_num_args()) { 358 | $this->setUserAgent($userAgent); 359 | } 360 | $userAgent = $this->getUserAgent(); 361 | $browser = '*'; 362 | foreach ($this->browsers as $browserKey => $details) { 363 | $regEx = $this->getBrowserRegex($browserKey); 364 | if (1 === preg_match($regEx, $userAgent)) { 365 | $browser = $browserKey; 366 | break; 367 | } 368 | } 369 | if (false !== ($browserDetails = $this->getBrowserDetails($browser)) 370 | && 'Default Browser' !== $browserDetails['Browser'] 371 | ) { 372 | $browserRegex = $this->getBrowserRegex($browser); 373 | $returnBrowsDet = array( 374 | 'browser_name_regex' => substr($browserRegex, 0, strlen($browserRegex) - 1), 375 | 'browser_name_pattern' => $browser, 376 | ); 377 | foreach ($browserDetails as $key => $value) { 378 | if ($value == 'true') { 379 | $value = '1'; 380 | } elseif ($value == 'false') { 381 | $value = ''; 382 | } 383 | $returnBrowsDet[strtolower($key)] = $value; 384 | } 385 | return ($returnArray === true) ? $returnBrowsDet : (object) $returnBrowsDet; 386 | } 387 | return false; 388 | } 389 | 390 | /** 391 | * Converts a browscap browser pattern into a regular expression 392 | * 393 | * @param string $browserPattern 394 | * @return string 395 | */ 396 | private function getBrowserRegex($browserPattern) 397 | { 398 | return '?^' . 399 | strtolower( 400 | str_replace( 401 | array('\?', '\*'), 402 | array('.', '.*'), 403 | preg_quote($browserPattern) 404 | ) 405 | ) . 406 | '$?i'; 407 | } 408 | 409 | /** 410 | * {@inheritdoc} 411 | * 412 | * @param string $userAgent [optional] 413 | * @param string $remoteAddress [optional] 414 | * @return bool 415 | */ 416 | public function isBot($userAgent = null, $remoteAddress = null) 417 | { 418 | $noArgs = func_num_args(); 419 | if ($noArgs >= 1) { 420 | $this->setUserAgent($userAgent); 421 | } 422 | if ($noArgs >= 2) { 423 | $this->setRemoteAddress($remoteAddress); 424 | } 425 | $browserDetails = $this->getBrowser(); 426 | return ( 427 | false === $browserDetails 428 | || (isset($browserDetails->crawler) && $browserDetails->crawler == 1) 429 | || (isset($browserDetails->isbanned) && $browserDetails->isbanned == 1) 430 | || !isset($browserDetails->javascript) || $browserDetails->javascript != 1 431 | || !isset($browserDetails->cookies) || $browserDetails->cookies != 1 432 | ); 433 | } 434 | 435 | /** 436 | * Returns the specified option with static::OPT_SAVE_PATH prepended 437 | * 438 | * @param string $optionName 439 | * @return string|null 440 | */ 441 | private function getFilePath($optionName) 442 | { 443 | $path = $this->getOption(static::OPT_SAVE_PATH); 444 | $filename = $this->getOption($optionName); 445 | if (!empty($path) && !empty($filename)) { 446 | return rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $filename; 447 | } 448 | return null; 449 | } 450 | 451 | /** 452 | * Save contents to a specific path 453 | * 454 | * @param string $filePath 455 | * @param mixed $fileContents 456 | * @throws RuntimeException 457 | * @return $this 458 | */ 459 | private function saveToFile($filePath, $fileContents) 460 | { 461 | $parentDirPath = dirname($filePath); 462 | $folderNotWritable = (!file_exists($filePath) && !is_writable($parentDirPath)); 463 | $fileNotWritable = (file_exists($filePath) && !is_writable($filePath)); 464 | if ($folderNotWritable 465 | || $fileNotWritable 466 | || false === @file_put_contents($filePath, $fileContents, LOCK_EX) 467 | ) { 468 | $errorMsg = 'error message not available. You may have a custom error handler in place.'; 469 | if ($folderNotWritable) { 470 | $errorMsg = 'Folder ' . $parentDirPath . ' is not writable'; 471 | } elseif ($fileNotWritable) { 472 | $errorMsg = 'File is not writable'; 473 | } else { 474 | $errorDet = error_get_last(); 475 | if (!empty($errorDet['message'])) { 476 | $errorMsg = $errorDet['message']; 477 | } 478 | } 479 | throw new RuntimeException( 480 | 'Cannot save file ' . $filePath . ' due to: ' . $errorMsg 481 | ); 482 | } 483 | } 484 | } 485 | -------------------------------------------------------------------------------- /Gass/BotInfo/Multi.php: -------------------------------------------------------------------------------- 1 | getAdapters() as $adapter) { 79 | $adapter->{__FUNCTION__}($remoteAddress); 80 | } 81 | return $this; 82 | } 83 | 84 | /** 85 | * {@inheritdoc} 86 | * 87 | * @param string $userAgent 88 | * @return $this 89 | */ 90 | public function setUserAgent($userAgent) 91 | { 92 | foreach ($this->getAdapters() as $adapter) { 93 | $adapter->{__FUNCTION__}($userAgent); 94 | } 95 | return $this; 96 | } 97 | 98 | /** 99 | * {@inheritdoc} 100 | * 101 | * @param string $userAgent [optional] 102 | * @param string $remoteAddress [optional] 103 | * 104 | * @return bool 105 | */ 106 | public function isBot($userAgent = null, $remoteAddress = null) 107 | { 108 | foreach ($this->getAdapters() as $adapter) { 109 | if (true === $adapter->{__FUNCTION__}($userAgent, $remoteAddress)) { 110 | return true; 111 | } 112 | } 113 | return false; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Gass/BotInfo/UserAgentStringInfo.php: -------------------------------------------------------------------------------- 1 | 'bot user agent' 76 | * 77 | * @var array 78 | */ 79 | private $bots = array(); 80 | 81 | /** 82 | * List of IPs in use by bots that the class should ignore 83 | * array_format: 'IP address' => 'bot name' 84 | * 85 | * @var array 86 | */ 87 | private $botIps = array(); 88 | 89 | /** 90 | * Last date the cache was saved 91 | * 92 | * @var number|null 93 | */ 94 | private $cacheDate; 95 | 96 | /** 97 | * Options to use with the class 98 | * 99 | * @var array 100 | */ 101 | protected $options = array( 102 | self::OPT_CACHE_PATH => null, 103 | self::OPT_CACHE_FILENAME => 'bots.csv', 104 | self::OPT_CACHE_LIFETIME => 2592000, // 30 days 105 | ); 106 | 107 | /** 108 | * Sets the bots list 109 | * 110 | * @return $this 111 | */ 112 | public function set() 113 | { 114 | if (null === ($bots = $this->getFromCache())) { 115 | $bots = $this->getFromWeb(); 116 | } 117 | $botInfo = $this->parseCsv($bots); 118 | $this->bots = (is_array($botInfo->distinctBots)) 119 | ? $botInfo->distinctBots 120 | : array(); 121 | $this->botIps = (is_array($botInfo->distinctIPs)) 122 | ? $botInfo->distinctIPs 123 | : array(); 124 | return $this; 125 | } 126 | 127 | /** 128 | * Returns the current bots 129 | * 130 | * @return array 131 | */ 132 | public function get() 133 | { 134 | return $this->bots; 135 | } 136 | 137 | /** 138 | * Retrieves the contents from the external csv source 139 | * and then parses it into the class level variable bots 140 | * 141 | * @throws RuntimeException 142 | * @return array|null 143 | */ 144 | private function getFromCache() 145 | { 146 | if (null !== ($csvPathname = $this->getOption(static::OPT_CACHE_PATH))) { 147 | $this->setCacheDate(); 148 | if (null !== ($lastCacheDate = $this->getCacheDate())) { 149 | $csvPath = $csvPathname . DIRECTORY_SEPARATOR . $this->getOption(static::OPT_CACHE_FILENAME); 150 | if ($lastCacheDate > (time() - $this->getOption(static::OPT_CACHE_LIFETIME)) 151 | && file_exists($csvPath) && is_readable($csvPath) 152 | && false !== ($botsCsv = file_get_contents($csvPath)) 153 | ) { 154 | return $botsCsv; 155 | } elseif (file_exists($csvPath) && false === unlink($csvPath)) { 156 | throw new RuntimeException('Cannot delete "' . $csvPath . '". Please check permissions.'); 157 | } 158 | } 159 | } 160 | $this->setCacheDate(null); 161 | return null; 162 | } 163 | 164 | /** 165 | * Retrieves the bots csv from the default source 166 | * 167 | * @throws RuntimeException 168 | * @return string 169 | */ 170 | private function getFromWeb() 171 | { 172 | $csvSource = Http::getInstance()->request(static::CSV_URL)->getResponse(); 173 | $botsCsv = trim($csvSource); 174 | if (empty($botsCsv)) { 175 | throw new RuntimeException( 176 | 'Bots CSV retrieved from external source seems to be empty. ' . 177 | 'Please either set botInfo to null or ensure the bots csv file can be retrieved.' 178 | ); 179 | } 180 | return $botsCsv; 181 | } 182 | 183 | /** 184 | * Parses the contents of the csv from the default source and 185 | * returns an array of bots in the default format 186 | * 187 | * @param string $fileContexts 188 | * @return stdClass 189 | */ 190 | private function parseCsv($fileContexts) 191 | { 192 | $botList = explode("\n", $fileContexts); 193 | $botInfo = new \stdClass; 194 | $botInfo->distinctBots = array(); 195 | $botInfo->distinctIPs = array(); 196 | foreach ($botList as $line) { 197 | $line = trim($line); 198 | if (!empty($line)) { 199 | $csvLine = str_getcsv($line); 200 | if (!isset($botInfo->distinctBots[$csvLine[0]]) 201 | && isset($csvLine[0]) 202 | && (isset($csvLine[6]) || isset($csvLine[2])) 203 | ) { 204 | $botInfo->distinctBots[$csvLine[0]] = (isset($csvLine[6])) 205 | ? $csvLine[6] 206 | : $csvLine[2]; 207 | } 208 | if (!isset($botInfo->distinctIPs[$csvLine[1]]) 209 | && isset($csvLine[1], $csvLine[0]) 210 | ) { 211 | $botInfo->distinctIPs[$csvLine[1]] = $csvLine[0]; 212 | } 213 | } 214 | } 215 | return $botInfo; 216 | } 217 | 218 | /** 219 | * Saves the current list of bots to the cache directory for use next time the script is run 220 | * 221 | * @throws RuntimeException 222 | * @return GoogleAnalyticsServerSide 223 | */ 224 | private function saveToCache() 225 | { 226 | if (null === $this->getCacheDate() 227 | && null !== ($csvPath = $this->getOption(static::OPT_CACHE_PATH)) 228 | && file_exists($csvPath) && is_writable($csvPath) 229 | ) { 230 | $csvLines = array(); 231 | foreach ($this->botIps as $ipAddress => $name) { 232 | $csvLines[] = '"' . addslashes($name) . '","' . 233 | addslashes($ipAddress) . '","' . 234 | addslashes($this->bots[$name]) . '"'; 235 | } 236 | $csvString = implode("\n", $csvLines); 237 | if (false === @file_put_contents( 238 | $csvPath . DIRECTORY_SEPARATOR . $this->getOption(static::OPT_CACHE_FILENAME), 239 | $csvString, 240 | LOCK_EX 241 | )) { 242 | $errorMsg = 'error message not available. You may have a custom error handler in place.'; 243 | $errorDet = error_get_last(); 244 | if (!empty($errorDet['message'])) { 245 | $errorMsg = $errorDet['message']; 246 | } 247 | throw new RuntimeException( 248 | 'Unable to write to file ' . 249 | $csvPath . 250 | DIRECTORY_SEPARATOR . 251 | $this->getOption(static::OPT_CACHE_FILENAME) . 252 | ' due to: ' . 253 | $errorMsg 254 | ); 255 | } 256 | } 257 | return $this; 258 | } 259 | 260 | /** 261 | * Sets the last bot cache date from the last cache file created. 262 | * 263 | * @param int $cacheDate [optional] 264 | * @throws DomainException 265 | * @return GoogleAnalyticsServerSide 266 | */ 267 | private function setCacheDate($cacheDate = null) 268 | { 269 | if (0 == func_num_args()) { 270 | $fileRelPath = DIRECTORY_SEPARATOR . $this->getOption(static::OPT_CACHE_FILENAME); 271 | $cacheDate = (null !== ($csvPathname = $this->getOption(static::OPT_CACHE_PATH)) 272 | && file_exists($csvPathname . $fileRelPath) 273 | && is_readable($csvPathname . $fileRelPath) 274 | && false !== ($fileModifiedTime = filemtime($csvPathname . $fileRelPath)) 275 | ) 276 | ? $fileModifiedTime 277 | : null; 278 | } elseif (null !== $cacheDate && !is_numeric($cacheDate)) { 279 | throw new DomainException('cacheDate must be numeric or null.'); 280 | } 281 | $this->cacheDate = $cacheDate; 282 | return $this; 283 | } 284 | 285 | /** 286 | * Returns the current cache date 287 | * 288 | * @return number|null 289 | */ 290 | public function getCacheDate() 291 | { 292 | return $this->cacheDate; 293 | } 294 | 295 | /** 296 | * {@inheritdoc} 297 | * 298 | * @param string $userAgent [optional] 299 | * @param string $remoteAddress [optional] 300 | * 301 | * @return bool 302 | */ 303 | public function isBot($userAgent = null, $remoteAddress = null) 304 | { 305 | if (empty($this->bots)) { 306 | $this->set(); 307 | } 308 | $noArgs = func_num_args(); 309 | if ($noArgs >= 1) { 310 | $this->setUserAgent($userAgent); 311 | } 312 | if ($noArgs >= 2) { 313 | $this->setRemoteAddress($remoteAddress); 314 | } 315 | $userAgent = $this->getUserAgent(); 316 | $remoteAddress = $this->getRemoteAddress(); 317 | return ((!empty($this->bots) 318 | && (in_array($userAgent, $this->bots) || array_key_exists($userAgent, $this->bots))) 319 | || (!empty($this->botIps) && array_key_exists($remoteAddress, $this->botIps)) 320 | ); 321 | } 322 | 323 | /** 324 | * {@inheritdoc} 325 | * 326 | * @param array $options 327 | * @return $this 328 | */ 329 | public function setOptions(array $options) 330 | { 331 | foreach ($options as $name => $value) { 332 | $this->getOption($name); 333 | } 334 | return parent::setOptions($options); 335 | } 336 | 337 | /** 338 | * Class Destructor 339 | */ 340 | public function __destruct() 341 | { 342 | $this->saveToCache(); 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /Gass/Exception/BadFunctionCallException.php: -------------------------------------------------------------------------------- 1 | acceptLanguage; 83 | } 84 | 85 | /** 86 | * {@inheritdoc} 87 | * 88 | * @return string 89 | */ 90 | public function getRemoteAddress() 91 | { 92 | return $this->remoteAddress; 93 | } 94 | 95 | /** 96 | * {@inheritdoc} 97 | * 98 | * @return mixed 99 | */ 100 | public function getResponse() 101 | { 102 | return $this->response; 103 | } 104 | 105 | /** 106 | * {@inheritdoc} 107 | * 108 | * @return string 109 | */ 110 | public function getUserAgent() 111 | { 112 | return $this->userAgent; 113 | } 114 | 115 | /** 116 | * {@inheritdoc} 117 | * 118 | * @param string $acceptLanguage 119 | * @throws InvalidArgumentException 120 | * @return $this 121 | */ 122 | public function setAcceptLanguage($acceptLanguage) 123 | { 124 | $langValidator = new ValidateLanguageCode; 125 | if (!$langValidator->isValid($acceptLanguage)) { 126 | throw new InvalidArgumentException( 127 | 'Accept Language validation errors: ' . implode(', ', $langValidator->getMessages()) 128 | ); 129 | } 130 | $this->acceptLanguage = $acceptLanguage; 131 | return $this; 132 | } 133 | 134 | /** 135 | * {@inheritdoc} 136 | * 137 | * @param string $remoteAddress 138 | * @throws InvalidArgumentException 139 | * @return $this 140 | */ 141 | public function setRemoteAddress($remoteAddress) 142 | { 143 | if (!empty($remoteAddress)) { 144 | $ipValidator = new ValidateIpAddress; 145 | if (!$ipValidator->isValid($remoteAddress)) { 146 | throw new InvalidArgumentException( 147 | 'Remote Address validation errors: ' . implode(', ', $ipValidator->getMessages()) 148 | ); 149 | } 150 | } 151 | $this->remoteAddress = $remoteAddress; 152 | return $this; 153 | } 154 | 155 | /** 156 | * {@inheritdoc} 157 | * 158 | * @param mixed $response 159 | * @return $this 160 | */ 161 | public function setResponse($response) 162 | { 163 | $this->response = $response; 164 | return $this; 165 | } 166 | 167 | /** 168 | * {@inheritdoc} 169 | * 170 | * @param string $userAgent 171 | * @return $this 172 | */ 173 | public function setUserAgent($userAgent) 174 | { 175 | $this->userAgent = $userAgent; 176 | return $this; 177 | } 178 | 179 | /** 180 | * Checks the return code and throws an exception if an issue with the response 181 | * 182 | * @param int $code 183 | * 184 | * @throws InvalidArgumentException 185 | * @throws RuntimeException 186 | */ 187 | protected function checkResponseCode($code) 188 | { 189 | if (!is_numeric($code)) { 190 | throw new InvalidArgumentException('HTTP Status Code must be numeric.'); 191 | } 192 | switch ($code) { 193 | case '204': 194 | $message = 'No Content'; 195 | break; 196 | case '205': 197 | $message = 'Reset Content'; 198 | break; 199 | case '206': 200 | $message = 'Partial Content'; 201 | break; 202 | case '207': 203 | $message = 'Multi-Status'; 204 | break; 205 | case '400': 206 | $message = 'Bad Request'; 207 | break; 208 | case '401': 209 | $message = 'Unauthorised Request'; 210 | break; 211 | case '402': 212 | $message = 'Payment Required'; 213 | break; 214 | case '403': 215 | $message = 'Forbidden'; 216 | break; 217 | case '404': 218 | $message = 'Not Found'; 219 | break; 220 | case '405': 221 | $message = 'Method Not Allowed'; 222 | break; 223 | case '406': 224 | $message = 'Not Acceptable'; 225 | break; 226 | case '407': 227 | $message = 'Proxy Authentication Required'; 228 | break; 229 | case '408': 230 | $message = 'Request Timeout'; 231 | break; 232 | case '409': 233 | $message = 'Conflict'; 234 | break; 235 | case '410': 236 | $message = 'Gone'; 237 | break; 238 | case '411': 239 | $message = 'Length Required'; 240 | break; 241 | case '412': 242 | $message = 'Precondition Failed'; 243 | break; 244 | case '413': 245 | $message = 'Request Entity Too Large'; 246 | break; 247 | case '414': 248 | $message = 'Request-URI Too Long'; 249 | break; 250 | case '415': 251 | $message = 'Unsupported Media Type'; 252 | break; 253 | case '416': 254 | $message = 'Request Range Not Satisfiable'; 255 | break; 256 | case '417': 257 | $message = 'Expectation Failed'; 258 | break; 259 | case '418': 260 | $message = 'I\'m a Teapot'; 261 | break; 262 | case '422': 263 | $message = 'Unprocessable Entity (WebDAV)'; 264 | break; 265 | case '423': 266 | $message = 'Locked (WebDAV)'; 267 | break; 268 | case '424': 269 | $message = 'Failed Dependancy (WebDAV)'; 270 | break; 271 | case '425': 272 | $message = 'Unordered Collection'; 273 | break; 274 | case '426': 275 | $message = 'Upgrade Required'; 276 | break; 277 | case '444': 278 | $message = 'No Response'; 279 | break; 280 | case '449': 281 | $message = 'Retry With'; 282 | break; 283 | case '450': 284 | $message = 'Blocked by Windows Parental Controls'; 285 | break; 286 | case '499': 287 | $message = 'Client Closed Request'; 288 | break; 289 | case '500': 290 | $message = 'Internal Server Error'; 291 | break; 292 | case '501': 293 | $message = 'Not Implemented'; 294 | break; 295 | case '502': 296 | $message = 'Bad Gateway'; 297 | break; 298 | case '503': 299 | $message = 'Service Unavailable'; 300 | break; 301 | case '504': 302 | $message = 'Gateway Timeout'; 303 | break; 304 | case '505': 305 | $message = 'HTTP Version Not Supported'; 306 | break; 307 | case '506': 308 | $message = 'Variant Also Negotiates'; 309 | break; 310 | case '507': 311 | $message = 'Insufficient Storage (WebDAV)'; 312 | break; 313 | case '509': 314 | $message = 'Bandwidth Limit Exceeded'; 315 | break; 316 | case '510': 317 | $message = 'Not Exceeded'; 318 | break; 319 | default: 320 | } 321 | if (isset($message)) { 322 | throw new RuntimeException($message, $code); 323 | } 324 | } 325 | 326 | /** 327 | * Makes a request with either the existing options set or the ones provided 328 | * 329 | * @param string $url 330 | * @param array $options 331 | * @return $this 332 | */ 333 | public function request($url = null, array $options = array()) 334 | { 335 | if ($url !== null) { 336 | $this->setUrl($url); 337 | } 338 | if (is_array($options) && !empty($options)) { 339 | $this->setOptions($options); 340 | } 341 | return $this; 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /Gass/Http/Curl.php: -------------------------------------------------------------------------------- 1 | 1, 59 | CURLOPT_CONNECTTIMEOUT => 5, 60 | CURLOPT_FOLLOWLOCATION => true, 61 | ); 62 | 63 | /** 64 | * Class Constructor 65 | * 66 | * @param array $options 67 | * @throws RuntimeException 68 | * @codeCoverageIgnore 69 | */ 70 | public function __construct(array $options = array()) 71 | { 72 | if (!extension_loaded('curl')) { 73 | throw new RuntimeException('cURL PHP extension is not loaded.'); 74 | } 75 | parent::__construct($options); 76 | } 77 | 78 | /** 79 | * {@inheritdoc} 80 | * 81 | * @param mixed $index [optional] 82 | * @throws DomainException 83 | * @return mixed 84 | */ 85 | public function getInfo($index = null) 86 | { 87 | if (is_resource($this->curl)) { 88 | if ($index === null) { 89 | return curl_getinfo($this->curl); 90 | } 91 | return curl_getinfo($this->curl, $index); 92 | } 93 | throw new DomainException('A cURL request has not been made yet.'); 94 | } 95 | 96 | /** 97 | * {@inheritdoc} 98 | * 99 | * @param string $url 100 | * @return $this 101 | */ 102 | public function setUrl($url) 103 | { 104 | return $this->setOption(CURLOPT_URL, $url); 105 | } 106 | 107 | /** 108 | * Closes the curl connection if one is present 109 | * 110 | * @return $this 111 | */ 112 | protected function close() 113 | { 114 | if (is_resource($this->curl)) { 115 | curl_close($this->curl); 116 | } 117 | $this->curl = null; 118 | $this->setResponse(null); 119 | return $this; 120 | } 121 | 122 | /** 123 | * {@inheritdoc} 124 | * 125 | * @param string $url 126 | * @param array $options 127 | * @throws RuntimeException 128 | * @throws UnexpectedValueException 129 | * @return $this 130 | */ 131 | public function request($url = null, array $options = array()) 132 | { 133 | parent::request($url, $options); 134 | 135 | $this->close(); 136 | $this->curl = curl_init(); 137 | 138 | if (null !== ($userAgent = $this->getUserAgent())) { 139 | $this->setOption(CURLOPT_USERAGENT, $userAgent); 140 | } 141 | 142 | $currentHeaders = $this->getOption(CURLOPT_HTTPHEADER); 143 | $extraHeaders = (is_array($currentHeaders)) ? $currentHeaders : array(); 144 | if (null !== ($acceptedLanguage = $this->getAcceptLanguage())) { 145 | $extraHeaders[] = 'Accepts-Language: ' . $acceptedLanguage; 146 | } 147 | if (null !== ($remoteAddress = $this->getRemoteAddress())) { 148 | $extraHeaders[] = 'X-Forwarded-For: ' . $remoteAddress; 149 | } 150 | if (!empty($extraHeaders)) { 151 | $this->setOption(CURLOPT_HTTPHEADER, $extraHeaders); 152 | } 153 | 154 | $extraCurlOptions = $this->getOptions(); 155 | if (!empty($extraCurlOptions) && false === @curl_setopt_array($this->curl, $extraCurlOptions)) { 156 | throw new UnexpectedValueException( 157 | 'One of the extra curl options specified is invalid. Error: ' . curl_error($this->curl) 158 | ); 159 | } 160 | 161 | if (false === ($response = curl_exec($this->curl))) { 162 | throw new RuntimeException('Source could not be retrieved. Error: ' . curl_error($this->curl)); 163 | } 164 | 165 | $this->checkResponseCode( 166 | $this->getInfo(CURLINFO_HTTP_CODE) 167 | ); 168 | 169 | $this->setResponse($response); 170 | 171 | return $this; 172 | } 173 | 174 | /** 175 | * Class Destructor 176 | */ 177 | public function __destruct() 178 | { 179 | $this->close(); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /Gass/Http/Http.php: -------------------------------------------------------------------------------- 1 | setAdapter($adapter); 75 | if (0 < func_num_args()) { 76 | $this->setOptions($options); 77 | } 78 | } 79 | 80 | /** 81 | * Protect against class clone to ensure singleton anti-pattern 82 | * 83 | * @throws RuntimeException 84 | * @final 85 | */ 86 | final public function __clone() 87 | { 88 | throw new RuntimeException('You cannot clone ' . __CLASS__); 89 | } 90 | 91 | /** 92 | * Returns the current instance of Gass\Http 93 | * Accepts the same parameters as __construct 94 | * 95 | * @see Gass\Http::__construct 96 | * @param array $options 97 | * @param string|\Gass\Http\HttpInterface $adapter 98 | * @return $this 99 | * @static 100 | */ 101 | public static function getInstance(array $options = array(), $adapter = null) 102 | { 103 | $className = __CLASS__; 104 | if (static::$instance === null || !static::$instance instanceof $className) { 105 | static::$instance = new $className($options, $adapter); 106 | } elseif (0 < func_num_args()) { 107 | if ($adapter === null && !empty($options['adapter'])) { 108 | $adapter = $options['adapter']; 109 | unset($options['adapter']); 110 | } 111 | if ($adapter !== null) { 112 | static::$instance->setAdapter($adapter); 113 | } 114 | static::$instance->setOptions($options); 115 | } 116 | return static::$instance; 117 | } 118 | 119 | /** 120 | * Call magic method 121 | * 122 | * @param string $name 123 | * @param array $arguments 124 | * @throws Exception\BadMethodCallException 125 | * @return mixed 126 | */ 127 | public function __call($name, $arguments) 128 | { 129 | if (method_exists($this->adapter, $name)) { 130 | return call_user_func_array(array($this->adapter, $name), $arguments); 131 | } 132 | throw new BadMethodCallException( 133 | 'Method ' . get_class($this->adapter) . '::' . $name . ' does not exist.' 134 | ); 135 | } 136 | 137 | /** 138 | * Call Static magic method 139 | * 140 | * @param string $name 141 | * @param array $arguments 142 | * @throws Exception\BadMethodCallException 143 | * @return mixed 144 | * @static 145 | */ 146 | public static function __callStatic($name, $arguments) 147 | { 148 | $instance = static::getInstance(); 149 | $adapter = $instance->getAdapter(); 150 | if (method_exists($adapter, $name)) { 151 | return call_user_func_array(array($adapter, $name), $arguments); 152 | } 153 | throw new BadMethodCallException( 154 | 'Method ' . get_class($adapter) . '::' . $name . ' does not exist.' 155 | ); 156 | } 157 | 158 | /** 159 | * Sets the current adapter to use 160 | * 161 | * @param string $adapter 162 | * @throws InvalidArgumentException 163 | * @return $this 164 | */ 165 | public function setAdapter($adapter) 166 | { 167 | if (is_string($adapter)) { 168 | $adapterName = 'Gass\Http\\' . ucfirst($adapter); 169 | $adapter = new $adapterName(); 170 | } 171 | if ($adapter instanceof HttpInterface) { 172 | $this->adapter = $adapter; 173 | return $this; 174 | } 175 | throw new InvalidArgumentException('The Gass\Http adapter must implement Gass\Http\HttpInterface.'); 176 | } 177 | 178 | /** 179 | * Returns the current adapter in use 180 | * 181 | * @return \Gass\Http\HttpInterface 182 | */ 183 | public function getAdapter() 184 | { 185 | return $this->adapter; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /Gass/Http/HttpInterface.php: -------------------------------------------------------------------------------- 1 | array('http' => array('method' => 'GET')), 51 | ); 52 | 53 | /** 54 | * The headers returned in response to the previous request 55 | * 56 | * @var array 57 | */ 58 | protected $responseHeaders = array(); 59 | 60 | /** 61 | * {@inheritdoc} 62 | * 63 | * @param mixed $index [optional] 64 | * @throws DomainException 65 | * @return mixed 66 | */ 67 | public function getInfo($index = null) 68 | { 69 | if (!empty($this->responseHeaders)) { 70 | if ($index !== null) { 71 | return (isset($this->responseHeaders[$index])) ? $this->responseHeaders[$index] : null; 72 | } 73 | return $this->responseHeaders; 74 | } 75 | throw new DomainException('A Http Request has not been made yet.'); 76 | } 77 | 78 | /** 79 | * Returns all options set 80 | * 81 | * @return array 82 | */ 83 | public function getOptions() 84 | { 85 | return $this->options['context']['http']; 86 | } 87 | 88 | /** 89 | * Sets a specific option 90 | * 91 | * @see https://secure.php.net/manual/en/context.http.php 92 | * @param string $name 93 | * @param mixed $value 94 | * @return $this 95 | */ 96 | public function setOption($name, $value) 97 | { 98 | $this->options['context']['http'][$name] = $value; 99 | return $this; 100 | } 101 | 102 | /** 103 | * Returns a specific option 104 | * 105 | * @see https://secure.php.net/manual/en/context.http.php 106 | * @param string $name 107 | * @return mixed 108 | */ 109 | public function getOption($name) 110 | { 111 | return (isset($this->options['context']['http'][$name])) 112 | ? $this->options['context']['http'][$name] 113 | : null; 114 | } 115 | 116 | /** 117 | * {@inheritdoc} 118 | * 119 | * @param string $url 120 | * @return $this 121 | */ 122 | public function setUrl($url) 123 | { 124 | return parent::setOption('url', $url); 125 | } 126 | 127 | /** 128 | * {@inheritdoc} 129 | * 130 | * @param string $url [optional] 131 | * @param array $options [optional] 132 | * @throws RuntimeException 133 | * @return $this 134 | */ 135 | public function request($url = null, array $options = array()) 136 | { 137 | parent::request($url, $options); 138 | 139 | $currentHeaders = $this->getOption('header'); 140 | $headersArray = ($currentHeaders === null) ? array() : $this->parseHeaders($currentHeaders); 141 | if (null !== ($userAgent = $this->getUserAgent())) { 142 | $headersArray['User-Agent'] = $userAgent; 143 | } 144 | if (null !== ($acceptedLanguage = $this->getAcceptLanguage())) { 145 | $headersArray['Accepts-Language'] = $acceptedLanguage; 146 | } 147 | if (null !== ($remoteAddress = $this->getRemoteAddress())) { 148 | $headersArray['X-Forwarded-For'] = $remoteAddress; 149 | } 150 | $newHeaders = array(); 151 | foreach ($headersArray as $key => $header) { 152 | if (!is_array($header)) { 153 | $header = array($header); 154 | } 155 | foreach ($header as $value) { 156 | $newHeaders[] = $key . ': ' . $value; 157 | } 158 | } 159 | $headerString = implode("\r\n", $newHeaders); 160 | 161 | if (!empty($headerString)) { 162 | $this->setOption('header', $headerString); 163 | } 164 | 165 | $context = stream_context_create(parent::getOption('context')); 166 | 167 | if (false === ($response = @file_get_contents(parent::getOption('url'), false, $context))) { 168 | $errorMsg = 'error message not available. You may have a custom error handler in place.'; 169 | $errorDet = error_get_last(); 170 | if (!empty($errorDet['message'])) { 171 | $errorMsg = $errorDet['message']; 172 | } 173 | throw new RuntimeException('Source could not be retrieved. Error: ' . $errorMsg); 174 | } 175 | $this->setResponseHeaders($http_response_header); 176 | if (null !== ($statusCode = $this->getInfo('Http-Code'))) { 177 | $this->checkResponseCode($statusCode); 178 | } 179 | 180 | $this->setResponse($response); 181 | 182 | return $this; 183 | } 184 | 185 | /** 186 | * Sets the response headers to the class level variable 187 | * 188 | * @param array $responseHeaders 189 | */ 190 | private function setResponseHeaders($responseHeaders) 191 | { 192 | $this->responseHeaders = $this->parseHeaders($responseHeaders); 193 | } 194 | 195 | /** 196 | * Parses HTTP headers into an associative array whether in 197 | * the $http_response_header or stream_context_create format 198 | * 199 | * @param string|array $headers 200 | * @throws InvalidArgumentException 201 | * @return array 202 | */ 203 | private function parseHeaders($headers) 204 | { 205 | if (is_string($headers)) { 206 | $headers = explode("\n", $headers); 207 | } 208 | if (!is_array($headers)) { 209 | throw new InvalidArgumentException( 210 | 'Headers must be provided in either string or numerically indexed array format.' 211 | ); 212 | } 213 | $returnHeaders = array(); 214 | foreach ($headers as $header) { 215 | $header = trim($header); 216 | if (1 === preg_match('/^([^:]+?)\s*?:([\s\S]+?)$/', $header, $matches)) { 217 | $headerVal = trim($matches[2]); 218 | if (in_array(strtolower($matches[1]), array('set-cookie'))) { 219 | if (!isset($returnHeaders[$matches[1]])) { 220 | $returnHeaders[$matches[1]] = array(); 221 | } 222 | $returnHeaders[$matches[1]][] = $headerVal; 223 | } else { 224 | $returnHeaders[$matches[1]] = $headerVal; 225 | } 226 | } elseif (1 === preg_match('/^HTTP\/\d+?\.\d+?\s+?(\d+)[\s\S]+?$/i', $header, $matches)) { 227 | $returnHeaders['Http-Code'] = $matches[1]; 228 | } 229 | } 230 | return $returnHeaders; 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /Gass/Loader/SplClassLoader.php: -------------------------------------------------------------------------------- 1 | register(); 12 | * 13 | * @author Jonathan H. Wage 14 | * @author Roman S. Borschel 15 | * @author Matthew Weier O'Phinney 16 | * @author Kris Wallsmith 17 | * @author Fabien Potencier 18 | */ 19 | 20 | namespace Gass\Loader; 21 | 22 | /** 23 | * SPL Class Cloader 24 | * 25 | * @codeCoverageIgnore Is an external dependency, intended to replace the composer requirement. 26 | */ 27 | class SplClassLoader 28 | { 29 | /** 30 | * File Extension 31 | * 32 | * @var string 33 | */ 34 | private $fileExtension = '.php'; 35 | 36 | /** 37 | * Namespace 38 | * 39 | * @var string 40 | */ 41 | private $namespace; 42 | 43 | /** 44 | * Include Path 45 | * 46 | * @var string 47 | */ 48 | private $includePath; 49 | 50 | /** 51 | * Namespace Separator 52 | * 53 | * @var string 54 | */ 55 | private $namespaceSeparator = '\\'; 56 | 57 | /** 58 | * Creates a new SplClassLoader that loads classes of the 59 | * specified namespace. 60 | * 61 | * @param string $ns [optional] The namespace to use 62 | * @param string $includePath [optional] The include path to use 63 | */ 64 | public function __construct($ns = null, $includePath = null) 65 | { 66 | $this->namespace = $ns; 67 | $this->includePath = $includePath; 68 | } 69 | 70 | /** 71 | * Sets the namespace separator used by classes in the namespace of this class loader. 72 | * 73 | * @param string $sep The separator to use 74 | */ 75 | public function setNamespaceSeparator($sep) 76 | { 77 | $this->namespaceSeparator = $sep; 78 | } 79 | 80 | /** 81 | * Gets the namespace seperator used by classes in the namespace of this class loader. 82 | * 83 | * @return string 84 | */ 85 | public function getNamespaceSeparator() 86 | { 87 | return $this->namespaceSeparator; 88 | } 89 | 90 | /** 91 | * Sets the base include path for all class files in the namespace of this class loader. 92 | * 93 | * @param string $includePath 94 | */ 95 | public function setIncludePath($includePath) 96 | { 97 | $this->includePath = $includePath; 98 | } 99 | 100 | /** 101 | * Gets the base include path for all class files in the namespace of this class loader. 102 | * 103 | * @return string $includePath 104 | */ 105 | public function getIncludePath() 106 | { 107 | return $this->includePath; 108 | } 109 | 110 | /** 111 | * Sets the file extension of class files in the namespace of this class loader. 112 | * 113 | * @param string $fileExtension 114 | */ 115 | public function setFileExtension($fileExtension) 116 | { 117 | $this->fileExtension = $fileExtension; 118 | } 119 | 120 | /** 121 | * Gets the file extension of class files in the namespace of this class loader. 122 | * 123 | * @return string $fileExtension 124 | */ 125 | public function getFileExtension() 126 | { 127 | return $this->fileExtension; 128 | } 129 | 130 | /** 131 | * Installs this class loader on the SPL autoload stack. 132 | */ 133 | public function register() 134 | { 135 | spl_autoload_register(array($this, 'loadClass')); 136 | } 137 | 138 | /** 139 | * Uninstalls this class loader from the SPL autoloader stack. 140 | */ 141 | public function unregister() 142 | { 143 | spl_autoload_unregister(array($this, 'loadClass')); 144 | } 145 | 146 | /** 147 | * Loads the given class or interface. 148 | * 149 | * @param string $className The name of the class to load 150 | */ 151 | public function loadClass($className) 152 | { 153 | if (null === $this->namespace 154 | || $this->namespace . $this->namespaceSeparator === substr( 155 | $className, 156 | 0, 157 | strlen($this->namespace . $this->namespaceSeparator) 158 | ) 159 | ) { 160 | $fileName = ''; 161 | $namespace = ''; 162 | if (false !== ($lastNsPos = strripos($className, $this->namespaceSeparator))) { 163 | $namespace = substr($className, 0, $lastNsPos); 164 | $className = substr($className, $lastNsPos + 1); 165 | $fileName = str_replace( 166 | $this->namespaceSeparator, 167 | DIRECTORY_SEPARATOR, 168 | $namespace 169 | ) . DIRECTORY_SEPARATOR; 170 | } 171 | $fileName .= str_replace('_', DIRECTORY_SEPARATOR, $className) . $this->fileExtension; 172 | 173 | require($this->includePath !== null ? $this->includePath . DIRECTORY_SEPARATOR : '') . $fileName; 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /Gass/Proxy/ProxyInterface.php: -------------------------------------------------------------------------------- 1 | messages; 61 | } 62 | 63 | /** 64 | * Returns the value being validated 65 | * 66 | * @return mixed 67 | */ 68 | public function getValue() 69 | { 70 | return $this->value; 71 | } 72 | 73 | /** 74 | * Set the validation messages 75 | * 76 | * @param array $messages 77 | * @return $this 78 | */ 79 | public function setMessages(array $messages) 80 | { 81 | $this->messages = $messages; 82 | return $this; 83 | } 84 | 85 | /** 86 | * Sets the value being validated 87 | * 88 | * @param mixed $value 89 | * @return $this 90 | */ 91 | public function setValue($value) 92 | { 93 | $this->value = $value; 94 | return $this; 95 | } 96 | 97 | /** 98 | * Adds a validation message 99 | * 100 | * @param string $message 101 | * @param string|null $value [optional] 102 | * 103 | * @return $this 104 | */ 105 | public function addMessage($message, $value = null) 106 | { 107 | if ($value === null) { 108 | $value = $this->getValue(); 109 | } 110 | $this->messages[] = (false !== strpos($message, '%value%')) 111 | ? str_replace('%value%', (string) $value, $message) 112 | : $message; 113 | return $this; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Gass/Validate/IpAddress.php: -------------------------------------------------------------------------------- 1 | true, 59 | self::OPT_ALLOW_IPV6 => true, 60 | ); 61 | 62 | /** 63 | * {@inheritdoc} 64 | * 65 | * @param array $options 66 | * @return $this 67 | */ 68 | public function setOptions(array $options) 69 | { 70 | parent::setOptions($options); 71 | if (!$this->getOption(self::OPT_ALLOW_IPV4) && !$this->getOption(self::OPT_ALLOW_IPV6)) { 72 | throw new InvalidArgumentException('Cannot validate with all IP versions disabled'); 73 | } 74 | return $this; 75 | } 76 | 77 | /** 78 | * Returns whether or not the value is valid 79 | * 80 | * @param mixed $value 81 | * @return bool 82 | */ 83 | public function isValid($value) 84 | { 85 | $value = $this->setValue($value)->getValue(); 86 | if (!is_string($value)) { 87 | $this->addMessage('The provided IP address must be a string.'); 88 | return false; 89 | } 90 | $allowIPv4 = $this->getOption(self::OPT_ALLOW_IPV4); 91 | if ($allowIPv4 && false !== filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { 92 | return true; 93 | } 94 | $allowIPv6 = $this->getOption(self::OPT_ALLOW_IPV6); 95 | if ($allowIPv6 && false !== filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { 96 | return true; 97 | } 98 | 99 | // Generate correct failed validation message 100 | $version = ''; 101 | if ($allowIPv4 && !$allowIPv6) { 102 | $version = 'v4'; 103 | } 104 | if (!$allowIPv4 && $allowIPv6) { 105 | $version = 'v6'; 106 | } 107 | $this->addMessage('"%value%" is an invalid IP' . $version . ' address', $value); 108 | return false; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Gass/Validate/LanguageCode.php: -------------------------------------------------------------------------------- 1 | setValue($value)->getValue(); 46 | if (!is_string($value)) { 47 | $this->addMessage('The provided language code must be a string.'); 48 | return false; 49 | } 50 | if (1 !== preg_match('/^([a-z]{2,3})(-[a-z]{2})??$/i', $value)) { 51 | $this->addMessage('"%value%" is an invalid language code.'); 52 | return false; 53 | } 54 | return true; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Gass/Validate/Url.php: -------------------------------------------------------------------------------- 1 | setValue($value)->getValue(); 46 | if (!is_string($value)) { 47 | $this->addMessage('The provided URL must be a string.'); 48 | return false; 49 | } 50 | $validated = filter_var($value, FILTER_VALIDATE_URL); 51 | if (false === $validated) { 52 | $this->addMessage('"%value%" is an invalid URL'); 53 | return false; 54 | } 55 | return true; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Gass/Validate/ValidateInterface.php: -------------------------------------------------------------------------------- 1 | 24 | BUGS: 25 | 26 | Google Analytics was developed by [Google][4]. 27 | This PHP adaptation is maintained by [Tom Chapman][5]. 28 | 29 | [1]: http://git.io/gass 30 | [2]: https://developers.google.com/analytics/devguides/collection/gajs/ 31 | [3]: http://www.php.net/ 32 | [4]: http://www.google.com/analytics 33 | [5]: http://tom-chapman.uk/ 34 | [6]: http://en.wikipedia.org/wiki/ECMAScript 35 | 36 | [![Build Status](https://secure.travis-ci.org/chappy84/google-analytics-server-side.png?branch=master)](http://travis-ci.org/chappy84/google-analytics-server-side) 37 | [![Master Code Coverage Status](https://coveralls.io/repos/chappy84/google-analytics-server-side/badge.png?branch=master)](https://coveralls.io/r/chappy84/google-analytics-server-side) 38 | [![SensioLabsInsight](https://insight.sensiolabs.com/projects/655a47b5-a324-487f-9b14-67da007b24d1/mini.png)](https://insight.sensiolabs.com/projects/655a47b5-a324-487f-9b14-67da007b24d1) 39 | [![Latest Stable Version](https://poser.pugx.org/chappy84/google-analytics-server-side/v/stable)](https://packagist.org/packages/chappy84/google-analytics-server-side) 40 | [![Total Downloads](https://poser.pugx.org/chappy84/google-analytics-server-side/downloads)](https://packagist.org/packages/chappy84/google-analytics-server-side) 41 | [![License](https://poser.pugx.org/chappy84/google-analytics-server-side/license)](https://packagist.org/packages/chappy84/google-analytics-server-side) 42 | 43 | Installation 44 | ------------ 45 | 46 | The package is available to install using [composer][7] from the [packagist][8] repository since 47 | v0.8.6-beta. Simply require [chappy84/google-analytics-server-side][9] and it'll be installed, 48 | checking the requirements. 49 | 50 | Alternatively if you don't want to use composer, the package, without tests, can be included by 51 | using the following: 52 | 53 | ```php 54 | require_once '' . DIRECTORY_SEPARATOR . 'Gass' . DIRECTORY_SEPARATOR . 'Bootstrap.php'; 55 | ``` 56 | 57 | where `` is the base directory of Google Analytics Server Side on your filesystem. 58 | 59 | [7]: http://getcomposer.org/ 60 | [8]: https://packagist.org/ 61 | [9]: https://packagist.org/packages/chappy84/google-analytics-server-side 62 | 63 | Usage 64 | ----- 65 | 66 | Google Analytics Server Side can be used simply in the following manner: 67 | 68 | ```php 69 | $gass = new \Gass\GoogleAnalyticsServerSide; 70 | $gass->setAccount('UA-XXXXXXX-X') 71 | ->trackPageView(); 72 | ``` 73 | 74 | The class constructor accepts an optional associative array parameter of available 75 | configuration options. Basically if there's a public method to set the variable 76 | then it can be passed as part of the array to the class. 77 | 78 | e.g. 79 | 80 | ```php 81 | $gass = new \Gass\GoogleAnalyticsServerSide; 82 | $gass->setAccount('UA-XXXXXXX-X') 83 | ->setBotInfo(true); 84 | ``` 85 | 86 | could also be done like this: 87 | 88 | ```php 89 | $gass = new \Gass\GoogleAnalyticsServerSide( 90 | array( 91 | 'account' => 'UA-XXXXXXX-X', 92 | 'botInfo' => true 93 | ) 94 | ); 95 | ``` 96 | 97 | These options can also be set individually by the method setOption, 98 | or in one go with the method `setOptions` 99 | 100 | Most of the [current basic methods][10] available in the `ga.js` tracking code have been 101 | implemented. 102 | The methods implemented are: 103 | 104 | - `deleteCustomVar` 105 | - `getAccount` 106 | - `getVersion` 107 | - `getVisitorCustomVar` 108 | - `setAccount` 109 | - `setCustomVar` 110 | - `setSessionCookieTimeout` 111 | - `setVisitorCookieTimeout` 112 | - `trackPageview` 113 | 114 | The methods not implemented yet are: 115 | 116 | - `getName` 117 | - `setSampleRate` 118 | - `setSiteSpeedSampleRate` 119 | 120 | Extra methods are also available for the information which would normally be 121 | pre-determined in the javascript or http request object from the browser. The User Agent, 122 | Server Name, Remote Address, Document Path, Document Referer, Google Analytics Version, 123 | Accepted Language, Cookies and Search Engines are all set automatically without any method 124 | calls being required by the developer. However, the following methods are available to set 125 | these variables and should be called before the `trackPageView` / `trackEvent` method to save 126 | the tracking information: 127 | 128 | - `setVersion` 129 | - `setAcceptLanguage` 130 | - `setUserAgent` 131 | - `setServerName` 132 | - `setRemoteAddress` 133 | - `setDocumentPath` 134 | - `setDocumentReferer` 135 | - `setCookies` 136 | 137 | On top of this there are also set methods to alter the default values for the the page 138 | title and document character set. 139 | These are available via the following methods: 140 | 141 | - `setPageTitle` 142 | - `setCharset` 143 | 144 | Get methods are also provided for all of the above. 145 | All methods but `get` methods allow chaining for ease of use. 146 | 147 | ### Event Tracking 148 | 149 | Event tracking is implemented using the [same functionality as in the ga.js tracking code][11] 150 | 151 | ```php 152 | \Gass\GoogleAnalyticsServerSide::trackEvent( 153 | string $category, 154 | string $action, 155 | [string $label = null, 156 | [int $value = null, 157 | [bool $nonInteraction = false]]] 158 | ); 159 | ``` 160 | 161 | N.B. `trackEvent()` does not require `trackPageView()` to be called first. 162 | However if you do not call `trackPageView` first or set `nonInteraction` to `true` then your 163 | pages/visit metric may become less than 1. 164 | 165 | [10]: https://developers.google.com/analytics/devguides/collection/gajs/methods/gaJSApiBasicConfiguration 166 | [11]: https://developers.google.com/analytics/devguides/collection/gajs/methods/gaJSApiEventTracking 167 | 168 | BotInfo 169 | ------- 170 | 171 | You must enable `BotInfo` for it to ignore any search/trawler bots. 172 | To do this you need to pass one of `true`, and associative array, or an instance of the 173 | adapter you want to use into the class. The code will default to the `BrowsCap` adapter. 174 | Setting this to true will use the default. If you pass an associative array, this will be 175 | passed to `BotInfo` and through to the Adapter. When providing an associative array you can 176 | also pass the element `'adapter'` which will tell `BotInfo` which class to use as the adapter. 177 | You can also pass an instance of a `Gass\BotInfo\BotInfoInterface` adapter which will be used by the 178 | `Gass\BotInfo` class. 179 | 180 | ### Adapters 181 | 182 | There are two adapters available in the GASS package 183 | 184 | #### BrowsCap 185 | There are five options as part of the array configuration parameter: 186 | 187 | - `\Gass\BotInfo\BrowsCap::OPT_SAVE_PATH`: The Path where the ini file and latest version file are stored. 188 | - `\Gass\BotInfo\BrowsCap::OPT_INI_FILE`: The name of the ini file to store the browscap ini data in. 189 | - `\Gass\BotInfo\BrowsCap::OPT_LATEST_VERSION_DATE_FILE`: The name of the text file to store the latest version timestamp in. 190 | - `\Gass\BotInfo\BrowsCap::OPT_BROWSCAP`: This is the same as the php ini setting [browscap][12], a file-system location where the [full_php_browscap.ini file][13] is located / can be downloaded to. 191 | - `\Gass\BotInfo\BrowsCap::OPT_DISABLE_AUTO_UPDATE`: This disables the auto-update feature for the browscap.ini file. See the [update](#updating-the-ini-file) section below for further information. 192 | 193 | N/B: `OPT_BROWSCAP` will be ignored if you have set either `OPT_SAVE_PATH` or `OPT_INI_FILE`. `OPT_SAVE_PATH` or `OPT_INI_FILE` will also override any value derived from `OPT_BROWSCAP`. This is as `OPT_BROWSCAP` is intended as a fallback for the browscap ini setting, and backwards compatibility with previous versions of this package. 194 | 195 | e.g. 196 | 197 | ```php 198 | $gass = new \Gass\GoogleAnalyticsServerSide( 199 | array( 200 | 'botInfo' => true, 201 | 'account' => 'UA-XXXXXXX-X', 202 | ) 203 | ); 204 | ``` 205 | 206 | or 207 | 208 | ```php 209 | $gass = new \Gass\GoogleAnalyticsServerSide( 210 | array( 211 | 'botInfo' => array( 212 | 'adapter' => 'BrowsCap', 213 | \Gass\BotInfo\BrowsCap::OPT_SAVE_PATH => '/var/lib/browscap', 214 | \Gass\BotInfo\BrowsCap::OPT_INI_FILE => 'full_php_browscap.ini', 215 | ), 216 | 'account' => 'UA-XXXXXXX-X' 217 | ) 218 | ); 219 | ``` 220 | 221 | or 222 | 223 | ```php 224 | $gass = new \Gass\GoogleAnalyticsServerSide(array('account' => 'UA-XXXXXXX-X')); 225 | $browsCapAdapter = new \Gass\BotInfo\BrowsCap; 226 | $gass->setBotInfo($browsCapAdapter); 227 | ``` 228 | ##### Updating the ini file 229 | 230 | When an update for the browscap ini file is available [on the server][13] the code will 231 | automatically download the file into the location provided. 232 | This functionality can be disabled by setting the `\Gass\BotInfo\BrowsCap::OPT_DISABLE_AUTO_UPDATE` configuration option to `true`. 233 | 234 | With this disabled, this package provides another method to allow you to update the ini file automatically. 235 | The script `bin/gass-browscap-updater` can be setup to run with a scheduler. 236 | This uses the same code from the auto-update feature, checking the cache file, and the latest update date stored on [browscap.org][13], then downloads a new version if one is available. 237 | 238 | Ths script has the following command line options: 239 | 240 | - `-s` / `--save-path` The path to save the ini file and latest update cache file in 241 | - `-f` / `--ini-filename` The filename given to the browscap file downloaded 242 | - `-c` / `--cache-filename`: The filename given to the latest version date cache file 243 | - `-v` / `--version`: Output's the current version 244 | - `-h` / `--help`: Displays the help 245 | 246 | See the help provided by the script for further information on usage. 247 | 248 | ##### Notes 249 | 250 | * You MUST either provide location info for the browscap ini file or have the browscap ini setting 251 | set in php.ini, otherwise this adapter will not work. 252 | * Due to an issue with the browscap ini file only being loaded when PHP starts up 253 | (which is with the web-server apache, PHP-FPM etc.) the code deals with the ini file 254 | itself, rather than using the built in get_browser function. This ensures the auto-update 255 | functionality will work without the need to restart the web-server. 256 | 257 | #### UserAgentStringInfo 258 | 259 | ***DEPRECATED*** - until udger.com (or a comparable service) implements csvs (or another data source) 260 | to replace user agent string info's csv, as user-agent-string.info has now shut down 261 | 262 | This downloaded a csv list of search engine crawlers from [user-agent-string.info][14]. 263 | There are three options as part of the array configuration parameter: 264 | 265 | - `\Gass\BotInfo\UserAgentStringInfo::OPT_CACHE_PATH`: where to save the list of bots downloaded from user-agent-string.info (required) 266 | - `\Gass\BotInfo\UserAgentStringInfo::OPT_CACHE_FILENAME`: the filename to save the list of bots to (optional, defaults to bots.csv) 267 | - `\Gass\BotInfo\UserAgentStringInfo::OPT_CACHE_LIFETIME`: number of secods before the cache expires (optional, defaults to 2592000 (30 days)) 268 | 269 | This can be implemented in the same way as the BrowsCap adapter. 270 | 271 | [12]: http://www.php.net/manual/en/misc.configuration.php#ini.browscap 272 | [13]: http://browscap.org/ 273 | [14]: http://user-agent-string.info/download 274 | 275 | Http 276 | ---- 277 | 278 | This is a singleton class which provides http functionality across all sections of the 279 | GASS package. 280 | This will default to using the `Curl` adapter if the php curl extension is available, otherwise it'll 281 | fall back to the `Stream` adapter. It requires no options. All options should be passed as a 282 | configuration option to `GoogleAnalyticsServerSide`, either via the configuration parameter 283 | in the `'http'` element or via the `setHttp` method's parameter. This can either be an associative 284 | array or an instance of the required adapter. 285 | 286 | e.g. 287 | 288 | ```php 289 | $gass = new \Gass\GoogleAnalyticsServerSide( 290 | array( 291 | 'account' => 'UA-XXXXXXX-X', 292 | 'http' => array( 293 | 'adapter' => 'Curl', 294 | CURLOPT_PROXY => 'http://exampleproxy.local:8080' 295 | ) 296 | ) 297 | ); 298 | ``` 299 | 300 | or 301 | 302 | ```php 303 | $gass = new \Gass\GoogleAnalyticsServerSide(array('account' => 'UA-XXXXXXX-X')); 304 | $httpAdapter = new \Gass\Http\Stream; 305 | $gass->setHttp($httpAdapter); 306 | ``` 307 | 308 | ### Adapters 309 | 310 | There are two Adapters available in `Gass\Http`, these are: 311 | 312 | #### Stream 313 | `Stream` creates a stream context and utilises this stream with `file_get_contents`. See 314 | [php's example][15]. Any [available options][16] provided to this class will go into the 315 | `'http'` array for the stream context, thus you may pass any headers or proxy information etc. 316 | into this to use in the connection when made. 317 | 318 | #### Curl 319 | This utilises the php extension cURL. cURL is recommended, however as it's not always 320 | available the code falls back to stream to allow all servers make http requests in the 321 | correct way. 322 | Any options provided to this class must be passed using the [curl constants][17] as 323 | identifiers (associative array keys or option names). 324 | 325 | [15]: http://www.php.net/manual/en/function.file-get-contents.php#refsect1-function.file-get-contents-examples 326 | [16]: https://secure.php.net/manual/en/context.http.php 327 | [17]: http://www.php.net/manual/en/function.curl-setopt.php#refsect1-function.curl-setopt-parameters 328 | 329 | End User Location 330 | ----------------- 331 | 332 | The End User's Location will be reported as the location of the server if you use the GA Account 333 | number in the format `UA-XXXXXXX-X` as provided by Google. If you alter this to the format 334 | `MO-XXXXXXX-X` then the location will be tracked correctly and appear on the location map as 335 | it does with the normal ECMAScript tracking. 336 | 337 | Cookies 338 | ------- 339 | 340 | Cookies are automatically set when either `trackPageView` or `trackEvent` are called. 341 | They are however only sent as headers to the browser once, thus if you call either 342 | function more than once, or call both functions, then they will only be included in the 343 | headers when the first call is made. 344 | 345 | You do have the option to turn off the sending of the cookie headers to the browser which 346 | can be done by calling `disableCookieHeaders` before calling trackPageView / trackEvent for 347 | the first time. 348 | 349 | Test Suite & CI 350 | --------------- 351 | 352 | This package uses [PHPUnit][20], along with [TravisCI][21], to test functionality on the 353 | supported PHP minor versions 5.3, 5.4, 5.5, 5.6, 7.0, and 7.1, with unofficial support for 7.2, 7.3, and 7.4. 354 | This is done by default on the latest bug fix point release of that minor point version to ensure it works. 355 | 356 | If you're submitting a pull request, please ensure you've run the test suite with PHPUnit, installed via 357 | [composer][7]. Please see the instructions [here][18] on how to install it. After which you can [install][19] 358 | phpunit, and the other required dev dependencies using `composer install`. 359 | 360 | [18]: https://getcomposer.org/doc/00-intro.md#downloading-the-composer-executable 361 | [19]: https://getcomposer.org/doc/03-cli.md#install 362 | [20]: https://github.com/sebastianbergmann/phpunit 363 | [21]: https://travis-ci.org/ 364 | 365 | PHP Version 366 | ----------- 367 | 368 | The minimum supported version is PHP 5.3.23 369 | 370 | #### Un-supported versions of PHP 371 | 372 | I've left the following branches of versions which worked with the now un-supported versions of PHP: 373 | 374 | - [PHP 5.2 Branch][22] 375 | 376 | Please feel free to use, fork etc. any of these branches. Any issues which arise in them won't 377 | have fixes attempted I'm afraid. However if you've attempted a fix yourself, please lodge a 378 | pull-request and It'll be considered. 379 | 380 | [22]: https://github.com/chappy84/google-analytics-server-side/tree/php-5.2 381 | 382 | LICENSE 383 | ------- 384 | 385 | This software uses the BSD 3-Clause license: 386 | 387 | Copyright (c) 2011-2020, Tom Chapman (http://tom-chapman.uk) 388 | All rights reserved. 389 | 390 | Redistribution and use in source and binary forms, with or without modification, are 391 | permitted provided that the following conditions are met: 392 | 393 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions 394 | and the following disclaimer. 395 | 396 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of 397 | conditions and the following disclaimer in the documentation and/or other materials provided with 398 | the distribution. 399 | 400 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to 401 | endorse or promote products derived from this software without specific prior written permission. 402 | 403 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR 404 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY 405 | AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 406 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 407 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 408 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 409 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY 410 | WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE 411 | 412 | N/B: This code is nether written or endorsed by Google or any of it's employees. 413 | "Google" and "Google Analytics" are trademarks of Google Inc. and it's respective subsidiaries. 414 | -------------------------------------------------------------------------------- /bin/gass-browscap-updater: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | getOption(BrowsCap::OPT_LATEST_VERSION_DATE_FILE); 63 | echo << -f 79 | gass-browscap-updater -c 80 | gass-browscap-updater -s -f -c 81 | 82 | Examples: 83 | gass-browscap-updater -s /var/lib/browscap -f full_php_browscap.ini 84 | gass-browscap-updater -c $defaultCacheFilename 85 | gass-browscap-updater -s /var/lib/browscap -f full_php_browscap.ini -c $defaultCacheFilename 86 | 87 | Options: 88 | -s, --save-path The path to save the ini file and latest update cache file in 89 | -f, --ini-filename The filename given to the browscap file downloaded 90 | -c, --cache-filename The filename given to the latest version date cache file 91 | -v, --version Output's the current version 92 | -h, --help Displays this help 93 | 94 | If either of the options '--save-path' or '--ini-filename' is not provided, 95 | they will be derived from the 'browscap' php.ini setting, if it is present. 96 | 97 | '--cache-filename' will default to "$defaultCacheFilename" if not provided 98 | 99 | 100 | We'd recommend this be run monthly. To do this, add the following to your crontab via 'crontab -e': 101 | 102 | 0 0 1 * * /path/to/bin/gass-browscap-updater -s -f -c 103 | 104 | This will run it at midnight on the 1st of every month 105 | 106 | 107 | HELP; 108 | exit(0); 109 | } 110 | 111 | if (array_key_exists('v', $cliOpts) || array_key_exists('version', $cliOpts)) { 112 | echo $version, PHP_EOL; 113 | exit(0); 114 | } 115 | 116 | $missingOptions = array(); 117 | if (empty($cliOpts['s']) && empty($cliOpts['save-path'])) { 118 | $missingOptions[] = '-s / --save-path'; 119 | } else { 120 | $options[BrowsCap::OPT_SAVE_PATH] = (!empty($cliOpts['s'])) ? $cliOpts['s'] : $cliOpts['save-path']; 121 | } 122 | if (empty($cliOpts['f']) && empty($cliOpts['ini-filename'])) { 123 | $missingOptions[] = '-f / --ini-filename'; 124 | } else { 125 | $options[BrowsCap::OPT_INI_FILE] = (!empty($cliOpts['f'])) ? $cliOpts['f'] : $cliOpts['ini-filename']; 126 | } 127 | if (!empty($missingOptions) && empty($options[BrowsCap::OPT_BROWSCAP])) { 128 | $missingOptsStr = implode(PHP_EOL . ' ', $missingOptions); 129 | echo <<checkIniFile(); 148 | ini_restore('memory_limit'); 149 | } catch (\Exception $e) { 150 | echo $e->getMessage(), PHP_EOL; 151 | exit(1); 152 | } 153 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "chappy84/google-analytics-server-side", 3 | "description" : "Google Analytics Server Side", 4 | "authors" : [ 5 | { 6 | "name" : "Tom Chapman", 7 | "homepage" : "http://tom-chapman.uk", 8 | "role" : "Author, Maintainer" 9 | } 10 | ], 11 | "homepage" : "http://chappy84.github.io/google-analytics-server-side/", 12 | "license" : [ 13 | "BSD-3-Clause" 14 | ], 15 | "require" : { 16 | "php" : ">=5.3.23" 17 | }, 18 | "require-dev" : { 19 | "friendsofphp/php-cs-fixer": "^1.12.0", 20 | "mikey179/vfsstream" : "^1.0.0", 21 | "mockery/mockery": "^0.9.6", 22 | "phpdocumentor/phpdocumentor": "^2.0.0", 23 | "phpunit/phpunit" : "^4.0.0", 24 | "phpunit/phpunit-mock-objects": "~2.3.0", 25 | "satooshi/php-coveralls": "^1.0.0", 26 | "squizlabs/php_codesniffer": "^2.0.0" 27 | }, 28 | "autoload" : { 29 | "psr-4" : { 30 | "Gass\\" : "Gass/" 31 | } 32 | }, 33 | "autoload-dev" : { 34 | "psr-4" : { 35 | "GassTests\\" : "Tests/" 36 | } 37 | }, 38 | "bin": [ 39 | "bin/gass-browscap-updater" 40 | ], 41 | "support" : { 42 | "source" : "https://github.com/chappy84/google-analytics-server-side", 43 | "issues" : "https://github.com/chappy84/google-analytics-server-side/issues" 44 | } 45 | } 46 | --------------------------------------------------------------------------------