diff --git a/.gitmodules b/.gitmodules index 26ff2ad7415..002cd98f552 100644 --- a/.gitmodules +++ b/.gitmodules @@ -30,3 +30,6 @@ path = plugins/LoginHttpAuth url = https://github.com/piwik/plugin-LoginHttpAuth.git branch = master +[submodule "plugins/QueuedTracking"] + path = plugins/QueuedTracking + url = https://github.com/piwik/plugin-QueuedTracking.git diff --git a/.travis.yml b/.travis.yml index e5f317ffef0..54575ea7979 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,6 +14,9 @@ php: - 5.3.3 # - hhvm +services: + - redis-server + # Separate different test suites env: matrix: diff --git a/config/global.ini.php b/config/global.ini.php index 1c5dc8dd398..6667af1b806 100644 --- a/config/global.ini.php +++ b/config/global.ini.php @@ -509,7 +509,14 @@ ; on a per-request basis; pivot_by_filter_default_column_limit = 10 + +[Redis] +host = 127.0.0.1 +port = 6379 +timeout = 0.0 + [Tracker] + ; Piwik uses first party cookies by default. If set to 1, ; the visit ID cookie will be set on the Piwik server domain as well ; this is useful when you want to do cross websites analysis @@ -709,6 +716,7 @@ Plugins[] = Morpheus Plugins[] = Contents Plugins[] = TestRunner +Plugins[] = BulkTracking [PluginsInstalled] PluginsInstalled[] = Login diff --git a/core/CliMulti/RequestCommand.php b/core/CliMulti/RequestCommand.php index a13b10c4889..948835ba1f9 100644 --- a/core/CliMulti/RequestCommand.php +++ b/core/CliMulti/RequestCommand.php @@ -35,11 +35,13 @@ protected function execute(InputInterface $input, OutputInterface $output) if ($this->isTestModeEnabled()) { Config::getInstance()->setTestEnvironment(); - $indexFile = '/tests/PHPUnit/proxy/index.php'; + $indexFile = '/tests/PHPUnit/proxy/'; } else { - $indexFile = '/index.php'; + $indexFile = '/'; } + $indexFile .= 'index.php'; + if (!empty($_GET['pid'])) { $process = new Process($_GET['pid']); diff --git a/core/Common.php b/core/Common.php index b5ed581d82a..f228053da01 100644 --- a/core/Common.php +++ b/core/Common.php @@ -518,7 +518,13 @@ public static function getRequestVar($varName, $varDefault = null, $varType = nu */ public static function generateUniqId() { - return md5(uniqid(rand(), true)); + if (function_exists('mt_rand')) { + $rand = mt_rand(); + } else { + $rand = rand(); + } + + return md5(uniqid($rand, true)); } /** @@ -1182,10 +1188,12 @@ public static function sendResponseCode($code) } if (strpos(PHP_SAPI, '-fcgi') === false) { - $key = $_SERVER['SERVER_PROTOCOL']; + $key = 'HTTP/1.1'; - if (strlen($key) > 15 || empty($key)) { - $key = 'HTTP/1.1'; + if (array_key_exists('SERVER_PROTOCOL', $_SERVER) + && strlen($_SERVER['SERVER_PROTOCOL']) < 15 + && strlen($_SERVER['SERVER_PROTOCOL']) > 1) { + $key = $_SERVER['SERVER_PROTOCOL']; } } else { diff --git a/core/Cookie.php b/core/Cookie.php index 993d97f1e29..39c0de95e48 100644 --- a/core/Cookie.php +++ b/core/Cookie.php @@ -374,7 +374,8 @@ public function get($name) */ public function __toString() { - $str = 'COOKIE ' . $this->name . ', rows count: ' . count($this->value) . ', cookie size = ' . strlen($this->generateContentString()) . " bytes\n"; + $str = 'COOKIE ' . $this->name . ', rows count: ' . count($this->value) . ', cookie size = ' . strlen($this->generateContentString()) . " bytes, "; + $str .= 'path: ' . $this->path. ', expire: ' . $this->expire . "\n"; $str .= var_export($this->value, $return = true); return $str; diff --git a/core/Date.php b/core/Date.php index 5b8e0fab6e0..abc43c69719 100644 --- a/core/Date.php +++ b/core/Date.php @@ -242,6 +242,17 @@ public static function adjustForTimezone($timestamp, $timezone) return strtotime($datetime); } + /** + * Returns the date in the "Y-m-d H:i:s" PHP format + * + * @param int $timestamp + * @return string + */ + public static function getDatetimeFromTimestamp($timestamp) + { + return date("Y-m-d H:i:s", $timestamp); + } + /** * Returns the Unix timestamp of the date in UTC. * diff --git a/core/Piwik.php b/core/Piwik.php index bc5244dd52e..b4ce8e06946 100644 --- a/core/Piwik.php +++ b/core/Piwik.php @@ -299,8 +299,10 @@ public static function hasTheUserSuperUserAccess($theUser) public static function hasUserSuperUserAccess() { try { - self::checkUserHasSuperUserAccess(); - return true; + $hasAccess = Access::getInstance()->hasSuperUserAccess(); + + return $hasAccess; + } catch (Exception $e) { return false; } diff --git a/core/Plugin.php b/core/Plugin.php index 55a70ab501b..f30c3ed23a2 100644 --- a/core/Plugin.php +++ b/core/Plugin.php @@ -131,14 +131,28 @@ public function __construct($pluginName = false) } $this->pluginName = $pluginName; - $metadataLoader = new MetadataLoader($pluginName); - $this->pluginInformation = $metadataLoader->load(); + $cache = new PersistentCache('Plugin' . $pluginName . 'Metadata'); - if ($this->hasDefinedPluginInformationInPluginClass() && $metadataLoader->hasPluginJson()) { - throw new \Exception('Plugin ' . $pluginName . ' has defined the method getInformation() and as well as having a plugin.json file. Please delete the getInformation() method from the plugin class. Alternatively, you may delete the plugin directory from plugins/' . $pluginName); + if ($cache->has()) { + $this->pluginInformation = $cache->get(); + } else { + + $metadataLoader = new MetadataLoader($pluginName); + $this->pluginInformation = $metadataLoader->load(); + + if ($this->hasDefinedPluginInformationInPluginClass() && $metadataLoader->hasPluginJson()) { + throw new \Exception('Plugin ' . $pluginName . ' has defined the method getInformation() and as well as having a plugin.json file. Please delete the getInformation() method from the plugin class. Alternatively, you may delete the plugin directory from plugins/' . $pluginName); + } + + $cache->set($this->pluginInformation); } + } - $this->cache = new PersistentCache('Plugin' . $pluginName); + private function createCacheIfNeeded() + { + if (is_null($this->cache)) { + $this->cache = new PersistentCache('Plugin' . $this->pluginName); + } } private function hasDefinedPluginInformationInPluginClass() @@ -305,6 +319,8 @@ final public function getPluginName() */ public function findComponent($componentName, $expectedSubclass) { + $this->createCacheIfNeeded(); + $this->cache->setCacheKey('Plugin' . $this->pluginName . $componentName . $expectedSubclass); $componentFile = sprintf('%s/plugins/%s/%s.php', PIWIK_INCLUDE_PATH, $this->pluginName, $componentName); @@ -349,6 +365,8 @@ public function findComponent($componentName, $expectedSubclass) public function findMultipleComponents($directoryWithinPlugin, $expectedSubclass) { + $this->createCacheIfNeeded(); + $this->cache->setCacheKey('Plugin' . $this->pluginName . $directoryWithinPlugin . $expectedSubclass); if ($this->cache->has()) { diff --git a/core/Plugin/Manager.php b/core/Plugin/Manager.php index 9f8941170b4..c1e8da7d7f1 100644 --- a/core/Plugin/Manager.php +++ b/core/Plugin/Manager.php @@ -93,6 +93,8 @@ class Manager extends Singleton 'ExampleTheme' ); + private $trackerPluginsNotToLoad = array(); + /** * Loads plugin that are enabled */ @@ -108,7 +110,7 @@ public function loadActivatedPlugins() public function loadCorePluginsDuringTracker() { $pluginsToLoad = Config::getInstance()->Plugins['Plugins']; - $pluginsToLoad = array_diff($pluginsToLoad, Tracker::getPluginsNotToLoad()); + $pluginsToLoad = array_diff($pluginsToLoad, $this->getTrackerPluginsNotToLoad()); $this->loadPlugins($pluginsToLoad); } @@ -139,18 +141,42 @@ public function loadTrackerPlugins() } } - $this->unloadPlugins(); - if (empty($pluginsTracker)) { + $this->unloadPlugins(); return array(); } - $pluginsTracker = array_diff($pluginsTracker, Tracker::getPluginsNotToLoad()); + $pluginsTracker = array_diff($pluginsTracker, $this->getTrackerPluginsNotToLoad()); $this->doNotLoadAlwaysActivatedPlugins(); $this->loadPlugins($pluginsTracker); + + // we could simply unload all plugins first before loading plugins but this way it is much faster + // since we won't have to create each plugin again and we won't have to parse each plugin metadata file + // again etc + $this->makeSureOnlyActivatedPluginsAreLoaded(); + return $pluginsTracker; } + /** + * Do not load the specified plugins (used during testing, to disable Provider plugin) + * @param array $plugins + */ + public function setTrackerPluginsNotToLoad($plugins) + { + $this->trackerPluginsNotToLoad = $plugins; + } + + /** + * Get list of plugins to not load + * + * @return array + */ + public function getTrackerPluginsNotToLoad() + { + return $this->trackerPluginsNotToLoad; + } + public function getCorePluginsDisabledByDefault() { return array_merge( $this->corePluginsDisabledByDefault, $this->coreThemesDisabledByDefault); @@ -237,7 +263,7 @@ private function isPluginUninstallable($name) public function isPluginActivated($name) { return in_array($name, $this->pluginsToLoad) - || $this->isPluginAlwaysActivated($name); + || ($this->doLoadAlwaysActivatedPlugins && $this->isPluginAlwaysActivated($name)); } /** @@ -1005,8 +1031,9 @@ private function executePluginInstall(Plugin $plugin) * * @param string $pluginName plugin name without prefix (eg. 'UserCountry') * @param Plugin $newPlugin + * @internal */ - private function addLoadedPlugin($pluginName, Plugin $newPlugin) + public function addLoadedPlugin($pluginName, Plugin $newPlugin) { $this->loadedPlugins[$pluginName] = $newPlugin; } @@ -1335,6 +1362,15 @@ private function removeInstalledVersionFromOptionTable($version) { Option::delete('version_' . $version); } + + private function makeSureOnlyActivatedPluginsAreLoaded() + { + foreach ($this->getLoadedPlugins() as $pluginName => $plugin) { + if (!in_array($pluginName, $this->pluginsToLoad)) { + $this->unloadPlugin($plugin); + } + } + } } /** diff --git a/core/Plugin/Settings.php b/core/Plugin/Settings.php index 23d1472ca96..8a674ae3708 100644 --- a/core/Plugin/Settings.php +++ b/core/Plugin/Settings.php @@ -180,12 +180,14 @@ public function getSettings() */ protected function addSetting(Setting $setting) { - if (!ctype_alnum($setting->getName())) { + $name = $setting->getName(); + + if (!ctype_alnum($name)) { $msg = sprintf('The setting name "%s" in plugin "%s" is not valid. Only alpha and numerical characters are allowed', $setting->getName(), $this->pluginName); throw new \Exception($msg); } - if (array_key_exists($setting->getName(), $this->settings)) { + if (array_key_exists($name, $this->settings)) { throw new \Exception(sprintf('A setting with name "%s" does already exist for plugin "%s"', $setting->getName(), $this->pluginName)); } @@ -195,7 +197,7 @@ protected function addSetting(Setting $setting) $setting->setStorage($this->storage); $setting->setPluginName($this->pluginName); - $this->settings[$setting->getName()] = $setting; + $this->settings[$name] = $setting; } /** @@ -265,11 +267,14 @@ private function getDefaultCONTROL($type) private function setDefaultTypeAndFieldIfNeeded(Setting $setting) { - if (!is_null($setting->uiControlType) && is_null($setting->type)) { + $hasControl = !is_null($setting->uiControlType); + $hasType = !is_null($setting->type); + + if ($hasControl && !$hasType) { $setting->type = $this->getDefaultType($setting->uiControlType); - } elseif (!is_null($setting->type) && is_null($setting->uiControlType)) { + } elseif ($hasType && !$hasControl) { $setting->uiControlType = $this->getDefaultCONTROL($setting->type); - } elseif (is_null($setting->uiControlType) && is_null($setting->type)) { + } elseif (!$hasControl && !$hasType) { $setting->type = static::TYPE_STRING; $setting->uiControlType = static::CONTROL_TEXT; } diff --git a/core/Settings/Storage/StaticStorage.php b/core/Settings/Storage/StaticStorage.php new file mode 100644 index 00000000000..ada437fa1c5 --- /dev/null +++ b/core/Settings/Storage/StaticStorage.php @@ -0,0 +1,34 @@ +stateValid = self::STATE_NOTHING_TO_NOTICE; + return array_key_exists('PIWIK_TRACKER_DEBUG', $GLOBALS) && $GLOBALS['PIWIK_TRACKER_DEBUG'] === true; } - /** - * Do not load the specified plugins (used during testing, to disable Provider plugin) - * @param array $plugins - */ - public static function setPluginsNotToLoad($plugins) + public function shouldRecordStatistics() { - self::$pluginsNotToLoad = $plugins; - } + $record = TrackerConfig::getConfigValue('record_statistics') != 0; - /** - * Get list of plugins to not load - * - * @return array - */ - public static function getPluginsNotToLoad() - { - return self::$pluginsNotToLoad; - } + if (!$record) { + Common::printDebug('Tracking is disabled in the config.ini.php via record_statistics=0'); + } - /** - * Update Tracker config - * - * @param string $name Setting name - * @param mixed $value Value - */ - private static function updateTrackerConfig($name, $value) - { - $section = Config::getInstance()->Tracker; - $section[$name] = $value; - Config::getInstance()->Tracker = $section; + return $record && $this->isInstalled(); } - protected function initRequests($args) + public static function loadTrackerEnvironment() { - $rawData = self::getRawBulkRequest(); - if (!empty($rawData)) { - $this->usingBulkTracking = strpos($rawData, '"requests"') || strpos($rawData, "'requests'"); - if ($this->usingBulkTracking) { - return $this->authenticateBulkTrackingRequests($rawData); - } - } - - // Not using bulk tracking - $this->requests = $args ? $args : (!empty($_GET) || !empty($_POST) ? array($_GET + $_POST) : array()); + SettingsServer::setIsTrackerApiRequest(); + $GLOBALS['PIWIK_TRACKER_DEBUG'] = (bool) TrackerConfig::getConfigValue('debug'); + PluginManager::getInstance()->loadTrackerPlugins(); } - private static function getRequestsArrayFromBulkRequest($rawData) + private function init() { - $rawData = trim($rawData); - $rawData = Common::sanitizeLineBreaks($rawData); - - // POST data can be array of string URLs or array of arrays w/ visit info - $jsonData = json_decode($rawData, $assoc = true); + \Piwik\FrontController::createConfigObject(); - $tokenAuth = Common::getRequestVar('token_auth', false, 'string', $jsonData); + if ($this->isDebugModeEnabled()) { + Error::setErrorHandler(); + ExceptionHandler::setUp(); - $requests = array(); - if (isset($jsonData['requests'])) { - $requests = $jsonData['requests']; + Common::printDebug("Debug enabled - Input parameters: "); + Common::printDebug(var_export($_GET, true)); } - - return array($requests, $tokenAuth); - } - - private function isBulkTrackingRequireTokenAuth() - { - return !empty(Config::getInstance()->Tracker['bulk_requests_require_authentication']); } - private function authenticateBulkTrackingRequests($rawData) + public function isInstalled() { - list($this->requests, $tokenAuth) = $this->getRequestsArrayFromBulkRequest($rawData); - - $bulkTrackingRequireTokenAuth = $this->isBulkTrackingRequireTokenAuth(); - if ($bulkTrackingRequireTokenAuth) { - if (empty($tokenAuth)) { - throw new Exception("token_auth must be specified when using Bulk Tracking Import. " - . " See Tracking Doc"); - } + if (is_null($this->isInstalled)) { + $this->isInstalled = SettingsPiwik::isPiwikInstalled(); } - if (!empty($this->requests)) { - foreach ($this->requests as &$request) { - // if a string is sent, we assume its a URL and try to parse it - if (is_string($request)) { - $params = array(); - - $url = @parse_url($request); - if (!empty($url)) { - @parse_str($url['query'], $params); - $request = $params; - } - } - - $requestObj = new Request($request, $tokenAuth); - $this->loadTrackerPlugins($requestObj); - - if ($bulkTrackingRequireTokenAuth - && !$requestObj->isAuthenticated() - ) { - throw new Exception(sprintf("token_auth specified does not have Admin permission for idsite=%s", $requestObj->getIdSite())); - } - $request = $requestObj; - } - } - - return $tokenAuth; + return $this->isInstalled; } - /** - * Main - tracks the visit/action - * - * @param array $args Optional Request Array - */ - public function main($args = null) + public function main(Handler $handler, RequestSet $requestSet) { - if (!SettingsPiwik::isPiwikInstalled()) { - return $this->handleEmptyRequest(); - } try { - $tokenAuth = $this->initRequests($args); - } catch (Exception $ex) { - $this->exitWithException($ex, true); - } - - $this->initOutputBuffer(); - - if (!empty($this->requests)) { - $this->beginTransaction(); - - try { - foreach ($this->requests as $params) { - $isAuthenticated = $this->trackRequest($params, $tokenAuth); - } - $this->runScheduledTasksIfAllowed($isAuthenticated); - $this->commitTransaction(); - } catch (DbException $e) { - Common::printDebug($e->getMessage()); - $this->rollbackTransaction(); - } - - } else { - $this->handleEmptyRequest(); + $this->init(); + $handler->init($this, $requestSet); + $this->track($handler, $requestSet); + } catch (Exception $e) { + $handler->onException($this, $requestSet, $e); } Piwik::postEvent('Tracker.end'); + $response = $handler->finish($this, $requestSet); - $this->end(); - - $this->flushOutputBuffer(); - - $this->performRedirectToUrlIfSet(); - } - - protected function initOutputBuffer() - { - ob_start(); - } - - protected function flushOutputBuffer() - { - ob_end_flush(); - } + $this->disconnectDatabase(); - protected function getOutputBuffer() - { - return ob_get_contents(); + return $response; } - protected function beginTransaction() + public function track(Handler $handler, RequestSet $requestSet) { - $this->transactionId = null; - if (!$this->shouldUseTransactions()) { + if (!$this->shouldRecordStatistics()) { return; } - $this->transactionId = self::getDatabase()->beginTransaction(); - } - protected function commitTransaction() - { - if (empty($this->transactionId)) { - return; - } - self::getDatabase()->commit($this->transactionId); - } + $requestSet->initRequestsAndTokenAuth(); - protected function rollbackTransaction() - { - if (empty($this->transactionId)) { - return; + if ($requestSet->hasRequests()) { + $handler->onStartTrackRequests($this, $requestSet); + $handler->process($this, $requestSet); + $handler->onAllRequestsTracked($this, $requestSet); } - self::getDatabase()->rollback($this->transactionId); } /** - * @return bool - */ - protected function shouldUseTransactions() - { - $isBulkRequest = count($this->requests) > 1; - return $isBulkRequest && $this->isTransactionSupported(); - } - - /** - * @return bool - */ - protected function isTransactionSupported() - { - return (bool)Config::getInstance()->Tracker['bulk_requests_use_transaction']; - } - - protected function shouldRunScheduledTasks() - { - // don't run scheduled tasks in CLI mode from Tracker, this is the case - // where we bulk load logs & don't want to lose time with tasks - return !Common::isPhpCliMode() - && $this->getState() != self::STATE_LOGGING_DISABLE; - } - - /** - * Tracker requests will automatically trigger the Scheduled tasks. - * This is useful for users who don't setup the cron, - * but still want daily/weekly/monthly PDF reports emailed automatically. - * - * This is similar to calling the API CoreAdminHome.runScheduledTasks + * @param Request $request + * @return array */ - protected static function runScheduledTasks() + public function trackRequest(Request $request) { - $now = time(); - - // Currently, there are no hourly tasks. When there are some, - // this could be too aggressive minimum interval (some hours would be skipped in case of low traffic) - $minimumInterval = Config::getInstance()->Tracker['scheduled_tasks_min_interval']; + if ($request->isEmptyRequest()) { + Common::printDebug("The request is empty"); + } else { + $this->loadTrackerPlugins(); - // If the user disabled browser archiving, he has already setup a cron - // To avoid parallel requests triggering the Scheduled Tasks, - // Get last time tasks started executing - $cache = Cache::getCacheGeneral(); + Common::printDebug("Current datetime: " . date("Y-m-d H:i:s", $request->getCurrentTimestamp())); - if ($minimumInterval <= 0 - || empty($cache['isBrowserTriggerEnabled']) - ) { - Common::printDebug("-> Scheduled tasks not running in Tracker: Browser archiving is disabled."); - return; + $visit = Visit\Factory::make(); + $visit->setRequest($request); + $visit->handle(); } - $nextRunTime = $cache['lastTrackerCronRun'] + $minimumInterval; - - if ((defined('DEBUG_FORCE_SCHEDULED_TASKS') && DEBUG_FORCE_SCHEDULED_TASKS) - || $cache['lastTrackerCronRun'] === false - || $nextRunTime < $now - ) { - $cache['lastTrackerCronRun'] = $now; - Cache::setCacheGeneral($cache); - self::initCorePiwikInTrackerMode(); - Option::set('lastTrackerCronRun', $cache['lastTrackerCronRun']); - Common::printDebug('-> Scheduled Tasks: Starting...'); - - // save current user privilege and temporarily assume Super User privilege - $isSuperUser = Piwik::hasUserSuperUserAccess(); - - // Scheduled tasks assume Super User is running - Piwik::setUserHasSuperUserAccess(); - - // While each plugins should ensure that necessary languages are loaded, - // we ensure English translations at least are loaded - Translate::loadEnglishTranslation(); - - ob_start(); - CronArchive::$url = SettingsPiwik::getPiwikUrl(); - $cronArchive = new CronArchive(); - $cronArchive->runScheduledTasksInTrackerMode(); - - $resultTasks = ob_get_contents(); - ob_clean(); - - // restore original user privilege - Piwik::setUserHasSuperUserAccess($isSuperUser); - - foreach (explode('', $resultTasks) as $resultTask) { - Common::printDebug(str_replace('
', '', $resultTask));
-            }
-
-            Common::printDebug('Finished Scheduled Tasks.');
-        } else {
-            Common::printDebug("-> Scheduled tasks not triggered.");
-        }
-        Common::printDebug("Next run will be from: " . date('Y-m-d H:i:s', $nextRunTime) . ' UTC');
+        // increment successfully logged request count. make sure to do this after try-catch,
+        // since an excluded visit is considered 'successfully logged'
+        ++$this->countOfLoggedRequests;
     }
 
-    public static $initTrackerMode = false;
-
     /**
      * Used to initialize core Piwik components on a piwik.php request
      * Eg. when cache is missed and we will be calling some APIs to generate cache
@@ -405,356 +165,67 @@ public static function initCorePiwikInTrackerMode()
                 Db::createDatabaseObject();
             }
 
-            \Piwik\Plugin\Manager::getInstance()->loadCorePluginsDuringTracker();
+            PluginManager::getInstance()->loadCorePluginsDuringTracker();
         }
     }
 
-    /**
-     * Echos an error message & other information, then exits.
-     *
-     * @param Exception $e
-     * @param bool $authenticated
-     * @param int  $statusCode eg 500
-     */
-    protected function exitWithException($e, $authenticated = false, $statusCode = 500)
+    public function getCountOfLoggedRequests()
     {
-        if ($this->hasRedirectUrl()) {
-            $this->performRedirectToUrlIfSet();
-            exit;
-        }
-
-        Common::sendResponseCode($statusCode);
-        error_log(sprintf("Error in Piwik (tracker): %s", str_replace("\n", " ", $this->getMessageFromException($e))));
-
-        if ($this->usingBulkTracking) {
-            // when doing bulk tracking we return JSON so the caller will know how many succeeded
-            $result = array(
-                'status' => 'error',
-                'tracked' => $this->countOfLoggedRequests
-            );
-            // send error when in debug mode or when authenticated (which happens when doing log importing,
-            if ((isset($GLOBALS['PIWIK_TRACKER_DEBUG']) && $GLOBALS['PIWIK_TRACKER_DEBUG'])
-                || $authenticated
-            ) {
-                $result['message'] = $this->getMessageFromException($e);
-            }
-            Common::sendHeader('Content-Type: application/json');
-            echo json_encode($result);
-            die(1);
-            exit;
-        }
-
-        if (isset($GLOBALS['PIWIK_TRACKER_DEBUG']) && $GLOBALS['PIWIK_TRACKER_DEBUG']) {
-            Common::sendHeader('Content-Type: text/html; charset=utf-8');
-            $trailer = 'Backtrace:
' . $e->getTraceAsString() . '
'; - $headerPage = file_get_contents(PIWIK_INCLUDE_PATH . '/plugins/Morpheus/templates/simpleLayoutHeader.tpl'); - $footerPage = file_get_contents(PIWIK_INCLUDE_PATH . '/plugins/Morpheus/templates/simpleLayoutFooter.tpl'); - $headerPage = str_replace('{$HTML_TITLE}', 'Piwik › Error', $headerPage); - - echo $headerPage . '

' . $this->getMessageFromException($e) . '

' . $trailer . $footerPage; - } // If not debug, but running authenticated (eg. during log import) then we display raw errors - elseif ($authenticated) { - Common::sendHeader('Content-Type: text/html; charset=utf-8'); - echo $this->getMessageFromException($e); - } else { - $this->sendResponse(); - } - - die(1); - exit; + return $this->countOfLoggedRequests; } - /** - * Returns the date in the "Y-m-d H:i:s" PHP format - * - * @param int $timestamp - * @return string - */ - public static function getDatetimeFromTimestamp($timestamp) + public function setCountOfLoggedRequests($numLoggedRequests) { - return date("Y-m-d H:i:s", $timestamp); + $this->countOfLoggedRequests = $numLoggedRequests; } - /** - * Initialization - * @param Request $request - */ - protected function init(Request $request) + public function hasLoggedRequests() { - $this->loadTrackerPlugins($request); - $this->handleDisabledTracker(); - $this->handleEmptyRequest($request); + return 0 !== $this->countOfLoggedRequests; } /** - * Cleanup + * @deprecated since 2.10.0 use {@link Date::getDatetimeFromTimestamp()} instead */ - protected function end() + public static function getDatetimeFromTimestamp($timestamp) { - if ($this->usingBulkTracking) { - $result = array( - 'status' => 'success', - 'tracked' => $this->countOfLoggedRequests - ); - - $this->outputAccessControlHeaders(); - - Common::sendHeader('Content-Type: application/json'); - echo json_encode($result); - exit; - } - switch ($this->getState()) { - case self::STATE_LOGGING_DISABLE: - $this->sendResponse(); - Common::printDebug("Logging disabled, display transparent logo"); - break; - - case self::STATE_EMPTY_REQUEST: - Common::printDebug("Empty request => Piwik page"); - echo "Piwik is a free/libre web analytics that lets you keep control of your data."; - break; - - case self::STATE_NOSCRIPT_REQUEST: - case self::STATE_NOTHING_TO_NOTICE: - default: - $this->sendResponse(); - Common::printDebug("Nothing to notice => default behaviour"); - break; - } - Common::printDebug("End of the page."); - - if ($GLOBALS['PIWIK_TRACKER_DEBUG'] === true) { - if (isset(self::$db)) { - self::$db->recordProfiling(); - Profiler::displayDbTrackerProfile(self::$db); - } - } - - self::disconnectDatabase(); + return Date::getDatetimeFromTimestamp($timestamp); } - /** - * Factory to create database objects - * - * @param array $configDb Database configuration - * @throws Exception - * @return \Piwik\Tracker\Db\Mysqli|\Piwik\Tracker\Db\Pdo\Mysql - */ - public static function factory($configDb) + public function isDatabaseConnected() { - /** - * Triggered before a connection to the database is established by the Tracker. - * - * This event can be used to change the database connection settings used by the Tracker. - * - * @param array $dbInfos Reference to an array containing database connection info, - * including: - * - * - **host**: The host name or IP address to the MySQL database. - * - **username**: The username to use when connecting to the - * database. - * - **password**: The password to use when connecting to the - * database. - * - **dbname**: The name of the Piwik MySQL database. - * - **port**: The MySQL database port to use. - * - **adapter**: either `'PDO\MYSQL'` or `'MYSQLI'` - * - **type**: The MySQL engine to use, for instance 'InnoDB' - */ - Piwik::postEvent('Tracker.getDatabaseConfig', array(&$configDb)); - - switch ($configDb['adapter']) { - case 'PDO\MYSQL': - case 'PDO_MYSQL': // old format pre Piwik 2 - require_once PIWIK_INCLUDE_PATH . '/core/Tracker/Db/Pdo/Mysql.php'; - return new Mysql($configDb); - - case 'MYSQLI': - require_once PIWIK_INCLUDE_PATH . '/core/Tracker/Db/Mysqli.php'; - return new Mysqli($configDb); - } - - throw new Exception('Unsupported database adapter ' . $configDb['adapter']); + return !is_null(self::$db); } - public static function connectPiwikTrackerDb() - { - $db = null; - $configDb = Config::getInstance()->database; - - if (!isset($configDb['port'])) { - // before 0.2.4 there is no port specified in config file - $configDb['port'] = '3306'; - } - - $db = Tracker::factory($configDb); - $db->connect(); - - return $db; - } - - protected static function connectDatabaseIfNotConnected() + public static function getDatabase() { - if (!is_null(self::$db)) { - return; - } - - try { - self::$db = self::connectPiwikTrackerDb(); - } catch (Exception $e) { - throw new DbException($e->getMessage(), $e->getCode()); + if (is_null(self::$db)) { + try { + self::$db = TrackerDb::connectPiwikTrackerDb(); + } catch (Exception $e) { + throw new DbException($e->getMessage(), $e->getCode()); + } } - } - /** - * @return Db - */ - public static function getDatabase() - { - self::connectDatabaseIfNotConnected(); return self::$db; } - public static function disconnectDatabase() + protected function disconnectDatabase() { - if (isset(self::$db)) { + if ($this->isDatabaseConnected()) { // note: I think we do this only for the tests self::$db->disconnect(); self::$db = null; } } - /** - * Returns the Tracker_Visit object. - * This method can be overwritten to use a different Tracker_Visit object - * - * @throws Exception - * @return \Piwik\Tracker\Visit - */ - protected function getNewVisitObject() - { - $visit = null; - - /** - * Triggered before a new **visit tracking object** is created. Subscribers to this - * event can force the use of a custom visit tracking object that extends from - * {@link Piwik\Tracker\VisitInterface}. - * - * @param \Piwik\Tracker\VisitInterface &$visit Initialized to null, but can be set to - * a new visit object. If it isn't modified - * Piwik uses the default class. - */ - Piwik::postEvent('Tracker.makeNewVisitObject', array(&$visit)); - - if (is_null($visit)) { - $visit = new Visit(); - } elseif (!($visit instanceof VisitInterface)) { - throw new Exception("The Visit object set in the plugin must implement VisitInterface"); - } - return $visit; - } - - private function sendResponse() - { - if (isset($GLOBALS['PIWIK_TRACKER_DEBUG']) - && $GLOBALS['PIWIK_TRACKER_DEBUG'] - ) { - return; - } - - if (strlen($this->getOutputBuffer()) > 0) { - // If there was an error during tracker, return so errors can be flushed - return; - } - - $this->outputAccessControlHeaders(); - - $request = $_GET + $_POST; - - if (array_key_exists('send_image', $request) && $request['send_image'] === '0') { - Common::sendResponseCode(204); - - return; - } - - $this->outputTransparentGif(); - } - - protected function outputTransparentGif () - { - $transGifBase64 = "R0lGODlhAQABAIAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="; - Common::sendHeader('Content-Type: image/gif'); - - print(base64_decode($transGifBase64)); - } - - protected function isVisitValid() - { - return $this->stateValid !== self::STATE_LOGGING_DISABLE - && $this->stateValid !== self::STATE_EMPTY_REQUEST; - } - - protected function getState() - { - return $this->stateValid; - } - - protected function setState($value) - { - $this->stateValid = $value; - } - - protected function loadTrackerPlugins(Request $request) - { - // Adding &dp=1 will disable the provider plugin, if token_auth is used (used to speed up bulk imports) - $disableProvider = $request->getParam('dp'); - if (!empty($disableProvider)) { - Tracker::setPluginsNotToLoad(array('Provider')); - } - - try { - $pluginsTracker = \Piwik\Plugin\Manager::getInstance()->loadTrackerPlugins(); - Common::printDebug("Loading plugins: { " . implode(", ", $pluginsTracker) . " }"); - } catch (Exception $e) { - Common::printDebug("ERROR: " . $e->getMessage()); - } - } - - protected function handleEmptyRequest(Request $request = null) - { - if (is_null($request)) { - $request = new Request($_GET + $_POST); - } - $countParameters = $request->getParamsCount(); - if ($countParameters == 0) { - $this->setState(self::STATE_EMPTY_REQUEST); - } - if ($countParameters == 1) { - $this->setState(self::STATE_NOSCRIPT_REQUEST); - } - } - - protected function handleDisabledTracker() - { - $saveStats = Config::getInstance()->Tracker['record_statistics']; - if ($saveStats == 0) { - $this->setState(self::STATE_LOGGING_DISABLE); - } - } - - protected function getTokenAuth() - { - if (!is_null($this->tokenAuth)) { - return $this->tokenAuth; - } - - return Common::getRequestVar('token_auth', false); - } - public static function setTestEnvironment($args = null, $requestMethod = null) { if (is_null($args)) { - $postData = self::getRequestsArrayFromBulkRequest(self::getRawBulkRequest()); - $args = $_GET + $postData; + $requests = new Requests(); + $args = $requests->getRequestsArrayFromBulkRequest($requests->getRawBulkRequest()); + $args = $_GET + $args; } + if (is_null($requestMethod) && array_key_exists('REQUEST_METHOD', $_SERVER)) { $requestMethod = $_SERVER['REQUEST_METHOD']; } else if (is_null($requestMethod)) { @@ -762,17 +233,17 @@ public static function setTestEnvironment($args = null, $requestMethod = null) } // Do not run scheduled tasks during tests - self::updateTrackerConfig('scheduled_tasks_min_interval', 0); + TrackerConfig::setConfigValue('scheduled_tasks_min_interval', 0); // if nothing found in _GET/_POST and we're doing a POST, assume bulk request. in which case, // we have to bypass authentication if (empty($args) && $requestMethod == 'POST') { - self::updateTrackerConfig('tracking_requests_require_authentication', 0); + TrackerConfig::setConfigValue('tracking_requests_require_authentication', 0); } // Tests can force the use of 3rd party cookie for ID visitor if (Common::getRequestVar('forceUseThirdPartyCookie', false, null, $args) == 1) { - self::updateTrackerConfig('use_third_party_id_cookie', 1); + TrackerConfig::setConfigValue('use_third_party_id_cookie', 1); } // Tests using window_look_back_for_visitor @@ -780,13 +251,13 @@ public static function setTestEnvironment($args = null, $requestMethod = null) // also look for this in bulk requests (see fake_logs_replay.log) || strpos(json_encode($args, true), '"forceLargeWindowLookBackForVisitor":"1"') !== false ) { - self::updateTrackerConfig('window_look_back_for_visitor', 2678400); + TrackerConfig::setConfigValue('window_look_back_for_visitor', 2678400); } // Tests can force the enabling of IP anonymization if (Common::getRequestVar('forceIpAnonymization', false, null, $args) == 1) { - self::connectDatabaseIfNotConnected(); + self::getDatabase(); // make sure db is initialized $privacyConfig = new PrivacyManagerConfig(); $privacyConfig->ipAddressMaskLength = 2; @@ -797,155 +268,18 @@ public static function setTestEnvironment($args = null, $requestMethod = null) $pluginsDisabled = array('Provider'); // Disable provider plugin, because it is so slow to do many reverse ip lookups - self::setPluginsNotToLoad($pluginsDisabled); - } - - /** - * Gets the error message to output when a tracking request fails. - * - * @param Exception $e - * @return string - */ - private function getMessageFromException($e) - { - // Note: duplicated from FormDatabaseSetup.isAccessDenied - // Avoid leaking the username/db name when access denied - if ($e->getCode() == 1044 || $e->getCode() == 42000) { - return "Error while connecting to the Piwik database - please check your credentials in config/config.ini.php file"; - } - if(Common::isPhpCliMode()) { - return $e->getMessage() . "\n" . $e->getTraceAsString(); - } - return $e->getMessage(); - } - - /** - * @param $params - * @param $tokenAuth - * @return array - */ - protected function trackRequest($params, $tokenAuth) - { - if ($params instanceof Request) { - $request = $params; - } else { - $request = new Request($params, $tokenAuth); - } - - $this->init($request); - - $isAuthenticated = $request->isAuthenticated(); - - try { - if ($this->isVisitValid()) { - Common::printDebug("Current datetime: " . date("Y-m-d H:i:s", $request->getCurrentTimestamp())); - - $visit = $this->getNewVisitObject(); - $visit->setRequest($request); - $visit->handle(); - } else { - Common::printDebug("The request is invalid: empty request, or maybe tracking is disabled in the config.ini.php via record_statistics=0"); - } - } catch (UnexpectedWebsiteFoundException $e) { - Common::printDebug("Exception: " . $e->getMessage()); - $this->exitWithException($e, $isAuthenticated, 400); - } catch (InvalidRequestParameterException $e) { - Common::printDebug("Exception: " . $e->getMessage()); - $this->exitWithException($e, $isAuthenticated, 400); - } catch (DbException $e) { - Common::printDebug("Exception: " . $e->getMessage()); - $this->exitWithException($e, $isAuthenticated); - } catch (Exception $e) { - $this->exitWithException($e, $isAuthenticated); - } - $this->clear(); - - // increment successfully logged request count. make sure to do this after try-catch, - // since an excluded visit is considered 'successfully logged' - ++$this->countOfLoggedRequests; - return $isAuthenticated; + PluginManager::getInstance()->setTrackerPluginsNotToLoad($pluginsDisabled); } - protected function runScheduledTasksIfAllowed($isAuthenticated) + protected function loadTrackerPlugins() { - // Do not run schedule task if we are importing logs - // or doing custom tracking (as it could slow down) try { - if (!$isAuthenticated - && $this->shouldRunScheduledTasks() - ) { - self::runScheduledTasks(); - } + $pluginManager = PluginManager::getInstance(); + $pluginsTracker = $pluginManager->loadTrackerPlugins(); + Common::printDebug("Loading plugins: { " . implode(", ", $pluginsTracker) . " }"); } catch (Exception $e) { - $this->exitWithException($e); - } - } - - /** - * @return string - */ - protected static function getRawBulkRequest() - { - return file_get_contents("php://input"); - } - - private function getRedirectUrl() - { - return Common::getRequestVar('redirecturl', false, 'string'); - } - - private function hasRedirectUrl() - { - $redirectUrl = $this->getRedirectUrl(); - - return !empty($redirectUrl); - } - - private function performRedirectToUrlIfSet() - { - if (!$this->hasRedirectUrl()) { - return; - } - - if (empty($this->requests)) { - return; - } - - $redirectUrl = $this->getRedirectUrl(); - $host = Url::getHostFromUrl($redirectUrl); - - if (empty($host)) { - return; - } - - $urls = new SiteUrls(); - $siteUrls = $urls->getAllCachedSiteUrls(); - $siteIds = $this->getAllSiteIdsWithinRequest(); - - foreach ($siteIds as $siteId) { - if (empty($siteUrls[$siteId])) { - continue; - } - - if (Url::isHostInUrls($host, $siteUrls[$siteId])) { - Url::redirectToUrl($redirectUrl); - } - } - } - - private function getAllSiteIdsWithinRequest() - { - if (empty($this->requests)) { - return array(); - } - - $siteIds = array(); - - foreach ($this->requests as $request) { - $siteIds[] = (int) $request['idsite']; + Common::printDebug("ERROR: " . $e->getMessage()); } - - return array_unique($siteIds); } } diff --git a/core/Tracker/Db.php b/core/Tracker/Db.php index 0c419d8f6eb..e5ec25f57fd 100644 --- a/core/Tracker/Db.php +++ b/core/Tracker/Db.php @@ -11,8 +11,13 @@ use Exception; use PDOStatement; use Piwik\Common; +use Piwik\Config; +use Piwik\Piwik; use Piwik\Timer; +use Piwik\Tracker; use Piwik\Tracker\Db\DbException; +use Piwik\Tracker\Db\Mysqli; +use Piwik\Tracker\Db\Pdo\Mysql; /** * Simple database wrapper. @@ -226,4 +231,63 @@ abstract public function lastInsertId(); * @return bool True if error number matches; false otherwise */ abstract public function isErrNo($e, $errno); + + /** + * Factory to create database objects + * + * @param array $configDb Database configuration + * @throws Exception + * @return \Piwik\Tracker\Db\Mysqli|\Piwik\Tracker\Db\Pdo\Mysql + */ + public static function factory($configDb) + { + /** + * Triggered before a connection to the database is established by the Tracker. + * + * This event can be used to change the database connection settings used by the Tracker. + * + * @param array $dbInfos Reference to an array containing database connection info, + * including: + * + * - **host**: The host name or IP address to the MySQL database. + * - **username**: The username to use when connecting to the + * database. + * - **password**: The password to use when connecting to the + * database. + * - **dbname**: The name of the Piwik MySQL database. + * - **port**: The MySQL database port to use. + * - **adapter**: either `'PDO\MYSQL'` or `'MYSQLI'` + * - **type**: The MySQL engine to use, for instance 'InnoDB' + */ + Piwik::postEvent('Tracker.getDatabaseConfig', array(&$configDb)); + + switch ($configDb['adapter']) { + case 'PDO\MYSQL': + case 'PDO_MYSQL': // old format pre Piwik 2 + require_once PIWIK_INCLUDE_PATH . '/core/Tracker/Db/Pdo/Mysql.php'; + return new Mysql($configDb); + + case 'MYSQLI': + require_once PIWIK_INCLUDE_PATH . '/core/Tracker/Db/Mysqli.php'; + return new Mysqli($configDb); + } + + throw new Exception('Unsupported database adapter ' . $configDb['adapter']); + } + + public static function connectPiwikTrackerDb() + { + $db = null; + $configDb = Config::getInstance()->database; + + if (!isset($configDb['port'])) { + // before 0.2.4 there is no port specified in config file + $configDb['port'] = '3306'; + } + + $db = self::factory($configDb); + $db->connect(); + + return $db; + } } diff --git a/core/Tracker/Db/Mysqli.php b/core/Tracker/Db/Mysqli.php index 94525516837..e1922e11e5d 100644 --- a/core/Tracker/Db/Mysqli.php +++ b/core/Tracker/Db/Mysqli.php @@ -291,11 +291,11 @@ public function rowCount($queryResult) */ public function beginTransaction() { - if (!$this->activeTransaction === false ) { + if (!$this->activeTransaction === false) { return; } - if ( $this->connection->autocommit(false) ) { + if ( $this->connection->autocommit(false)) { $this->activeTransaction = uniqid(); return $this->activeTransaction; } @@ -309,15 +309,17 @@ public function beginTransaction() */ public function commit($xid) { - if ($this->activeTransaction != $xid || $this->activeTransaction === false ) { + if ($this->activeTransaction != $xid || $this->activeTransaction === false) { return; } + $this->activeTransaction = false; if (!$this->connection->commit() ) { throw new DbException("Commit failed"); } + $this->connection->autocommit(true); } @@ -329,14 +331,16 @@ public function commit($xid) */ public function rollBack($xid) { - if ($this->activeTransaction != $xid || $this->activeTransaction === false ) { + if ($this->activeTransaction != $xid || $this->activeTransaction === false) { return; } + $this->activeTransaction = false; if (!$this->connection->rollback() ) { throw new DbException("Rollback failed"); } + $this->connection->autocommit(true); } } diff --git a/core/Tracker/Db/Pdo/Mysql.php b/core/Tracker/Db/Pdo/Mysql.php index 1cb72c11a6d..4d6094b4786 100644 --- a/core/Tracker/Db/Pdo/Mysql.php +++ b/core/Tracker/Db/Pdo/Mysql.php @@ -270,12 +270,13 @@ public function beginTransaction() */ public function commit($xid) { - if ($this->activeTransaction != $xid || $this->activeTransaction === false ) { + if ($this->activeTransaction != $xid || $this->activeTransaction === false) { return; } + $this->activeTransaction = false; - if (!$this->connection->commit() ) { + if (!$this->connection->commit()) { throw new DbException("Commit failed"); } } @@ -288,12 +289,13 @@ public function commit($xid) */ public function rollBack($xid) { - if ($this->activeTransaction != $xid || $this->activeTransaction === false ) { + if ($this->activeTransaction != $xid || $this->activeTransaction === false) { return; } + $this->activeTransaction = false; - if (!$this->connection->rollBack() ) { + if (!$this->connection->rollBack()) { throw new DbException("Rollback failed"); } } diff --git a/core/Tracker/GoalManager.php b/core/Tracker/GoalManager.php index 0d4bd2bfcbb..913522696a3 100644 --- a/core/Tracker/GoalManager.php +++ b/core/Tracker/GoalManager.php @@ -10,6 +10,7 @@ use Exception; use Piwik\Common; +use Piwik\Date; use Piwik\Piwik; use Piwik\Plugin\Dimension\ConversionDimension; use Piwik\Plugin\Dimension\VisitDimension; @@ -837,7 +838,7 @@ private function getGoalFromVisitor(Visitor $visitor, $visitorInformation, $acti $goal = array( 'idvisit' => $visitorInformation['idvisit'], 'idvisitor' => $visitorInformation['idvisitor'], - 'server_time' => Tracker::getDatetimeFromTimestamp($visitorInformation['visit_last_action_time']) + 'server_time' => Date::getDatetimeFromTimestamp($visitorInformation['visit_last_action_time']) ); $visitDimensions = VisitDimension::getAllDimensions(); diff --git a/core/Tracker/Handler.php b/core/Tracker/Handler.php new file mode 100644 index 00000000000..3970b910d17 --- /dev/null +++ b/core/Tracker/Handler.php @@ -0,0 +1,118 @@ +setResponse(new Response()); + } + + public function setResponse($response) + { + $this->response = $response; + } + + public function init(Tracker $tracker, RequestSet $requestSet) + { + $this->response->init($tracker); + } + + public function process(Tracker $tracker, RequestSet $requestSet) + { + foreach ($requestSet->getRequests() as $request) { + $tracker->trackRequest($request); + } + } + + public function onStartTrackRequests(Tracker $tracker, RequestSet $requestSet) + { + } + + public function onAllRequestsTracked(Tracker $tracker, RequestSet $requestSet) + { + $tasks = $this->getScheduledTasksRunner(); + if ($tasks->shouldRun($tracker)) { + $tasks->runScheduledTasks(); + } + } + + private function getScheduledTasksRunner() + { + if (is_null($this->tasksRunner)) { + $this->tasksRunner = new ScheduledTasksRunner(); + } + + return $this->tasksRunner; + } + + /** + * @internal + */ + public function setScheduledTasksRunner(ScheduledTasksRunner $runner) + { + $this->tasksRunner = $runner; + } + + public function onException(Tracker $tracker, RequestSet $requestSet, Exception $e) + { + Common::printDebug("Exception: " . $e->getMessage()); + + $statusCode = 500; + if ($e instanceof UnexpectedWebsiteFoundException) { + $statusCode = 400; + } elseif ($e instanceof InvalidRequestParameterException) { + $statusCode = 400; + } + + $this->response->outputException($tracker, $e, $statusCode); + $this->redirectIfNeeded($requestSet); + } + + public function finish(Tracker $tracker, RequestSet $requestSet) + { + $this->response->outputResponse($tracker); + $this->redirectIfNeeded($requestSet); + return $this->response->getOutput(); + } + + public function getResponse() + { + return $this->response; + } + + protected function redirectIfNeeded(RequestSet $requestSet) + { + $redirectUrl = $requestSet->shouldPerformRedirectToUrl(); + + if (!empty($redirectUrl)) { + Url::redirectToUrl($redirectUrl); + } + } + +} diff --git a/core/Tracker/Handler/Factory.php b/core/Tracker/Handler/Factory.php new file mode 100644 index 00000000000..0f1421d0a25 --- /dev/null +++ b/core/Tracker/Handler/Factory.php @@ -0,0 +1,42 @@ +params = $params; + $this->rawParams = $params; $this->tokenAuth = $tokenAuth; $this->timestamp = time(); + $this->isEmptyRequest = empty($params); // When the 'url' and referrer url parameter are not given, we might be in the 'Simple Image Tracker' mode. // The URL can default to the Referrer, which will be in this case // the URL of the page containing the Simple Image beacon if (empty($this->params['urlref']) && empty($this->params['url']) + && array_key_exists('HTTP_REFERER', $_SERVER) ) { - $url = @$_SERVER['HTTP_REFERER']; + $url = $_SERVER['HTTP_REFERER']; if (!empty($url)) { $this->params['url'] = $url; } } } + /** + * Get the params that were originally passed to the instance. These params do not contain any params that were added + * within this object. + * @return array + */ + public function getRawParams() + { + return $this->rawParams; + } + + public function getTokenAuth() + { + return $this->tokenAuth; + } + /** * @return bool */ @@ -82,21 +102,27 @@ public function isAuthenticated() * This method allows to set custom IP + server time + visitor ID, when using Tracking API. * These two attributes can be only set by the Super User (passing token_auth). */ - protected function authenticateTrackingApi($tokenAuthFromBulkRequest) + protected function authenticateTrackingApi($tokenAuth) { - $shouldAuthenticate = Config::getInstance()->Tracker['tracking_requests_require_authentication']; + $shouldAuthenticate = TrackerConfig::getConfigValue('tracking_requests_require_authentication'); + if ($shouldAuthenticate) { - $tokenAuth = $tokenAuthFromBulkRequest ? $tokenAuthFromBulkRequest : Common::getRequestVar('token_auth', false, 'string', $this->params); + + if (empty($tokenAuth)) { + $tokenAuth = Common::getRequestVar('token_auth', false, 'string', $this->params); + } + try { $idSite = $this->getIdSite(); - $this->isAuthenticated = $this->authenticateSuperUserOrAdmin($tokenAuth, $idSite); + $this->isAuthenticated = self::authenticateSuperUserOrAdmin($tokenAuth, $idSite); } catch (Exception $e) { $this->isAuthenticated = false; } - if (!$this->isAuthenticated) { - return; + + if ($this->isAuthenticated) { + Common::printDebug("token_auth is authenticated!"); } - Common::printDebug("token_auth is authenticated!"); + } else { $this->isAuthenticated = true; Common::printDebug("token_auth authentication not required"); @@ -124,10 +150,12 @@ public static function authenticateSuperUserOrAdmin($tokenAuth, $idSite) // Now checking the list of admin token_auth cached in the Tracker config file if (!empty($idSite) && $idSite > 0) { $website = Cache::getCacheWebsiteAttributes($idSite); + if (array_key_exists('admin_token_auth', $website) && in_array($tokenAuth, $website['admin_token_auth'])) { return true; } } + Common::printDebug("WARNING! token_auth = $tokenAuth is not valid, Super User / Admin was NOT authenticated"); return false; @@ -139,11 +167,13 @@ public static function authenticateSuperUserOrAdmin($tokenAuth, $idSite) public function getDaysSinceFirstVisit() { $cookieFirstVisitTimestamp = $this->getParam('_idts'); + if (!$this->isTimestampValid($cookieFirstVisitTimestamp)) { $cookieFirstVisitTimestamp = $this->getCurrentTimestamp(); } $daysSinceFirstVisit = round(($this->getCurrentTimestamp() - $cookieFirstVisitTimestamp) / 86400, $precision = 0); + if ($daysSinceFirstVisit < 0) { $daysSinceFirstVisit = 0; } @@ -324,21 +354,31 @@ public function getParams() public function getCurrentTimestamp() { $cdt = $this->getCustomTimestamp(); - if(!empty($cdt)) { + + if (!empty($cdt)) { return $cdt; } + return $this->timestamp; } + public function setCurrentTimestamp($timestamp) + { + $this->timestamp = $timestamp; + } + protected function getCustomTimestamp() { $cdt = $this->getParam('cdt'); + if (empty($cdt)) { return false; } + if (!is_numeric($cdt)) { $cdt = strtotime($cdt); } + if (!$this->isTimestampValid($cdt, $this->timestamp)) { Common::printDebug(sprintf("Datetime %s is not valid", date("Y-m-d H:i:m", $cdt))); return false; @@ -347,6 +387,7 @@ protected function getCustomTimestamp() // If timestamp in the past, token_auth is required $timeFromNow = $this->timestamp - $cdt; $isTimestampRecent = $timeFromNow < self::CUSTOM_TIMESTAMP_DOES_NOT_REQUIRE_TOKENAUTH_WHEN_NEWER_THAN; + if (!$isTimestampRecent) { if(!$this->isAuthenticated()) { Common::printDebug(sprintf("Custom timestamp is %s seconds old, requires &token_auth...", $timeFromNow)); @@ -354,6 +395,7 @@ protected function getCustomTimestamp() return false; } } + return $cdt; } @@ -366,9 +408,10 @@ protected function getCustomTimestamp() */ protected function isTimestampValid($time, $now = null) { - if(empty($now)) { + if (empty($now)) { $now = $this->getCurrentTimestamp(); } + return $time <= $now && $time > $now - 10 * 365 * 86400; } @@ -400,10 +443,29 @@ public function getIdSite() public function getUserAgent() { - $default = @$_SERVER['HTTP_USER_AGENT']; - return Common::getRequestVar('ua', is_null($default) ? false : $default, 'string', $this->params); + $default = false; + + if (array_key_exists('HTTP_USER_AGENT', $_SERVER)) { + $default = $_SERVER['HTTP_USER_AGENT']; + } + + return Common::getRequestVar('ua', $default, 'string', $this->params); } + public function getCustomVariablesInVisitScope() + { + return $this->getCustomVariables('visit'); + } + + public function getCustomVariablesInPageScope() + { + return $this->getCustomVariables('page'); + } + + /** + * @deprecated since Piwik 2.10.0. Use Request::getCustomVariablesInPageScope() or Request::getCustomVariablesInVisitScope() instead. + * When we "remove" this method we will only set visibility to "private" and pass $parameter = _cvar|cvar as an argument instead of $scope + */ public function getCustomVariables($scope) { if ($scope == 'visit') { @@ -412,16 +474,19 @@ public function getCustomVariables($scope) $parameter = 'cvar'; } - $customVar = Common::unsanitizeInputValues(Common::getRequestVar($parameter, '', 'json', $this->params)); + $cvar = Common::getRequestVar($parameter, '', 'json', $this->params); + $customVar = Common::unsanitizeInputValues($cvar); if (!is_array($customVar)) { return array(); } $customVariables = array(); - $maxCustomVars = CustomVariables::getMaxCustomVariables(); + $maxCustomVars = CustomVariables::getMaxCustomVariables(); + foreach ($customVar as $id => $keyValue) { $id = (int)$id; + if ($id < 1 || $id > $maxCustomVars || count($keyValue) != 2 @@ -437,10 +502,8 @@ public function getCustomVariables($scope) // We keep in the URL when Custom Variable have empty names // and values, as it means they can be deleted server side - $key = self::truncateCustomVariable($keyValue[0]); - $value = self::truncateCustomVariable($keyValue[1]); - $customVariables['custom_var_k' . $id] = $key; - $customVariables['custom_var_v' . $id] = $value; + $customVariables['custom_var_k' . $id] = self::truncateCustomVariable($keyValue[0]); + $customVariables['custom_var_v' . $id] = self::truncateCustomVariable($keyValue[1]); } return $customVariables; @@ -485,17 +548,17 @@ protected function makeThirdPartyCookie() protected function getCookieName() { - return Config::getInstance()->Tracker['cookie_name']; + return TrackerConfig::getConfigValue('cookie_name'); } protected function getCookieExpire() { - return $this->getCurrentTimestamp() + Config::getInstance()->Tracker['cookie_expire']; + return $this->getCurrentTimestamp() + TrackerConfig::getConfigValue('cookie_expire'); } protected function getCookiePath() { - return Config::getInstance()->Tracker['cookie_path']; + return TrackerConfig::getConfigValue('cookie_path'); } /** @@ -594,9 +657,9 @@ public function getPlugins() return $plugins; } - public function getParamsCount() + public function isEmptyRequest() { - return count($this->params); + return $this->isEmptyRequest; } const GENERATION_TIME_MS_MAXIMUM = 3600000; // 1 hour @@ -637,18 +700,19 @@ public function getUserIdHashed($userId) * @return mixed|string * @throws Exception */ - private function getIpString() + public function getIpString() { $cip = $this->getParam('cip'); - if(empty($cip)) { + if (empty($cip)) { return IP::getIpFromHeader(); } - if(!$this->isAuthenticated()) { + if (!$this->isAuthenticated()) { Common::printDebug("WARN: Tracker API 'cip' was used with invalid token_auth"); return IP::getIpFromHeader(); } + return $cip; } } diff --git a/core/Tracker/RequestSet.php b/core/Tracker/RequestSet.php new file mode 100644 index 00000000000..9f09c3ca524 --- /dev/null +++ b/core/Tracker/RequestSet.php @@ -0,0 +1,248 @@ +requests = array(); + + foreach ($requests as $request) { + + if (empty($request) && !is_array($request)) { + continue; + } + + if (!$request instanceof Request) { + $request = new Request($request, $this->getTokenAuth()); + } + + $this->requests[] = $request; + } + } + + public function setTokenAuth($tokenAuth) + { + $this->tokenAuth = $tokenAuth; + } + + public function getNumberOfRequests() + { + if (is_array($this->requests)) { + return count($this->requests); + } + + return 0; + } + + public function getRequests() + { + if (!$this->areRequestsInitialized()) { + return array(); + } + + return $this->requests; + } + + public function getTokenAuth() + { + if (!is_null($this->tokenAuth)) { + return $this->tokenAuth; + } + + return Common::getRequestVar('token_auth', false); + } + + private function areRequestsInitialized() + { + return !is_null($this->requests); + } + + public function initRequestsAndTokenAuth() + { + if ($this->areRequestsInitialized()) { + return; + } + + Piwik::postEvent('Tracker.initRequestSet', array($this)); + + if (!$this->areRequestsInitialized()) { + $this->requests = array(); + + if (!empty($_GET) || !empty($_POST)) { + $this->setRequests(array($_GET + $_POST)); + } + } + } + + public function hasRequests() + { + return !empty($this->requests); + } + + protected function getRedirectUrl() + { + return Common::getRequestVar('redirecturl', false, 'string'); + } + + protected function hasRedirectUrl() + { + $redirectUrl = $this->getRedirectUrl(); + + return !empty($redirectUrl); + } + + protected function getAllSiteIdsWithinRequest() + { + if (empty($this->requests)) { + return array(); + } + + $siteIds = array(); + foreach ($this->requests as $request) { + $siteIds[] = (int) $request->getIdSite(); + } + + return array_values(array_unique($siteIds)); + } + + // TODO maybe move to reponse? or somewhere else? not sure where! + public function shouldPerformRedirectToUrl() + { + if (!$this->hasRedirectUrl()) { + return false; + } + + if (!$this->hasRequests()) { + return false; + } + + $redirectUrl = $this->getRedirectUrl(); + $host = Url::getHostFromUrl($redirectUrl); + + if (empty($host)) { + return false; + } + + $urls = new SiteUrls(); + $siteUrls = $urls->getAllCachedSiteUrls(); + $siteIds = $this->getAllSiteIdsWithinRequest(); + + foreach ($siteIds as $siteId) { + if (empty($siteUrls[$siteId])) { + $siteUrls[$siteId] = array(); + } + + if (Url::isHostInUrls($host, $siteUrls[$siteId])) { + return $redirectUrl; + } + } + + return false; + } + + public function getState() + { + $requests = array( + 'requests' => array(), + 'env' => $this->getEnvironment(), + 'tokenAuth' => $this->getTokenAuth(), + 'time' => time() + ); + + foreach ($this->getRequests() as $request) { + $requests['requests'][] = $request->getRawParams(); + // todo we maybe need to save cdt (timestamp), tokenAuth, maybe also urlref and IP as well but we need to be + // careful with restoring those values etc since we'd probably need to check permissions etc in some cases + } + + return $requests; + } + + public function restoreState($state) + { + $backupEnv = $this->getCurrentEnvironment(); + + $this->setEnvironment($state['env']); + $this->setTokenAuth($state['tokenAuth']); + + $this->restoreEnvironment(); + $this->setRequests($state['requests']); + + foreach ($this->getRequests() as $request) { + $request->setCurrentTimestamp($state['time']); + } + + $this->resetEnvironment($backupEnv); + } + + public function rememberEnvironment() + { + $this->setEnvironment($this->getEnvironment()); + } + + public function setEnvironment($env) + { + $this->env = $env; + } + + protected function getEnvironment() + { + if (!empty($this->env)) { + return $this->env; + } + + return $this->getCurrentEnvironment(); + } + + public function restoreEnvironment() + { + if (empty($this->env)) { + return; + } + + $this->resetEnvironment($this->env); + } + + private function resetEnvironment($env) + { + $_SERVER = $env['server']; + } + + private function getCurrentEnvironment() + { + return array( + 'server' => $_SERVER + ); + } + + +} diff --git a/core/Tracker/Response.php b/core/Tracker/Response.php new file mode 100644 index 00000000000..5258d65cd2b --- /dev/null +++ b/core/Tracker/Response.php @@ -0,0 +1,175 @@ +isDebugModeEnabled()) { + $this->timer = new Timer(); + + TrackerDb::enableProfiling(); + } + } + + public function getOutput() + { + $this->outputAccessControlHeaders(); + + if (is_null($this->content) && ob_get_level() > 0) { + $this->content = ob_get_clean(); + } + + return $this->content; + } + + /** + * Echos an error message & other information, then exits. + * + * @param Tracker $tracker + * @param Exception $e + * @param int $statusCode eg 500 + */ + public function outputException(Tracker $tracker, Exception $e, $statusCode) + { + Common::sendResponseCode($statusCode); + $this->logExceptionToErrorLog($e); + + if ($tracker->isDebugModeEnabled()) { + Common::sendHeader('Content-Type: text/html; charset=utf-8'); + $trailer = 'Backtrace:
' . $e->getTraceAsString() . '
'; + $headerPage = file_get_contents(PIWIK_INCLUDE_PATH . '/plugins/Morpheus/templates/simpleLayoutHeader.tpl'); + $footerPage = file_get_contents(PIWIK_INCLUDE_PATH . '/plugins/Morpheus/templates/simpleLayoutFooter.tpl'); + $headerPage = str_replace('{$HTML_TITLE}', 'Piwik › Error', $headerPage); + + echo $headerPage . '

' . $this->getMessageFromException($e) . '

' . $trailer . $footerPage; + } else { + $this->outputApiResponse($tracker); + } + } + + public function outputResponse(Tracker $tracker) + { + if (!$tracker->shouldRecordStatistics()) { + $this->outputApiResponse($tracker); + Common::printDebug("Logging disabled, display transparent logo"); + } elseif (!$tracker->hasLoggedRequests()) { + Common::printDebug("Empty request => Piwik page"); + echo "Piwik is a free/libre web analytics that lets you keep control of your data."; + } else { + $this->outputApiResponse($tracker); + Common::printDebug("Nothing to notice => default behaviour"); + } + + Common::printDebug("End of the page."); + + if ($tracker->isDebugModeEnabled() + && $tracker->isDatabaseConnected() + && TrackerDb::isProfilingEnabled()) { + $db = Tracker::getDatabase(); + $db->recordProfiling(); + Profiler::displayDbTrackerProfile($db); + } + + if ($tracker->isDebugModeEnabled()) { + Common::printDebug($_COOKIE); + Common::printDebug((string)$this->timer); + } + } + + private function outputAccessControlHeaders() + { + $requestMethod = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET'; + + if ($requestMethod !== 'GET') { + $origin = isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : '*'; + Common::sendHeader('Access-Control-Allow-Origin: ' . $origin); + Common::sendHeader('Access-Control-Allow-Credentials: true'); + } + } + + private function getOutputBuffer() + { + return ob_get_contents(); + } + + protected function hasAlreadyPrintedOutput() + { + return strlen($this->getOutputBuffer()) > 0; + } + + private function outputApiResponse(Tracker $tracker) + { + if ($tracker->isDebugModeEnabled()) { + return; + } + + if ($this->hasAlreadyPrintedOutput()) { + return; + } + + $request = $_GET + $_POST; + + if (array_key_exists('send_image', $request) && $request['send_image'] === '0') { + Common::sendResponseCode(204); + return; + } + + $this->outputTransparentGif(); + } + + private function outputTransparentGif () + { + $transGifBase64 = "R0lGODlhAQABAIAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="; + Common::sendHeader('Content-Type: image/gif'); + + echo base64_decode($transGifBase64); + } + + /** + * Gets the error message to output when a tracking request fails. + * + * @param Exception $e + * @return string + */ + protected function getMessageFromException($e) + { + // Note: duplicated from FormDatabaseSetup.isAccessDenied + // Avoid leaking the username/db name when access denied + if ($e->getCode() == 1044 || $e->getCode() == 42000) { + return "Error while connecting to the Piwik database - please check your credentials in config/config.ini.php file"; + } + + if (Common::isPhpCliMode()) { + return $e->getMessage() . "\n" . $e->getTraceAsString(); + } + + return $e->getMessage(); + } + + protected function logExceptionToErrorLog(Exception $e) + { + error_log(sprintf("Error in Piwik (tracker): %s", str_replace("\n", " ", $this->getMessageFromException($e)))); + } + +} diff --git a/core/Tracker/ScheduledTasksRunner.php b/core/Tracker/ScheduledTasksRunner.php new file mode 100644 index 00000000000..3f4602ef2b2 --- /dev/null +++ b/core/Tracker/ScheduledTasksRunner.php @@ -0,0 +1,107 @@ +shouldRecordStatistics(); + } + + /** + * Tracker requests will automatically trigger the Scheduled tasks. + * This is useful for users who don't setup the cron, + * but still want daily/weekly/monthly PDF reports emailed automatically. + * + * This is similar to calling the API CoreAdminHome.runScheduledTasks + */ + public function runScheduledTasks() + { + $now = time(); + + // Currently, there are no hourly tasks. When there are some, + // this could be too aggressive minimum interval (some hours would be skipped in case of low traffic) + $minimumInterval = TrackerConfig::getConfigValue('scheduled_tasks_min_interval'); + + // If the user disabled browser archiving, he has already setup a cron + // To avoid parallel requests triggering the Scheduled Tasks, + // Get last time tasks started executing + $cache = Cache::getCacheGeneral(); + + if ($minimumInterval <= 0 + || empty($cache['isBrowserTriggerEnabled']) + ) { + Common::printDebug("-> Scheduled tasks not running in Tracker: Browser archiving is disabled."); + return; + } + + $nextRunTime = $cache['lastTrackerCronRun'] + $minimumInterval; + + if ((defined('DEBUG_FORCE_SCHEDULED_TASKS') && DEBUG_FORCE_SCHEDULED_TASKS) + || $cache['lastTrackerCronRun'] === false + || $nextRunTime < $now + ) { + $cache['lastTrackerCronRun'] = $now; + Cache::setCacheGeneral($cache); + Tracker::initCorePiwikInTrackerMode(); + Option::set('lastTrackerCronRun', $cache['lastTrackerCronRun']); + Common::printDebug('-> Scheduled Tasks: Starting...'); + + // save current user privilege and temporarily assume Super User privilege + $isSuperUser = Piwik::hasUserSuperUserAccess(); + + // Scheduled tasks assume Super User is running + Piwik::setUserHasSuperUserAccess(); + + // While each plugins should ensure that necessary languages are loaded, + // we ensure English translations at least are loaded + Translate::loadEnglishTranslation(); + + ob_start(); + CronArchive::$url = SettingsPiwik::getPiwikUrl(); + $cronArchive = new CronArchive(); + $cronArchive->runScheduledTasksInTrackerMode(); + + $resultTasks = ob_get_contents(); + ob_clean(); + + // restore original user privilege + Piwik::setUserHasSuperUserAccess($isSuperUser); + + foreach (explode('
', $resultTasks) as $resultTask) { + Common::printDebug(str_replace('
', '', $resultTask));
+            }
+
+            Common::printDebug('Finished Scheduled Tasks.');
+        } else {
+            Common::printDebug("-> Scheduled tasks not triggered.");
+        }
+
+        Common::printDebug("Next run will be from: " . date('Y-m-d H:i:s', $nextRunTime) . ' UTC');
+    }
+}
diff --git a/core/Tracker/SettingsStorage.php b/core/Tracker/SettingsStorage.php
index 6c54b1b993b..2e3ac5e17d2 100644
--- a/core/Tracker/SettingsStorage.php
+++ b/core/Tracker/SettingsStorage.php
@@ -9,6 +9,7 @@
 
 namespace Piwik\Tracker;
 
+use Piwik\Cache\PersistentCache;
 use Piwik\Settings\Storage;
 use Piwik\Tracker;
 
@@ -17,27 +18,16 @@
  */
 class SettingsStorage extends Storage
 {
-
     protected function loadSettings()
     {
-        $trackerCache = Cache::getCacheGeneral();
-        $settings = null;
-
-        if (array_key_exists('settingsStorage', $trackerCache)) {
-            $allSettings = $trackerCache['settingsStorage'];
+        $cache = $this->getCache();
 
-            if (is_array($allSettings) && array_key_exists($this->getOptionKey(), $allSettings)) {
-                $settings = $allSettings[$this->getOptionKey()];
-            }
+        if ($cache->has()) {
+            $settings = $cache->get();
         } else {
-            $trackerCache['settingsStorage'] = array();
-        }
-
-        if (is_null($settings)) {
             $settings = parent::loadSettings();
 
-            $trackerCache['settingsStorage'][$this->getOptionKey()] = $settings;
-            Cache::setCacheGeneral($trackerCache);
+            $cache->set($settings);
         }
 
         return $settings;
@@ -49,9 +39,15 @@ public function save()
         self::clearCache();
     }
 
+    private function getCache()
+    {
+        return new PersistentCache($this->getOptionKey());
+    }
+
     public static function clearCache()
     {
-        Cache::clearCacheGeneral();
+        Cache::deleteTrackerCache();
+        PersistentCache::_reset();
     }
 
 }
diff --git a/core/Tracker/TrackerConfig.php b/core/Tracker/TrackerConfig.php
new file mode 100644
index 00000000000..537dc8f0c90
--- /dev/null
+++ b/core/Tracker/TrackerConfig.php
@@ -0,0 +1,39 @@
+Tracker = $section;
+    }
+
+    public static function getConfigValue($name)
+    {
+        $config = self::getConfig();
+        return $config[$name];
+    }
+
+    private static function getConfig()
+    {
+        return Config::getInstance()->Tracker;
+    }
+}
diff --git a/core/Tracker/Visit/Factory.php b/core/Tracker/Visit/Factory.php
new file mode 100644
index 00000000000..71362dddeac
--- /dev/null
+++ b/core/Tracker/Visit/Factory.php
@@ -0,0 +1,48 @@
+activatePlugin('BulkTracking');
+        } catch(\Exception $e) {
+        }
+    }
+}
diff --git a/core/Url.php b/core/Url.php
index d57276d5a50..daf6c73c95b 100644
--- a/core/Url.php
+++ b/core/Url.php
@@ -466,6 +466,21 @@ public static function redirectToReferrer()
         self::redirectToUrl(self::getCurrentUrlWithoutQueryString());
     }
 
+    private static function redirectToUrlNoExit($url)
+    {
+        if (UrlHelper::isLookLikeUrl($url)
+            || strpos($url, 'index.php') === 0
+        ) {
+            Common::sendHeader("Location: $url");
+        } else {
+            echo "Invalid URL to redirect to.";
+        }
+
+        if (Common::isPhpCliMode()) {
+            throw new Exception("If you were using a browser, Piwik would redirect you to this URL: $url \n\n");
+        }
+    }
+
     /**
      * Redirects the user to the specified URL.
      *
@@ -480,17 +495,8 @@ public static function redirectToUrl($url)
         // but it is not always called fast enough
         Session::close();
 
-        if (UrlHelper::isLookLikeUrl($url)
-            || strpos($url, 'index.php') === 0
-        ) {
-            Common::sendHeader("Location: $url");
-        } else {
-            echo "Invalid URL to redirect to.";
-        }
+        self::redirectToUrlNoExit($url);
 
-        if (Common::isPhpCliMode()) {
-            throw new Exception("If you were using a browser, Piwik would redirect you to this URL: $url \n\n");
-        }
         exit;
     }
 
diff --git a/misc/others/cli-script-bootstrap.php b/misc/others/cli-script-bootstrap.php
index f26d45abcc2..9f89cfe319f 100644
--- a/misc/others/cli-script-bootstrap.php
+++ b/misc/others/cli-script-bootstrap.php
@@ -34,7 +34,5 @@
 $GLOBALS['PIWIK_TRACKER_DEBUG'] = false;
 define('PIWIK_ENABLE_DISPATCH', false);
 
-Config::getInstance()->log['log_writers'][] = 'screen';
-Config::getInstance()->log['log_level'] = 'VERBOSE';
 Config::getInstance()->log['string_message_format'] = "%message%";
 FrontController::getInstance()->init();
\ No newline at end of file
diff --git a/misc/others/geoipUpdateRows.php b/misc/others/geoipUpdateRows.php
index 09ae69d3d42..23e72fc413a 100755
--- a/misc/others/geoipUpdateRows.php
+++ b/misc/others/geoipUpdateRows.php
@@ -23,6 +23,12 @@
         Log::error('[error] You must be logged in as Super User to run this script. Please login in to Piwik and refresh this page.');
         exit;
     }
+}
+
+Log::getInstance()->setLogLevel(Log::VERBOSE);
+Log::getInstance()->addLogWriter('screen');
+
+if (!Common::isPhpCliMode()) {
     // the 'start' query param will be supplied by the AJAX requests, so if it's not there, the
     // user is viewing the page in the browser.
     if (Common::getRequestVar('start', false) === false) {
diff --git a/piwik.php b/piwik.php
index 636d6f8b220..271825f81a6 100644
--- a/piwik.php
+++ b/piwik.php
@@ -8,15 +8,13 @@
  * @package Piwik
  */
 
-use Piwik\Common;
-use Piwik\Timer;
+use Piwik\Tracker\RequestSet;
 use Piwik\Tracker;
+use Piwik\Tracker\Handler;
 
 // Note: if you wish to debug the Tracking API please see this documentation:
 // http://developer.piwik.org/api-reference/tracking-api#debugging-the-tracker
 
-define('PIWIK_ENABLE_TRACKING', true);
-
 if (!defined('PIWIK_DOCUMENT_ROOT')) {
     define('PIWIK_DOCUMENT_ROOT', dirname(__FILE__) == '/' ? '' : dirname(__FILE__));
 }
@@ -25,7 +23,6 @@
     require_once PIWIK_DOCUMENT_ROOT . '/bootstrap.php';
 }
 
-$GLOBALS['PIWIK_TRACKER_MODE'] = true;
 error_reporting(E_ALL | E_NOTICE);
 @ini_set('xdebug.show_exception_trace', 0);
 @ini_set('magic_quotes_runtime', 0);
@@ -62,62 +59,37 @@
 require_once PIWIK_INCLUDE_PATH . '/core/UrlHelper.php';
 require_once PIWIK_INCLUDE_PATH . '/core/Url.php';
 require_once PIWIK_INCLUDE_PATH . '/core/SettingsPiwik.php';
+require_once PIWIK_INCLUDE_PATH . '/core/SettingsServer.php';
 require_once PIWIK_INCLUDE_PATH . '/core/Tracker.php';
 require_once PIWIK_INCLUDE_PATH . '/core/Config.php';
 require_once PIWIK_INCLUDE_PATH . '/core/Translate.php';
 require_once PIWIK_INCLUDE_PATH . '/core/Tracker/Cache.php';
-require_once PIWIK_INCLUDE_PATH . '/core/Tracker/Db.php';
-require_once PIWIK_INCLUDE_PATH . '/core/Tracker/Db/DbException.php';
-require_once PIWIK_INCLUDE_PATH . '/core/Tracker/IgnoreCookie.php';
-require_once PIWIK_INCLUDE_PATH . '/core/Tracker/VisitInterface.php';
-require_once PIWIK_INCLUDE_PATH . '/core/Tracker/Visit.php';
-require_once PIWIK_INCLUDE_PATH . '/core/Tracker/GoalManager.php';
-require_once PIWIK_INCLUDE_PATH . '/core/Tracker/PageUrl.php';
-require_once PIWIK_INCLUDE_PATH . '/core/Tracker/TableLogAction.php';
-require_once PIWIK_INCLUDE_PATH . '/core/Tracker/Action.php';
-require_once PIWIK_INCLUDE_PATH . '/core/Tracker/ActionPageview.php';
 require_once PIWIK_INCLUDE_PATH . '/core/Tracker/Request.php';
-require_once PIWIK_INCLUDE_PATH . '/core/Tracker/VisitExcluded.php';
-require_once PIWIK_INCLUDE_PATH . '/core/Tracker/VisitorNotFoundInDb.php';
-require_once PIWIK_INCLUDE_PATH . '/core/CacheFile.php';
-require_once PIWIK_INCLUDE_PATH . '/core/Filesystem.php';
 require_once PIWIK_INCLUDE_PATH . '/core/Cookie.php';
 
+Tracker::loadTrackerEnvironment();
+
 session_cache_limiter('nocache');
 @date_default_timezone_set('UTC');
 
-if (!defined('PIWIK_ENABLE_TRACKING') || PIWIK_ENABLE_TRACKING) {
-    ob_start();
-}
+$tracker    = new Tracker();
+$requestSet = new RequestSet();
 
-\Piwik\FrontController::createConfigObject();
+ob_start();
 
-$GLOBALS['PIWIK_TRACKER_DEBUG'] = (bool) \Piwik\Config::getInstance()->Tracker['debug'];
-if ($GLOBALS['PIWIK_TRACKER_DEBUG'] === true) {
-    require_once PIWIK_INCLUDE_PATH . '/core/Error.php';
-    \Piwik\Error::setErrorHandler();
-    require_once PIWIK_INCLUDE_PATH . '/core/ExceptionHandler.php';
-    \Piwik\ExceptionHandler::setUp();
+try {
+    $handler  = Handler\Factory::make();
+    $response = $tracker->main($handler, $requestSet);
 
-    $timer = new Timer();
-    Common::printDebug("Debug enabled - Input parameters: ");
-    Common::printDebug(var_export($_GET, true));
+    if (!is_null($response)) {
+        echo $response;
+    }
 
-    \Piwik\Tracker\Db::enableProfiling();
+} catch (Exception $e) {
+    echo "Error:" . $e->getMessage();
+    exit(1);
 }
 
-if (!defined('PIWIK_ENABLE_TRACKING') || PIWIK_ENABLE_TRACKING) {
-    $process = new Tracker();
-
-    try {
-        $process->main();
-    } catch (Exception $e) {
-        echo "Error:" . $e->getMessage();
-        exit(1);
-    }
+if (ob_get_level() > 1) {
     ob_end_flush();
-    if ($GLOBALS['PIWIK_TRACKER_DEBUG'] === true) {
-        Common::printDebug($_COOKIE);
-        Common::printDebug((string)$timer);
-    }
-}
+}
\ No newline at end of file
diff --git a/plugins/BulkTracking/.gitignore b/plugins/BulkTracking/.gitignore
new file mode 100644
index 00000000000..c8c9480010d
--- /dev/null
+++ b/plugins/BulkTracking/.gitignore
@@ -0,0 +1 @@
+tests/System/processed/*xml
\ No newline at end of file
diff --git a/plugins/BulkTracking/BulkTracking.php b/plugins/BulkTracking/BulkTracking.php
new file mode 100644
index 00000000000..b55a681030f
--- /dev/null
+++ b/plugins/BulkTracking/BulkTracking.php
@@ -0,0 +1,84 @@
+ 'setHandlerIfBulkRequest',
+            'Tracker.initRequestSet' => 'initRequestSet',
+        );
+    }
+
+    public function setRequests(Requests $requests)
+    {
+        $this->requests = $requests;
+    }
+
+    public function initRequestSet(RequestSet $requestSet)
+    {
+        if ($this->isUsingBulkRequest()) {
+
+            $bulk = $this->buildBulkRequests();
+
+            list($requests, $token) = $bulk->initRequestsAndTokenAuth($bulk->getRawBulkRequest());
+
+            if ($bulk->requiresAuthentication()) {
+                $bulk->authenticateRequests($requests);
+            }
+
+            if (!$requestSet->getTokenAuth()) {
+                $requestSet->setTokenAuth($token);
+            }
+
+            $requestSet->setRequests($requests);
+        }
+    }
+
+    public function setHandlerIfBulkRequest(&$handler)
+    {
+        if (!is_null($handler)) {
+            return;
+        }
+
+        if ($this->isUsingBulkRequest()) {
+            $handler = new Handler();
+        }
+    }
+
+    private function isUsingBulkRequest()
+    {
+        $requests = $this->buildBulkRequests();
+        $rawData  = $requests->getRawBulkRequest();
+
+        return $requests->isUsingBulkRequest($rawData);
+    }
+
+    private function buildBulkRequests()
+    {
+        if (!is_null($this->requests)) {
+            return $this->requests;
+        }
+
+        return new Requests();
+    }
+}
diff --git a/plugins/BulkTracking/README.md b/plugins/BulkTracking/README.md
new file mode 100644
index 00000000000..ecb28c2d7bd
--- /dev/null
+++ b/plugins/BulkTracking/README.md
@@ -0,0 +1,18 @@
+# Piwik BulkTracking Plugin
+
+## Description
+
+Add your plugin description here.
+
+## FAQ
+
+__My question?__
+My answer
+
+## Changelog
+
+Here goes the changelog text.
+
+## Support
+
+Please direct any feedback to ...
\ No newline at end of file
diff --git a/plugins/BulkTracking/Tracker/Handler.php b/plugins/BulkTracking/Tracker/Handler.php
new file mode 100644
index 00000000000..e2602370938
--- /dev/null
+++ b/plugins/BulkTracking/Tracker/Handler.php
@@ -0,0 +1,82 @@
+setResponse(new Response());
+    }
+
+    public function onStartTrackRequests(Tracker $tracker, RequestSet $requestSet)
+    {
+        if ($this->isTransactionSupported()) {
+            $this->beginTransaction();
+        }
+    }
+
+    public function onAllRequestsTracked(Tracker $tracker, RequestSet $requestSet)
+    {
+        $this->commitTransaction();
+
+        // Do not run schedule task if we are importing logs or doing custom tracking (as it could slow down)
+    }
+
+    public function onException(Tracker $tracker, RequestSet $requestSet, Exception $e)
+    {
+        $this->rollbackTransaction();
+        parent::onException($tracker, $requestSet, $e);
+    }
+
+    private function beginTransaction()
+    {
+        if (empty($this->transactionId)) {
+            $this->transactionId = $this->getDb()->beginTransaction();
+        }
+    }
+
+    private function commitTransaction()
+    {
+        if (!empty($this->transactionId)) {
+            $this->getDb()->commit($this->transactionId);
+            $this->transactionId = null;
+        }
+    }
+
+    private function rollbackTransaction()
+    {
+        if (!empty($this->transactionId)) {
+            $this->getDb()->rollback($this->transactionId);
+            $this->transactionId = null;
+        }
+    }
+
+    private function getDb()
+    {
+        return Tracker::getDatabase();
+    }
+
+    /**
+     * @return bool
+     */
+    private function isTransactionSupported()
+    {
+        return (bool) TrackerConfig::getConfigValue('bulk_requests_use_transaction');
+    }
+
+}
diff --git a/plugins/BulkTracking/Tracker/Requests.php b/plugins/BulkTracking/Tracker/Requests.php
new file mode 100644
index 00000000000..af4eb23a158
--- /dev/null
+++ b/plugins/BulkTracking/Tracker/Requests.php
@@ -0,0 +1,111 @@
+checkTokenAuthNotEmpty($request->getTokenAuth());
+
+            if (!$request->isAuthenticated()) {
+                $msg = sprintf("token_auth specified does not have Admin permission for idsite=%s", $request->getIdSite());
+                throw new Exception($msg);
+            }
+        }
+    }
+
+    private function checkTokenAuthNotEmpty($token)
+    {
+        if (empty($token)) {
+            throw new Exception("token_auth must be specified when using Bulk Tracking Import. "
+                . " See Tracking Doc");
+        }
+    }
+
+    /**
+     * @return string
+     */
+    public function getRawBulkRequest()
+    {
+        return file_get_contents("php://input");
+    }
+
+    public function isUsingBulkRequest($rawData)
+    {
+        if (!empty($rawData)) {
+            return strpos($rawData, '"requests"') || strpos($rawData, "'requests'");
+        }
+
+        return false;
+    }
+
+    public function getRequestsArrayFromBulkRequest($rawData)
+    {
+        $rawData = trim($rawData);
+        $rawData = Common::sanitizeLineBreaks($rawData);
+
+        // POST data can be array of string URLs or array of arrays w/ visit info
+        $jsonData = json_decode($rawData, $assoc = true);
+
+        $tokenAuth = Common::getRequestVar('token_auth', false, 'string', $jsonData);
+
+        $requests = array();
+        if (isset($jsonData['requests'])) {
+            $requests = $jsonData['requests'];
+        }
+
+        return array($requests, $tokenAuth);
+    }
+
+    public function initRequestsAndTokenAuth($rawData)
+    {
+        list($requests, $tokenAuth) = $this->getRequestsArrayFromBulkRequest($rawData);
+
+        $validRequests = array();
+
+        if (!empty($requests)) {
+
+            foreach ($requests as $index => $request) {
+                // if a string is sent, we assume its a URL and try to parse it
+                if (is_string($request)) {
+                    $params = array();
+
+                    $url = @parse_url($request);
+                    if (!empty($url['query'])) {
+                        @parse_str($url['query'], $params);
+                        $validRequests[] = new Request($params, $tokenAuth);
+                    }
+                } else {
+                    $validRequests[] = new Request($request, $tokenAuth);
+                }
+            }
+        }
+
+        return array($validRequests, $tokenAuth);
+    }
+}
diff --git a/plugins/BulkTracking/Tracker/Response.php b/plugins/BulkTracking/Tracker/Response.php
new file mode 100644
index 00000000000..fd3e304a5ed
--- /dev/null
+++ b/plugins/BulkTracking/Tracker/Response.php
@@ -0,0 +1,77 @@
+logExceptionToErrorLog($e);
+
+        $result = $this->formatException($tracker, $e);
+
+        echo json_encode($result);
+    }
+
+    public function outputResponse(Tracker $tracker)
+    {
+        if ($this->hasAlreadyPrintedOutput()) {
+            return;
+        }
+
+        $result = $this->formatResponse($tracker);
+
+        echo json_encode($result);
+    }
+
+    public function getOutput()
+    {
+        Common::sendHeader('Content-Type: application/json');
+
+        return parent::getOutput();
+    }
+
+    private function formatException(Tracker $tracker, Exception $e)
+    {
+        // when doing bulk tracking we return JSON so the caller will know how many succeeded
+        $result = array(
+            'status'  => 'error',
+            'tracked' => $tracker->getCountOfLoggedRequests()
+        );
+
+        // send error when in debug mode
+        if ($tracker->isDebugModeEnabled()) {
+            $result['message'] = $this->getMessageFromException($e);
+        }
+
+        return $result;
+    }
+
+    private function formatResponse(Tracker $tracker)
+    {
+        return array(
+            'status' => 'success',
+            'tracked' => $tracker->getCountOfLoggedRequests()
+        );
+    }
+
+}
diff --git a/plugins/BulkTracking/screenshots/.gitkeep b/plugins/BulkTracking/screenshots/.gitkeep
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/plugins/BulkTracking/tests/Fixtures/SimpleFixtureTrackFewVisits.php b/plugins/BulkTracking/tests/Fixtures/SimpleFixtureTrackFewVisits.php
new file mode 100644
index 00000000000..aac9c14a2a8
--- /dev/null
+++ b/plugins/BulkTracking/tests/Fixtures/SimpleFixtureTrackFewVisits.php
@@ -0,0 +1,77 @@
+setUpWebsite();
+        $this->trackFirstVisit();
+        $this->trackSecondVisit();
+    }
+
+    public function tearDown()
+    {
+        // empty
+    }
+
+    private function setUpWebsite()
+    {
+        if (!self::siteCreated($this->idSite)) {
+            $idSite = self::createWebsite($this->dateTime, $ecommerce = 1);
+            $this->assertSame($this->idSite, $idSite);
+        }
+    }
+
+    protected function trackFirstVisit()
+    {
+        $t = self::getTracker($this->idSite, $this->dateTime, $defaultInit = true);
+
+        $t->setForceVisitDateTime(Date::factory($this->dateTime)->addHour(0.1)->getDatetime());
+        $t->setUrl('http://example.com/');
+        self::checkResponse($t->doTrackPageView('Viewing homepage'));
+
+        $t->setForceVisitDateTime(Date::factory($this->dateTime)->addHour(0.2)->getDatetime());
+        $t->setUrl('http://example.com/sub/page');
+        self::checkResponse($t->doTrackPageView('Second page view'));
+
+        $t->setForceVisitDateTime(Date::factory($this->dateTime)->addHour(0.25)->getDatetime());
+        $t->addEcommerceItem($sku = 'SKU_ID', $name = 'Test item!', $category = 'Test & Category', $price = 777, $quantity = 33);
+        self::checkResponse($t->doTrackEcommerceOrder('TestingOrder', $grandTotal = 33 * 77));
+    }
+
+    protected function trackSecondVisit()
+    {
+        $t = self::getTracker($this->idSite, $this->dateTime, $defaultInit = true);
+        $t->setIp('56.11.55.73');
+
+        $t->setForceVisitDateTime(Date::factory($this->dateTime)->addHour(0.1)->getDatetime());
+        $t->setUrl('http://example.com/sub/page');
+        self::checkResponse($t->doTrackPageView('Viewing homepage'));
+
+        $t->setForceVisitDateTime(Date::factory($this->dateTime)->addHour(0.2)->getDatetime());
+        $t->setUrl('http://example.com/?search=this is a site search query');
+        self::checkResponse($t->doTrackPageView('Site search query'));
+
+        $t->setForceVisitDateTime(Date::factory($this->dateTime)->addHour(0.3)->getDatetime());
+        $t->addEcommerceItem($sku = 'SKU_ID2', $name = 'A durable item', $category = 'Best seller', $price = 321);
+        self::checkResponse($t->doTrackEcommerceCartUpdate($grandTotal = 33 * 77));
+    }
+}
\ No newline at end of file
diff --git a/plugins/BulkTracking/tests/Framework/Mock/Tracker/Requests.php b/plugins/BulkTracking/tests/Framework/Mock/Tracker/Requests.php
new file mode 100644
index 00000000000..c6c39c4ee94
--- /dev/null
+++ b/plugins/BulkTracking/tests/Framework/Mock/Tracker/Requests.php
@@ -0,0 +1,42 @@
+rawData = $rawData;
+    }
+
+    public function getRawBulkRequest()
+    {
+        if (!is_null($this->rawData)) {
+            return $this->rawData;
+        }
+
+        return parent::getRawBulkRequest();
+    }
+
+    public function enableRequiresAuth()
+    {
+        $this->requiresAuth = true;
+    }
+
+    public function requiresAuthentication()
+    {
+        return $this->requiresAuth;
+    }
+
+}
diff --git a/plugins/BulkTracking/tests/Framework/Mock/Tracker/Response.php b/plugins/BulkTracking/tests/Framework/Mock/Tracker/Response.php
new file mode 100644
index 00000000000..e514745ddb1
--- /dev/null
+++ b/plugins/BulkTracking/tests/Framework/Mock/Tracker/Response.php
@@ -0,0 +1,21 @@
+bulk = new BulkTracking();
+
+        $this->pluginBackup = Plugin\Manager::getInstance()->getLoadedPlugin('BulkTracking');
+        Plugin\Manager::getInstance()->addLoadedPlugin('BulkTracking', $this->bulk);
+    }
+
+    public function tearDown()
+    {
+        Plugin\Manager::getInstance()->addLoadedPlugin('BulkTracking', $this->pluginBackup);
+        parent::tearDown();
+    }
+
+    protected function getSuperUserToken()
+    {
+        Fixture::createSuperUser(false);
+        return Fixture::getTokenAuth();
+    }
+
+    protected function injectRawDataToBulk($rawData, $requiresAuth = false)
+    {
+        $requests = new Requests();
+        $requests->setRawData($rawData);
+
+        if ($requiresAuth) {
+            $requests->enableRequiresAuth();
+        }
+
+        $this->bulk->setRequests($requests);
+    }
+
+    protected function initRequestSet($rawData, $requiresAuth = false, $initToken = null)
+    {
+        $requestSet = new RequestSet();
+
+        if (!is_null($initToken)) {
+            $requestSet->setTokenAuth($initToken);
+        }
+
+        $this->injectRawDataToBulk($rawData, $requiresAuth);
+
+        $this->bulk->initRequestSet($requestSet);
+
+        return $requestSet;
+    }
+
+    protected function getDummyRequest($token = null)
+    {
+        $params = array(array('idsite' => '1', 'rec' => '1'), array('idsite' => '2', 'rec' => '1'));
+        $params = array('requests' => $params);
+
+        if (!is_null($token)) {
+            $params['token_auth'] = $token;
+        }
+
+        $request = json_encode($params);
+
+        return $request;
+    }
+}
diff --git a/plugins/BulkTracking/tests/Integration/BulkTrackingTest.php b/plugins/BulkTracking/tests/Integration/BulkTrackingTest.php
new file mode 100644
index 00000000000..c495afcee1e
--- /dev/null
+++ b/plugins/BulkTracking/tests/Integration/BulkTrackingTest.php
@@ -0,0 +1,147 @@
+bulk->initRequestSet($requestSet);
+
+        $this->assertEquals(array(), $requestSet->getRequests());
+        $this->assertEquals(false, $requestSet->getTokenAuth());
+    }
+
+    public function test_initRequestSet_shouldNotSetAnything_IfNotBulkRequestRawDataIsGiven()
+    {
+        $requestSet = $this->initRequestSet('invalid:requests');
+
+        $this->assertEquals(array(), $requestSet->getRequests());
+        $this->assertEquals(false, $requestSet->getTokenAuth());
+    }
+
+    public function test_initRequestSet_shouldInitialize_AsItIsABulkRequest()
+    {
+        $token   = $this->getSuperUserToken();
+        $request = $this->getDummyRequest($token);
+
+        $requestSet = $this->initRequestSet($request);
+
+        $requests = $requestSet->getRequests();
+        $this->assertCount(2, $requests);
+        $this->assertEquals(array('idsite' => '1', 'rec' => '1'), $requests[0]->getParams());
+        $this->assertEquals(array('idsite' => '2', 'rec' => '1'), $requests[1]->getParams());
+        $this->assertEquals($token, $requestSet->getTokenAuth());
+    }
+
+    public function test_initRequestSet_shouldNotOverwriteAToken_IfOneIsAlreadySet()
+    {
+        $token   = $this->getSuperUserToken();
+        $request = $this->getDummyRequest($token);
+
+        $requestSet = $this->initRequestSet($request, false, 'initialtoken');
+
+        $this->assertEquals('initialtoken', $requestSet->getTokenAuth());
+        $this->assertCount(2, $requestSet->getRequests());
+    }
+
+    public function test_initRequestSet_shouldNotFail_IfNoTokenProvided_AsAuthenticationIsDisabledByDefault()
+    {
+        $request = $this->getDummyRequest();
+
+        $requestSet = $this->initRequestSet($request);
+
+        $requests = $requestSet->getRequests();
+        $this->assertCount(2, $requests);
+    }
+
+    /**
+     * @expectedException \Exception
+     * @expectedExceptionMessage token_auth must be specified when using Bulk Tracking Import
+     */
+    public function test_initRequestSet_shouldTriggerException_InCaseNoValidTokenProvidedAndAuthenticationIsRequired()
+    {
+        $request = $this->getDummyRequest(false);
+
+        $this->initRequestSet($request, true);
+    }
+
+    public function test_setHandlerIfBulkRequest_shouldSetBulkHandler_InCaseNoHandlerIsSetAndItIsABulkRequest()
+    {
+        $this->injectRawDataToBulk($this->getDummyRequest());
+
+        $handler = null;
+        $this->bulk->setHandlerIfBulkRequest($handler);
+
+        $this->assertTrue($handler instanceof Handler);
+    }
+
+    public function test_setHandlerIfBulkRequest_shouldNotSetAHandler_IfOneIsAlreadySetEvenIfItIsABulkRequest()
+    {
+        $this->injectRawDataToBulk($this->getDummyRequest());
+
+        $default = new DefaultHandler();
+        $handler = $default;
+
+        $this->bulk->setHandlerIfBulkRequest($default);
+
+        $this->assertSame($default, $handler);
+    }
+
+    public function test_setHandlerIfBulkRequest_shouldNotSetAHandler_IfItIsNotABulkRequest()
+    {
+        $this->injectRawDataToBulk('{"test":"not a bulk request"}');
+
+        $handler = null;
+
+        $this->bulk->setHandlerIfBulkRequest($handler);
+
+        $this->assertNull($handler);
+    }
+
+    public function test_getListHooksRegistered_shouldListenToNewTrackerEventAndCreateBulkHandler_IfBulkRequest()
+    {
+        $this->injectRawDataToBulk($this->getDummyRequest());
+
+        $handler = DefaultHandler\Factory::make();
+
+        $this->assertTrue($handler instanceof Handler);
+    }
+
+    public function test_getListHooksRegistered_shouldListenToNewTrackerEventAndNotCreateBulkHandler_IfNotBulkRequest()
+    {
+        $handler = DefaultHandler\Factory::make();
+
+        $this->assertTrue($handler instanceof DefaultHandler);
+    }
+
+    public function test_getListHooksRegistered_shouldListenToInitRequestSetEventAndInit_IfBulkRequest()
+    {
+        $this->injectRawDataToBulk($this->getDummyRequest());
+
+        $requestSet = new RequestSet();
+        $requestSet->initRequestsAndTokenAuth();
+
+        $this->assertCount(2, $requestSet->getRequests());
+    }
+
+}
diff --git a/plugins/BulkTracking/tests/Integration/HandlerTest.php b/plugins/BulkTracking/tests/Integration/HandlerTest.php
new file mode 100644
index 00000000000..1b2b46c8cb3
--- /dev/null
+++ b/plugins/BulkTracking/tests/Integration/HandlerTest.php
@@ -0,0 +1,161 @@
+response = new Response();
+        $this->handler  = new Handler();
+        $this->handler->setResponse($this->response);
+        $this->tracker  = new Tracker();
+        $this->requestSet = new RequestSet();
+    }
+
+    public function test_init_ShouldInitiateResponseInstance()
+    {
+        $this->handler->init($this->tracker, $this->requestSet);
+
+        $this->assertTrue($this->response->isInit);
+        $this->assertFalse($this->response->isResponseOutput);
+        $this->assertFalse($this->response->isSend);
+    }
+
+    public function test_finish_ShouldOutputAndSendResponse()
+    {
+        $response = $this->handler->finish($this->tracker, $this->requestSet);
+
+        $this->assertEquals('My Dummy Content', $response);
+
+        $this->assertFalse($this->response->isInit);
+        $this->assertFalse($this->response->isExceptionOutput);
+        $this->assertTrue($this->response->isResponseOutput);
+        $this->assertTrue($this->response->isSend);
+    }
+
+    public function test_onException_ShouldOutputAndSendResponse()
+    {
+        $this->executeOnException($this->buildException());
+
+        $this->assertFalse($this->response->isInit);
+        $this->assertFalse($this->response->isResponseOutput);
+        $this->assertTrue($this->response->isExceptionOutput);
+        $this->assertFalse($this->response->isSend);
+    }
+
+    public function test_onException_ShouldPassExceptionToResponse()
+    {
+        $exception = $this->buildException();
+
+        $this->executeOnException($exception);
+
+        $this->assertSame($exception, $this->response->exception);
+        $this->assertSame(500, $this->response->statusCode);
+    }
+
+    public function test_onException_ShouldSendStatusCode400IfUnexpectedWebsite()
+    {
+        $this->executeOnException(new UnexpectedWebsiteFoundException('test'));
+        $this->assertSame(400, $this->response->statusCode);
+    }
+
+    public function test_onException_ShouldSendStatusCode400IfInvalidRequestParameterException()
+    {
+        $this->executeOnException(new InvalidRequestParameterException('test'));
+        $this->assertSame(400, $this->response->statusCode);
+    }
+
+    public function test_onException_ShouldNotRethrowAnException()
+    {
+        $exception = $this->buildException();
+
+        $this->handler->onException($this->tracker, $this->requestSet, $exception);
+        $this->assertTrue(true);
+    }
+
+    public function test_onAllRequestsTracked_ShouldNeverTriggerScheduledTasksEvenIfEnabled()
+    {
+        $runner = new ScheduledTasksRunner();
+        $runner->shouldRun = true;
+
+        $this->handler->setScheduledTasksRunner($runner);
+        $this->handler->onAllRequestsTracked($this->tracker, $this->requestSet);
+
+        $this->assertFalse($runner->ranScheduledTasks);
+    }
+
+    public function test_process_ShouldTrackAllSetRequests()
+    {
+        $this->assertSame(0, $this->tracker->getCountOfLoggedRequests());
+
+        $this->requestSet->setRequests(array(
+            array('idsite' => 1, 'url' => 'http://localhost/foo?bar'),
+            array('idsite' => 1, 'url' => 'http://localhost'),
+        ));
+
+        $this->handler->process($this->tracker, $this->requestSet);
+
+        $this->assertSame(2, $this->tracker->getCountOfLoggedRequests());
+    }
+
+    private function buildException()
+    {
+        return new \Exception('MyMessage', 292);
+    }
+
+    private function executeOnException(Exception $exception)
+    {
+        try {
+            $this->handler->onException($this->tracker, $this->requestSet, $exception);
+        } catch (Exception $e) {
+        }
+    }
+}
diff --git a/plugins/BulkTracking/tests/Integration/RequestsTest.php b/plugins/BulkTracking/tests/Integration/RequestsTest.php
new file mode 100644
index 00000000000..ab452043037
--- /dev/null
+++ b/plugins/BulkTracking/tests/Integration/RequestsTest.php
@@ -0,0 +1,127 @@
+requests = new Requests();
+    }
+
+    public function tearDown()
+    {
+        // clean up your test here if needed
+
+        parent::tearDown();
+    }
+
+    public function test_requiresAuthentication_shouldReturnTrue_IfEnabled()
+    {
+        $oldConfig = TrackerConfig::getConfigValue('bulk_requests_require_authentication');
+        TrackerConfig::setConfigValue('bulk_requests_require_authentication', 1);
+
+        $this->assertTrue($this->requests->requiresAuthentication());
+
+        TrackerConfig::setConfigValue('bulk_requests_require_authentication', $oldConfig);
+    }
+
+    public function test_requiresAuthentication_shouldReturnFalse_IfDisabled()
+    {
+        $oldConfig = TrackerConfig::getConfigValue('bulk_requests_require_authentication');
+        TrackerConfig::setConfigValue('bulk_requests_require_authentication', 0);
+
+        $this->assertFalse($this->requests->requiresAuthentication());
+
+        TrackerConfig::setConfigValue('bulk_requests_require_authentication', $oldConfig);
+    }
+
+    /**
+     * @expectedException \Exception
+     * @expectedExceptionMessage token_auth must be specified when using Bulk Tracking Import
+     */
+    public function test_authenticateRequests_shouldThrowAnException_IfTokenAuthIsEmpty()
+    {
+        $requests = array($this->buildDummyRequest());
+        $this->requests->authenticateRequests($requests);
+    }
+
+    /**
+     * @expectedException \Exception
+     * @expectedExceptionMessage token_auth must be specified when using Bulk Tracking Import
+     */
+    public function test_authenticateRequests_shouldThrowAnException_IfAnyTokenAuthIsEmpty()
+    {
+        $requests = array($this->buildDummyRequest($this->getSuperUserToken()), $this->buildDummyRequest());
+        $this->requests->authenticateRequests($requests);
+    }
+
+    /**
+     * @expectedException \Exception
+     * @expectedExceptionMessage token_auth specified does not have Admin permission for idsite=1
+     */
+    public function test_authenticateRequests_shouldThrowAnException_IfTokenIsNotValid()
+    {
+        $dummyToken = API::getInstance()->getTokenAuth('test', UsersManager::getPasswordHash('2'));
+        $superUserToken = $this->getSuperUserToken();
+
+        $requests = array($this->buildDummyRequest($superUserToken), $this->buildDummyRequest($dummyToken));
+        $this->requests->authenticateRequests($requests);
+    }
+
+    public function test_authenticateRequests_shouldNotFail_IfAllTokensAreValid()
+    {
+        $superUserToken = $this->getSuperUserToken();
+
+        $requests = array($this->buildDummyRequest($superUserToken), $this->buildDummyRequest($superUserToken));
+        $this->requests->authenticateRequests($requests);
+
+        $this->assertTrue(true);
+    }
+
+    public function test_authenticateRequests_shouldNotFail_IfEmptyRequestSetGiven()
+    {
+        $this->requests->authenticateRequests(array());
+
+        $this->assertTrue(true);
+    }
+
+    private function getSuperUserToken()
+    {
+        Fixture::createSuperUser(false);
+        return Fixture::getTokenAuth();
+    }
+
+    private function buildDummyRequest($token = false)
+    {
+        return new Request(array('idsite' => '1'), $token);
+    }
+
+}
diff --git a/plugins/BulkTracking/tests/Integration/TrackerTest.php b/plugins/BulkTracking/tests/Integration/TrackerTest.php
new file mode 100644
index 00000000000..dd1e37ea700
--- /dev/null
+++ b/plugins/BulkTracking/tests/Integration/TrackerTest.php
@@ -0,0 +1,133 @@
+tracker = new TestIntegrationTracker();
+
+        Fixture::createWebsite('2014-01-01 00:00:00');
+        Fixture::createWebsite('2014-01-01 00:00:00');
+
+        $this->injectRawDataToBulk($this->getDummyRequest());
+    }
+
+    public function test_main_shouldIncreaseLoggedRequestsCounter()
+    {
+        $this->tracker->main($this->getHandler(), $this->getEmptyRequestSet());
+
+        $this->assertSame(2, $this->tracker->getCountOfLoggedRequests());
+    }
+
+    public function test_main_shouldUseBulkHandler()
+    {
+        $handler = $this->getHandler();
+        $this->assertTrue($handler instanceof Handler);
+    }
+
+    public function test_main_shouldReturnBulkTrackingResponse()
+    {
+        $response = $this->tracker->main($this->getHandler(), $this->getEmptyRequestSet());
+
+        $this->assertSame('{"status":"success","tracked":2}', $response);
+    }
+
+    public function test_main_shouldReturnErrorResponse_InCaseOfAnyError()
+    {
+        $requestSet = new RequestSet();
+        $requestSet->enableThrowExceptionOnInit();
+
+        $handler = $this->getHandler();
+        $handler->setResponse(new Response());
+
+        $response = $this->tracker->main($handler, $requestSet);
+
+        $this->assertSame('{"status":"error","tracked":0}', $response);
+    }
+
+    public function test_main_shouldReturnErrorResponse_IfNotAuthorized()
+    {
+        $this->injectRawDataToBulk($this->getDummyRequest(), true);
+
+        $handler = $this->getHandler();
+        $handler->setResponse(new Response());
+
+        $response = $this->tracker->main($handler, $this->getEmptyRequestSet());
+
+        $this->assertSame('{"status":"error","tracked":0}', $response);
+    }
+
+    public function test_main_shouldActuallyTrack()
+    {
+        $this->assertEmpty($this->getIdVisit(1));
+        $this->assertEmpty($this->getIdVisit(2));
+
+        $requestSet = $this->getEmptyRequestSet();
+        $this->tracker->main($this->getHandler(), $requestSet);
+
+        $this->assertCount(2, $requestSet->getRequests(), 'Nothing tracked because it could not find 2 requests');
+
+        $visit1 = $this->getIdVisit(1);
+        $visit2 = $this->getIdVisit(2);
+
+        $this->assertNotEmpty($visit1);
+        $this->assertEquals(1, $visit1['idsite']);
+        $this->assertNotEmpty($visit2);
+        $this->assertEquals(2, $visit2['idsite']);
+
+        $this->assertEmpty($this->getIdVisit(3));
+    }
+
+    private function getHandler()
+    {
+        return Tracker\Handler\Factory::make();
+    }
+
+    private function getEmptyRequestSet()
+    {
+        return new Tracker\RequestSet();
+    }
+
+    private function getIdVisit($idVisit)
+    {
+        return Tracker::getDatabase()->fetchRow("SELECT * FROM " . Common::prefixTable('log_visit') . " WHERE idvisit = ?", array($idVisit));
+    }
+
+}
\ No newline at end of file
diff --git a/plugins/BulkTracking/tests/System/TrackerTest.php b/plugins/BulkTracking/tests/System/TrackerTest.php
new file mode 100644
index 00000000000..5974a1acd57
--- /dev/null
+++ b/plugins/BulkTracking/tests/System/TrackerTest.php
@@ -0,0 +1,53 @@
+tracker = Fixture::getTracker($idSite, $dateTime, $defaultInit = true);
+        $this->tracker->enableBulkTracking();
+    }
+
+    public function test_response_ShouldContainBulkTrackingApiResponse()
+    {
+        $this->tracker->doTrackPageView('Test');
+        $this->tracker->doTrackPageView('Test');
+
+        $response = $this->tracker->doBulkTrack();
+
+        $this->assertEquals('{"status":"success","tracked":2}', $response);
+    }
+}
\ No newline at end of file
diff --git a/plugins/BulkTracking/tests/Unit/RequestsTest.php b/plugins/BulkTracking/tests/Unit/RequestsTest.php
new file mode 100644
index 00000000000..8a89e5c220c
--- /dev/null
+++ b/plugins/BulkTracking/tests/Unit/RequestsTest.php
@@ -0,0 +1,172 @@
+requests = new Requests();
+    }
+
+    public function test_isUsingBulkRequest_shouldReturnFalse_IfRequestIsEmpty()
+    {
+        $this->assertFalse($this->requests->isUsingBulkRequest(false));
+        $this->assertFalse($this->requests->isUsingBulkRequest(null));
+        $this->assertFalse($this->requests->isUsingBulkRequest(''));
+        $this->assertFalse($this->requests->isUsingBulkRequest(0));
+    }
+
+    public function test_isUsingBulkRequest_shouldReturnFalse_IfRequestIsNotABulkRequest()
+    {
+        $this->assertFalse($this->requests->isUsingBulkRequest(5));
+        $this->assertFalse($this->requests->isUsingBulkRequest('test'));
+        $this->assertFalse($this->requests->isUsingBulkRequest('requests'));
+        $this->assertFalse($this->requests->isUsingBulkRequest('{"test": "val", "request:" []}'));
+        $this->assertFalse($this->requests->isUsingBulkRequest('[5, 10, "request"]'));
+    }
+
+    public function test_isUsingBulkRequest_shouldReturnTrue_IfRequestIsABulkRequest()
+    {
+        $request = $this->buildRequestRawData(array(array('idsite' => '1')));
+        $this->assertTrue($this->requests->isUsingBulkRequest($request));
+
+        // don't know why this one is supposed to count as bulk request!
+        $this->assertTrue($this->requests->isUsingBulkRequest("{'requests'"));
+    }
+
+    public function test_getRequestsArrayFromBulkRequest_ShouldFindRequestsAndEmptyTokenAndItShouldTrimWhitespaceFromRawData()
+    {
+        $requests = array(array('idsite' => '1'), array('idsite' => '2'));
+        $request  = $this->buildRequestRawData($requests);
+
+        $result   = $this->requests->getRequestsArrayFromBulkRequest(' ' . $request . '     ');
+
+        $expected = array(array(array('idsite' => '1'), array('idsite' => '2')), '');
+        $this->assertEquals($expected, $result);
+    }
+
+    public function test_getRequestsArrayFromBulkRequest_shouldRecognize()
+    {
+        $token = md5('2');
+
+        $request  = $this->buildRequestRawData(array(), $token);
+        $result   = $this->requests->getRequestsArrayFromBulkRequest($request);
+
+        $expected = array(array(), $token);
+        $this->assertEquals($expected, $result);
+    }
+
+    public function test_initRequestsAndTokenAuth_NoRequestsSetShouldStillFindToken()
+    {
+        $token = md5('2');
+
+        $request  = json_encode(array('requests' => array(), 'token_auth' => $token));
+        $result   = $this->requests->initRequestsAndTokenAuth($request);
+
+        $expected = array(array(), $token);
+        $this->assertEquals($expected, $result);
+    }
+
+    public function test_initRequestsAndTokenAuth_ShouldFindRequestsAndEmptyToken()
+    {
+        $params  = array(array('idsite' => '1'), array('idsite' => '2'));
+        $request = $this->buildRequestRawData($params);
+
+        $result  = $this->requests->initRequestsAndTokenAuth($request);
+
+        /** @var Request[] $requests */
+        $requests  = $result[0];
+        $tokenAuth = $result[1];
+
+        $this->assertEquals('', $tokenAuth); // none was set
+
+        $this->assertEquals(array('idsite' => '1'), $requests[0]->getParams());
+        $this->assertEquals('', $requests[0]->getTokenAuth());
+        $this->assertEquals(array('idsite' => '2'), $requests[1]->getParams());
+        $this->assertEquals('', $requests[1]->getTokenAuth());
+        $this->assertCount(2, $requests);
+    }
+
+    public function test_initRequestsAndTokenAuth_ShouldFindRequestsAndASetTokenAndPassItToRequestInstances()
+    {
+        $token = md5(2);
+        $params  = array(array('idsite' => '1'), array('idsite' => '2'));
+        $request = $this->buildRequestRawData($params, $token);
+
+        $result  = $this->requests->initRequestsAndTokenAuth($request);
+
+        /** @var Request[] $requests */
+        $requests  = $result[0];
+
+        $this->assertEquals($token, $result[1]);
+        $this->assertEquals($token, $requests[0]->getTokenAuth());
+        $this->assertEquals($token, $requests[1]->getTokenAuth());
+    }
+
+    public function test_initRequestsAndTokenAuth_ShouldIgnoreEmptyUrls()
+    {
+        $token = md5(2);
+        $params  = array(array('idsite' => '1'), '', array('idsite' => '2'));
+        $request = $this->buildRequestRawData($params, $token);
+
+        $result  = $this->requests->initRequestsAndTokenAuth($request);
+
+        /** @var Request[] $requests */
+        $requests  = $result[0];
+
+        $this->assertEquals(array('idsite' => '1'), $requests[0]->getParams());
+        $this->assertEquals(array('idsite' => '2'), $requests[1]->getParams());
+        $this->assertCount(2, $requests);
+    }
+
+    public function test_initRequestsAndTokenAuth_ShouldResolveUrls()
+    {
+        $token = md5(2);
+        $params  = array('piwik.php?idsite=1', '', 'piwik.php?idsite=3&rec=0', array('idsite' => '2'));
+        $request = $this->buildRequestRawData($params, $token);
+
+        $result  = $this->requests->initRequestsAndTokenAuth($request);
+
+        /** @var Request[] $requests */
+        $requests  = $result[0];
+
+        $this->assertEquals(array('idsite' => '1'), $requests[0]->getParams());
+        $this->assertEquals(array('idsite' => '3', 'rec' => '0'), $requests[1]->getParams());
+        $this->assertEquals(array('idsite' => '2'), $requests[2]->getParams());
+        $this->assertCount(3, $requests);
+    }
+
+    private function buildRequestRawData($requests, $token = null)
+    {
+        $params = array('requests' => $requests);
+
+        if (!empty($token)) {
+            $params['token_auth'] = $token;
+        }
+
+        return json_encode($params);
+    }
+
+}
diff --git a/plugins/BulkTracking/tests/Unit/ResponseTest.php b/plugins/BulkTracking/tests/Unit/ResponseTest.php
new file mode 100644
index 00000000000..9da1e9a6b34
--- /dev/null
+++ b/plugins/BulkTracking/tests/Unit/ResponseTest.php
@@ -0,0 +1,93 @@
+response = new TestResponse();
+        $this->response->init(new Tracker());
+    }
+
+    public function test_outputException_shouldOutputBulkResponse()
+    {
+        $tracker = $this->getTrackerWithCountedRequests();
+
+        $this->response->outputException($tracker, new Exception('My Custom Message'), 400);
+        $content = $this->response->getOutput();
+
+        $this->assertEquals('{"status":"error","tracked":5}', $content);
+    }
+
+    public function test_outputException_shouldOutputDebugMessageIfEnabled()
+    {
+        $tracker = $this->getTrackerWithCountedRequests();
+        $tracker->enableDebugMode();
+
+        $this->response->outputException($tracker, new Exception('My Custom Message'), 400);
+        $content = $this->response->getOutput();
+
+        $this->assertStringStartsWith('{"status":"error","tracked":5,"message":"My Custom Message\n', $content);
+    }
+
+    public function test_outputResponse_shouldOutputBulkResponse()
+    {
+        $tracker = $this->getTrackerWithCountedRequests();
+
+        $this->response->outputResponse($tracker);
+        $content = $this->response->getOutput();
+
+        $this->assertEquals('{"status":"success","tracked":5}', $content);
+    }
+
+    public function test_outputResponse_shouldNotOutputAnything_IfExceptionResponseAlreadySent()
+    {
+        $tracker = $this->getTrackerWithCountedRequests();
+
+        $this->response->outputException($tracker, new Exception('My Custom Message'), 400);
+        $this->response->outputResponse($tracker);
+        $content = $this->response->getOutput();
+
+        $this->assertEquals('{"status":"error","tracked":5}', $content);
+    }
+
+    private function getTrackerWithCountedRequests()
+    {
+        $tracker = new Tracker();
+        $tracker->setCountOfLoggedRequests(5);
+        return $tracker;
+    }
+
+}
diff --git a/plugins/CoreAdminHome/stylesheets/pluginSettings.less b/plugins/CoreAdminHome/stylesheets/pluginSettings.less
index 9633c0423d9..0d8c8469cfd 100644
--- a/plugins/CoreAdminHome/stylesheets/pluginSettings.less
+++ b/plugins/CoreAdminHome/stylesheets/pluginSettings.less
@@ -12,6 +12,10 @@
     width:200px
   }
 
+  .control_text, .control_password {
+    max-width: 220px;
+  }
+
   .title {
     font-weight: bold
   }
diff --git a/plugins/CoreAdminHome/templates/pluginSettings.twig b/plugins/CoreAdminHome/templates/pluginSettings.twig
index 3dfe2873f4f..fa1e032ea8f 100644
--- a/plugins/CoreAdminHome/templates/pluginSettings.twig
+++ b/plugins/CoreAdminHome/templates/pluginSettings.twig
@@ -123,6 +123,7 @@
                                     {% if setting.uiControlType == 'checkbox' and settingValue %}
                                         checked="checked"
                                     {% endif %}
+                                    class="control_{{ setting.uiControlType|e('html_attr') }}"
                                     type="{{ setting.uiControlType|e('html_attr') }}"
                                     name="{{ setting.getKey|e('html_attr') }}"
                                     value="{{ settingValue|e('html_attr') }}"
diff --git a/plugins/CoreHome/Columns/ServerTime.php b/plugins/CoreHome/Columns/ServerTime.php
index 72cdcb1357d..297c4100588 100644
--- a/plugins/CoreHome/Columns/ServerTime.php
+++ b/plugins/CoreHome/Columns/ServerTime.php
@@ -8,6 +8,7 @@
  */
 namespace Piwik\Plugins\CoreHome\Columns;
 
+use Piwik\Date;
 use Piwik\Db;
 use Piwik\Plugin\Dimension\ActionDimension;
 use Piwik\Tracker\Action;
@@ -32,6 +33,6 @@ public function onNewAction(Request $request, Visitor $visitor, Action $action)
     {
         $timestamp = $request->getCurrentTimestamp();
 
-        return Tracker::getDatetimeFromTimestamp($timestamp);
+        return Date::getDatetimeFromTimestamp($timestamp);
     }
 }
diff --git a/plugins/CoreHome/Columns/VisitFirstActionTime.php b/plugins/CoreHome/Columns/VisitFirstActionTime.php
index 92b6619fed0..3985d53339b 100644
--- a/plugins/CoreHome/Columns/VisitFirstActionTime.php
+++ b/plugins/CoreHome/Columns/VisitFirstActionTime.php
@@ -8,6 +8,7 @@
  */
 namespace Piwik\Plugins\CoreHome\Columns;
 
+use Piwik\Date;
 use Piwik\Plugin\Dimension\VisitDimension;
 use Piwik\Tracker\Action;
 use Piwik\Tracker\Request;
@@ -27,6 +28,6 @@ class VisitFirstActionTime extends VisitDimension
      */
     public function onNewVisit(Request $request, Visitor $visitor, $action)
     {
-        return Tracker::getDatetimeFromTimestamp($request->getCurrentTimestamp());
+        return Date::getDatetimeFromTimestamp($request->getCurrentTimestamp());
     }
 }
\ No newline at end of file
diff --git a/plugins/CoreHome/Columns/VisitLastActionTime.php b/plugins/CoreHome/Columns/VisitLastActionTime.php
index 988757301ba..d25f09ababa 100644
--- a/plugins/CoreHome/Columns/VisitLastActionTime.php
+++ b/plugins/CoreHome/Columns/VisitLastActionTime.php
@@ -8,6 +8,7 @@
  */
 namespace Piwik\Plugins\CoreHome\Columns;
 
+use Piwik\Date;
 use Piwik\Plugin\Dimension\VisitDimension;
 use Piwik\Tracker\Action;
 use Piwik\Tracker\Request;
@@ -31,7 +32,7 @@ class VisitLastActionTime extends VisitDimension
      */
     public function onNewVisit(Request $request, Visitor $visitor, $action)
     {
-        return Tracker::getDatetimeFromTimestamp($request->getCurrentTimestamp());
+        return Date::getDatetimeFromTimestamp($request->getCurrentTimestamp());
     }
 
     /**
diff --git a/plugins/ExampleSettingsPlugin/Settings.php b/plugins/ExampleSettingsPlugin/Settings.php
index 1853873924c..7529407b3f4 100644
--- a/plugins/ExampleSettingsPlugin/Settings.php
+++ b/plugins/ExampleSettingsPlugin/Settings.php
@@ -53,7 +53,7 @@ protected function init()
         // User setting --> textbox converted to int defining a validator and filter
         $this->createRefreshIntervalSetting();
 
-        // User setting --> readio
+        // User setting --> radio
         $this->createColorSetting();
 
         // System setting --> allows selection of a single value
diff --git a/plugins/Provider/Columns/Provider.php b/plugins/Provider/Columns/Provider.php
index cba1b8b9f58..24ab6cecd2e 100644
--- a/plugins/Provider/Columns/Provider.php
+++ b/plugins/Provider/Columns/Provider.php
@@ -42,6 +42,13 @@ protected function configureSegments()
      */
     public function onNewVisit(Request $request, Visitor $visitor, $action)
     {
+        // Adding &dp=1 will disable the provider plugin, this is an "unofficial" parameter used to speed up log importer
+        $disableProvider = $request->getParam('dp');
+
+        if (!empty($disableProvider)) {
+            return false;
+        }
+
         // if provider info has already been set, abort
         $locationValue = $visitor->getVisitorColumn('location_provider');
         if (!empty($locationValue)) {
diff --git a/plugins/QueuedTracking b/plugins/QueuedTracking
new file mode 160000
index 00000000000..7f33537161a
--- /dev/null
+++ b/plugins/QueuedTracking
@@ -0,0 +1 @@
+Subproject commit 7f33537161a81172bb00bbb5a4a3c338e7513ce7
diff --git a/plugins/ScheduledReports/API.php b/plugins/ScheduledReports/API.php
index 54cbf411259..1eade02df06 100644
--- a/plugins/ScheduledReports/API.php
+++ b/plugins/ScheduledReports/API.php
@@ -21,6 +21,7 @@
 use Piwik\Plugins\SitesManager\API as SitesManagerApi;
 use Piwik\ReportRenderer;
 use Piwik\Site;
+use Piwik\Tracker;
 use Piwik\Translate;
 
 /**
@@ -554,7 +555,8 @@ public function sendReport($idReport, $period = false, $date = false, $force = f
         $this->getModel()->updateReport($report['idreport'], array('ts_last_sent' => $now));
 
         // If running from piwik.php with debug, do not delete the PDF after sending the email
-        if (!isset($GLOBALS['PIWIK_TRACKER_DEBUG']) || !$GLOBALS['PIWIK_TRACKER_DEBUG']) {
+        $tracker = new Tracker();
+        if (!$tracker->isDebugModeEnabled()) {
             @chmod($outputFilename, 0600);
         }
     }
diff --git a/plugins/ScheduledReports/ScheduledReports.php b/plugins/ScheduledReports/ScheduledReports.php
index 4b8190fead2..6d171768a70 100644
--- a/plugins/ScheduledReports/ScheduledReports.php
+++ b/plugins/ScheduledReports/ScheduledReports.php
@@ -20,6 +20,7 @@
 use Piwik\Plugins\UsersManager\Model as UserModel;
 use Piwik\ReportRenderer;
 use Piwik\ScheduledTime;
+use Piwik\Tracker;
 use Piwik\View;
 use Zend_Mime;
 use Piwik\Config;
@@ -406,7 +407,8 @@ public function sendReport($reportType, $report, $contents, $filename, $prettyDa
             } catch (Exception $e) {
 
                 // If running from piwik.php with debug, we ignore the 'email not sent' error
-                if (!isset($GLOBALS['PIWIK_TRACKER_DEBUG']) || !$GLOBALS['PIWIK_TRACKER_DEBUG']) {
+                $tracker = new Tracker();
+                if (!$tracker->isDebugModeEnabled()) {
                     throw new Exception("An error occured while sending '$filename' " .
                         " to " . implode(', ', $mail->getRecipients()) .
                         ". Error was '" . $e->getMessage() . "'");
diff --git a/plugins/TestRunner/README.md b/plugins/TestRunner/README.md
index be10873613a..d0c9c864d94 100644
--- a/plugins/TestRunner/README.md
+++ b/plugins/TestRunner/README.md
@@ -10,13 +10,17 @@ __I want to run the tests with different parameters on AWS, is it possible?__
 
 Yes, at the time of writing this you have to edit the file `Runner/Remote.php`
 
+__Why am I getting an error "AWS was not able to validate the provided access credentials"?__
+
+It could be caused by an invalid set date. Execute `date` on the command line and make sure it is correct.
+
 __How can I change the base image (AMI) that is used for AWS tests?__
 
 * Log in to AWS
 * Select `EC2 => AMI`
 * Launch a new instance of the current AMI by selecting it and pressing `Launch`
 * Select a `c3.large` instance type
-* Press `Review and Launch` and on next page `Launch` (in theory you have to select your keypair somewhere otherwise you will not be able to log in but I couldn't find where)
+* Press `Review and Launch` and on next page `Launch` (there you have to select your keypair otherwise you can't log in)
 * Log in to the newly created instance. To get login information 
   * Go to `EC2 => Instances`
   * Select the created instance
@@ -33,7 +37,7 @@ __How can I change the base image (AMI) that is used for AWS tests?__
     or if you don't know Puppet at least add it in this shell script https://github.com/piwik/piwik-dev-environment/blob/master/puppet/files/setup.sh
     For instance if you installed a new package you can simply add a new entry here https://github.com/piwik/piwik-dev-environment/blob/master/puppet/modules/piwik/manifests/base.pp
 * In `EC2 => Instances` menu select the instance you are currently using.
-* Select `Actions => Create Image`
+* Select `Actions => Image => Create Image`
 * Define the name `Piwik Testing vX.X` and a description like `Used to run Piwik tests via Piwik console`. Make sure to increase the box version in X.X (have a look in `EC2 => AMI` for current version)
 * Press `Create Image`
 * Go to `EC2 => AMIs` menu and while waiting for the image creation to complete add the following tags
@@ -54,6 +58,7 @@ __How do I create a new EC2 key/pair for a developer?__
 1. Go to: https://console.aws.amazon.com/ec2/v2/home?region=us-east-1
 2. Click `Create Key Pair`
 3. Send PGP email
+
 ```
 Here are info for running tests on Ec2
  * Access Key ID: 
diff --git a/plugins/TestRunner/TravisYmlView.php b/plugins/TestRunner/TravisYmlView.php
index 809de911442..01d41ac54a7 100644
--- a/plugins/TestRunner/TravisYmlView.php
+++ b/plugins/TestRunner/TravisYmlView.php
@@ -25,6 +25,7 @@ class TravisYmlView extends View
      */
     private static $travisYmlSectionNames = array(
         'php',
+        'services',
         'language',
         'script',
         'before_install',
diff --git a/plugins/TestRunner/templates/travis.yml.twig b/plugins/TestRunner/templates/travis.yml.twig
index 8623bd8b92c..1972feb6c82 100644
--- a/plugins/TestRunner/templates/travis.yml.twig
+++ b/plugins/TestRunner/templates/travis.yml.twig
@@ -22,6 +22,9 @@ php:
 {% endfor %}
 {% endif %}
 
+services:
+  - redis-server
+
 # Separate different test suites
 {% if existingEnv|default is empty -%}
 env:
diff --git a/tests/LocalTracker.php b/tests/LocalTracker.php
index e0867a746d1..a86d3a565cc 100755
--- a/tests/LocalTracker.php
+++ b/tests/LocalTracker.php
@@ -5,9 +5,6 @@
 use Piwik\Tracker\Cache;
 
 $GLOBALS['PIWIK_TRACKER_DEBUG'] = false;
-if (!defined('PIWIK_ENABLE_TRACKING')) {
-    define('PIWIK_ENABLE_TRACKING', true);
-}
 
 require_once PIWIK_INCLUDE_PATH . '/core/Tracker.php';
 require_once PIWIK_INCLUDE_PATH . '/core/Tracker/Db.php';
@@ -52,7 +49,7 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal
         \Piwik\Plugin\Manager::getInstance()->unloadPlugins();
 
         // modify config
-        $GLOBALS['PIWIK_TRACKER_MODE'] = true;
+        \Piwik\SettingsServer::setIsTrackerApiRequest();
         $GLOBALS['PIWIK_TRACKER_LOCAL_TRACKING'] = true;
         Tracker::$initTrackerMode = false;
         Tracker::setTestEnvironment($testEnvironmentArgs, $method);
@@ -73,7 +70,16 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal
         ob_start();
 
         $localTracker = new Tracker();
-        $localTracker->main($requests);
+        $request = new Tracker\RequestSet();
+        $request->setRequests($requests);
+
+        $handler = Tracker\Handler\Factory::make();
+
+        $response = $localTracker->main($handler, $request);
+
+        if (!is_null($response)) {
+            echo $response;
+        }
 
         $output = ob_get_contents();
 
@@ -85,7 +91,7 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal
         $_SERVER['HTTP_USER_AGENT'] = $oldUserAgent;
         $_COOKIE = $oldCookie;
         $GLOBALS['PIWIK_TRACKER_LOCAL_TRACKING'] = false;
-        $GLOBALS['PIWIK_TRACKER_MODE'] = false;
+        \Piwik\SettingsServer::setIsNotTrackerApiRequest();
         unset($_GET['bots']);
 
         // reload plugins
diff --git a/tests/PHPUnit/Framework/Mock/Tracker.php b/tests/PHPUnit/Framework/Mock/Tracker.php
new file mode 100644
index 00000000000..64a22b5ee24
--- /dev/null
+++ b/tests/PHPUnit/Framework/Mock/Tracker.php
@@ -0,0 +1,35 @@
+isDebugEnabled = true;
+    }
+
+    public function isDebugModeEnabled()
+    {
+        return $this->isDebugEnabled;
+    }
+
+    public function disableShouldRecordStatistics()
+    {
+        $this->shouldRecord = false;
+    }
+
+    public function shouldRecordStatistics()
+    {
+        return $this->shouldRecord;
+    }
+}
diff --git a/tests/PHPUnit/Framework/Mock/Tracker/Db.php b/tests/PHPUnit/Framework/Mock/Tracker/Db.php
new file mode 100644
index 00000000000..5e0baab51ee
--- /dev/null
+++ b/tests/PHPUnit/Framework/Mock/Tracker/Db.php
@@ -0,0 +1,53 @@
+dsn = 'testdrivername';
+        $this->username = 'testuser';
+        $this->password = 'testpassword';
+        $this->charset = 'testcharset';
+    }
+
+    public function connect()
+    {
+        $this->connectCalled = true;
+    }
+
+    /**
+     * Start Transaction
+     * @return string TransactionID
+     */
+    public function beginTransaction()
+    {
+        $this->beganTransaction = true;
+        return 'my4929transactionid';
+    }
+
+    public function commit($xid)
+    {
+        $this->commitTransactionId = $xid;
+    }
+
+    public function rollBack($xid)
+    {
+        $this->rollbackTransactionId = $xid;
+    }
+
+}
diff --git a/tests/PHPUnit/Framework/Mock/Tracker/Handler.php b/tests/PHPUnit/Framework/Mock/Tracker/Handler.php
new file mode 100644
index 00000000000..b1f241deef9
--- /dev/null
+++ b/tests/PHPUnit/Framework/Mock/Tracker/Handler.php
@@ -0,0 +1,70 @@
+isInit = true;
+    }
+
+    public function enableTriggerExceptionInProcess()
+    {
+        $this->doTriggerExceptionInProcess = true;
+    }
+
+    public function onStartTrackRequests(Tracker $tracker, TrackerRequestSet $TrackerRequestSet)
+    {
+        $this->isOnStartTrackRequests = true;
+    }
+
+    public function process(Tracker $tracker, TrackerRequestSet $TrackerRequestSet)
+    {
+        if ($this->doTriggerExceptionInProcess) {
+            throw new Exception('My Exception During Process');
+        }
+
+        $this->isProcessed = true;
+    }
+
+    public function onAllRequestsTracked(Tracker $tracker, TrackerRequestSet $TrackerRequestSet)
+    {
+        $this->isOnAllRequestsTracked = true;
+    }
+
+    public function onException(Tracker $tracker, TrackerRequestSet $TrackerRequestSet, Exception $e)
+    {
+        $this->isOnException = true;
+        $this->output = $e->getMessage();
+    }
+
+    public function finish(Tracker $tracker, TrackerRequestSet $TrackerRequestSet)
+    {
+        $this->isFinished = true;
+
+        return $this->output;
+    }
+
+}
diff --git a/tests/PHPUnit/Framework/Mock/Tracker/RequestSet.php b/tests/PHPUnit/Framework/Mock/Tracker/RequestSet.php
new file mode 100644
index 00000000000..bbaf933572b
--- /dev/null
+++ b/tests/PHPUnit/Framework/Mock/Tracker/RequestSet.php
@@ -0,0 +1,32 @@
+throwExceptionOnInit = true;
+    }
+
+    public function initRequestsAndTokenAuth()
+    {
+        if ($this->throwExceptionOnInit) {
+            throw new Exception('Init requests and token auth exception', 493);
+        }
+
+        parent::initRequestsAndTokenAuth();
+    }
+
+}
diff --git a/tests/PHPUnit/Framework/Mock/Tracker/Response.php b/tests/PHPUnit/Framework/Mock/Tracker/Response.php
new file mode 100644
index 00000000000..076d37c9f02
--- /dev/null
+++ b/tests/PHPUnit/Framework/Mock/Tracker/Response.php
@@ -0,0 +1,51 @@
+isInit = true;
+    }
+
+    public function getOutput()
+    {
+        $this->isSend = true;
+
+        return $this->output;
+    }
+
+    public function outputException(Tracker $tracker, Exception $e, $statusCode)
+    {
+        $this->isExceptionOutput = true;
+        $this->statusCode = $statusCode;
+        $this->exception = $e;
+
+        $this->output = $e->getMessage();
+    }
+
+    public function outputResponse(Tracker $tracker)
+    {
+        $this->isResponseOutput = true;
+    }
+
+}
diff --git a/tests/PHPUnit/Framework/Mock/Tracker/ScheduledTasksRunner.php b/tests/PHPUnit/Framework/Mock/Tracker/ScheduledTasksRunner.php
new file mode 100644
index 00000000000..2f3d0fbb386
--- /dev/null
+++ b/tests/PHPUnit/Framework/Mock/Tracker/ScheduledTasksRunner.php
@@ -0,0 +1,28 @@
+shouldRun;
+    }
+
+    public function runScheduledTasks()
+    {
+        $this->ranScheduledTasks = true;
+    }
+
+}
diff --git a/tests/PHPUnit/Framework/TestCase/UnitTestCase.php b/tests/PHPUnit/Framework/TestCase/UnitTestCase.php
index e12bec330fc..f08eaa19530 100755
--- a/tests/PHPUnit/Framework/TestCase/UnitTestCase.php
+++ b/tests/PHPUnit/Framework/TestCase/UnitTestCase.php
@@ -7,6 +7,7 @@
  */
 
 namespace Piwik\Tests\Framework\TestCase;
+use Piwik\EventDispatcher;
 use Piwik\Tests\Framework\Mock\File;
 
 
@@ -21,6 +22,7 @@ public function setup()
     {
         parent::setUp();
         File::reset();
+        EventDispatcher::getInstance()->clearAllObservers();
     }
 
     public function tearDown()
diff --git a/tests/PHPUnit/Integration/Plugin/ManagerTest.php b/tests/PHPUnit/Integration/Plugin/ManagerTest.php
new file mode 100644
index 00000000000..3b6265b4a52
--- /dev/null
+++ b/tests/PHPUnit/Integration/Plugin/ManagerTest.php
@@ -0,0 +1,90 @@
+manager = Plugin\Manager::getInstance();
+    }
+
+    public function test_loadTrackerPlugins_shouldDetectTrackerPlugins()
+    {
+        $this->assertGreaterThan(50, count($this->manager->getLoadedPlugins())); // make sure all plugins are loaded
+
+        $pluginsToLoad = $this->manager->loadTrackerPlugins();
+
+        $this->assertOnlyTrackerPluginsAreLoaded($pluginsToLoad);
+    }
+
+    public function test_loadTrackerPlugins_shouldCacheListOfPlugins()
+    {
+        $cache = $this->getCacheForTrackerPlugins();
+        $this->assertFalse($cache->has());
+
+        $pluginsToLoad = $this->manager->loadTrackerPlugins();
+
+        $this->assertTrue($cache->has());
+        $this->assertEquals($pluginsToLoad, $cache->get());
+    }
+
+    public function test_loadTrackerPlugins_shouldBeAbleToLoadPluginsCorrectWhenItIsCached()
+    {
+        $pluginsToLoad = array('CoreHome', 'UserSettings', 'Login', 'CoreAdminHome');
+        $this->getCacheForTrackerPlugins()->set($pluginsToLoad);
+
+        $pluginsToLoad = $this->manager->loadTrackerPlugins();
+
+        $this->assertCount(4, $this->manager->getLoadedPlugins());
+        $this->assertEquals($pluginsToLoad, array_keys($this->manager->getLoadedPlugins()));
+    }
+
+    public function test_loadTrackerPlugins_shouldUnloadAllPlugins_IfThereAreNoneToLoad()
+    {
+        $pluginsToLoad = array();
+        $this->getCacheForTrackerPlugins()->set($pluginsToLoad);
+
+        $pluginsToLoad = $this->manager->loadTrackerPlugins();
+
+        $this->assertEquals(array(), $pluginsToLoad);
+        $this->assertEquals(array(), $this->manager->getLoadedPlugins());
+    }
+
+    private function getCacheForTrackerPlugins()
+    {
+        return new PersistentCache('PluginsTracker');
+    }
+
+    private function assertOnlyTrackerPluginsAreLoaded($expectedPluginNamesLoaded)
+    {
+        // should currently load between 10 and 25 plugins
+        $this->assertLessThan(25, count($this->manager->getLoadedPlugins()));
+        $this->assertGreaterThan(10, count($this->manager->getLoadedPlugins()));
+
+        // we need to make sure it actually only loaded the correct ones
+        $this->assertEquals($expectedPluginNamesLoaded, array_keys($this->manager->getLoadedPlugins()));
+    }
+}
diff --git a/tests/PHPUnit/Integration/Plugin/SettingsTest.php b/tests/PHPUnit/Integration/Plugin/SettingsTest.php
index f8802674a4c..f086b2b3845 100644
--- a/tests/PHPUnit/Integration/Plugin/SettingsTest.php
+++ b/tests/PHPUnit/Integration/Plugin/SettingsTest.php
@@ -11,6 +11,7 @@
 use Piwik\Db;
 use Piwik\Plugin\Settings as PluginSettings;
 use Piwik\Settings\Storage;
+use Piwik\SettingsServer;
 use Piwik\Tests\Integration\Settings\CorePluginTestSettings;
 use Piwik\Tests\Integration\Settings\IntegrationTestCase;
 use Piwik\Tracker\Cache;
@@ -112,13 +113,13 @@ public function test_addSetting_shouldPassTrackerStorage_IfInTrackerMode()
     {
         $this->setSuperUser();
 
-        $GLOBALS['PIWIK_TRACKER_MODE'] = true;
+        SettingsServer::setIsTrackerApiRequest();
 
         $settings = $this->createSettingsInstance();
         $setting = $this->buildUserSetting('myname', 'mytitle', 'myRandomName');
         $settings->addSetting($setting);
 
-        unset($GLOBALS['PIWIK_TRACKER_MODE']);
+        SettingsServer::setIsNotTrackerApiRequest();
 
         $storage = $setting->getStorage();
         $this->assertTrue($storage instanceof SettingsStorage);
diff --git a/tests/PHPUnit/Integration/ReleaseCheckListTest.php b/tests/PHPUnit/Integration/ReleaseCheckListTest.php
index 2c4ab2a00de..f6bbc7a0bdc 100644
--- a/tests/PHPUnit/Integration/ReleaseCheckListTest.php
+++ b/tests/PHPUnit/Integration/ReleaseCheckListTest.php
@@ -12,6 +12,7 @@
 use Piwik\Config;
 use Piwik\Filesystem;
 use Piwik\Plugin\Manager;
+use Piwik\Tracker;
 use RecursiveDirectoryIterator;
 use RecursiveIteratorIterator;
 
@@ -142,6 +143,9 @@ public function testPiwikTrackerDebugIsOff()
     {
         $this->assertTrue(!isset($GLOBALS['PIWIK_TRACKER_DEBUG']));
         $this->assertEquals(0, $this->globalConfig['Tracker']['debug']);
+
+        $tracker = new Tracker();
+        $this->assertFalse($tracker->isDebugModeEnabled());
     }
 
     /**
diff --git a/tests/PHPUnit/Integration/Settings/Storage/FactoryTest.php b/tests/PHPUnit/Integration/Settings/Storage/FactoryTest.php
index 0c63124dfe9..9c2de666670 100644
--- a/tests/PHPUnit/Integration/Settings/Storage/FactoryTest.php
+++ b/tests/PHPUnit/Integration/Settings/Storage/FactoryTest.php
@@ -10,6 +10,7 @@
 
 use Piwik\Settings\Storage;
 use Piwik\Settings\Storage\Factory;
+use Piwik\SettingsServer;
 use Piwik\Tests\Framework\TestCase\IntegrationTestCase;
 use Piwik\Tracker\SettingsStorage;
 
@@ -51,11 +52,11 @@ public function test_make_shouldPassThePluginNameToTheSettingsStorage()
 
     private function makeTrackerInstance()
     {
-        $GLOBALS['PIWIK_TRACKER_MODE'] = true;
+        SettingsServer::setIsTrackerApiRequest();
 
         $storage = Factory::make('PluginName');
 
-        unset($GLOBALS['PIWIK_TRACKER_MODE']);
+        SettingsServer::setIsNotTrackerApiRequest();
 
         return $storage;
     }
diff --git a/tests/PHPUnit/Integration/Tracker/Handler/FactoryTest.php b/tests/PHPUnit/Integration/Tracker/Handler/FactoryTest.php
new file mode 100644
index 00000000000..fd77913bfa5
--- /dev/null
+++ b/tests/PHPUnit/Integration/Tracker/Handler/FactoryTest.php
@@ -0,0 +1,75 @@
+clearObservers('Tracker.initRequestSet');
+        parent::tearDown();
+    }
+
+    public function test_make_shouldCreateDefaultInstance()
+    {
+        $handler = Factory::make();
+        $this->assertInstanceOf('Piwik\\Tracker\\Handler', $handler);
+    }
+
+    public function test_make_shouldTriggerEventOnce()
+    {
+        $called = 0;
+        $self   = $this;
+        Piwik::addAction('Tracker.newHandler', function ($handler) use (&$called, $self) {
+            $called++;
+            $self->assertNull($handler);
+        });
+
+        Factory::make();
+        $this->assertSame(1, $called);
+    }
+
+    public function test_make_shouldPreferManuallyCreatedHandlerInstanceInEventOverDefaultHandler()
+    {
+        $handlerToUse = new Handler();
+        Piwik::addAction('Tracker.newHandler', function (&$handler) use ($handlerToUse) {
+            $handler = $handlerToUse;
+        });
+
+        $handler = Factory::make();
+        $this->assertSame($handlerToUse, $handler);
+    }
+
+    /**
+     * @expectedException \Exception
+     * @expectedExceptionMessage The Handler object set in the plugin
+     */
+    public function test_make_shouldTriggerExceptionInCaseWrongInstanceCreatedInHandler()
+    {
+        Piwik::addAction('Tracker.newHandler', function (&$handler) {
+            $handler = new Tracker();
+        });
+
+        Factory::make();
+    }
+}
diff --git a/tests/PHPUnit/Integration/Tracker/HandlerTest.php b/tests/PHPUnit/Integration/Tracker/HandlerTest.php
new file mode 100644
index 00000000000..812c3413363
--- /dev/null
+++ b/tests/PHPUnit/Integration/Tracker/HandlerTest.php
@@ -0,0 +1,246 @@
+response = new Response();
+        $this->handler  = new Handler();
+        $this->handler->setResponse($this->response);
+        $this->tracker  = new Tracker();
+        $this->requestSet = new RequestSet();
+    }
+
+    public function test_init_ShouldInitiateResponseInstance()
+    {
+        $this->handler->init($this->tracker, $this->requestSet);
+
+        $this->assertTrue($this->response->isInit);
+        $this->assertFalse($this->response->isResponseOutput);
+        $this->assertFalse($this->response->isSend);
+    }
+
+    public function test_finish_ShouldOutputAndSendResponse()
+    {
+        $response = $this->handler->finish($this->tracker, $this->requestSet);
+
+        $this->assertEquals('My Dummy Content', $response);
+        $this->assertFalse($this->response->isInit);
+        $this->assertFalse($this->response->isExceptionOutput);
+        $this->assertTrue($this->response->isResponseOutput);
+        $this->assertTrue($this->response->isSend);
+    }
+
+    public function test_finish_ShouldRedirectIfThereIsAValidUrl()
+    {
+        $_GET['redirecturl'] = 'http://localhost/test?foo=bar';
+
+        $this->requestSet->setRequests(array(array('idsite' => '1')));
+
+        try {
+            $this->handler->finish($this->tracker, $this->requestSet);
+            $this->fail('An expected exception was not thrown');
+        } catch (Exception $e) {
+            $this->assertContains('Piwik would redirect you to this URL: ' . $_GET['redirecturl'], $e->getMessage());
+            unset($_GET['redirecturl']);
+        }
+    }
+
+    public function test_finish_ShouldRedirectIfThereIsAValidBelongingToTheSite()
+    {
+        $_GET['redirecturl'] = 'http://piwik.net/';
+
+        $this->requestSet->setRequests(array(array('idsite' => '1')));
+
+        try {
+            $this->handler->finish($this->tracker, $this->requestSet);
+            $this->fail('An expected exception was not thrown');
+        } catch (Exception $e) {
+            $this->assertContains('Piwik would redirect you to this URL: http://piwik.net/', $e->getMessage());
+            unset($_GET['redirecturl']);
+        }
+    }
+
+    public function test_finish_ShouldNotRedirectIfThereIsAUrlThatDoesNotBelongToAnySite()
+    {
+        $_GET['redirecturl'] = 'http://random.piwik.org/test?foo=bar';
+
+        $this->requestSet->setRequests(array(array('idsite' => '1')));
+        $this->handler->finish($this->tracker, $this->requestSet);
+        unset($_GET['redirecturl']);
+
+        $this->assertTrue(true);
+    }
+
+    public function test_finish_ShouldNotRedirectIfUrlIsValidButNoRequests()
+    {
+        $_GET['redirecturl'] = 'http://localhost/test';
+
+        $this->requestSet->setRequests(array());
+        $this->handler->finish($this->tracker, $this->requestSet);
+        unset($_GET['redirecturl']);
+
+        $this->assertTrue(true);
+    }
+
+    public function test_finish_ShoulAlsoReturnAPossibleRenderedException()
+    {
+        $this->executeOnException($this->buildException());
+        $response = $this->handler->finish($this->tracker, $this->requestSet);
+
+        $this->assertEquals('MyMessage', $response);
+    }
+
+    public function test_onException_ShouldOutputResponse()
+    {
+        $this->executeOnException($this->buildException());
+
+        $this->assertFalse($this->response->isInit);
+        $this->assertFalse($this->response->isResponseOutput);
+        $this->assertTrue($this->response->isExceptionOutput);
+        $this->assertFalse($this->response->isSend);
+    }
+
+    public function test_onException_ShouldPassExceptionToResponse()
+    {
+        $exception = $this->buildException();
+
+        $this->executeOnException($exception);
+
+        $this->assertSame($exception, $this->response->exception);
+        $this->assertSame(500, $this->response->statusCode);
+    }
+
+    public function test_onException_ShouldSendStatusCode400IfUnexpectedWebsite()
+    {
+        $this->executeOnException(new UnexpectedWebsiteFoundException('test'));
+        $this->assertSame(400, $this->response->statusCode);
+    }
+
+    public function test_onException_ShouldSendStatusCode400IfInvalidRequestParameterException()
+    {
+        $this->executeOnException(new InvalidRequestParameterException('test'));
+        $this->assertSame(400, $this->response->statusCode);
+    }
+
+    public function test_onException_ShouldStilRedirectIfThereIsAnExceptionAndAValidRedirectUrl()
+    {
+        $_GET['redirecturl'] = 'http://localhost/test?foo=bar';
+
+        $this->requestSet->setRequests(array(array('idsite' => '1')));
+
+        try {
+            $this->handler->onException($this->tracker, $this->requestSet, $this->buildException());
+            $this->fail('An expected exception was not thrown');
+        } catch (Exception $e) {
+            $this->assertContains('Piwik would redirect you to this URL: ' . $_GET['redirecturl'], $e->getMessage());
+            unset($_GET['redirecturl']);
+        }
+    }
+
+    public function test_onException_ShouldNotRethrowExceptionToExitTrackerImmediately()
+    {
+        $exception = $this->buildException();
+
+        $this->handler->onException($this->tracker, $this->requestSet, $exception);
+        $this->assertTrue(true);
+    }
+
+    public function test_onAllRequestsTracked_ShouldTriggerScheduledTasksIfEnabled()
+    {
+        $runner = new ScheduledTasksRunner();
+        $runner->shouldRun = true;
+
+        $this->handler->setScheduledTasksRunner($runner);
+        $this->handler->onAllRequestsTracked($this->tracker, $this->requestSet);
+
+        $this->assertTrue($runner->ranScheduledTasks);
+    }
+
+    public function test_onAllRequestsTracked_ShouldNotTriggerScheduledTasksIfDisabled()
+    {
+        $runner = new ScheduledTasksRunner();
+        $runner->shouldRun = false;
+
+        $this->handler->setScheduledTasksRunner($runner);
+        $this->handler->onAllRequestsTracked($this->tracker, $this->requestSet);
+
+        $this->assertFalse($runner->ranScheduledTasks);
+    }
+
+    public function test_process_ShouldTrackAllSetRequests()
+    {
+        $this->assertSame(0, $this->tracker->getCountOfLoggedRequests());
+
+        $this->requestSet->setRequests(array(
+            array('idsite' => 1, 'url' => 'http://localhost/foo?bar'),
+            array('idsite' => 1, 'url' => 'http://localhost'),
+        ));
+
+        $this->handler->process($this->tracker, $this->requestSet);
+
+        $this->assertSame(2, $this->tracker->getCountOfLoggedRequests());
+    }
+
+    private function buildException()
+    {
+        return new \Exception('MyMessage', 292);
+    }
+
+    private function executeOnException(Exception $exception)
+    {
+        try {
+            $this->handler->onException($this->tracker, $this->requestSet, $exception);
+        } catch (Exception $e) {
+        }
+    }
+}
diff --git a/tests/PHPUnit/Integration/Tracker/RequestSetTest.php b/tests/PHPUnit/Integration/Tracker/RequestSetTest.php
new file mode 100644
index 00000000000..a551149fa0c
--- /dev/null
+++ b/tests/PHPUnit/Integration/Tracker/RequestSetTest.php
@@ -0,0 +1,175 @@
+redirectUrl = $url;
+    }
+
+    public function getRedirectUrl()
+    {
+        return $this->redirectUrl;
+    }
+}
+/**
+ * @group RequestSetTest
+ * @group RequestSet
+ * @group Tracker
+ */
+class RequestSetTest extends IntegrationTestCase
+{
+    /**
+     * @var TestRequestSet
+     */
+    private $requestSet;
+    private $get;
+    private $post;
+
+    public function setUp()
+    {
+        parent::setUp();
+
+        Fixture::createWebsite('2014-01-01 00:00:00');
+        Fixture::createWebsite('2014-01-01 00:00:00', 0, false, 'http://www.example.com');
+
+        $this->requestSet = $this->buildNewRequestSetThatIsNotInitializedYet();
+        $this->requestSet->setRequests(array(array('idsite' => 1), array('idsite' => 2)));
+
+        $this->get  = $_GET;
+        $this->post = $_POST;
+
+        $_GET  = array();
+        $_POST = array();
+    }
+
+    public function tearDown()
+    {
+        $_GET  = $this->get;
+        $_POST = $this->post;
+
+        EventDispatcher::getInstance()->clearObservers('Tracker.initRequestSet');
+        parent::tearDown();
+    }
+
+    public function test_shouldPerformRedirectToUrl_shouldNotRedirect_IfNoUrlIsSet()
+    {
+        $this->assertFalse($this->requestSet->shouldPerformRedirectToUrl());
+    }
+
+    public function test_shouldPerformRedirectToUrl_shouldNotRedirect_IfUrlIsSetButNoRequests()
+    {
+        $this->requestSet->setRedirectUrl('http://localhost');
+        $this->assertEquals('http://localhost', $this->requestSet->getRedirectUrl());
+
+        $this->requestSet->setRequests(array());
+
+        $this->assertFalse($this->requestSet->shouldPerformRedirectToUrl());
+    }
+
+    public function test_shouldPerformRedirectToUrl_shouldNotRedirect_IfUrlHasNoHostOrIsNotUrl()
+    {
+        $this->requestSet->setRedirectUrl('abc');
+
+        $this->assertFalse($this->requestSet->shouldPerformRedirectToUrl());
+    }
+
+    public function test_shouldPerformRedirectToUrl_shouldNotRedirect_IfUrlIsNotWhitelistedInAnySiteId()
+    {
+        $this->requestSet->setRedirectUrl('http://example.org');
+
+        $this->assertFalse($this->requestSet->shouldPerformRedirectToUrl());
+    }
+
+    public function test_shouldPerformRedirectToUrl_shouldRedirect_IfUrlIsGivenAndWhitelistedInAnySiteId()
+    {
+        $this->requestSet->setRedirectUrl('http://www.example.com');
+
+        $this->assertEquals('http://www.example.com', $this->requestSet->shouldPerformRedirectToUrl());
+    }
+
+    public function test_shouldPerformRedirectToUrl_shouldRedirect_IfBaseDomainIsGivenAndWhitelistedInAnySiteId()
+    {
+        $this->requestSet->setRedirectUrl('http://example.com');
+
+        $this->assertEquals('http://example.com', $this->requestSet->shouldPerformRedirectToUrl());
+    }
+
+    public function test_initRequestsAndTokenAuth_shouldTriggerEventToInitRequestsButOnlyOnce()
+    {
+        $requestSet = $this->buildNewRequestSetThatIsNotInitializedYet();
+
+        $called = 0;
+        $passedRequest = null;
+        Piwik::addAction('Tracker.initRequestSet', function ($param) use (&$called, &$passedRequest) {
+            $called++;
+            $passedRequest = $param;
+        });
+
+        $requestSet->initRequestsAndTokenAuth();
+        $this->assertSame(1, $called);
+        $this->assertSame($requestSet, $passedRequest);
+
+        $requestSet->initRequestsAndTokenAuth(); // should not be called again
+        $this->assertSame(1, $called);
+    }
+
+    public function test_initRequestsAndTokednAuth_shouldInitializeRequestsWithEmptyArray()
+    {
+        $requestSet = $this->buildNewRequestSetThatIsNotInitializedYet();
+        $requestSet->initRequestsAndTokenAuth();
+        $this->assertEquals(array(), $requestSet->getRequests());
+    }
+
+    public function test_initRequestsAndTokednAuth_shouldInitializeFromGetAndPostIfEventDoesNotHandleRequests()
+    {
+        $_GET  = array('idsite' => 1);
+        $_POST = array('c_i' => 'click');
+
+        Piwik::addAction('Tracker.initRequestSet', function (RequestSet $requestSet) {
+            $requestSet->setRequests(array(array('idsite' => '2'), array('idsite' => '3')));
+        });
+
+        $requestSet = $this->buildNewRequestSetThatIsNotInitializedYet();
+
+        $requestSet->initRequestsAndTokenAuth();
+
+        $requests = $requestSet->getRequests();
+        $this->assertCount(2, $requests);
+        $this->assertEquals(array('idsite' => '2'), $requests[0]->getParams());
+        $this->assertEquals(array('idsite' => '3'), $requests[1]->getParams());
+    }
+
+    public function test_initRequestsAndTokednAuth_shouldIgnoreGetAndPostIfInitializedByEvent()
+    {
+        $_GET  = array('idsite' => '1');
+        $_POST = array('c_i' => 'click');
+
+        $requestSet = $this->buildNewRequestSetThatIsNotInitializedYet();
+        $requestSet->initRequestsAndTokenAuth();
+        $requests = $requestSet->getRequests();
+        $this->assertCount(1, $requests);
+        $this->assertEquals(array('idsite' => 1, 'c_i' => 'click'), $requests[0]->getParams());
+    }
+
+    private function buildNewRequestSetThatIsNotInitializedYet()
+    {
+        return new TestRequestSet();
+    }
+}
diff --git a/tests/PHPUnit/Integration/Tracker/RequestTest.php b/tests/PHPUnit/Integration/Tracker/RequestTest.php
new file mode 100644
index 00000000000..fe4ba830cac
--- /dev/null
+++ b/tests/PHPUnit/Integration/Tracker/RequestTest.php
@@ -0,0 +1,393 @@
+isAuthenticated = true;
+    }
+
+}
+
+/**
+ * @group RequestTest
+ * @group Request
+ * @group Tracker
+ */
+class RequestTest extends IntegrationTestCase
+{
+    /**
+     * @var TestRequest
+     */
+    private $request;
+
+    public function setUp()
+    {
+        parent::setUp();
+
+        Fixture::createWebsite('2014-01-01 00:00:00');
+        Fixture::createWebsite('2014-01-01 00:00:00');
+        Cache::deleteTrackerCache();
+
+        $this->request = $this->buildRequest(array('idsite' => '1'));
+    }
+
+    public function tearDown()
+    {
+        EventDispatcher::getInstance()->clearObservers('Request.initAuthenticationObject');
+        parent::tearDown();
+    }
+
+    public function test_getCustomVariablesInVisitScope_ShouldReturnNoCustomVars_IfNoWerePassedInParams()
+    {
+        $this->assertEquals(array(), $this->request->getCustomVariablesInVisitScope());
+    }
+
+    public function test_getCustomVariablesInVisitScope_ShouldReturnNoCustomVars_IfPassedParamIsNotAnArray()
+    {
+        $this->assertCustomVariablesInVisitScope(array(), '{"mykey":"myval"}');
+    }
+
+    public function test_getCustomVariablesInVisitScope_ShouldReturnCustomVars_IfTheyAreValid()
+    {
+        $customVars = $this->buildCustomVars(array('mykey' => 'myval', 'test' => 'value'));
+        $expected   = $this->buildExpectedCustomVars(array('mykey' => 'myval', 'test' => 'value'));
+
+        $this->assertCustomVariablesInVisitScope($expected, $customVars);
+    }
+
+    public function test_getCustomVariablesInVisitScope_ShouldIgnoreIndexesLowerThan1()
+    {
+        $customVars = array(
+            array('mykey', 'myval'),
+            array('test', 'value'),
+        );
+        $expected   = $this->buildExpectedCustomVars(array('test' => 'value'));
+
+        $this->assertCustomVariablesInVisitScope($expected, json_encode($customVars));
+    }
+
+    public function test_getCustomVariablesInVisitScope_ShouldTruncateValuesIfTheyAreTooLong()
+    {
+        $maxLen = CustomVariables::getMaxLengthCustomVariables();
+
+        $customVars = $this->buildCustomVars(array(
+            'mykey' => 'myval',
+            'test'  => str_pad('test', $maxLen + 5, 't'),
+        ));
+        $expected = $this->buildExpectedCustomVars(array(
+            'mykey' => 'myval',
+            'test'  => str_pad('test', $maxLen, 't'),
+        ));
+
+        $this->assertCustomVariablesInVisitScope($expected, $customVars);
+    }
+
+    public function test_getCustomVariablesInVisitScope_ShouldIgnoreVarsThatDoNotHaveKeyAndValue()
+    {
+        $customVars = array(
+            1 => array('mykey', 'myval'),
+            2 => array('test'),
+        );
+        $expected = $this->buildExpectedCustomVars(array('mykey' => 'myval'));
+
+        $this->assertCustomVariablesInVisitScope($expected, json_encode($customVars));
+    }
+
+    public function test_getCustomVariablesInVisitScope_ShouldSetDefaultValueToEmptyStringAndHandleOtherTypes()
+    {
+        $input = array(
+            'myfloat'  => 5.55,
+            'myint'    => 53,
+            'mystring' => '',
+        );
+        $customVars = $this->buildCustomVars($input);
+        $expected   = $this->buildExpectedCustomVars($input);
+
+        $this->assertCustomVariablesInVisitScope($expected, $customVars);
+    }
+
+    public function test_getCustomVariablesInPageScope_ShouldReturnNoCustomVars_IfNoWerePassedInParams()
+    {
+        $this->assertEquals(array(), $this->request->getCustomVariablesInPageScope());
+    }
+
+    public function test_getCustomVariablesInPageScope_ShouldReturnNoCustomVars_IfPassedParamIsNotAnArray()
+    {
+        $this->assertCustomVariablesInPageScope(array(), '{"mykey":"myval"}');
+    }
+
+    public function test_getCustomVariablesInPageScope_ShouldReturnCustomVars_IfTheyAreValid()
+    {
+        $customVars = $this->buildCustomVars(array('mykey' => 'myval', 'test' => 'value'));
+        $expected   = $this->buildExpectedCustomVars(array('mykey' => 'myval', 'test' => 'value'));
+
+        $this->assertCustomVariablesInPageScope($expected, $customVars);
+    }
+
+    public function test_getCustomVariablesInPageScope_ShouldIgnoreIndexesLowerThan1()
+    {
+        $customVars = array(
+            array('mykey', 'myval'),
+            array('test', 'value'),
+        );
+        $expected   = $this->buildExpectedCustomVars(array('test' => 'value'));
+
+        $this->assertCustomVariablesInPageScope($expected, json_encode($customVars));
+    }
+
+    public function test_getCustomVariablesInPageScope_ShouldTruncateValuesIfTheyAreTooLong()
+    {
+        $maxLen = CustomVariables::getMaxLengthCustomVariables();
+
+        $customVars = $this->buildCustomVars(array(
+            'mykey' => 'myval',
+            'test'  => str_pad('test', $maxLen + 5, 't'),
+        ));
+        $expected = $this->buildExpectedCustomVars(array(
+            'mykey' => 'myval',
+            'test'  => str_pad('test', $maxLen, 't'),
+        ));
+
+        $this->assertCustomVariablesInPageScope($expected, $customVars);
+    }
+
+    public function test_getCustomVariablesInPageScope_ShouldIgnoreVarsThatDoNotHaveKeyAndValue()
+    {
+        $customVars = array(
+            1 => array('mykey', 'myval'),
+            2 => array('test'),
+        );
+        $expected = $this->buildExpectedCustomVars(array('mykey' => 'myval'));
+
+        $this->assertCustomVariablesInPageScope($expected, json_encode($customVars));
+    }
+
+    public function test_getCustomVariablesInPageScope_ShouldSetDefaultValueToEmptyStringAndHandleOtherTypes()
+    {
+        $input = array(
+            'myfloat'  => 5.55,
+            'myint'    => 53,
+            'mystring' => '',
+        );
+        $customVars = $this->buildCustomVars($input);
+        $expected   = $this->buildExpectedCustomVars($input);
+
+        $this->assertCustomVariablesInPageScope($expected, $customVars);
+    }
+
+    public function test_isAuthenticated_ShouldBeNotAuthenticatedInTestsByDefault()
+    {
+        $this->assertFalse($this->request->isAuthenticated());
+    }
+
+    public function test_isAuthenticated_ShouldBeAuthenticatedIfCheckIsDisabledInConfig()
+    {
+        $oldConfig = TrackerConfig::getConfigValue('tracking_requests_require_authentication');
+        TrackerConfig::setConfigValue('tracking_requests_require_authentication', 0);
+
+        $this->assertTrue($this->request->isAuthenticated());
+
+        TrackerConfig::setConfigValue('tracking_requests_require_authentication', $oldConfig);
+    }
+
+    public function test_isAuthenticated_ShouldReadTheIsAuthenticatedPropertyAndIgnoreACheck()
+    {
+        $this->assertFalse($this->request->isAuthenticated());
+        $this->request->setIsAuthenticated();
+        $this->assertTrue($this->request->isAuthenticated());
+    }
+
+    public function test_isAuthenticated_ShouldWorkIfTokenIsCorrect()
+    {
+        $token = $this->createAdminUserForSite(2);
+
+        $request = $this->buildRequestWithToken(array('idsite' => '1'), $token);
+        $this->assertFalse($request->isAuthenticated());
+
+        $request = $this->buildRequestWithToken(array('idsite' => '2'), $token);
+        $this->assertTrue($request->isAuthenticated());
+    }
+
+    public function test_isAuthenticated_ShouldAlwaysWorkForSuperUser()
+    {
+        Fixture::createSuperUser(false);
+        $token = Fixture::getTokenAuth();
+
+        $request = $this->buildRequestWithToken(array('idsite' => '1'), $token);
+        $this->assertTrue($request->isAuthenticated());
+
+        $request = $this->buildRequestWithToken(array('idsite' => '2'), $token);
+        $this->assertTrue($request->isAuthenticated());
+    }
+
+    public function test_authenticateSuperUserOrAdmin_ShouldFailIfTokenIsEmpty()
+    {
+        $isAuthenticated = Request::authenticateSuperUserOrAdmin('', 2);
+        $this->assertFalse($isAuthenticated);
+
+        $isAuthenticated = Request::authenticateSuperUserOrAdmin(null, 2);
+        $this->assertFalse($isAuthenticated);
+    }
+
+    public function test_authenticateSuperUserOrAdmin_ShouldPostAuthInitEvent_IfTokenIsGiven()
+    {
+        $called = 0;
+        Piwik::addAction('Request.initAuthenticationObject', function () use (&$called) {
+            $called++;
+        });
+
+        Request::authenticateSuperUserOrAdmin('', 2);
+        $this->assertSame(0, $called);
+
+        Request::authenticateSuperUserOrAdmin('atoken', 2);
+        $this->assertSame(1, $called);
+
+        Request::authenticateSuperUserOrAdmin('anothertoken', 2);
+        $this->assertSame(2, $called);
+
+        Request::authenticateSuperUserOrAdmin(null, 2);
+        $this->assertSame(2, $called);
+    }
+
+    public function test_authenticateSuperUserOrAdmin_ShouldNotBeAllowedToAccessSitesHavingInvalidId()
+    {
+        $token = $this->createAdminUserForSite(2);
+
+        $isAuthenticated = Request::authenticateSuperUserOrAdmin($token, -2);
+        $this->assertFalse($isAuthenticated);
+
+        $isAuthenticated = Request::authenticateSuperUserOrAdmin($token, 0);
+        $this->assertFalse($isAuthenticated);
+    }
+
+    public function test_authenticateSuperUserOrAdmin_ShouldWorkIfTokenIsCorrect()
+    {
+        $token = $this->createAdminUserForSite(2);
+
+        $isAuthenticated = Request::authenticateSuperUserOrAdmin($token, 1);
+        $this->assertFalse($isAuthenticated);
+
+        $isAuthenticated = Request::authenticateSuperUserOrAdmin($token, 2);
+        $this->assertTrue($isAuthenticated);
+    }
+
+    public function test_authenticateSuperUserOrAdmin_ShouldAlwaysWorkForSuperUser()
+    {
+        Fixture::createSuperUser(false);
+        $token = Fixture::getTokenAuth();
+
+        $isAuthenticated = Request::authenticateSuperUserOrAdmin($token, 1);
+        $this->assertTrue($isAuthenticated);
+
+        $isAuthenticated = Request::authenticateSuperUserOrAdmin($token, 2);
+        $this->assertTrue($isAuthenticated);
+    }
+
+    private function createAdminUserForSite($idSite)
+    {
+        $login = 'myadmin';
+        $passwordHash = UsersManager::getPasswordHash('password');
+
+        $token = API::getInstance()->getTokenAuth($login, $passwordHash);
+
+        $user = new Model();
+        $user->addUser($login, $passwordHash, 'admin@piwik', 'alias', $token, '2014-01-01 00:00:00');
+        $user->addUserAccess($login, 'admin', array($idSite));
+
+        return $token;
+    }
+
+    public function test_internalBuildExpectedCustomVars()
+    {
+        $this->assertEquals(array(), $this->buildExpectedCustomVars(array()));
+
+        $this->assertEquals(array('custom_var_k1' => 'key', 'custom_var_v1' => 'val'),
+                            $this->buildExpectedCustomVars(array('key' => 'val')));
+
+        $this->assertEquals(array(
+            'custom_var_k1' => 'key', 'custom_var_v1' => 'val',
+            'custom_var_k2' => 'key2', 'custom_var_v2' => 'val2',
+        ), $this->buildExpectedCustomVars(array('key' => 'val', 'key2' => 'val2')));
+    }
+
+    public function test_internalBuildCustomVars()
+    {
+        $this->assertEquals('[]', $this->buildCustomVars(array()));
+
+        $this->assertEquals('{"1":["key","val"]}',
+                            $this->buildCustomVars(array('key' => 'val')));
+
+        $this->assertEquals('{"1":["key","val"],"2":["key2","val2"]}',
+                            $this->buildCustomVars(array('key' => 'val', 'key2' => 'val2')));
+    }
+
+    private function assertCustomVariablesInVisitScope($expectedCvars, $cvarsJsonEncoded)
+    {
+        $request = $this->buildRequest(array('_cvar' => $cvarsJsonEncoded));
+        $this->assertEquals($expectedCvars, $request->getCustomVariablesInVisitScope());
+    }
+
+    private function assertCustomVariablesInPageScope($expectedCvars, $cvarsJsonEncoded)
+    {
+        $request = $this->buildRequest(array('cvar' => $cvarsJsonEncoded));
+        $this->assertEquals($expectedCvars, $request->getCustomVariablesInPageScope());
+    }
+
+    private function buildExpectedCustomVars($customVars)
+    {
+        $vars  = array();
+        $index = 1;
+
+        foreach ($customVars as $key => $value) {
+            $vars['custom_var_k' . $index] = $key;
+            $vars['custom_var_v' . $index] = $value;
+            $index++;
+        }
+
+        return $vars;
+    }
+
+    private function buildCustomVars($customVars)
+    {
+        $vars  = array();
+        $index = 1;
+
+        foreach ($customVars as $key => $value) {
+            $vars[$index] = array($key, $value);
+            $index++;
+        }
+
+        return json_encode($vars);
+    }
+
+    private function buildRequest($params)
+    {
+        return new TestRequest($params);
+    }
+
+    private function buildRequestWithToken($params, $token)
+    {
+        return new TestRequest($params, $token);
+    }
+}
diff --git a/tests/PHPUnit/Integration/Tracker/SettingsStorageTest.php b/tests/PHPUnit/Integration/Tracker/SettingsStorageTest.php
index 1bda1105e69..0aa2a49627d 100644
--- a/tests/PHPUnit/Integration/Tracker/SettingsStorageTest.php
+++ b/tests/PHPUnit/Integration/Tracker/SettingsStorageTest.php
@@ -8,10 +8,9 @@
 
 namespace Piwik\Tests\Integration\Tracker;
 
+use Piwik\Cache\PersistentCache;
 use Piwik\Option;
 use Piwik\Settings\Storage;
-use Piwik\Settings\Setting;
-use Piwik\Tests\Integration\Settings\IntegrationTestCase;
 use Piwik\Tests\Integration\Settings\StorageTest;
 use Piwik\Tracker\Cache;
 use Piwik\Tracker\SettingsStorage;
@@ -44,11 +43,11 @@ public function test_clearCache_shouldActuallyClearTheCacheEntry()
     {
         $this->setSettingValueInCache('my0815RandomName');
 
-        $this->assertArrayHasKey('settingsStorage', Cache::getCacheGeneral());
+        $this->assertTrue($this->getCache()->has());
 
         SettingsStorage::clearCache();
 
-        $this->assertArrayNotHasKey('settingsStorage', Cache::getCacheGeneral());
+        $this->assertFalse($this->getCache()->has());
     }
 
     public function test_storageShouldNotCastAnyCachedValue()
@@ -63,10 +62,12 @@ public function test_storageShouldFallbackToDatebaseInCaseNoCacheExists()
         $this->storage->setValue($this->setting, 5);
         $this->storage->save();
 
+        $this->assertFalse($this->getCache()->has());
         $this->assertNotFalse($this->getValueFromOptionTable()); // make sure saved in db
 
         $storage = $this->buildStorage();
         $this->assertEquals(5, $storage->getValue($this->setting));
+        $this->assertTrue($this->getCache()->has());
     }
 
     public function test_storageCreateACacheEntryIfNoCacheExistsYet()
@@ -76,53 +77,28 @@ public function test_storageCreateACacheEntryIfNoCacheExistsYet()
 
         $this->setSettingValueAndMakeSureCacheGetsCreated('myVal');
 
-        $cache = Cache::getCacheGeneral();
+        $cache = $this->getCache()->get();
 
         $this->assertEquals(array(
-            $this->storage->getOptionKey() => array(
-                $this->setting->getKey() => 'myVal'
-            )
-        ), $cache['settingsStorage']);
+            $this->setting->getKey() => 'myVal'
+        ), $cache);
     }
 
-    public function test_shouldAddACacheEntryToAnotherCacheEntryAndNotOverwriteAll()
+    protected function buildStorage()
     {
-        $dummyCacheEntry = array(
-            'Plugin_PluginNameOther_Settings' => array(
-                'anything' => 'anyval',
-                'any' => 'other'
-            )
-        );
-        Cache::setCacheGeneral(array(
-            'settingsStorage' => $dummyCacheEntry
-        ));
-
-        Option::set($this->storage->getOptionKey(), serialize(array('mykey' => 'myVal')));
-
-        $this->buildStorage()->getValue($this->setting); // force adding new cache entry
-
-        $cache = Cache::getCacheGeneral();
-
-        $dummyCacheEntry[$this->storage->getOptionKey()] = array(
-            'mykey' => 'myVal'
-        );
-
-        $this->assertEquals($dummyCacheEntry, $cache['settingsStorage']);
+        return new SettingsStorage('PluginName');
     }
 
-    protected function buildStorage()
+    private function getCache()
     {
-        return new SettingsStorage('PluginName');
+        return new PersistentCache($this->storage->getOptionKey());
     }
 
     private function setSettingValueInCache($value)
     {
-        Cache::setCacheGeneral(array(
-            'settingsStorage' => array(
-                $this->storage->getOptionKey() => array(
-                    $this->setting->getKey() => $value
-                )
-            )
+        $cache = $this->getCache();
+        $cache->set(array(
+            $this->setting->getKey() => $value
         ));
     }
 
diff --git a/tests/PHPUnit/Integration/Tracker/Visit/FactoryTest.php b/tests/PHPUnit/Integration/Tracker/Visit/FactoryTest.php
new file mode 100644
index 00000000000..2cefabc23bb
--- /dev/null
+++ b/tests/PHPUnit/Integration/Tracker/Visit/FactoryTest.php
@@ -0,0 +1,76 @@
+clearObservers('Tracker.makeNewVisitObject');
+        parent::tearDown();
+    }
+
+    public function test_make_shouldCreateDefaultInstance()
+    {
+        $visit = Factory::make();
+        $this->assertInstanceOf('Piwik\\Tracker\\Visit', $visit);
+    }
+
+    public function test_make_shouldTriggerEventOnce()
+    {
+        $called = 0;
+        $self   = $this;
+        Piwik::addAction('Tracker.makeNewVisitObject', function ($visit) use (&$called, $self) {
+            $called++;
+            $self->assertNull($visit);
+        });
+
+        Factory::make();
+        $this->assertSame(1, $called);
+    }
+
+    public function test_make_shouldPreferManuallyCreatedHandlerInstanceInEventOverDefaultHandler()
+    {
+        $visitToUse = new Visit();
+        Piwik::addAction('Tracker.makeNewVisitObject', function (&$visit) use ($visitToUse) {
+            $visit = $visitToUse;
+        });
+
+        $visit = Factory::make();
+        $this->assertSame($visitToUse, $visit);
+    }
+
+    /**
+     * @expectedException \Exception
+     * @expectedExceptionMessage The Visit object set in the plugin
+     */
+    public function test_make_shouldTriggerExceptionInCaseWrongInstanceCreatedInHandler()
+    {
+        Piwik::addAction('Tracker.makeNewVisitObject', function (&$visit) {
+            $visit = new Tracker();
+        });
+
+        Factory::make();
+    }
+}
diff --git a/tests/PHPUnit/Integration/TrackerTest.php b/tests/PHPUnit/Integration/TrackerTest.php
index bc4c99090c8..273acbbc5da 100644
--- a/tests/PHPUnit/Integration/TrackerTest.php
+++ b/tests/PHPUnit/Integration/TrackerTest.php
@@ -9,184 +9,313 @@
 namespace Piwik\Tests\Integration;
 
 use Piwik\Common;
-use Piwik\Db;
+use Piwik\EventDispatcher;
+use Piwik\Piwik;
+use Piwik\Plugin;
+use Piwik\SettingsServer;
 use Piwik\Tests\Framework\Fixture;
 use Piwik\Tests\Framework\TestCase\IntegrationTestCase;
 use Piwik\Tracker;
+use Piwik\Tracker\RequestSet;
+use Piwik\Tracker\Request;
+use Piwik\Translate;
+
+class TestTracker extends Tracker
+{
+    public function __construct()
+    {
+        $this->isInstalled = true;
+    }
+
+    public function setIsNotInstalled()
+    {
+        $this->isInstalled = false;
+    }
+
+    public function disconnectDatabase()
+    {
+        parent::disconnectDatabase();
+    }
+}
 
 /**
- * @group Core
+ * @group TrackerTest
+ * @group Tracker
  */
 class TrackerTest extends IntegrationTestCase
 {
+    /**
+     * @var TestTracker
+     */
+    private $tracker;
+
+    /**
+     * @var Request
+     */
+    private $request;
+
     public function setUp()
     {
         parent::setUp();
-        Fixture::createWebsite('2014-02-04');
+
+        Fixture::createWebsite('2014-01-01 00:00:00');
+
+        $this->tracker = new TestTracker();
+        $this->request = $this->buildRequest(array('idsite' => 1));
     }
 
-    protected static function configureFixture($fixture)
+    public function tearDown()
     {
-        $fixture->createSuperUser = true;
+        $this->tracker->disconnectDatabase();
+        EventDispatcher::getInstance()->clearObservers('Tracker.makeNewVisitObject');
+        if (array_key_exists('PIWIK_TRACKER_DEBUG', $GLOBALS)) {
+            unset($GLOBALS['PIWIK_TRACKER_DEBUG']);
+        }
+        parent::tearDown();
     }
 
-    /**
-     * Test the Bulk tracking API as documented in: http://developer.piwik.org/api-reference/tracking-api#bulk-tracking
-     *
-     * With invalid token_auth the request would still work
-     */
-    public function test_trackingApiWithBulkRequests_viaCurl_withWrongTokenAuth()
+    public function test_isInstalled_shouldReturnTrue_AsPiwikIsInstalled()
     {
-        $token_auth = '33dc3f2536d3025974cccb4b4d2d98f4';
-        $this->issueBulkTrackingRequest($token_auth, $expectTrackingToSucceed = true);
+        $this->assertTrue($this->tracker->isInstalled());
     }
 
-    public function test_trackingApiWithBulkRequests_viaCurl_withCorrectTokenAuth()
+    public function test_shouldRecordStatistics_shouldReturnTrue_IfEnabled_WhichItIsByDefault()
     {
-        $token_auth = Fixture::getTokenAuth();
-        \Piwik\Filesystem::deleteAllCacheOnUpdate();
-        $this->issueBulkTrackingRequest($token_auth, $expectTrackingToSucceed = true);
+        $this->assertTrue($this->tracker->shouldRecordStatistics());
     }
 
-    public function test_trackingEcommerceOrder_WithHtmlEscapedText_InsertsCorrectLogs()
+    public function test_shouldRecordStatistics_shouldReturnFalse_IfEnabledButNotInstalled()
     {
-        // item sku, item name, item category, item price, item quantity
-        // NOTE: used to test with '𝌆' character, however, mysql on travis fails with this when
-        //       inserting this character decoded.
-        $ecItems = array(array('"scarysku', 'superscarymovie"', 'scary & movies', 12.99, 1),
-                         array('> scary', 'but < "super', 'scary"', 14, 15),
-                         array("'Foo ©", " bar ", " baz ☃ qux", 16, 17));
+        $this->tracker->setIsNotInstalled();
+        $this->assertFalse($this->tracker->shouldRecordStatistics());
+    }
 
-        $urlToTest = $this->getEcommerceItemsUrl($ecItems);
+    public function test_shouldRecordStatistics_shouldReturnFalse_IfDisabledButInstalled()
+    {
+        $oldConfig = Tracker\TrackerConfig::getConfigValue('record_statistics');
+        Tracker\TrackerConfig::setConfigValue('record_statistics', 0);
 
-        $response = $this->sendTrackingRequestByCurl($urlToTest);
-        Fixture::checkResponse($response);
+        $this->assertFalse($this->tracker->shouldRecordStatistics());
 
-        $this->assertEquals(1, $this->getCountOfConversions());
+        Tracker\TrackerConfig::setConfigValue('record_statistics', $oldConfig); // reset
+    }
+
+    public function test_loadTrackerEnvironment_shouldSetGlobalsDebugVar_WhichShouldBeDisabledByDefault()
+    {
+        $this->assertTrue(!array_key_exists('PIWIK_TRACKER_DEBUG', $GLOBALS));
+
+        Tracker::loadTrackerEnvironment();
+
+        $this->assertFalse($GLOBALS['PIWIK_TRACKER_DEBUG']);
+    }
 
-        $conversionItems = $this->getConversionItems();
-        $this->assertEquals(3, count($conversionItems));
+    public function test_loadTrackerEnvironment_shouldSetGlobalsDebugVar()
+    {
+        $this->assertTrue(!array_key_exists('PIWIK_TRACKER_DEBUG', $GLOBALS));
+
+        $oldConfig = Tracker\TrackerConfig::getConfigValue('debug');
+        Tracker\TrackerConfig::setConfigValue('debug', 1);
 
-        $this->assertActionEquals('"scarysku', $conversionItems[0]['idaction_sku']);
-        $this->assertActionEquals('superscarymovie"', $conversionItems[0]['idaction_name']);
-        $this->assertActionEquals('scary & movies', $conversionItems[0]['idaction_category']);
+        Tracker::loadTrackerEnvironment();
+        $this->assertTrue($this->tracker->isDebugModeEnabled());
 
-        $this->assertActionEquals('> scary', $conversionItems[1]['idaction_sku']);
-        $this->assertActionEquals('but < "super', $conversionItems[1]['idaction_name']);
-        $this->assertActionEquals('scary"', $conversionItems[1]['idaction_category']);
+        Tracker\TrackerConfig::setConfigValue('debug', $oldConfig); // reset
 
-        $this->assertActionEquals('\'Foo ©', $conversionItems[2]['idaction_sku']);
-        $this->assertActionEquals('bar', $conversionItems[2]['idaction_name']);
-        $this->assertActionEquals('baz ☃ qux', $conversionItems[2]['idaction_category']);
+        $this->assertTrue($GLOBALS['PIWIK_TRACKER_DEBUG']);
     }
 
-    public function test_trackingEcommerceOrder_WithAmpersandAndQuotes_InsertsCorrectLogs()
+    public function test_loadTrackerEnvironment_shouldEnableTrackerMode()
     {
-        // item sku, item name, item category, item price, item quantity
-        $ecItems = array(array("\"scarysku&", "superscarymovie'", 'scary <> movies', 12.99, 1));
+        $this->assertFalse(SettingsServer::isTrackerApiRequest());
 
-        $urlToTest = $this->getEcommerceItemsUrl($ecItems);
+        Tracker::loadTrackerEnvironment();
 
-        $response = $this->sendTrackingRequestByCurl($urlToTest);
-        Fixture::checkResponse($response);
+        $this->assertTrue(SettingsServer::isTrackerApiRequest());
+    }
 
-        $this->assertEquals(1, $this->getCountOfConversions());
+    public function test_isDatabaseConnected_shouldReturnFalse_IfNotConnected()
+    {
+        $this->tracker->disconnectDatabase();
 
-        $conversionItems = $this->getConversionItems();
-        $this->assertEquals(1, count($conversionItems));
+        $this->assertFalse($this->tracker->isDatabaseConnected());
+    }
 
-        $this->assertActionEquals('"scarysku&', $conversionItems[0]['idaction_sku']);
-        $this->assertActionEquals('superscarymovie\'', $conversionItems[0]['idaction_name']);
-        $this->assertActionEquals('scary <> movies', $conversionItems[0]['idaction_category']);
+    public function test_getDatabase_shouldReturnDbInstance()
+    {
+        $db = $this->tracker->getDatabase();
+
+        $this->assertInstanceOf('Piwik\\Tracker\\Db', $db);
     }
 
-    public function test_trackingEcommerceOrder_DoesNotFail_WhenEmptyEcommerceItemsParamUsed()
+    public function test_isDatabaseConnected_shouldReturnTrue_WhenDbIsConnected()
     {
-        // item sku, item name, item category, item price, item quantity
-        $urlToTest = $this->getEcommerceItemsUrl("");
+        $db = $this->tracker->getDatabase(); // make sure connected
+        $this->assertNotEmpty($db);
 
-        $response = $this->sendTrackingRequestByCurl($urlToTest);
-        Fixture::checkResponse($response);
+        $this->assertTrue($this->tracker->isDatabaseConnected());
+    }
+
+    public function test_disconnectDatabase_shouldDisconnectDb()
+    {
+        $this->tracker->getDatabase(); // make sure connected
+        $this->assertTrue($this->tracker->isDatabaseConnected());
+
+        $this->tracker->disconnectDatabase();
+
+        $this->assertFalse($this->tracker->isDatabaseConnected());
+    }
+
+    public function test_trackRequest_shouldNotTrackAnything_IfRequestIsEmpty()
+    {
+        $called = false;
+        Piwik::addAction('Tracker.makeNewVisitObject', function () use (&$called) {
+            $called = true;
+        });
 
-        $this->assertEquals(1, $this->getCountOfConversions());
-        $this->assertEquals(0, count($this->getConversionItems()));
+        $this->tracker->trackRequest(new Request(array()));
+
+        $this->assertFalse($called);
+    }
+
+    public function test_trackRequest_shouldTrack_IfRequestIsNotEmpty()
+    {
+        $called = false;
+        Piwik::addAction('Tracker.makeNewVisitObject', function () use (&$called) {
+            $called = true;
+        });
+
+        $this->tracker->trackRequest($this->request);
+
+        $this->assertTrue($called);
+    }
+
+    public function test_trackRequest_shouldIncreaseLoggedRequestsCounter()
+    {
+        $this->tracker->trackRequest($this->request);
+        $this->assertSame(1, $this->tracker->getCountOfLoggedRequests());
+
+        $this->tracker->trackRequest($this->request);
+        $this->assertSame(2, $this->tracker->getCountOfLoggedRequests());
+    }
+
+    public function test_trackRequest_shouldIncreaseLoggedRequestsCounter_EvenIfRequestIsEmpty()
+    {
+        $request = $this->buildRequest(array());
+        $this->assertTrue($request->isEmptyRequest());
+
+        $this->tracker->trackRequest($request);
+        $this->assertSame(1, $this->tracker->getCountOfLoggedRequests());
+
+        $this->tracker->trackRequest($request);
+        $this->assertSame(2, $this->tracker->getCountOfLoggedRequests());
+    }
+
+    public function test_trackRequest_shouldActuallyTrack()
+    {
+        $request = $this->buildRequest(array('idsite' => 1, 'url' => 'http://www.example.com', 'action_name' => 'test', 'rec' => 1));
+        $this->tracker->trackRequest($request);
+
+        $this->assertActionEquals('test', 1);
+        $this->assertActionEquals('example.com', 2);
+    }
+
+    public function test_main_shouldReturnEmptyPiwikResponse_IfNoRequestsAreGiven()
+    {
+        $requestSet = $this->getEmptyRequestSet();
+        $requestSet->setRequests(array());
+
+        $response = $this->tracker->main($this->getDefaultHandler(), $requestSet);
+
+        $expected = "Piwik is a free/libre web analytics that lets you keep control of your data.";
+        $this->assertEquals($expected, $response);
     }
 
-    public function test_trackingEcommerceOrder_DoesNotFail_WhenNonArrayUsedWithEcommerceItemsParam()
+    public function test_main_shouldReturnApiResponse_IfRequestsAreGiven()
     {
-        // item sku, item name, item category, item price, item quantity
-        $urlToTest = $this->getEcommerceItemsUrl("45");
+        $response = $this->tracker->main($this->getDefaultHandler(), $this->getRequestSetWithRequests());
 
-        $response = $this->sendTrackingRequestByCurl($urlToTest);
         Fixture::checkResponse($response);
+    }
+
+    public function test_main_shouldReturnNotReturnAnyApiResponse_IfImageIsDisabled()
+    {
+        $_GET['send_image'] = '0';
 
-        $this->assertEquals(0, $this->getCountOfConversions());
-        $this->assertEquals(0, count($this->getConversionItems()));
+        $response = $this->tracker->main($this->getDefaultHandler(), $this->getRequestSetWithRequests());
+
+        unset($_GET['send_image']);
+
+        $this->assertEquals('', $response);
     }
 
-    protected function issueBulkTrackingRequest($token_auth, $expectTrackingToSucceed)
+    public function test_main_shouldActuallyTrackNumberOfTrackedRequests()
     {
-        $piwikHost = Fixture::getRootUrl() . 'tests/PHPUnit/proxy/piwik.php';
+        $this->assertSame(0, $this->tracker->getCountOfLoggedRequests());
 
-        $command = 'curl -s -X POST -d \'{"requests":["?idsite=1&url=http://example.org&action_name=Test bulk log Pageview&rec=1","?idsite=1&url=http://example.net/test.htm&action_name=Another bulk page view&rec=1"],"token_auth":"' . $token_auth . '"}\' ' . $piwikHost;
+        $this->tracker->main($this->getDefaultHandler(), $this->getRequestSetWithRequests());
 
-        exec($command, $output, $result);
-        if ($result !== 0) {
-            throw new \Exception("tracking bulk failed: " . implode("\n", $output) . "\n\ncommand used: $command");
-        }
-        $output = implode("", $output);
-        $this->assertStringStartsWith('{"status":', $output);
-
-        if($expectTrackingToSucceed) {
-            $this->assertNotContains('error', $output);
-            $this->assertContains('success', $output);
-        } else {
-            $this->assertContains('error', $output);
-            $this->assertNotContains('success', $output);
-        }
+        $this->assertSame(2, $this->tracker->getCountOfLoggedRequests());
     }
 
-    private function sendTrackingRequestByCurl($url)
+    public function test_main_shouldNotTrackAnythingButStillReturnApiResponse_IfNotInstalledOrShouldNotRecordStats()
     {
-        if (!function_exists('curl_init')) {
-            $this->markTestSkipped('Curl is not installed');
-        }
+        $this->tracker->setIsNotInstalled();
+        $response = $this->tracker->main($this->getDefaultHandler(), $this->getRequestSetWithRequests());
+
+        Fixture::checkResponse($response);
+        $this->assertSame(0, $this->tracker->getCountOfLoggedRequests());
+    }
 
-        $ch = curl_init();
-        curl_setopt($ch, CURLOPT_URL, Fixture::getRootUrl() . 'tests/PHPUnit/proxy/piwik.php' . $url);
-        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
-        curl_setopt($ch, CURLOPT_HEADER, true);
-        curl_setopt($ch, CURLOPT_TIMEOUT, 10);
+    public function test_main_shouldReadValuesFromGETandPOSTifNoRequestSet()
+    {
+        $_GET  = array('idsite' => '1');
+        $_POST = array('url' => 'http://localhost/post');
 
-        $response = curl_exec($ch);
-        $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
-        $response = substr($response, $headerSize);
+        $requestSet = $this->getEmptyRequestSet();
+        $response   = $this->tracker->main($this->getDefaultHandler(), $requestSet);
 
-        curl_close($ch);
+        $_GET  = array();
+        $_POST = array();
 
-        return $response;
+        Fixture::checkResponse($response);
+        $this->assertSame(1, $this->tracker->getCountOfLoggedRequests());
+
+        $identifiedRequests = $requestSet->getRequests();
+        $this->assertCount(1, $identifiedRequests);
+        $this->assertEquals(array('idsite' => '1', 'url' => 'http://localhost/post'),
+                            $identifiedRequests[0]->getParams());
     }
 
-    private function assertActionEquals($expected, $idaction)
+    private function getDefaultHandler()
     {
-        $actionName = Db::fetchOne("SELECT name FROM " . Common::prefixTable('log_action') . " WHERE idaction = ?", array($idaction));
-        $this->assertEquals($expected, $actionName);
+        return new Tracker\Handler();
     }
 
-    private function getCountOfConversions()
+    private function getEmptyRequestSet()
     {
-        return Db::fetchOne("SELECT COUNT(*) FROM " . Common::prefixTable('log_conversion'));
+        return new RequestSet();
     }
 
-    private function getConversionItems()
+    private function getRequestSetWithRequests()
     {
-        return Db::fetchAll("SELECT * FROM " . Common::prefixTable('log_conversion_item'));
+        $requestSet = $this->getEmptyRequestSet();
+        $requestSet->setRequests(array(
+            $this->buildRequest(array('idsite' => '1', 'url' => 'http://localhost')),
+            $this->buildRequest(array('idsite' => '1', 'url' => 'http://localhost/test'))
+        ));
+
+        return $requestSet;
     }
 
-    private function getEcommerceItemsUrl($ecItems, $doJsonEncode = true)
+    private function assertActionEquals($expected, $idaction)
     {
-        $ecItemsStr = $doJsonEncode ? json_encode($ecItems) : $ecItems;
-        return "?idsite=1&idgoal=0&rec=1&url=" . urlencode('http://quellehorreur.com/movies') . "&ec_items="
-        . urlencode($ecItemsStr) . '&ec_id=myspecial-id-1234&revenue=16.99&ec_st=12.99&ec_tx=0&ec_sh=3';
+        $actionName = Tracker::getDatabase()->fetchOne("SELECT name FROM " . Common::prefixTable('log_action') . " WHERE idaction = ?", array($idaction));
+        $this->assertEquals($expected, $actionName);
     }
+
+    private function buildRequest($params)
+    {
+        return new Request($params);
+    }
+
 }
\ No newline at end of file
diff --git a/tests/PHPUnit/System/TrackerResponseTest.php b/tests/PHPUnit/System/TrackerResponseTest.php
new file mode 100755
index 00000000000..7bd0f7aef78
--- /dev/null
+++ b/tests/PHPUnit/System/TrackerResponseTest.php
@@ -0,0 +1,106 @@
+tracker = Fixture::getTracker($idSite, $dateTime, $defaultInit = true);
+    }
+
+    public function test_response_ShouldContainAnImage()
+    {
+        $response = $this->tracker->doTrackPageView('Test');
+
+        Fixture::checkResponse($response);
+        $this->assertNotEmpty($response);
+    }
+
+    public function test_response_ShouldBeEmpty_IfImageIsDisabled()
+    {
+        $this->tracker->disableSendImageResponse();
+
+        $response = $this->tracker->doTrackPageView('Test');
+
+        $this->assertSame('', $response);
+    }
+
+    public function test_response_ShouldSend200ResponseCode_IfImageIsEnabled()
+    {
+        $url = $this->tracker->getUrlTrackPageView('Test');
+
+        $this->assertResponseCode(200, $url);
+    }
+
+    public function test_response_ShouldSend204ResponseCode_IfImageIsDisabled()
+    {
+        $url = $this->tracker->getUrlTrackPageView('Test');
+        $url .= '&send_image=0';
+
+        $this->assertResponseCode(204, $url);
+    }
+
+    public function test_response_ShouldSend400ResponseCode_IfSiteIdIsInvalid()
+    {
+        $url = $this->tracker->getUrlTrackPageView('Test');
+        $url .= '&idsite=100';
+
+        $this->assertResponseCode(400, $url);
+    }
+
+    public function test_response_ShouldSend400ResponseCode_IfSiteIdIsZero()
+    {
+        $url = $this->tracker->getUrlTrackPageView('Test');
+        $url .= '&idsite=0';
+
+        $this->assertResponseCode(400, $url);
+    }
+
+    public function test_response_ShouldSend400ResponseCode_IfInvalidRequestParameterIsGiven()
+    {
+        $url = $this->tracker->getUrlTrackPageView('Test');
+        $url .= '&cid=' . str_pad('1', 16, '1');
+
+        $this->assertResponseCode(200, $url);
+        $this->assertResponseCode(400, $url . '1'); // has to be 16 char, but is 17 now
+    }
+
+    public function test_response_ShouldReturnPiwikMessage_InCaseOfEmptyRequest()
+    {
+        $url = Fixture::getTrackerUrl();
+        $response = file_get_contents($url);
+
+        $expected = "Piwik is a free/libre web analytics that lets you keep control of your data.";
+        $this->assertEquals($expected, $response);
+    }
+
+}
diff --git a/tests/PHPUnit/System/TrackerTest.php b/tests/PHPUnit/System/TrackerTest.php
old mode 100755
new mode 100644
index 84f5db3aac3..65b16f6feae
--- a/tests/PHPUnit/System/TrackerTest.php
+++ b/tests/PHPUnit/System/TrackerTest.php
@@ -2,96 +2,192 @@
 /**
  * Piwik - free/libre analytics platform
  *
- * @link    http://piwik.org
+ * @link http://piwik.org
  * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  */
+
 namespace Piwik\Tests\System;
 
+use Piwik\Common;
+use Piwik\Db;
 use Piwik\Tests\Framework\Fixture;
-use Piwik\Tests\Framework\TestCase\SystemTestCase;
+use Piwik\Tests\Framework\TestCase\IntegrationTestCase;
+use Piwik\Tracker;
 
 /**
- * @group TrackerTest
- * @group Plugins
+ * @group Core
+ * @group Tracker
  */
-class TrackerTest extends SystemTestCase
+class TrackerTest extends IntegrationTestCase
 {
-    public static $fixture = null;
+    public function setUp()
+    {
+        parent::setUp();
+        Fixture::createWebsite('2014-02-04');
+    }
+
+    protected static function configureFixture($fixture)
+    {
+        $fixture->createSuperUser = true;
+    }
 
     /**
-     * @var \PiwikTracker
+     * Test the Bulk tracking API as documented in: http://developer.piwik.org/api-reference/tracking-api#bulk-tracking
+     *
+     * With invalid token_auth the request would still work
      */
-    private $tracker;
+    public function test_trackingApiWithBulkRequests_viaCurl_withWrongTokenAuth()
+    {
+        $token_auth = '33dc3f2536d3025974cccb4b4d2d98f4';
+        $this->issueBulkTrackingRequest($token_auth, $expectTrackingToSucceed = true);
+    }
 
-    public function setUp()
+    public function test_trackingApiWithBulkRequests_viaCurl_withCorrectTokenAuth()
     {
-        parent::setUp();
+        $token_auth = Fixture::getTokenAuth();
+        \Piwik\Filesystem::deleteAllCacheOnUpdate();
+        $this->issueBulkTrackingRequest($token_auth, $expectTrackingToSucceed = true);
+    }
 
-        $idSite = 1;
-        $dateTime = '2014-01-01 00:00:01';
+    public function test_trackingEcommerceOrder_WithHtmlEscapedText_InsertsCorrectLogs()
+    {
+        // item sku, item name, item category, item price, item quantity
+        // NOTE: used to test with '𝌆' character, however, mysql on travis fails with this when
+        //       inserting this character decoded.
+        $ecItems = array(array('"scarysku', 'superscarymovie"', 'scary & movies', 12.99, 1),
+                         array('> scary', 'but < "super', 'scary"', 14, 15),
+                         array("'Foo ©", " bar ", " baz ☃ qux", 16, 17));
 
-        if (!Fixture::siteCreated($idSite)) {
-            Fixture::createWebsite($dateTime);
-        }
+        $urlToTest = $this->getEcommerceItemsUrl($ecItems);
+
+        $response = $this->sendTrackingRequestByCurl($urlToTest);
+        Fixture::checkResponse($response);
+
+        $this->assertEquals(1, $this->getCountOfConversions());
+
+        $conversionItems = $this->getConversionItems();
+        $this->assertEquals(3, count($conversionItems));
+
+        $this->assertActionEquals('"scarysku', $conversionItems[0]['idaction_sku']);
+        $this->assertActionEquals('superscarymovie"', $conversionItems[0]['idaction_name']);
+        $this->assertActionEquals('scary & movies', $conversionItems[0]['idaction_category']);
 
-        $this->tracker = Fixture::getTracker($idSite, $dateTime, $defaultInit = true);
+        $this->assertActionEquals('> scary', $conversionItems[1]['idaction_sku']);
+        $this->assertActionEquals('but < "super', $conversionItems[1]['idaction_name']);
+        $this->assertActionEquals('scary"', $conversionItems[1]['idaction_category']);
+
+        $this->assertActionEquals('\'Foo ©', $conversionItems[2]['idaction_sku']);
+        $this->assertActionEquals('bar', $conversionItems[2]['idaction_name']);
+        $this->assertActionEquals('baz ☃ qux', $conversionItems[2]['idaction_category']);
     }
 
-    public function test_response_ShouldContainAnImage()
+    public function test_trackingEcommerceOrder_WithAmpersandAndQuotes_InsertsCorrectLogs()
     {
-        $response = $this->tracker->doTrackPageView('Test');
+        // item sku, item name, item category, item price, item quantity
+        $ecItems = array(array("\"scarysku&", "superscarymovie'", 'scary <> movies', 12.99, 1));
+
+        $urlToTest = $this->getEcommerceItemsUrl($ecItems);
 
+        $response = $this->sendTrackingRequestByCurl($urlToTest);
         Fixture::checkResponse($response);
-        $this->assertNotEmpty($response);
+
+        $this->assertEquals(1, $this->getCountOfConversions());
+
+        $conversionItems = $this->getConversionItems();
+        $this->assertEquals(1, count($conversionItems));
+
+        $this->assertActionEquals('"scarysku&', $conversionItems[0]['idaction_sku']);
+        $this->assertActionEquals('superscarymovie\'', $conversionItems[0]['idaction_name']);
+        $this->assertActionEquals('scary <> movies', $conversionItems[0]['idaction_category']);
     }
 
-    public function test_response_ShouldBeEmpty_IfImageIsDisabled()
+    public function test_trackingEcommerceOrder_DoesNotFail_WhenEmptyEcommerceItemsParamUsed()
     {
-        $this->tracker->disableSendImageResponse();
+        // item sku, item name, item category, item price, item quantity
+        $urlToTest = $this->getEcommerceItemsUrl("");
 
-        $response = $this->tracker->doTrackPageView('Test');
+        $response = $this->sendTrackingRequestByCurl($urlToTest);
+        Fixture::checkResponse($response);
 
-        $this->assertSame('', $response);
+        $this->assertEquals(1, $this->getCountOfConversions());
+        $this->assertEquals(0, count($this->getConversionItems()));
     }
 
-    public function test_response_ShouldSend200ResponseCode_IfImageIsEnabled()
+    public function test_trackingEcommerceOrder_DoesNotFail_WhenNonArrayUsedWithEcommerceItemsParam()
     {
-        $url = $this->tracker->getUrlTrackPageView('Test');
+        // item sku, item name, item category, item price, item quantity
+        $urlToTest = $this->getEcommerceItemsUrl("45");
+
+        $response = $this->sendTrackingRequestByCurl($urlToTest);
+        Fixture::checkResponse($response);
 
-        $this->assertResponseCode(200, $url);
+        $this->assertEquals(0, $this->getCountOfConversions());
+        $this->assertEquals(0, count($this->getConversionItems()));
     }
 
-    public function test_response_ShouldSend204ResponseCode_IfImageIsDisabled()
+    protected function issueBulkTrackingRequest($token_auth, $expectTrackingToSucceed)
     {
-        $url = $this->tracker->getUrlTrackPageView('Test');
-        $url .= '&send_image=0';
+        $piwikHost = Fixture::getRootUrl() . 'tests/PHPUnit/proxy/piwik.php';
 
-        $this->assertResponseCode(204, $url);
+        $command = 'curl -s -X POST -d \'{"requests":["?idsite=1&url=http://example.org&action_name=Test bulk log Pageview&rec=1","?idsite=1&url=http://example.net/test.htm&action_name=Another bulk page view&rec=1"],"token_auth":"' . $token_auth . '"}\' ' . $piwikHost;
+
+        exec($command, $output, $result);
+        if ($result !== 0) {
+            throw new \Exception("tracking bulk failed: " . implode("\n", $output) . "\n\ncommand used: $command");
+        }
+        $output = implode("", $output);
+        $this->assertStringStartsWith('{"status":', $output);
+
+        if($expectTrackingToSucceed) {
+            $this->assertNotContains('error', $output);
+            $this->assertContains('success', $output);
+        } else {
+            $this->assertContains('error', $output);
+            $this->assertNotContains('success', $output);
+        }
     }
 
-    public function test_response_ShouldSend400ResponseCode_IfSiteIdIsInvalid()
+    private function sendTrackingRequestByCurl($url)
     {
-        $url = $this->tracker->getUrlTrackPageView('Test');
-        $url .= '&idsite=100';
+        if (!function_exists('curl_init')) {
+            $this->markTestSkipped('Curl is not installed');
+        }
+
+        $ch = curl_init();
+        curl_setopt($ch, CURLOPT_URL, Fixture::getRootUrl() . 'tests/PHPUnit/proxy/piwik.php' . $url);
+        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+        curl_setopt($ch, CURLOPT_HEADER, true);
+        curl_setopt($ch, CURLOPT_TIMEOUT, 10);
+
+        $response = curl_exec($ch);
+        $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
+        $response = substr($response, $headerSize);
+
+        curl_close($ch);
 
-        $this->assertResponseCode(400, $url);
+        return $response;
     }
 
-    public function test_response_ShouldSend400ResponseCode_IfSiteIdIsZero()
+    private function assertActionEquals($expected, $idaction)
     {
-        $url = $this->tracker->getUrlTrackPageView('Test');
-        $url .= '&idsite=0';
-
-        $this->assertResponseCode(400, $url);
+        $actionName = Db::fetchOne("SELECT name FROM " . Common::prefixTable('log_action') . " WHERE idaction = ?", array($idaction));
+        $this->assertEquals($expected, $actionName);
     }
 
-    public function test_response_ShouldSend400ResponseCode_IfInvalidRequestParameterIsGiven()
+    private function getCountOfConversions()
     {
-        $url = $this->tracker->getUrlTrackPageView('Test');
-        $url .= '&cid=' . str_pad('1', 16, '1');
+        return Db::fetchOne("SELECT COUNT(*) FROM " . Common::prefixTable('log_conversion'));
+    }
 
-        $this->assertResponseCode(200, $url);
-        $this->assertResponseCode(400, $url . '1'); // has to be 16 char, but is 17 now
+    private function getConversionItems()
+    {
+        return Db::fetchAll("SELECT * FROM " . Common::prefixTable('log_conversion_item'));
     }
 
-}
+    private function getEcommerceItemsUrl($ecItems, $doJsonEncode = true)
+    {
+        $ecItemsStr = $doJsonEncode ? json_encode($ecItems) : $ecItems;
+        return "?idsite=1&idgoal=0&rec=1&url=" . urlencode('http://quellehorreur.com/movies') . "&ec_items="
+        . urlencode($ecItemsStr) . '&ec_id=myspecial-id-1234&revenue=16.99&ec_st=12.99&ec_tx=0&ec_sh=3';
+    }
+}
\ No newline at end of file
diff --git a/tests/PHPUnit/Unit/Tracker/RequestSetTest.php b/tests/PHPUnit/Unit/Tracker/RequestSetTest.php
new file mode 100644
index 00000000000..4edb3f23102
--- /dev/null
+++ b/tests/PHPUnit/Unit/Tracker/RequestSetTest.php
@@ -0,0 +1,470 @@
+requestSet = $this->createRequestSet();
+        $this->time = time();
+    }
+
+    private function createRequestSet()
+    {
+        return new TestRequestSet();
+    }
+
+    public function test_internalBuildRequest_ShoulBuildOneRequest()
+    {
+        $request = new Request(array('idsite' => '2'));
+        $request->setCurrentTimestamp($this->time);
+
+        $this->assertEquals($request, $this->buildRequest(2));
+    }
+
+    public function test_internalBuildRequests_ShoulBuildASetOfRequests()
+    {
+        $this->assertEquals(array(), $this->buildRequests(0));
+
+        $this->assertEquals(array($this->buildRequest(1)), $this->buildRequests(1));
+
+        $this->assertEquals(array(
+            $this->buildRequest(1),
+            $this->buildRequest(2),
+            $this->buildRequest(3)
+        ), $this->buildRequests(3));
+    }
+
+    public function test_getRequests_shouldReturnEmptyArray_IfThereAreNoRequestsInitializedYet()
+    {
+        $this->assertEquals(array(), $this->requestSet->getRequests());
+    }
+
+    public function test_setRequests_shouldNotFail_IfEmptyArrayGiven()
+    {
+        $this->requestSet->setRequests(array());
+        $this->assertEquals(array(), $this->requestSet->getRequests());
+    }
+
+    public function test_setRequests_shouldSetAndOverwriteRequests()
+    {
+        $this->requestSet->setRequests($this->buildRequests(3));
+        $this->assertEquals($this->buildRequests(3), $this->requestSet->getRequests());
+
+        // overwrite
+        $this->requestSet->setRequests($this->buildRequests(5));
+        $this->assertEquals($this->buildRequests(5), $this->requestSet->getRequests());
+
+        // overwrite
+        $this->requestSet->setRequests($this->buildRequests(1));
+        $this->assertEquals($this->buildRequests(1), $this->requestSet->getRequests());
+
+        // clear
+        $this->requestSet->setRequests(array());
+        $this->assertEquals(array(), $this->requestSet->getRequests());
+    }
+
+    public function test_setRequests_shouldConvertNonRequestInstancesToARequestInstance()
+    {
+        $requests = array(
+            $this->buildRequest(5),
+            array('idsite' => 9),
+            $this->buildRequest(2),
+            array('idsite' => 3),
+            $this->buildRequest(6)
+        );
+
+        $this->requestSet->setRequests($requests);
+
+        $setRequests = $this->requestSet->getRequests();
+        $this->assertEquals($this->buildRequest(5), $setRequests[0]);
+        $this->assertEquals($this->buildRequest(2), $setRequests[2]);
+        $this->assertEquals($this->buildRequest(6), $setRequests[4]);
+
+        $this->assertTrue($setRequests[1] instanceof Request);
+        $this->assertEquals(array('idsite' => 9), $setRequests[1]->getParams());
+
+        $this->assertTrue($setRequests[3] instanceof Request);
+        $this->assertEquals(array('idsite' => 3), $setRequests[3]->getParams());
+
+        $this->assertCount(5, $setRequests);
+    }
+
+    public function test_setRequests_shouldIgnoreEmptyRequestsButNotArrays()
+    {
+        $requests = array(
+            $this->buildRequest(5),
+            null,
+            $this->buildRequest(2),
+            0,
+            $this->buildRequest(6),
+            array()
+        );
+
+        $this->requestSet->setRequests($requests);
+
+        $expected = array($this->buildRequest(5), $this->buildRequest(2), $this->buildRequest(6), new Request(array()));
+        $this->assertEquals($expected, $this->requestSet->getRequests());
+    }
+
+    public function test_getNumberOfRequests_shouldReturnZeroIfNothingSet()
+    {
+        $this->assertEquals(0, $this->requestSet->getNumberOfRequests());
+    }
+
+    public function test_getNumberOfRequests_shouldReturnNumberOfRequests()
+    {
+        $this->requestSet->setRequests($this->buildRequests(3));
+        $this->assertSame(3, $this->requestSet->getNumberOfRequests());
+
+        $this->requestSet->setRequests($this->buildRequests(5));
+        $this->assertSame(5, $this->requestSet->getNumberOfRequests());
+
+        $this->requestSet->setRequests($this->buildRequests(1));
+        $this->assertSame(1, $this->requestSet->getNumberOfRequests());
+    }
+
+    public function test_hasRequests_shouldReturnFalse_IfNotInitializedYetOrNoDataSet()
+    {
+        $this->assertFalse($this->requestSet->hasRequests());
+
+        $this->requestSet->setRequests(array());
+        $this->assertFalse($this->requestSet->hasRequests());
+    }
+
+    public function test_hasRequests_shouldReturnTrue_IfAtLeastOneRequestIsSet()
+    {
+        $this->assertFalse($this->requestSet->hasRequests());
+
+        $this->requestSet->setRequests($this->buildRequests(1));
+        $this->assertTrue($this->requestSet->hasRequests());
+
+        $this->requestSet->setRequests($this->buildRequests(5));
+        $this->assertTrue($this->requestSet->hasRequests());
+
+        $this->requestSet->setRequests(array(null, 0));
+        $this->assertFalse($this->requestSet->hasRequests());
+    }
+
+    public function test_getTokenAuth_ShouldReturnFalse_IfNoTokenIsSetAndNoRequestParam()
+    {
+        $this->assertFalse($this->requestSet->getTokenAuth());
+    }
+
+    public function test_getTokenAuth_setTokenAuth_shouldOverwriteTheToken()
+    {
+        $this->requestSet->setTokenAuth('MKyKTokenTestIn');
+
+        $this->assertEquals('MKyKTokenTestIn', $this->requestSet->getTokenAuth());
+    }
+
+    public function test_getTokenAuth_setTokenAuth_shouldBePossibleToClearASetToken()
+    {
+        $this->requestSet->setTokenAuth('MKyKTokenTestIn');
+        $this->assertNotEmpty($this->requestSet->getTokenAuth());
+
+        $this->requestSet->setTokenAuth(null);
+        $this->assertFalse($this->requestSet->getTokenAuth()); // does now fallback to get param
+    }
+
+    public function test_getTokenAuth_ShouldFallbackToRequestParam_IfNoTokenSet()
+    {
+        $_GET['token_auth'] = 'MyTokenAuthTest';
+
+        $this->assertSame('MyTokenAuthTest', $this->requestSet->getTokenAuth());
+
+        unset($_GET['token_auth']);
+    }
+
+    public function test_getEnvironment_shouldReturnCurrentServerVar()
+    {
+        $this->assertEquals(array(
+            'server' => $_SERVER
+        ), $this->requestSet->getEnvironment());
+    }
+
+    public function test_intertnalFakeEnvironment_shouldActuallyReturnAValue()
+    {
+        $myEnv = $this->getFakeEnvironment();
+        $this->assertInternalType('array', $myEnv);
+        $this->assertNotEmpty($myEnv);
+    }
+
+    public function test_setEnvironment_shouldOverwriteAnEnvironment()
+    {
+        $this->requestSet->setEnvironment($this->getFakeEnvironment());
+
+        $this->assertEquals($this->getFakeEnvironment(), $this->requestSet->getEnvironment());
+    }
+
+    public function test_restoreEnvironment_shouldRestoreAPreviouslySetEnvironment()
+    {
+        $serverBackup = $_SERVER;
+
+        $this->requestSet->setEnvironment($this->getFakeEnvironment());
+        $this->requestSet->restoreEnvironment();
+
+        $this->assertEquals(array('mytest' => 'test'), $_SERVER);
+
+        $_SERVER = $serverBackup;
+    }
+
+    public function test_rememberEnvironment_shouldSaveCurrentEnvironment()
+    {
+        $expected = $_SERVER;
+
+        $this->requestSet->rememberEnvironment();
+
+        $this->assertEquals(array('server' => $expected), $this->requestSet->getEnvironment());
+
+        // should not change anything
+        $this->requestSet->restoreEnvironment();
+        $this->assertEquals($expected, $_SERVER);
+    }
+
+    public function test_getState_shouldReturnCurrentStateOfRequestSet()
+    {
+        $this->requestSet->setRequests($this->buildRequests(2));
+        $this->requestSet->setTokenAuth('mytoken');
+
+        $state = $this->requestSet->getState();
+
+        $expectedKeys = array('requests', 'env', 'tokenAuth', 'time');
+        $this->assertEquals($expectedKeys, array_keys($state));
+
+        $expectedRequests = array(
+            array('idsite' => 1),
+            array('idsite' => 2)
+        );
+
+        $this->assertEquals($expectedRequests, $state['requests']);
+        $this->assertEquals('mytoken', $state['tokenAuth']);
+        $this->assertTrue(is_numeric($state['time']));
+        $this->assertEquals(array('server' => $_SERVER), $state['env']);
+    }
+
+    public function test_getState_shouldRememberAnyAddedParamsFromRequestConstructor()
+    {
+        $_SERVER['HTTP_REFERER'] = 'test';
+
+        $requests = $this->buildRequests(1);
+
+        $this->requestSet->setRequests($requests);
+        $this->requestSet->setTokenAuth('mytoken');
+
+        $state = $this->requestSet->getState();
+
+        unset($_SERVER['HTTP_REFERER']);
+
+        $expectedRequests = array(
+            array('idsite' => 1)
+        );
+
+        $this->assertEquals($expectedRequests, $state['requests']);
+
+        // the actual params include an added urlref param which should NOT be in the state. otherwise we cannot detect empty requests etc
+        $this->assertEquals(array('idsite' => 1, 'url' => 'test'), $requests[0]->getParams());
+    }
+
+    public function test_restoreState_shouldRestoreRequestSet()
+    {
+        $serverBackup = $_SERVER;
+
+        $state = array(
+            'requests' => array(array('idsite' => 1), array('idsite' => 2), array('idsite' => 3)),
+            'time' => $this->time,
+            'tokenAuth' => 'tokenAuthRestored',
+            'env' => $this->getFakeEnvironment()
+        );
+
+        $this->requestSet->restoreState($state);
+
+        $this->assertEquals($this->getFakeEnvironment(), $this->requestSet->getEnvironment());
+        $this->assertEquals('tokenAuthRestored', $this->requestSet->getTokenAuth());
+
+        $expectedRequests = array(
+            new Request(array('idsite' => 1), 'tokenAuthRestored'),
+            new Request(array('idsite' => 2), 'tokenAuthRestored'),
+            new Request(array('idsite' => 3), 'tokenAuthRestored'),
+        );
+        $expectedRequests[0]->setCurrentTimestamp($this->time);
+        $expectedRequests[1]->setCurrentTimestamp($this->time);
+        $expectedRequests[2]->setCurrentTimestamp($this->time);
+
+        $requests = $this->requestSet->getRequests();
+        $this->assertEquals($expectedRequests, $requests);
+
+        // verify again just to be sure (only first one)
+        $this->assertEquals('tokenAuthRestored', $requests[0]->getTokenAuth());
+        $this->assertEquals($this->time, $requests[0]->getCurrentTimestamp());
+
+        // should not restoreEnvironment, only set the environment
+        $this->assertSame($serverBackup, $_SERVER);
+    }
+
+    public function test_restoreState_ifRequestWasEmpty_ShouldBeStillEmptyWhenRestored()
+    {
+        $_SERVER['HTTP_REFERER'] = 'test';
+
+        $this->requestSet->setRequests(array(new Request(array())));
+        $state = $this->requestSet->getState();
+
+        $requestSet = $this->createRequestSet();
+        $requestSet->restoreState($state);
+
+        unset($_SERVER['HTTP_REFERER']);
+
+        $requests = $requestSet->getRequests();
+        $this->assertTrue($requests[0]->isEmptyRequest());
+    }
+
+    public function test_restoreState_shouldResetTheStoredEnvironmentBeforeRestoringRequests()
+    {
+        $this->requestSet->setRequests(array(new Request(array())));
+        $state = $this->requestSet->getState();
+        $state['env']['server']['HTTP_REFERER'] = 'mytesturl';
+
+        $requestSet = $this->createRequestSet();
+        $requestSet->restoreState($state);
+
+        $requests = $requestSet->getRequests();
+        $this->assertTrue($requests[0]->isEmptyRequest());
+        $this->assertEquals(array('url' => 'mytesturl'), $requests[0]->getParams());
+        $this->assertTrue(empty($_SERVER['HTTP_REFERER']));
+    }
+
+    public function test_getRedirectUrl_ShouldReturnEmptyString_IfNoUrlSet()
+    {
+        $this->assertEquals('', $this->requestSet->getRedirectUrl());
+    }
+
+    public function test_getRedirectUrl_ShouldReturnTrue_IfAUrlSetIsSetViaGET()
+    {
+        $_GET['redirecturl'] = 'whatsoever';
+        $this->assertEquals('whatsoever', $this->requestSet->getRedirectUrl());
+        unset($_GET['redirecturl']);
+    }
+
+    public function test_getRedirectUrl_ShouldReturnTrue_IfAUrlSetIsSetViaPOST()
+    {
+        $_POST['redirecturl'] = 'whatsoeverPOST';
+        $this->assertEquals('whatsoeverPOST', $this->requestSet->getRedirectUrl());
+        unset($_POST['redirecturl']);
+    }
+
+    public function test_hasRedirectUrl_ShouldReturnFalse_IfNoUrlSet()
+    {
+        $this->assertFalse($this->requestSet->hasRedirectUrl());
+    }
+
+    public function test_hasRedirectUrl_ShouldReturnTrue_IfAUrlSetIsSetViaGET()
+    {
+        $_GET['redirecturl'] = 'whatsoever';
+        $this->assertTrue($this->requestSet->hasRedirectUrl());
+        unset($_GET['redirecturl']);
+    }
+
+    public function test_hasRedirectUrl_ShouldReturnTrue_IfAUrlSetIsSetViaPOST()
+    {
+        $_POST['redirecturl'] = 'whatsoever';
+        $this->assertTrue($this->requestSet->hasRedirectUrl());
+        unset($_POST['redirecturl']);
+    }
+
+    public function test_getAllSiteIdsWithinRequest_ShouldReturnEmptyArray_IfNoRequestsSet()
+    {
+        $this->assertEquals(array(), $this->requestSet->getAllSiteIdsWithinRequest());
+    }
+
+    public function test_getAllSiteIdsWithinRequest_ShouldReturnTheSiteIds_FromRequests()
+    {
+        $this->requestSet->setRequests($this->buildRequests(3));
+
+        $this->assertEquals(array(1, 2, 3), $this->requestSet->getAllSiteIdsWithinRequest());
+    }
+
+    public function test_getAllSiteIdsWithinRequest_ShouldReturnUniqueSiteIds_Unordered()
+    {
+        $this->requestSet->setRequests(array(
+            $this->buildRequest(1),
+            $this->buildRequest(5),
+            $this->buildRequest(1),
+            $this->buildRequest(2),
+            $this->buildRequest(2),
+            $this->buildRequest(9),
+        ));
+
+        $this->assertEquals(array(1, 5, 2, 9), $this->requestSet->getAllSiteIdsWithinRequest());
+    }
+
+    private function buildRequests($numRequests)
+    {
+        $requests = array();
+        for ($index = 1; $index <= $numRequests; $index++) {
+            $requests[] = $this->buildRequest($index);
+        }
+        return $requests;
+    }
+
+    private function buildRequest($idsite)
+    {
+        $request = new Request(array('idsite' => ('' . $idsite)));
+        $request->setCurrentTimestamp($this->time);
+
+        return $request;
+    }
+
+    private function getFakeEnvironment()
+    {
+        return array('server' => array('mytest' => 'test'));
+    }
+
+
+}
\ No newline at end of file
diff --git a/tests/PHPUnit/Unit/Tracker/RequestTest.php b/tests/PHPUnit/Unit/Tracker/RequestTest.php
new file mode 100644
index 00000000000..e3ff0376382
--- /dev/null
+++ b/tests/PHPUnit/Unit/Tracker/RequestTest.php
@@ -0,0 +1,595 @@
+isAuthenticated = true;
+    }
+
+}
+
+/**
+ * @group RequestSetTest
+ * @group RequestSet
+ * @group Tracker
+ */
+class RequestTest extends UnitTestCase
+{
+    /**
+     * @var TestRequest
+     */
+    private $request;
+    private $time;
+
+    public function setUp()
+    {
+        parent::setUp();
+
+        $this->time = 1416795617;
+        $this->request = $this->buildRequest(array('idsite' => '1'));
+    }
+
+    public function test_getCurrentTimestamp_ShouldReturnTheSetTimestamp_IfNoCustomValueGiven()
+    {
+        $this->assertSame($this->time, $this->request->getCurrentTimestamp());
+    }
+
+    public function test_getCurrentTimestamp_ShouldReturnTheCurrentTimestamp_IfTimestampIsInvalid()
+    {
+        $request = $this->buildRequest(array('cdt' => '' . 5));
+        $request->setIsAuthenticated();
+        $this->assertSame($this->time, $request->getCurrentTimestamp());
+    }
+
+    public function test_cdt_ShouldReturnTheCurrentTimestamp_IfNotAuthenticatedAndTimestampIsNotRecent()
+    {
+        $request = $this->buildRequest(array('cdt' => '' . $this->time - 28800));
+        $this->assertSame($this->time, $request->getCurrentTimestamp());
+    }
+
+    public function test_cdt_ShouldReturnTheCustomTimestamp_IfNotAuthenticatedButTimestampIsRecent()
+    {
+        $request = $this->buildRequest(array('cdt' => '' . ($this->time - 5)));
+
+        $this->assertSame('' . ($this->time - 5), $request->getCurrentTimestamp());
+    }
+
+    public function test_cdt_ShouldReturnTheCustomTimestamp_IfAuthenticatedAndValid()
+    {
+        $request = $this->buildRequest(array('cdt' => '' . ($this->time - 28800)));
+        $request->setIsAuthenticated();
+        $this->assertSame('' . ($this->time - 28800), $request->getCurrentTimestamp());
+    }
+
+    public function test_cdt_ShouldReturnTheCustomTimestamp_IfTimestampIsInFuture()
+    {
+        $request = $this->buildRequest(array('cdt' => '' . ($this->time + 30800)));
+        $this->assertSame($this->time, $request->getCurrentTimestamp());
+    }
+
+    public function test_cdt_ShouldReturnTheCustomTimestamp_ShouldUseStrToTime_IfItIsNotATime()
+    {
+        $request = $this->buildRequest(array('cdt' => '5 years ago'));
+        $request->setIsAuthenticated();
+        $this->assertNotSame($this->time, $request->getCurrentTimestamp());
+        $this->assertNotEmpty($request->getCurrentTimestamp());
+    }
+
+    public function test_isEmptyRequest_ShouldReturnTrue_InCaseNoParamsSet()
+    {
+        $request = $this->buildRequest(array());
+        $this->assertTrue($request->isEmptyRequest());
+    }
+
+    public function test_isEmptyRequest_ShouldReturnTrue_InCaseNullIsSet()
+    {
+        $request = $this->buildRequest(null);
+        $this->assertTrue($request->isEmptyRequest());
+    }
+
+    public function test_isEmptyRequest_ShouldRecognizeEmptyRequest_EvenIfConstructorAddsAParam()
+    {
+        $_SERVER['HTTP_REFERER'] = 'http://www.example.com';
+
+        $request = $this->buildRequest(array());
+        $this->assertCount(1, $request->getParams());
+
+        $this->assertTrue($request->isEmptyRequest());
+    }
+
+    public function test_isEmptyRequest_ShouldReturnFalse_InCaseAtLEastOneParamIssSet()
+    {
+        $request = $this->buildRequest(array('idsite' => 1));
+        $this->assertFalse($request->isEmptyRequest());
+    }
+
+    public function test_getTokenAuth_shouldReturnDefaultValue_IfNoneSet()
+    {
+        $request = $this->buildRequest(array('idsite' => 1));
+        $this->assertFalse($request->getTokenAuth());
+    }
+
+    public function test_getTokenAuth_shouldReturnSetTokenAuth()
+    {
+        $request = $this->buildRequestWithToken(array('idsite' => 1), 'myToken');
+        $this->assertEquals('myToken', $request->getTokenAuth());
+    }
+
+    public function test_getForcedUserId_shouldReturnFalseByDefault()
+    {
+        $this->assertFalse($this->request->getForcedUserId());
+    }
+
+    public function test_getForcedUserId_shouldReturnCustomUserId_IfSet()
+    {
+        $request = $this->buildRequest(array('uid' => 'mytest'));
+        $this->assertEquals('mytest', $request->getForcedUserId());
+    }
+
+    public function test_getForcedUserId_shouldReturnFalse_IfCustomUserIdIsEmpty()
+    {
+        $request = $this->buildRequest(array('uid' => ''));
+        $this->assertFalse($request->getForcedUserId());
+    }
+
+    public function test_getDaysSinceFirstVisit_shouldReturnZeroIfNow()
+    {
+        $this->assertEquals(0.0, $this->request->getDaysSinceFirstVisit());
+    }
+
+    public function test_getDaysSinceFirstVisit_ShouldNotReturnMinusValue()
+    {
+        $request = $this->buildRequest(array('_idts' => '' . ($this->time + 43200)));
+        $request->setIsAuthenticated();
+        $this->assertEquals(0.0, $request->getDaysSinceFirstVisit());
+    }
+
+    public function test_getDaysSinceFirstVisit_TodayMinusHalfDay()
+    {
+        $request = $this->buildRequest(array('_idts' => '' . ($this->time - 43200)));
+        $request->setIsAuthenticated();
+        $this->assertEquals(1.0, $request->getDaysSinceFirstVisit());
+    }
+
+    public function test_getDaysSinceFirstVisit_Yesterday()
+    {
+        $request = $this->buildRequest(array('_idts' => '' .($this->time - 86400)));
+        $request->setIsAuthenticated();
+        $this->assertEquals(1.0, $request->getDaysSinceFirstVisit());
+    }
+
+    public function test_getDaysSinceFirstVisit_12Days()
+    {
+        $request = $this->buildRequest(array('_idts' => '' . ($this->time - (86400 * 12))));
+        $request->setIsAuthenticated();
+        $this->assertEquals(12.0, $request->getDaysSinceFirstVisit());
+    }
+
+    public function test_getDaysSinceFirstVisit_IfTimestampIsNotValidShouldIgnoreParam()
+    {
+        $request = $this->buildRequest(array('_idts' => '' . ($this->time - (86400 * 15 * 365))));
+        $this->assertEquals(0.0, $request->getDaysSinceFirstVisit());
+    }
+
+    public function test_getDaysSinceLastOrder_shouldReturnZeroIfNow()
+    {
+        $this->assertEquals(0.0, $this->request->getDaysSinceLastOrder());
+    }
+
+    public function test_getDaysSinceLastOrder_ShouldNotReturnMinusValue()
+    {
+        $request = $this->buildRequest(array('_ects' => '' . ($this->time + 43200)));
+        $request->setIsAuthenticated();
+        $this->assertEquals(0.0, $request->getDaysSinceLastOrder());
+    }
+
+    public function test_getDaysSinceLastOrder_TodayMinusHalfDay()
+    {
+        $request = $this->buildRequest(array('_ects' => '' . ($this->time - 43200)));
+        $request->setIsAuthenticated();
+        $this->assertEquals(1.0, $request->getDaysSinceLastOrder());
+    }
+
+    public function test_getDaysSinceLastOrder_Yesterday()
+    {
+        $request = $this->buildRequest(array('_ects' => '' . ($this->time - 86400)));
+        $request->setIsAuthenticated();
+        $this->assertEquals(1.0, $request->getDaysSinceLastOrder());
+    }
+
+    public function test_getDaysSinceLastOrder_12Days()
+    {
+        $request = $this->buildRequest(array('_ects' => '' . ($this->time - (86400 * 12))));
+        $request->setIsAuthenticated();
+        $this->assertEquals(12.0, $request->getDaysSinceLastOrder());
+    }
+
+    public function test_getDaysSinceLastOrder_ShouldIgnoreParamIfInvalid()
+    {
+        $request = $this->buildRequest(array('_ects' => 5));
+        $this->assertFalse($request->getDaysSinceLastOrder());
+    }
+
+    public function test_getDaysSinceLastVisit_shouldReturnZeroIfNow()
+    {
+        $this->assertEquals(0.0, $this->request->getDaysSinceLastVisit());
+    }
+
+    public function test_getDaysSinceLastVisit_ShouldNotReturnMinusValue()
+    {
+        $request = $this->buildRequest(array('_viewts' => '' . ($this->time + 43200)));
+        $request->setIsAuthenticated();
+        $this->assertEquals(0.0, $request->getDaysSinceLastVisit());
+    }
+
+    public function test_getDaysSinceLastVisit_TodayMinusHalfDay()
+    {
+        $request = $this->buildRequest(array('_viewts' => '' . ($this->time - 43200)));
+        $request->setIsAuthenticated();
+        $this->assertEquals(1.0, $request->getDaysSinceLastVisit());
+    }
+
+    public function test_getDaysSinceLastVisit_Yesterday()
+    {
+        $request = $this->buildRequest(array('_viewts' => '' . ($this->time - 86400)));
+        $request->setIsAuthenticated();
+        $this->assertEquals(1.0, $request->getDaysSinceLastVisit());
+    }
+
+    public function test_getDaysSinceLastVisit_12Days()
+    {
+        $request = $this->buildRequest(array('_viewts' => '' . ($this->time - (86400 * 12))));
+        $request->setIsAuthenticated();
+        $this->assertEquals(12.0, $request->getDaysSinceLastVisit());
+    }
+
+    public function test_getDaysSinceLastVisit_ShouldIgnoreParamIfInvalid()
+    {
+        $request = $this->buildRequest(array('_viewts' => '' . 5));
+        $this->assertSame(0, $request->getDaysSinceLastVisit());
+    }
+
+    public function test_getGoalRevenue_ShouldReturnDefaultValue_IfNothingSet()
+    {
+        $this->assertFalse($this->request->getGoalRevenue(false));
+    }
+
+    public function test_getGoalRevenue_ShouldReturnParam_IfSet()
+    {
+        $request = $this->buildRequest(array('revenue' => '5.51'));
+        $this->assertSame(5.51, $request->getGoalRevenue(false));
+    }
+
+    public function test_getUserIdHashed_shouldReturnSetTokenAuth()
+    {
+        $hash = $this->request->getUserIdHashed(1);
+
+        $this->assertEquals('356a192b7913b04c', $hash);
+        $this->assertSame(16, strlen($hash));
+        $this->assertTrue(ctype_alnum($hash));
+
+        $this->assertEquals('da4b9237bacccdf1', $this->request->getUserIdHashed(2));
+    }
+
+    public function test_getVisitCount_shouldReturnOne_IfNotSet()
+    {
+        $this->assertEquals(1, $this->request->getVisitCount());
+    }
+
+    public function test_getVisitCount_shouldReturnTheSetValue_IfHigherThanOne()
+    {
+        $request = $this->buildRequest(array('_idvc' => 13));
+        $this->assertEquals(13, $request->getVisitCount());
+    }
+
+    public function test_getVisitCount_shouldReturnAtLEastOneEvenIfLowerValueIsSet()
+    {
+        $request = $this->buildRequest(array('_idvc' => 0));
+        $this->assertEquals(1, $request->getVisitCount());
+
+        $request = $this->buildRequest(array('_idvc' => -1));
+        $this->assertEquals(1, $request->getVisitCount());
+    }
+
+    public function test_getLocalTime_shouldFallbackToCurrentDate_IfNoParamIsSet()
+    {
+        $this->assertEquals('02:20:17', $this->request->getLocalTime());
+    }
+
+    public function test_getLocalTime_shouldReturnAtLEastOneEvenIfLowerValueIsSet()
+    {
+        $request = $this->buildRequest(array('h' => 15, 'm' => 3, 's' => 4));
+        $this->assertEquals('15:03:04', $request->getLocalTime());
+    }
+
+    public function test_getLocalTime_shouldFallbackToPartsOfCurrentDate()
+    {
+        $request = $this->buildRequest(array('h' => 5));
+        $this->assertEquals('05:20:17', $request->getLocalTime());
+    }
+
+    /**
+     * @expectedException \Exception
+     * @expectedExceptionMessage Requested parameter myCustomFaKeParaM is not a known Tracking API Parameter
+     */
+    public function test_getParam_shouldThrowException_IfTryingToAccessInvalidParam()
+    {
+        $this->request->getParam('myCustomFaKeParaM');
+    }
+
+    public function test_getParam_aString()
+    {
+        $request = $this->buildRequest(array('url' => 'test'));
+        $this->assertEquals('test', $request->getParam('url'));
+    }
+
+    public function test_getParam_aInt()
+    {
+        $request = $this->buildRequest(array('new_visit' => '12'));
+        $this->assertSame(12, $request->getParam('new_visit'));
+    }
+
+    public function test_getPlugins_shouldReturnZeroForAllIfNothingGiven()
+    {
+        $expected = array_fill(0, 10, 0);
+
+        $this->assertEquals($expected, $this->request->getPlugins());
+    }
+
+    public function test_getPlugins_shouldReturnAllOneIfAllGiven()
+    {
+        $plugins = array('fla', 'java', 'dir', 'qt', 'realp', 'pdf', 'wma', 'gears', 'ag', 'cookie');
+        $request = $this->buildRequest(array_fill_keys($plugins, '1'));
+
+        $this->assertEquals(array_fill(0, 10, 1), $request->getPlugins());
+    }
+
+    public function test_getPlugins_shouldDetectSome()
+    {
+        $plugins = array('fla' => 1, 'java', 'dir' => '1', 'qt' => '0', 'realp' => 0, 'gears', 'ag' => 1, 'cookie');
+        $request = $this->buildRequest($plugins);
+
+        $expected = array(1, 0, 1, 0, 0, 0, 0, 0, 1, 0);
+        $this->assertEquals($expected, $request->getPlugins());
+    }
+
+    public function test_getPageGenerationTime_shouldDefaultToFalse_IfNotGiven()
+    {
+        $this->assertFalse($this->request->getPageGenerationTime());
+    }
+
+    public function test_getPageGenerationTime_shouldIgnoreAnyValueLowerThan0()
+    {
+        $request = $this->buildRequest(array('gt_ms' => '0'));
+        $this->assertFalse($request->getPageGenerationTime());
+
+        $request = $this->buildRequest(array('gt_ms' => '-5'));
+        $this->assertFalse($request->getPageGenerationTime());
+    }
+
+    public function test_getPageGenerationTime_shouldIgnoreAnyValueThatIsTooHigh()
+    {
+        $request = $this->buildRequest(array('gt_ms' => '3600002'));
+        $this->assertFalse($request->getPageGenerationTime());
+    }
+
+    public function test_getPageGenerationTime_shouldReturnAValidValue()
+    {
+        $request = $this->buildRequest(array('gt_ms' => '1942'));
+        $this->assertSame(1942, $request->getPageGenerationTime());
+    }
+
+    public function test_truncateCustomVariable_shouldNotTruncateAnything_IfValueIsShortEnough()
+    {
+        $len = CustomVariables::getMaxLengthCustomVariables();
+        $input = str_pad('test', $len - 2, 't');
+
+        $result = Request::truncateCustomVariable($input);
+
+        $this->assertSame($result, $input);
+    }
+
+    public function test_truncateCustomVariable_shouldActuallyTruncateTheValue()
+    {
+        $len = CustomVariables::getMaxLengthCustomVariables();
+        $input = str_pad('test', $len + 2, 't');
+
+        $this->assertGreaterThan(100, $len);
+
+        $truncated = Request::truncateCustomVariable($input);
+
+        $this->assertEquals(str_pad('test', $len, 't'), $truncated);
+    }
+
+    public function test_getUserAgent_ShouldReturnEmptyString_IfNoneIsSet()
+    {
+        $this->assertEquals('', $this->request->getUserAgent());
+    }
+
+    public function test_getUserAgent_ShouldDefaultToServerUa_IfPossibleAndNoneIsSet()
+    {
+        $_SERVER['HTTP_USER_AGENT'] = 'MyUserAgent';
+        $this->assertSame('MyUserAgent', $this->request->getUserAgent());
+        unset($_SERVER['HTTP_USER_AGENT']);
+    }
+
+    public function test_getUserAgent_ShouldReturnTheUaFromParams_IfOneIsSet()
+    {
+        $request = $this->buildRequest(array('idsite' => '14', 'ua' => 'My Custom UA'));
+        $this->assertSame('My Custom UA', $request->getUserAgent());
+    }
+
+    public function test_getBrowserLanguage_ShouldReturnACustomSetLangParam_IfOneIsSet()
+    {
+        $request = $this->buildRequest(array('lang' => 'CusToMLang'));
+        $this->assertSame('CusToMLang', $request->getBrowserLanguage());
+    }
+
+    public function test_getBrowserLanguage_ShouldReturnADefaultLanguageInCaseNoneIsSet()
+    {
+        $lang = $this->request->getBrowserLanguage();
+        $this->assertNotEmpty($lang);
+        $this->assertTrue(2 <= strlen($lang) && strlen($lang) <= 10);
+    }
+
+    public function test_makeThirdPartyCookie_ShouldReturnAnInstanceOfCookie()
+    {
+        $cookie = $this->request->makeThirdPartyCookie();
+
+        $this->assertTrue($cookie instanceof Cookie);
+    }
+
+    public function test_makeThirdPartyCookie_ShouldPreconfigureTheCookieInstance()
+    {
+        $cookie = $this->request->makeThirdPartyCookie();
+
+        $this->assertCookieContains('COOKIE _pk_uid', $cookie);
+        $this->assertCookieContains('expire: 1450750817', $cookie);
+        $this->assertCookieContains('path: ,', $cookie);
+    }
+
+    private function assertCookieContains($needle, Cookie $cookie)
+    {
+        $this->assertContains($needle, $cookie . '');
+    }
+
+    public function test_getIdSite()
+    {
+        $request = $this->buildRequest(array('idsite' => '14'));
+        $this->assertSame(14, $request->getIdSite());
+    }
+
+    public function test_getIdSite_shouldTriggerEventAndReturnThatIdSite()
+    {
+        $self = $this;
+        Piwik::addAction('Tracker.Request.getIdSite', function (&$idSite, $params) use ($self) {
+            $self->assertSame(14, $idSite);
+            $self->assertEquals(array('idsite' => '14'), $params);
+            $idSite = 12;
+        });
+
+        $request = $this->buildRequest(array('idsite' => '14'));
+        $this->assertSame(12, $request->getIdSite());
+    }
+
+    /**
+     * @expectedException \Piwik\Exception\UnexpectedWebsiteFoundException
+     * @expectedExceptionMessage Invalid idSite: '0'
+     */
+    public function test_getIdSite_shouldThrowException_IfValueIsZero()
+    {
+        $request = $this->buildRequest(array('idsite' => '0'));
+        $request->getIdSite();
+    }
+
+    /**
+     * @expectedException \Piwik\Exception\UnexpectedWebsiteFoundException
+     * @expectedExceptionMessage Invalid idSite: '-1'
+     */
+    public function test_getIdSite_shouldThrowException_IfValueIsLowerThanZero()
+    {
+        $request = $this->buildRequest(array('idsite' => '-1'));
+        $request->getIdSite();
+    }
+
+    public function test_getIpString_ShouldDefaultToServerAddress()
+    {
+        $this->assertEquals($_SERVER['REMOTE_ADDR'], $this->request->getIpString());
+    }
+
+    public function test_getIpString_ShouldDefaultToServerAddress_IfCustomIpIsSetButNotAuthenticated()
+    {
+        $request = $this->buildRequest(array('cip' => '192.192.192.192'));
+        $this->assertEquals($_SERVER['REMOTE_ADDR'], $request->getIpString());
+    }
+
+    public function test_getIpString_ShouldReturnCustomIp_IfAuthenticated()
+    {
+        $request = $this->buildRequest(array('cip' => '192.192.192.192'));
+        $request->setIsAuthenticated();
+        $this->assertEquals('192.192.192.192', $request->getIpString());
+    }
+
+    public function test_getIp()
+    {
+        $ip = $_SERVER['REMOTE_ADDR'];
+        $this->assertEquals(IPUtils::stringToBinaryIP($ip), $this->request->getIp());
+    }
+
+    public function test_getCookieName_ShouldReturnConfigValue()
+    {
+        $this->assertEquals('_pk_uid', $this->request->getCookieName());
+    }
+
+    public function test_getCookieExpire_ShouldReturnConfigValue()
+    {
+        $this->assertEquals($this->time + (60 * 60 * 24 * 393), $this->request->getCookieExpire());
+    }
+
+    public function test_getCookiePath_ShouldBeEmptyByDefault()
+    {
+        $this->assertEquals('', $this->request->getCookiePath());
+    }
+
+    public function test_getCookiePath_ShouldReturnConfigValue()
+    {
+        $oldPath = TrackerConfig::getConfigValue('cookie_path');
+        TrackerConfig::setConfigValue('cookie_path', 'test');
+
+        $this->assertEquals('test', $this->request->getCookiePath());
+
+        TrackerConfig::setConfigValue('cookie_path', $oldPath);
+    }
+
+    private function buildRequest($params)
+    {
+        $request = new TestRequest($params);
+        $request->setCurrentTimestamp($this->time);
+
+        return $request;
+    }
+
+    private function buildRequestWithToken($params, $token)
+    {
+        return new TestRequest($params, $token);
+    }
+
+
+}
\ No newline at end of file
diff --git a/tests/PHPUnit/Unit/Tracker/ResponseTest.php b/tests/PHPUnit/Unit/Tracker/ResponseTest.php
new file mode 100644
index 00000000000..9a1d1f28f7a
--- /dev/null
+++ b/tests/PHPUnit/Unit/Tracker/ResponseTest.php
@@ -0,0 +1,178 @@
+response = new TestResponse();
+    }
+
+    public function test_outputException_shouldAlwaysOutputApiResponse_IfDebugModeIsDisabled()
+    {
+        $this->response->init($this->getTracker());
+        $this->response->outputException($this->getTracker(), new Exception('My Custom Message'), 400);
+
+        Fixture::checkResponse($this->response->getOutput());
+    }
+
+    public function test_outputException_shouldOutputDebugMessageIfEnabled()
+    {
+        $tracker = $this->getTracker();
+        $this->response->init($tracker);
+
+        $tracker->enableDebugMode();
+
+        $this->response->outputException($tracker, new Exception('My Custom Message'), 400);
+
+        $content = $this->response->getOutput();
+
+        $this->assertContains('Piwik › Error', $content);
+        $this->assertContains('

My Custom Message', $content); + } + + public function test_outputResponse_shouldOutputStandardApiResponse() + { + $this->response->init($this->getTracker()); + $this->response->outputResponse($this->getTracker()); + + Fixture::checkResponse($this->response->getOutput()); + } + + public function test_outputResponse_shouldNotOutputApiResponse_IfDebugModeIsEnabled_AsWePrintOtherStuff() + { + $this->response->init($this->getTracker()); + + $tracker = $this->getTracker(); + $tracker->enableDebugMode(); + $this->response->outputResponse($tracker); + + $this->assertEquals('', $this->response->getOutput()); + } + + public function test_outputResponse_shouldNotOutputApiResponse_IfSomethingWasPrintedUpfront() + { + $this->response->init($this->getTracker()); + + echo 5; + $this->response->outputResponse($this->getTracker()); + + $this->assertEquals('5', $this->response->getOutput()); + } + + public function test_outputResponse_shouldNotOutputResponseTwice_IfExceptionWasAlreadyOutput() + { + $this->response->init($this->getTracker()); + + $this->response->outputException($this->getTracker(), new Exception('My Custom Message'), 400); + $this->response->outputResponse($this->getTracker()); + + Fixture::checkResponse($this->response->getOutput()); + } + + public function test_outputResponse_shouldOutputNoResponse_If204HeaderIsRequested() + { + $this->response->init($this->getTracker()); + + $_GET['send_image'] = '0'; + $this->response->outputResponse($this->getTracker()); + unset($_GET['send_image']); + + $this->assertEquals('', $this->response->getOutput()); + } + + public function test_outputResponse_shouldOutputPiwikMessage_InCaseNothingWasTracked() + { + $this->response->init($this->getTracker()); + + $tracker = $this->getTracker(); + $tracker->setCountOfLoggedRequests(0); + $this->response->outputResponse($tracker); + + $this->assertEquals("Piwik is a free/libre web analytics that lets you keep control of your data.", + $this->response->getOutput()); + } + + public function test_getMessageFromException_ShouldNotOutputAnyDetails_IfErrorContainsDbCredentials() + { + $message = $this->response->getMessageFromException(new Exception('Test Message', 1044)); + $this->assertStringStartsWith("Error while connecting to the Piwik database", $message); + + $message = $this->response->getMessageFromException(new Exception('Test Message', 42000)); + $this->assertStringStartsWith("Error while connecting to the Piwik database", $message); + } + + public function test_getMessageFromException_ShouldReturnMessageAndTrace_InCaseIsCli() + { + $message = $this->response->getMessageFromException(new Exception('Test Message', 8150)); + $this->assertStringStartsWith("Test Message\n#0 [internal function]", $message); + } + + public function test_getMessageFromException_ShouldOnlyReturnMessage_InCaseIsNotCli() + { + Common::$isCliMode = false; + $message = $this->response->getMessageFromException(new Exception('Test Message', 8150)); + Common::$isCliMode = true; + + $this->assertStringStartsWith("Test Message", $message); + } + + public function test_outputResponse_shouldOutputApiResponse_IfTrackerIsDisabled() + { + $this->response->init($this->getTracker()); + + $tracker = $this->getTracker(); + $tracker->setCountOfLoggedRequests(0); + $tracker->disableShouldRecordStatistics(); + $this->response->outputResponse($tracker); + + Fixture::checkResponse($this->response->getOutput()); + } + + private function getTracker() + { + $tracker = new Tracker(); + $tracker->setCountOfLoggedRequests(5); + return $tracker; + } + +} diff --git a/tests/PHPUnit/Unit/TrackerTest.php b/tests/PHPUnit/Unit/TrackerTest.php new file mode 100644 index 00000000000..084ee627de3 --- /dev/null +++ b/tests/PHPUnit/Unit/TrackerTest.php @@ -0,0 +1,274 @@ +record = true; + } + + public function shouldRecordStatistics() + { + return $this->record; + } + + public function disalbeRecordStatistics() + { + $this->record = false; + } +} + +/** + * @group TrackerTest + * @group Tracker + */ +class TrackerTest extends UnitTestCase +{ + /** + * @var TestTracker + */ + private $tracker; + + /** + * @var Handler + */ + private $handler; + + /** + * @var RequestSet + */ + private $requestSet; + + private $time; + + public function setUp() + { + parent::setUp(); + + $this->time = time(); + $this->tracker = new TestTracker(); + $this->handler = new Handler(); + $this->requestSet = new RequestSet(); + $this->requestSet->setRequests(array($this->buildRequest(1), $this->buildRequest(1))); + } + + public function tearDown() + { + EventDispatcher::getInstance()->clearObservers('Tracker.end'); + parent::tearDown(); + } + + public function test_isDebugModeEnabled_shouldReturnFalse_ByDefault() + { + unset($GLOBALS['PIWIK_TRACKER_DEBUG']); + $this->assertFalse($this->tracker->isDebugModeEnabled()); + } + + public function test_isDebugModeEnabled_shouldReturnFalse_IfDisabled() + { + $GLOBALS['PIWIK_TRACKER_DEBUG'] = false; + + $this->assertFalse($this->tracker->isDebugModeEnabled()); + + unset($GLOBALS['PIWIK_TRACKER_DEBUG']); + } + + public function test_isDebugModeEnabled_shouldReturnTrue_IfEnabled() + { + $GLOBALS['PIWIK_TRACKER_DEBUG'] = true; + + $this->assertTrue($this->tracker->isDebugModeEnabled()); + + unset($GLOBALS['PIWIK_TRACKER_DEBUG']); + } + + public function test_main_shouldReturnFinishedResponse() + { + $response = $this->tracker->main($this->handler, $this->requestSet); + + $this->assertEquals('My Rendered Content', $response); + } + + public function test_main_shouldReturnResponse_EvenWhenThereWasAnExceptionDuringProcess() + { + $this->handler->enableTriggerExceptionInProcess(); + $response = $this->tracker->main($this->handler, $this->requestSet); + + $this->assertEquals('My Exception During Process', $response); + } + + public function test_main_shouldReturnResponse_EvenWhenThereWasAnExceptionDuringInitRequests() + { + $this->requestSet->enableThrowExceptionOnInit(); + $response = $this->tracker->main($this->handler, $this->requestSet); + + $this->assertEquals('Init requests and token auth exception', $response); + } + + public function test_main_shouldTriggerHandlerInitAndFinishEvent() + { + $this->tracker->main($this->handler, $this->requestSet); + + $this->assertTrue($this->handler->isInit); + $this->assertTrue($this->handler->isProcessed); + $this->assertTrue($this->handler->isFinished); + $this->assertFalse($this->handler->isOnException); + } + + public function test_main_shouldTriggerHandlerInitAndFinishEvent_EvenIfShouldNotRecordStats() + { + $this->tracker->disalbeRecordStatistics(); + $this->tracker->main($this->handler, $this->requestSet); + + $this->assertTrue($this->handler->isInit); + $this->assertFalse($this->handler->isProcessed); + $this->assertTrue($this->handler->isFinished); + $this->assertFalse($this->handler->isOnException); + } + + public function test_main_shouldTriggerHandlerInitAndFinishEvent_EvenIfThereIsAnException() + { + $this->handler->enableTriggerExceptionInProcess(); + $this->tracker->main($this->handler, $this->requestSet); + + $this->assertTrue($this->handler->isInit); + $this->assertTrue($this->handler->isFinished); + $this->assertTrue($this->handler->isOnException); + } + + public function test_main_shouldPostEndEvent() + { + $called = false; + Piwik::addAction('Tracker.end', function () use (&$called) { + $called = true; + }); + + $this->tracker->main($this->handler, $this->requestSet); + + $this->assertTrue($called); + } + + public function test_main_shouldPostEndEvent_EvenIfShouldNotRecordStats() + { + $called = false; + Piwik::addAction('Tracker.end', function () use (&$called) { + $called = true; + }); + + $this->tracker->disalbeRecordStatistics(); + $this->tracker->main($this->handler, $this->requestSet); + + $this->assertFalse($this->handler->isProcessed); + $this->assertTrue($called); + } + + public function test_main_shouldPostEndEvent_EvenIfThereIsAnException() + { + $called = false; + Piwik::addAction('Tracker.end', function () use (&$called) { + $called = true; + }); + + $this->handler->enableTriggerExceptionInProcess(); + $this->tracker->main($this->handler, $this->requestSet); + + $this->assertTrue($this->handler->isOnException); + $this->assertTrue($called); + } + + public function test_track_shouldTrack_IfThereAreRequests() + { + $this->tracker->track($this->handler, $this->requestSet); + + $this->assertTrue($this->handler->isOnStartTrackRequests); + $this->assertTrue($this->handler->isProcessed); + $this->assertTrue($this->handler->isOnAllRequestsTracked); + $this->assertFalse($this->handler->isOnException); + } + + public function test_track_shouldNotTrackAnything_IfTrackingIsDisabled() + { + $this->tracker->disalbeRecordStatistics(); + $this->tracker->track($this->handler, $this->requestSet); + + $this->assertFalse($this->handler->isOnStartTrackRequests); + $this->assertFalse($this->handler->isProcessed); + $this->assertFalse($this->handler->isOnAllRequestsTracked); + $this->assertFalse($this->handler->isOnException); + } + + public function test_track_shouldNotTrackAnything_IfNoRequestsAreSet() + { + $this->requestSet->setRequests(array()); + $this->tracker->track($this->handler, $this->requestSet); + + $this->assertFalse($this->handler->isOnStartTrackRequests); + $this->assertFalse($this->handler->isProcessed); + $this->assertFalse($this->handler->isOnAllRequestsTracked); + $this->assertFalse($this->handler->isOnException); + } + + /** + * @expectedException \Exception + * @expectedException My Exception During Process + */ + public function test_track_shouldNotCatchAnyException_IfExceptionWasThrown() + { + $this->handler->enableTriggerExceptionInProcess(); + $this->tracker->track($this->handler, $this->requestSet); + } + + public function test_getCountOfLoggedRequests_shouldReturnZero_WhenNothingTracked() + { + $this->assertEquals(0, $this->tracker->getCountOfLoggedRequests()); + } + + public function test_hasLoggedRequests_shouldReturnFalse_WhenNothingTracked() + { + $this->assertFalse($this->tracker->hasLoggedRequests()); + } + + public function test_setCountOfLoggedRequests_shouldOverwriteNumberOfLoggedRequests() + { + $this->tracker->setCountOfLoggedRequests(5); + $this->assertEquals(5, $this->tracker->getCountOfLoggedRequests()); + } + + public function test_hasLoggedRequests_shouldReturnTrue_WhenSomeRequestsWereLogged() + { + $this->tracker->setCountOfLoggedRequests(1); + $this->assertTrue($this->tracker->hasLoggedRequests()); + + $this->tracker->setCountOfLoggedRequests(5); + $this->assertTrue($this->tracker->hasLoggedRequests()); + + $this->tracker->setCountOfLoggedRequests(0); + $this->assertFalse($this->tracker->hasLoggedRequests()); + } + + private function buildRequest($idsite) + { + $request = new Request(array('idsite' => $idsite)); + $request->setCurrentTimestamp($this->time); + + return $request; + } + +} \ No newline at end of file diff --git a/tests/PHPUnit/proxy/piwik.php b/tests/PHPUnit/proxy/piwik.php index 2acaea81bd7..bc03155b25b 100755 --- a/tests/PHPUnit/proxy/piwik.php +++ b/tests/PHPUnit/proxy/piwik.php @@ -11,7 +11,6 @@ use Piwik\Option; use Piwik\Plugins\UserCountry\LocationProvider\GeoIp; use Piwik\Site; -use Piwik\Tracker\Cache; use Piwik\Tracker; require realpath(dirname(__FILE__)) . "/includes.php"; @@ -35,4 +34,7 @@ echo "Unexpected error during tracking: " . $ex->getMessage() . "\n" . $ex->getTraceAsString() . "\n"; } -ob_end_flush(); +if (ob_get_level() > 1) { + ob_end_flush(); +} + diff --git a/tests/travis/prepare.sh b/tests/travis/prepare.sh index 93531210ce2..a42932406d8 100755 --- a/tests/travis/prepare.sh +++ b/tests/travis/prepare.sh @@ -43,3 +43,9 @@ mkdir ./tmp/tcpdf mkdir ./tmp/climulti chmod a+rw ./tests/lib/geoip-files chmod a+rw ./plugins/*/tests/System/processed + +# install phpredis +echo 'extension="redis.so"' > ./tmp/redis.ini +phpenv config-add ./tmp/redis.ini + +php -i \ No newline at end of file