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:', $resultTasks) as $resultTask) { + Common::printDebug(str_replace('' . $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('
', '', $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