diff options
author | mattpiwik <matthieu.aubry@gmail.com> | 2008-08-04 04:11:03 +0400 |
---|---|---|
committer | mattpiwik <matthieu.aubry@gmail.com> | 2008-08-04 04:11:03 +0400 |
commit | 2854426e8609e0f9e3ceac2e27327532bf00a408 (patch) | |
tree | 5214705435461179efecb331075a9830a21a5594 | |
parent | 42b52b6d8a88b3fa4c4f3978c4e7bf00b1eac778 (diff) |
oops i totally screwed up my last commit, deleting /modules instead of renaming it...
git-svn-id: http://dev.piwik.org/svn/trunk@587 59fd770c-687e-43c8-a1e3-f5a4ff64c105
124 files changed, 22321 insertions, 0 deletions
diff --git a/core/API/APIable.php b/core/API/APIable.php new file mode 100644 index 0000000000..16c35f6746 --- /dev/null +++ b/core/API/APIable.php @@ -0,0 +1,55 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: APIable.php 482 2008-05-18 17:22:35Z matt $ + * + * @package Piwik_API + */ + + +require_once "Archive.php"; +/** + * This class is the parent class of all the plugins that can be called using the API Proxy. + * For example a plugin "Provider" can publish its API by creating a file plugins/Provider/API.php + * that is extending this Piwik_Apiable class. + * All the Piwik_Apiable classes are read and loaded by the Piwik_API_Proxy class. + * The public methods of this class are published in the API and are then callable using the API module. + * The parameters of the function are read directly from the GET request (they must have the same name). + * + * For example + * public function helloWorld($text) { return "hello " . $text; } + * call be called using + * ?module=API&method=PluginName.helloWorld&text=world! + * + * See the documentation on http://dev.piwik.org > API + * + * @package Piwik_API + * @see Piwik_API_Proxy + */ + +abstract class Piwik_Apiable +{ + /** + * This array contains the name of the methods of the class we don't want to publish in the API. + * By default only public methods are published. Names of public methods in this array won't be published. + * + * @var array of strings + */ + static public $methodsNotToPublish = array(); + + /** + * @see self::$methodsNotToPublish + * @param string Method name not to be published + */ + protected function doNotPublishMethod( $methodName ) + { + if(!method_exists($this, $methodName)) + { + throw new Exception("The method $methodName doesn't exist so it can't be added to the list of the methods not to be published in the API."); + } + $this->methodsNotToPublish[] = $methodName; + } +} diff --git a/core/API/Proxy.php b/core/API/Proxy.php new file mode 100644 index 0000000000..6522780c51 --- /dev/null +++ b/core/API/Proxy.php @@ -0,0 +1,485 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Proxy.php 581 2008-07-27 23:07:52Z matt $ + * + * @package Piwik_API + */ + + +/** + * The API Proxy receives all the API calls requests and forwards them to the given module. + * + * It registers all the APIable plugins (@see Piwik_Apiable) + * The class checks that a call to the API has the correct number of parameters. + * The proxy is a singleton that has the knowledge of every method available, their parameters and default values. + * + * It can also log the performances of the API calls (time spent, parameter values, etc.) + * + * @package Piwik_API + */ +class Piwik_API_Proxy +{ + static $classCalled = null; + + // array of already registered plugins names + protected $alreadyRegistered = array(); + + private $api = array(); + + // when a parameter doesn't have a default value we use this constant + const NO_DEFAULT_VALUE = null; + + static private $instance = null; + protected function __construct() + {} + + /** + * Singleton, returns instance + * + * @return Piwik_API_Proxy + */ + static public function getInstance() + { + if (self::$instance == null) + { + $c = __CLASS__; + self::$instance = new $c(); + } + return self::$instance; + } + + /** + * Registers the API information of a given module. + * + * The module to be registered must be + * - extending the Piwik_Apiable class + * - a singleton (providing a getInstance() method) + * - the API file must be located in plugins/ModuleName/API.php + * for example plugins/Referers/API.php + * + * The method will introspect the methods, their parameters, etc. + * + * @param string ModuleName eg. "UserSettings" + */ + public function registerClass( $fileName ) + { + if(isset($this->alreadyRegistered[$fileName])) + { + return; + } + + $potentialPaths = array( "plugins/". $fileName ."/API.php", ); + + $found = false; + foreach($potentialPaths as $path) + { + if(Zend_Loader::isReadable($path)) + { + require_once $path; + $found = true; + break; + } + } + + if(!$found) + { + throw new Exception("API module $fileName not found."); + } + + $class= $this->getClassNameFromModule($fileName); + + $rClass = new ReflectionClass($class); + + // check that it is a subclass of Piwik_APIable + if(!$rClass->isSubclassOf(new ReflectionClass("Piwik_Apiable"))) + { + throw new Exception("To publish its public methods in the API, the class '$class' must be a subclass of 'Piwik_Apiable'."); + } + + // check that is is singleton + $this->checkClassIsSingleton($class); + + $rMethods = $rClass->getMethods(); + foreach($rMethods as $method) + { + // use this trick to read the static attribute of the class + // $class::$methodsNotToPublish doesn't work + $variablesClass = get_class_vars($class); + $variablesClass['methodsNotToPublish'][] = 'getInstance'; + + if($method->isPublic() + && !$method->isConstructor() + && !in_array($method->getName(), $variablesClass['methodsNotToPublish'] ) + ) + { + $name = $method->getName(); + + $parameters = $method->getParameters(); + + $aParameters = array(); + foreach($parameters as $parameter) + { + $nameVariable = $parameter->getName(); + + $defaultValue = Piwik_API_Proxy::NO_DEFAULT_VALUE; + if($parameter->isDefaultValueAvailable()) + { + $defaultValue = $parameter->getDefaultValue(); + } + + $aParameters[$nameVariable] = $defaultValue; + } + $this->api[$class][$name]['parameters'] = $aParameters; + $this->api[$class][$name]['numberOfRequiredParameters'] = $method->getNumberOfRequiredParameters(); + } + } + + $this->alreadyRegistered[$fileName] = true; + } + + /** + * Returns the 'moduleName' part of 'Piwik_moduleName_API' classname + * + * @param string moduleName + * @return string className + */ + protected function getModuleNameFromClassName( $className ) + { + $start = strpos($className, '_') + 1; + return substr($className, $start , strrpos($className, '_') - $start); + } + + /** + * Returns a string containing links to examples on how to call a given method on a given API + * It will export links to XML, CSV, HTML, JSON, PHP, etc. + * It will not export links for methods such as deleteSite or deleteUser + * + * @param string the class + * @param methodName the method + * @return string|false when not possible + */ + public function getExampleUrl($class, $methodName, $parametersToSet = array()) + { + $knowExampleDefaultParametersValues = array( + 'access' => 'view', + 'userLogin' => 'test', + 'password' => 'passwordExample', + 'passwordMd5ied' => 'passwordExample', + 'email' => 'test@example.org', + + 'siteName' => 'new example website', + 'urls' => 'http://example.org', // used in addSite, updateSite + ); + + foreach($parametersToSet as $name => $value) + { + $knowExampleDefaultParametersValues[$name] = $value; + } + + // no links for these method names + $doNotPrintExampleForTheseMethods = array( + 'deleteSite', + 'deleteUser', + ); + + if(in_array($methodName,$doNotPrintExampleForTheseMethods)) + { + return false; + } + + + // we try to give an URL example to call the API + $aParameters = $this->getParametersList($class, $methodName); + $moduleName = $this->getModuleNameFromClassName($class); + $urlExample = '?module=API&method='.$moduleName.'.'.$methodName.'&'; + foreach($aParameters as $nameVariable=> $defaultValue) + { + // if there isn't a default value for a given parameter, + // we need a 'know default value' or we can't generate the link + if($defaultValue === Piwik_API_Proxy::NO_DEFAULT_VALUE) + { + if(isset($knowExampleDefaultParametersValues[$nameVariable])) + { + $exampleValue = $knowExampleDefaultParametersValues[$nameVariable]; + $urlExample .= $nameVariable . '=' . $exampleValue . '&'; + } + else + { + return false; + } + } + + } + + return substr($urlExample,0,-1); + } + + /** + * Returns a HTML page containing help for all the successfully loaded APIs. + * + * For each module it will return a mini help with the method names, parameters to give, + * links to get the result in Xml/Csv/etc + * + * @return string + */ + public function getAllInterfaceString( $outputExampleUrls = true, $prefixUrls = '' ) + { + $str = ''; + $token_auth = "&token_auth=" . Piwik::getCurrentUserTokenAuth(); + $parametersToSet = array( + 'idSite' => Piwik_Common::getRequestVar('idSite', 1, 'int'), + 'period' => Piwik_Common::getRequestVar('period', 'day', 'string'), + 'date' => Piwik_Common::getRequestVar('date', 'today', 'string') + ); + + foreach($this->api as $class => $info) + { + $moduleName = $this->getModuleNameFromClassName($class); + $str .= "\n<h2 id='$moduleName'>Module ".$moduleName."</h2>"; + + foreach($info as $methodName => $infoMethod) + { + $params = $this->getStrListParameters($class, $methodName); + $str .= "\n" . "- <b>$moduleName.$methodName " . $params . "</b>"; + $str .= '<small>'; + + if($outputExampleUrls) + { + // we prefix all URLs with $prefixUrls + // used when we include this output in the Piwik official documentation for example + $str .= "<span class=\"example\">"; + $exampleUrl = $this->getExampleUrl($class, $methodName, $parametersToSet); + if($exampleUrl !== false) + { + $lastNUrls = ''; + if( ereg('(&period)|(&date)',$exampleUrl)) + { + $exampleUrlRss1 = $prefixUrls . $this->getExampleUrl($class, $methodName, $parametersToSet + array('date' => 'last10')) ; + $exampleUrlRss2 = $prefixUrls . $this->getExampleUrl($class, $methodName, $parametersToSet + array('date' => 'last5','period' => 'week',)); + $lastNUrls = ", RSS of the last <a target=_blank href='$exampleUrlRss1&format=rss$token_auth'>10 days</a>, <a target=_blank href='$exampleUrlRss2&format=Rss'>5 weeks</a>, + XML of the <a target=_blank href='$exampleUrlRss1&format=xml$token_auth'>last 10 days</a>"; + } + $exampleUrl = $prefixUrls . $exampleUrl ; + $str .= " [ Example in + <a target=_blank href='$exampleUrl&format=xml$token_auth'>XML</a>, + <a target=_blank href='$exampleUrl&format=PHP&prettyDisplay=true$token_auth'>PHP</a>, + <a target=_blank href='$exampleUrl&format=JSON$token_auth'>Json</a>, + <a target=_blank href='$exampleUrl&format=Csv$token_auth'>Csv</a>, + <a target=_blank href='$exampleUrl&format=Html$token_auth'>Basic html</a> + $lastNUrls + ]"; + } + else + { + $str .= " [ No example available ]"; + } + $str .= "</span>"; + } + $str .= '</small>'; + $str .= "\n<br>"; + } + } + return $str; + } + + /** + * Returns the methods $class.$name parameters (and default value if provided) as a string. + * + * @param string The class name + * @param string The method name + * @return string For example "(idSite, period, date = 'today')" + */ + private function getStrListParameters($class, $name) + { + $aParameters = $this->getParametersList($class, $name); + $asParameters = array(); + foreach($aParameters as $nameVariable=> $defaultValue) + { + $str = $nameVariable; + if($defaultValue !== Piwik_API_Proxy::NO_DEFAULT_VALUE) + { + $str .= " = '$defaultValue'"; + } + $asParameters[] = $str; + } + $sParameters = implode(", ", $asParameters); + return "($sParameters)"; + } + + /** + * Returns the parameters names and default values for the method $name + * of the class $class + * + * @param string The class name + * @param string The method name + * @return array Format array( + * 'testParameter' => null, // no default value + * 'life' => 42, // default value = 42 + * 'date' => 'yesterday', + * ); + */ + public function getParametersList($class, $name) + { + return $this->api[$class][$name]['parameters']; + } + + /** + * Returns the number of required parameters (parameters without default values). + * + * @param string The class name + * @param string The method name + * @return int The number of required parameters + */ + private function getNumberOfRequiredParameters($class, $name) + { + return $this->api[$class][$name]['numberOfRequiredParameters']; + } + + /** + * Returns true if the method is found in the API of the given class name. + * + * @param string The class name + * @param string The method name + * @return bool + */ + private function isMethodAvailable( $className, $methodName) + { + return isset($this->api[$className][$methodName]); + } + + + /** + * Checks that the count of the given parameters do match with the count of the required ones + * + * @param string The class name + * @param string The method name + * @param array + * @throws exception If less parameters than required were given + */ + private function checkNumberOfParametersMatch($className, $methodName, $parameters) + { + $nbParamsGiven = count($parameters); + $nbParamsRequired = $this->getNumberOfRequiredParameters($className, $methodName); + + if($nbParamsGiven < $nbParamsRequired) + { + throw new Exception("The number of parameters provided ($nbParamsGiven) is less than the number of required parameters ($nbParamsRequired) for this method. + Please check the method API."); + } + } + + /** + * Checks that the class is a Singleton (presence of the getInstance() method) + * + * @param string The class name + * @throws exception If the class is not a Singleton + */ + private function checkClassIsSingleton($className) + { + if(!method_exists($className, "getInstance")) + { + throw new Exception("Objects that provide an API must be Singleton and have a 'static public function getInstance()' method."); + } + } + + /** + * Checks that the method exists in the class + * + * @param string The class name + * @param string The method name + * @throws exception If the method is not found + */ + public function checkMethodExists($className, $methodName) + { + if(!$this->isMethodAvailable($className, $methodName)) + { + throw new Exception("The method '$methodName' does not exist or is not available in the module '".$className."'."); + } + } + + /** + * Returns the API class name given the module name. + * + * For exemple for $module = 'Referers' it returns 'Piwik_Referers_API' + * Piwik_Referers_API is the class that extends Piwik_Apiable + * and that contains the methods to be published in the API. + * + * @param string module name + * @return string class name + */ + protected function getClassNameFromModule($module) + { + $class = Piwik::prefixClass($module ."_API"); + return $class; + } + + /** + * Magic method used to set a flag telling the module named currently being called + * + */ + public function __get($name) + { + self::$classCalled = $name; + return $this; + } + + /** + * Method always called when making an API request. + * It checks several things before actually calling the real method on the given module. + * + * It also logs the API calls, with the parameters values, the returned value, the performance, etc. + * You can enable logging in config/global.ini.php (log_api_call) + * + * @param string The method name + * @param array The parameters + * + * @throws Piwik_Access_NoAccessException + */ + public function __call($methodName, $parameterValues ) + { + $returnedValue = null; + + try { + $this->registerClass(self::$classCalled); + + $className = $this->getClassNameFromModule(self::$classCalled); + + // instanciate the object + $object = call_user_func(array($className, "getInstance")); + + // check method exists + $this->checkMethodExists($className, $methodName); + + // first check number of parameters do match + $this->checkNumberOfParametersMatch($className, $methodName, $parameterValues); + + // start the timer + $timer = new Piwik_Timer; + + // call the method + $returnedValue = call_user_func_array(array($object, $methodName), $parameterValues); + + // log the API Call + $parameterNamesDefaultValues = $this->getParametersList($className, $methodName); + Zend_Registry::get('logger_api_call')->log( + self::$classCalled, + $methodName, + $parameterNamesDefaultValues, + $parameterValues, + $timer->getTimeMs(), + $returnedValue + ); + } + catch( Piwik_Access_NoAccessException $e) { + throw $e; + } + + self::$classCalled = null; + + return $returnedValue; + } +} diff --git a/core/API/Request.php b/core/API/Request.php new file mode 100644 index 0000000000..e32173dc60 --- /dev/null +++ b/core/API/Request.php @@ -0,0 +1,588 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Request.php 506 2008-06-06 01:18:47Z matt $ + * + * + * @package Piwik_API + */ + + +/** + * An API request is the object used to make a call to the API and get the result. + * The request has the format of a normal GET request, ie. parameter_1=X¶meter_2=Y + * + * You can use this object from anywhere in piwik (inside plugins for example). + * You can even call it outside of piwik using the REST API over http + * or in a php script on the same server as piwik, by including piwik/index.php + * (see examples in the documentation http://dev.piwik.org/trac/wiki/API) + * + * Example: + * $request = new Piwik_API_Request(' + * method=UserSettings.getWideScreen + * &idSite=1 + * &date=yesterday + * &period=week + * &format=xml + * &filter_limit=5 + * &filter_offset=0 + * '); + * $result = $request->process(); + * echo $result; + * + * @see http://dev.piwik.org/trac/wiki/API + * @package Piwik_API + */ +class Piwik_API_Request +{ + protected $outputFormatRequested; + + /** + * Constructs the request to the API, given the request url + * + * @param string GET request that defines the API call (must at least contain a "method" parameter) + * Example: method=UserSettings.getWideScreen&idSite=1&date=yesterday&period=week&format=xml + * If a request is not provided, then we use the $_REQUEST superglobal and fetch + * the values directly from the HTTP GET query. + */ + function __construct($request = null) + { + $requestArray = $_REQUEST; + + // If an array is specified we use it + if(!is_null($request)) + { + $request = trim($request); + $request = str_replace(array("\n","\t"),'', $request); + parse_str($request, $requestArray); + + // but we handle the case when an array is specified but we also want + // to look for the value in the _REQUEST + $requestArray = array_merge( $_REQUEST, $requestArray); + } + + // remove all spaces from parameters values (when calling internally the API for example) + foreach($requestArray as &$element) + { + // sometimes GET parameters can be arrays but we assume module accepting arrays are correctly handling spaces + if(!is_array($element)) + { + $element = trim($element); + } + } + + $this->requestToUse = $requestArray; + } + + /** + * Returns array( $class, $method) from the given string $class.$method + * + * @return array + * @throws exception if the name is invalid + */ + private function extractModuleAndMethod($parameter) + { + $a = explode('.',$parameter); + if(count($a) != 2) + { + throw new Exception("The method name is invalid. Must be on the form 'module.methodName'"); + } + return $a; + } + + + /** + * Handles the request to the API. + * It first checks that the method called (parameter 'method') is available in the module (it means that the method exists and is public) + * It then reads the parameters from the request string and throws an exception if there are missing parameters. + * It then calls the API Proxy which will call the requested method. + * + * @see the method handleReturnedValue() for the data post process logic + * + * @return mixed The data resulting from the API call + */ + public function process() + { + try { + + // read the format requested for the output data + $this->outputFormatRequested = Piwik_Common::getRequestVar('format', 'xml', 'string', $this->requestToUse); + $this->outputFormatRequested = strtolower($this->outputFormatRequested); + + // read parameters + $moduleMethod = Piwik_Common::getRequestVar('method', null, null, $this->requestToUse); + + list($module, $method) = $this->extractModuleAndMethod($moduleMethod); + + if(!Piwik_PluginsManager::getInstance()->isPluginActivated($module)) + { + throw new Exception_PluginDeactivated($module); + } + // call the method via the API_Proxy class + $api = Piwik_Api_Proxy::getInstance(); + $api->registerClass($module); + + // read method to call meta information + $className = "Piwik_" . $module . "_API"; + + // check method exists + $api->checkMethodExists($className, $method); + + // get the list of parameters required by the method + $parameters = $api->getParametersList($className, $method); + + // load the parameters from the request URL + $finalParameters = $this->getRequestParametersArray( $parameters ); + + // call the method + $returnedValue = call_user_func_array( array( $api->$module, $method), $finalParameters ); + + // post process the data + $toReturn = $this->handleReturnedValue( $returnedValue ); + + + } catch(Exception $e ) { + + // if it is not a direct API call, we are requesting the original data structure + // and we actually are handling this exception at the top level in the FrontController + if($this->outputFormatRequested == 'original') + { + throw $e; + } + $message = $e->getMessage(); + + $toReturn = $this->getExceptionOutput( $message, $this->outputFormatRequested); + + } + + return $toReturn; + } + + /** + * Returns the values of the current request + * + * @param array Parameters array of the method called. Contains name and default values of the required parameters + * @return array Values of the given parameters + * @throws exception If there is a missing parameter + */ + protected function getRequestParametersArray( $parameters ) + { + $finalParameters = array(); + foreach($parameters as $name => $defaultValue) + { + try{ + // there is a default value specified + if($defaultValue !== Piwik_API_Proxy::NO_DEFAULT_VALUE) + { + $requestValue = Piwik_Common::getRequestVar($name, $defaultValue, null, $this->requestToUse); + } + else + { + $requestValue = Piwik_Common::getRequestVar($name, null, null, $this->requestToUse); + } + } catch(Exception $e) { + throw new Exception("The required variable '$name' is not correct or has not been found in the API Request. Add the parameter '&$name=' (with a value) in the URL."); + } + $finalParameters[] = $requestValue; + } + return $finalParameters; + } + + /** + * This method post processes the data resulting from the API call. + * + * - If the data resulted from the API call is a Piwik_DataTable then + * - we apply the standard filters if the parameters have been found + * in the URL. For example to offset,limit the Table you can add the following parameters to any API + * call that returns a DataTable: filter_limit=10&filter_offset=20 + * - we apply the filters that have been previously queued on the DataTable + * @see Piwik_DataTable::queueFilter() + * - we apply the renderer that generate the DataTable in a given format (XML, PHP, HTML, JSON, etc.) + * the format can be changed using the 'format' parameter in the request. + * Example: format=xml + * + * - If there is nothing returned (void) we display a standard success message + * + * - If there is a PHP array returned, we try to convert it to a dataTable + * It is then possible to convert this datatable to any requested format (xml/etc) + * + * - If a bool is returned we convert to a string (true is displayed as 'true' false as 'false') + * + * - If an integer / float is returned, we simply return it + * + * @throws Exception If an object/resource is returned, if any of conversion fails, etc. + * + * @param mixed The initial returned value, before post process + * @return mixed Usually a string, but can still be a PHP data structure if the format requested is 'original' + */ + protected function handleReturnedValue( $returnedValue ) + { + $toReturn = $returnedValue; + + // If the returned value is an object DataTable we + // apply the set of generic filters if asked in the URL + // and we render the DataTable according to the format specified in the URL + if($returnedValue instanceof Piwik_DataTable + || $returnedValue instanceof Piwik_DataTable_Array) + { + if($returnedValue instanceof Piwik_DataTable) + { + $this->applyDataTableGenericFilters($returnedValue); + } + elseif($returnedValue instanceof Piwik_DataTable_Array) + { + $tables = $returnedValue->getArray(); + foreach($tables as $table) + { + $this->applyDataTableGenericFilters($table); + } + } + + // if the flag disable_queued_filters is defined we skip the filters that were queued + // useful in some very rare cases but better to use this than a bad hack on the data returned... + if(Piwik_Common::getRequestVar('disable_queued_filters', 'false', 'string', $this->requestToUse) == 'false') + { + $returnedValue->applyQueuedFilters(); + } + + $toReturn = $this->getRenderedDataTable($returnedValue); + } + + // Case nothing returned (really nothing was 'return'ed), + // => the operation was successful + elseif(!isset($toReturn)) + { + $toReturn = $this->getStandardSuccessOutput($this->outputFormatRequested); + } + + // Case an array is returned from the API call, we convert it to the requested format + // - if calling from inside the application (format = original) + // => the data stays unchanged (ie. a standard php array or whatever data structure) + // - if any other format is requested, we have to convert this data structure (which we assume + // to be an array) to a DataTable in order to apply the requested DataTable_Renderer (for example XML) + elseif(is_array($toReturn)) + { + if($this->outputFormatRequested == 'original') + { + // we handle the serialization. Because some php array have a very special structure that + // couldn't be converted with the automatic DataTable->loadFromSimpleArray + // the user may want to request the original PHP data structure serialized by the API + // in case he has to setup serialize=1 in the URL + if($this->caseRendererPHPSerialize( $defaultSerialize = 0)) + { + $toReturn = serialize($toReturn); + } + } + else + { + $dataTable = new Piwik_DataTable(); + $dataTable->loadFromSimpleArray($toReturn); + $toReturn = $this->getRenderedDataTable($dataTable); + } + } + // bool // integer // float // object is serialized + // NB: null value is already handled by the isset() test above + else + { + // original data structure requested, we return without process + if( $this->outputFormatRequested == 'original' ) + { + return $toReturn; + } + + if( $toReturn === true ) + { + $toReturn = 'true'; + } + elseif( $toReturn === false ) + { + $toReturn = 'false'; + } + elseif( is_object($toReturn) + || is_resource($toReturn) + ) + { + return $this->getExceptionOutput( ' The API cannot handle this data structure. You can get the data internally by directly using the class.', $this->outputFormatRequested); + } + + require_once "DataTable/Simple.php"; + $dataTable = new Piwik_DataTable_Simple(); + $dataTable->loadFromArray( array($toReturn) ); + $toReturn = $this->getRenderedDataTable($dataTable); + } + return $toReturn; + } + + /** + * Returns a success $message in the requested $format + * + * @param string $format xml/json/php/csv + * @param string $message + * @return string + */ + protected function getStandardSuccessOutput($format, $message = 'ok') + { + switch($format) + { + case 'xml': + @header("Content-Type: text/xml;charset=utf-8"); + $return = + '<?xml version="1.0" encoding="utf-8" ?>'. + '<result>'. + ' <success message="'.$message.'" />'. + '</result>'; + break; + case 'json': + @header( "Content-type: application/json" ); + $return = '{"result":"success", "message":"'.$message.'"}'; + break; + case 'php': + $return = array('result' => 'success', 'message' => $message); + if($this->caseRendererPHPSerialize()) + { + $return = serialize($return); + } + break; + + case 'csv': + header("Content-type: application/vnd.ms-excel"); + header("Content-Disposition: attachment; filename=piwik-report-export.csv"); + $return = "message\n".$message; + break; + + default: + $return = 'Success:'.$message; + break; + } + + return $return; + } + + /** + * Returns an error $message in the requested $format + * + * @param string $format xml/json/php/csv + * @param string $message + * @return string + */ + function getExceptionOutput($message, $format) + { + switch($format) + { + case 'xml': + @header("Content-Type: text/xml;charset=utf-8"); + $return = + '<?xml version="1.0" encoding="utf-8" ?>'. + '<result>'. + ' <error message="'.htmlentities($message).'" />'. + '</result>'; + break; + case 'json': + @header( "Content-type: application/json" ); + // we remove the \n from the resulting string as this is not allowed in json + $message = str_replace("\n","",$message); + $return = '{"result":"error", "message":"'.htmlentities($message).'"}'; + break; + case 'php': + $return = array('result' => 'error', 'message' => $message); + if($this->caseRendererPHPSerialize()) + { + $return = serialize($return); + } + break; + default: + $return = 'Error: '.$message; + break; + } + + return $return; + } + + /** + * Apply the specified renderer to the DataTable + * + * @param Piwik_DataTable + * @return string + */ + protected function getRenderedDataTable($dataTable) + { + // Renderer + $format = Piwik_Common::getRequestVar('format', 'php', 'string', $this->requestToUse); + $format = strtolower($format); + + // if asked for original dataStructure + if($format == 'original') + { + // if the original dataStructure is a simpleDataTable and has only one row, we return the value + if($dataTable instanceof Piwik_DataTable_Simple + && $dataTable->getRowsCount() == 1) + { + return $dataTable->getRowFromId(0)->getColumn('value'); + } + + // the original data structure can be asked as serialized. + // but by default it's not serialized + if($this->caseRendererPHPSerialize( $defaultSerialize = 0)) + { + $dataTable = serialize($dataTable); + } + return $dataTable; + } + + $renderer = Piwik_DataTable_Renderer::factory($format); + $renderer->setTable($dataTable); + + if($format == 'php') + { + $renderer->setSerialize( $this->caseRendererPHPSerialize()); + } + + $toReturn = $renderer->render(); + return $toReturn; + } + + /** + * Returns true if the user requested to serialize the output data (&serialize=1 in the request) + * + * @param $defaultSerializeValue Default value in case the user hasn't specified a value + * @return bool + */ + protected function caseRendererPHPSerialize($defaultSerializeValue = 1) + { + $serialize = Piwik_Common::getRequestVar('serialize', $defaultSerializeValue, 'int', $this->requestToUse); + if($serialize) + { + return true; + } + else + { + return false; + } + } + + /** + * Returns an array containing the information of the generic Piwik_DataTable_Filter + * to be applied automatically to the data resulting from the API calls. + * + * @return array See the code for spec + */ + public static function getGenericFiltersInformation() + { + $genericFilters = array( + + 'Pattern' => array( + 'filter_column' => array('string'), + 'filter_pattern' => array('string'), + ), + 'PatternRecursive' => array( + 'filter_column_recursive' => array('string'), + 'filter_pattern_recursive' => array('string'), + ), + 'ExcludeLowPopulation' => array( + 'filter_excludelowpop' => array('string'), + 'filter_excludelowpop_value'=> array('float'), + ), + 'Sort' => array( + 'filter_sort_column' => array('string', Piwik_Archive::INDEX_NB_VISITS), + 'filter_sort_order' => array('string', Zend_Registry::get('config')->General->dataTable_default_sort_order), + ), + 'Limit' => array( + 'filter_offset' => array('integer', '0'), + 'filter_limit' => array('integer', Zend_Registry::get('config')->General->dataTable_default_limit), + ), + ); + + return $genericFilters; + } + + + /** + * Apply generic filters to the DataTable object resulting from the API Call. + * Disable this feature by setting the parameter disable_generic_filters to 1 in the API call request. + * + * @param Piwik_DataTable + * @return void + */ + protected function applyDataTableGenericFilters($dataTable) + { + if($dataTable instanceof Piwik_DataTable_Array ) + { + $tables = $dataTable->getArray(); + foreach($tables as $table) + { + $this->applyDataTableGenericFilters($table); + } + return; + } + + // Generic filters + // PatternFileName => Parameter names to match to constructor parameters + /* + * Order to apply the filters: + * 1 - Filter that remove filtered rows + * 2 - Filter that sort the remaining rows + * 3 - Filter that keep only a subset of the results + */ + $genericFilters = Piwik_API_Request::getGenericFiltersInformation(); + + // if the flag disable_generic_filters is defined we skip the generic filters + if(Piwik_Common::getRequestVar('disable_generic_filters', 'false', 'string', $this->requestToUse) != 'false') + { + return; + } + + foreach($genericFilters as $filterName => $parameters) + { + $filterParameters = array(); + $exceptionRaised = false; + + foreach($parameters as $name => $info) + { + // parameter type to cast to + $type = $info[0]; + + // default value if specified, when the parameter doesn't have a value + $defaultValue = null; + if(isset($info[1])) + { + $defaultValue = $info[1]; + } + + try { + $value = Piwik_Common::getRequestVar($name, $defaultValue, $type, $this->requestToUse); + settype($value, $type); + $filterParameters[] = $value; + } + catch(Exception $e) + { + $exceptionRaised = true; + break; + } + } + + if(!$exceptionRaised) + { + // a generic filter class name must follow this pattern + $class = "Piwik_DataTable_Filter_".$filterName; + + if($filterName == 'Limit') + { + $dataTable->setRowsCountBeforeLimitFilter(); + } + + // build the set of parameters for the filter + $filterParameters = array_merge(array($dataTable), $filterParameters); + + // make a reflection object + $reflectionObj = new ReflectionClass($class); + + // use Reflection to create a new instance, using the $args + $filter = $reflectionObj->newInstanceArgs($filterParameters); + } + } + } + +}
\ No newline at end of file diff --git a/core/Access.php b/core/Access.php new file mode 100644 index 0000000000..cfe06ad7e6 --- /dev/null +++ b/core/Access.php @@ -0,0 +1,326 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Access.php 581 2008-07-27 23:07:52Z matt $ + * + * @package Piwik + * + */ + +require_once 'SitesManager/API.php'; + +/** + * Class to handle User Access: + * - loads user access from the Piwik_Auth_Result object + * - provides easy to use API to check the permissions for the current (check* methods) + * + * In Piwik there are mainly 4 access levels + * - no access + * - VIEW access + * - ADMIN access + * - Super admin access + * + * An access level is on a per website basis. + * A given user has a given access level for a given website. + * For example: + * User Noemie has + * - VIEW access on the website 1, + * - ADMIN on the website 2 and 4, and + * - NO access on the website 3 and 5 + * + * There is only one Super User. He has ADMIN access to all the websites + * and he only can change the main configuration settings. + * + * @package Piwik + */ + +class Piwik_Access +{ + /** + * Array of idsites available to the current user, indexed by permission level + * @see getSitesIdWith*() + * + * @var array + */ + protected $idsitesByAccess = null; + + /** + * Login of the current user + * + * @var string + */ + protected $login = null; + + /** + * token_auth of the current user + * + * @var string + */ + protected $token_auth = null; + + /** + * Defines if the current user is the super user + * @see isSuperUser() + * + * @var bool + */ + protected $isSuperUser = false; + + /** + * List of available permissions in Piwik + * + * @var array + */ + static private $availableAccess = array('noaccess', 'view', 'admin', 'superuser'); + + /** + * Authentification object (see Piwik_Auth) + * + * @var Piwik_Auth + */ + private $auth; + + /** + * Returns the list of the existing Access level. + * Useful when a given API method requests a given acccess Level. + * We first check that the required access level exists. + */ + static public function getListAccess() + { + return self::$availableAccess; + } + + /** + * @param Piwik_Auth The authentification object + */ + public function __construct( Piwik_Auth $auth ) + { + $this->auth = $auth; + } + + /** + * Loads the access levels for the current user. + * + * Calls the authentication method to try to log the user in the system. + * If the user credentials are not correct we don't load anything. + * If the login/password is correct the user is either the SuperUser or a normal user. + * We load the access levels for this user for all the websites. + * + */ + public function loadAccess() + { + $idsitesByAccess = array( 'view' => array(), 'admin' => array(), 'superuser' => array()); + + // access = array ( idsite => accessIdSite, idsite2 => accessIdSite2) + $result = $this->auth->authenticate(); + + if($result->isValid()) + { + $this->login = $result->getIdentity(); + $this->token_auth = $result->getTokenAuth(); + + // case the superUser is logged in + if($result->getCode() == Piwik_Auth_Result::SUCCESS_SUPERUSER_AUTH_CODE) + { + $this->isSuperUser = true; + $idsitesByAccess['superuser'] = Piwik_SitesManager_API::getAllSitesId(); + } + // valid authentification (normal user logged in) + else + { + $db = Zend_Registry::get('db'); + + // we join with site in case there are rows in access for an idsite that doesn't exist anymore + // (backward compatibility ; before we deleted the site without deleting rows in _access table) + $accessRaw = $db->fetchAll("SELECT access, t2.idsite + FROM ".Piwik::prefixTable('access'). " as t1 + JOIN ".Piwik::prefixTable('site')." as t2 USING (idsite) ". + " WHERE login=?", $this->login); + + foreach($accessRaw as $access) + { + $idsitesByAccess[$access['access']][] = $access['idsite']; + } + } + } + + $this->idsitesByAccess = $idsitesByAccess; + } + + /** + * We bypass the normal auth method and give the current user Super User rights. + * This should be very carefully used. + * + * @return void + */ + public function setSuperUser() + { + $this->isSuperUser = true; + $this->idsitesByAccess['superuser'] = Piwik_SitesManager_API::getAllSitesId(); + } + + /** + * Returns true if the current user is logged in as the super user + * + * @return bool + */ + public function isSuperUser() + { + return $this->isSuperUser; + } + + /** + * Returns the current user login + * @return string + */ + public function getLogin() + { + return $this->login; + } + + /** + * Returns the token_auth used to authenticate this user in the API + * @return string + */ + public function getTokenAuth() + { + return $this->token_auth; + } + + /** + * Returns an array of ID sites for which the user has at least a VIEW access. + * Which means VIEW or ADMIN or SUPERUSER. + * + * @return array Example if the user is ADMIN for 4 + * and has VIEW access for 1 and 7, it returns array(1, 4, 7); + */ + public function getSitesIdWithAtLeastViewAccess() + { + return array_unique(array_merge( + $this->idsitesByAccess['view'], + $this->idsitesByAccess['admin'], + $this->idsitesByAccess['superuser'])); + } + + + /** + * Returns an array of ID sites for which the user has an ADMIN access. + * + * @return array Example if the user is ADMIN for 4 and 8 + * and has VIEW access for 1 and 7, it returns array(4, 8); + */ + public function getSitesIdWithAdminAccess() + { + return array_unique(array_merge( + $this->idsitesByAccess['admin'], + $this->idsitesByAccess['superuser'])); + } + + + /** + * Returns an array of ID sites for which the user has a VIEW access only. + * + * @return array Example if the user is ADMIN for 4 + * and has VIEW access for 1 and 7, it returns array(1, 7); + * @see getSitesIdWithAtLeastViewAccess() + */ + public function getSitesIdWithViewAccess() + { + return $this->idsitesByAccess['view']; + } + + /** + * Throws an exception if the user is not the SuperUser + * + * @throws Exception + */ + public function checkUserIsSuperUser() + { + if($this->isSuperUser === false) + { + throw new Piwik_Access_NoAccessException("You can't access this resource as it requires a 'superuser' access."); + } + } + + /** + * If the user doesn't have an ADMIN access for at least one website, throws an exception + * + * @throws Exception + */ + public function checkUserHasSomeAdminAccess() + { + $idSitesAccessible = $this->getSitesIdWithAdminAccess(); + if(count($idSitesAccessible) == 0) + { + throw new Piwik_Access_NoAccessException("You can't access this resource as it requires an 'admin' access for at least one website."); + } + } + + /** + * This method checks that the user has ADMIN access for the given list of websites. + * If the user doesn't have ADMIN access for at least one website of the list, we throw an exception. + * + * @param int|arrayOfIntegers List of ID sites to check + * @throws Exception If for any of the websites the user doesn't have an ADMIN access + */ + public function checkUserHasAdminAccess( $idSites ) + { + if($idSites === 'all') + { + $idSites = $this->getSitesIdWithAtLeastViewAccess(); + } + if(!is_array($idSites)) + { + $idSites = Piwik_Site::getIdSitesFromIdSitesString($idSites); + } + $idSitesAccessible = $this->getSitesIdWithAdminAccess(); + foreach($idSites as $idsite) + { + if(!in_array($idsite, $idSitesAccessible)) + { + throw new Piwik_Access_NoAccessException("You can't access this resource as it requires an 'admin' access for the website id = $idsite."); + } + } + } + + /** + * This method checks that the user has VIEW or ADMIN access for the given list of websites. + * If the user doesn't have VIEW or ADMIN access for at least one website of the list, we throw an exception. + * + * @param int|arrayOfIntegers|string List of ID sites to check (integer, array of integers, string comma separated list of integers) + * @throws Exception If for any of the websites the user doesn't have an VIEW or ADMIN access + */ + public function checkUserHasViewAccess( $idSites ) + { + if($idSites === 'all') + { + $idSites = $this->getSitesIdWithAtLeastViewAccess(); + } + + if(!is_array($idSites)) + { + $idSites = Piwik_Site::getIdSitesFromIdSitesString($idSites); + } + $idSitesAccessible = $this->getSitesIdWithAtLeastViewAccess(); + + foreach($idSites as $idsite) + { + if(!in_array($idsite, $idSitesAccessible)) + { + throw new Piwik_Access_NoAccessException("You can't access this resource as it requires a 'view' access for the website id = $idsite."); + } + } + } +} + +/** + * + * Exception thrown when a user doesn't have sufficient access. + * + * @package Piwik + */ +class Piwik_Access_NoAccessException extends Exception +{}
\ No newline at end of file diff --git a/core/Archive.php b/core/Archive.php new file mode 100644 index 0000000000..8218c9aa33 --- /dev/null +++ b/core/Archive.php @@ -0,0 +1,224 @@ +<?php
+/**
+ * Piwik - Open source web analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later
+ * @version $Id: Archive.php 585 2008-07-28 00:56:50Z matt $
+ *
+ * @package Piwik
+ */
+
+
+require_once 'Period.php';
+require_once 'Date.php';
+require_once 'ArchiveProcessing.php';
+require_once 'Archive/Single.php';
+
+/**
+ * The archive object is used to query specific data for a day or a period of statistics for a given website.
+ *
+ * Example:
+ * <pre>
+ * $archive = Piwik_Archive::build($idSite = 1, $period = 'week', '2008-03-08' );
+ * $dataTable = $archive->getDataTable('Provider_hostnameExt');
+ * $dataTable->queueFilter('Piwik_DataTable_Filter_ReplaceColumnNames');
+ * return $dataTable;
+ * </pre>
+ *
+ * Example bis:
+ * <pre>
+ * $archive = Piwik_Archive::build($idSite = 3, $period = 'day', $date = 'today' );
+ * $nbVisits = $archive->getNumeric('nb_visits');
+ * return $nbVisits;
+ * </pre>
+ *
+ * If the requested statistics are not yet processed, Archive uses ArchiveProcessing to archive the statistics.
+ *
+ * @package Piwik
+ * @subpackage Piwik_Archive
+ */
+abstract class Piwik_Archive
+{
+ /**
+ * When saving DataTables in the DB, we sometimes replace the columns name by these IDs so we save up lots of bytes
+ * Eg. INDEX_NB_UNIQ_VISITORS is an integer: 4 bytes, but 'nb_uniq_visitors' is 16 bytes at least
+ * (in php it's actually even much more)
+ *
+ */
+ const INDEX_NB_UNIQ_VISITORS = 1;
+ const INDEX_NB_VISITS = 2;
+ const INDEX_NB_ACTIONS = 3;
+ const INDEX_MAX_ACTIONS = 4;
+ const INDEX_SUM_VISIT_LENGTH = 5;
+ const INDEX_BOUNCE_COUNT = 6;
+
+ /**
+ * Website Piwik_Site
+ *
+ * @var Piwik_Site
+ */
+ protected $site = null;
+
+ /**
+ * Stores the already built archives.
+ * Act as a big caching array
+ *
+ * @var array of Piwik_Archive
+ */
+ static protected $alreadyBuilt = array();
+
+ /**
+ * Builds an Archive object or returns the same archive if previously built.
+ *
+ * @param string|int idSite integer, or comma separated list of integer
+ * @param string|Piwik_Date $date 'YYYY-MM-DD' or magic keywords 'today' @see Piwik_Date::factory()
+ * @param string $period 'week' 'day' etc.
+ *
+ * @return Piwik_Archive
+ */
+ static public function build($idSite, $period, $strDate )
+ {
+ if($idSite === 'all')
+ {
+ $sites = Piwik_SitesManager_API::getSitesIdWithAtLeastViewAccess();
+ }
+ else
+ {
+ $sites = Piwik_Site::getIdSitesFromIdSitesString($idSite);
+ } +
+ // idSite=1,3 or idSite=all
+ if( count($sites) > 1 + || $idSite === 'all' )
+ {
+ require_once 'Archive/Array/IndexedBySite.php';
+ $archive = new Piwik_Archive_Array_IndexedBySite($sites, $period, $strDate);
+ }
+ // if a period date string is detected: either 'last30', 'previous10' or 'YYYY-MM-DD,YYYY-MM-DD'
+ elseif(is_string($strDate)
+ && (
+ ereg('^(last|previous){1}([0-9]*)$', $strDate, $regs)
+ || ereg('^([0-9]{4}-[0-9]{1,2}-[0-9]{1,2}),([0-9]{4}-[0-9]{1,2}-[0-9]{1,2})$', $strDate, $regs)
+ )
+ )
+ {
+ $oSite = new Piwik_Site($idSite);
+ require_once 'Archive/Array/IndexedByDate.php';
+ $archive = new Piwik_Archive_Array_IndexedByDate($oSite, $period, $strDate);
+ }
+ // case we request a single archive
+ else
+ {
+ if(is_string($strDate))
+ {
+ $oDate = Piwik_Date::factory($strDate);
+ }
+ else
+ {
+ $oDate = $strDate;
+ }
+ $date = $oDate->toString();
+
+ if(isset(self::$alreadyBuilt[$idSite][$date][$period]))
+ {
+ return self::$alreadyBuilt[$idSite][$date][$period];
+ }
+
+ $oPeriod = Piwik_Period::factory($period, $oDate);
+
+ $archive = new Piwik_Archive_Single;
+ $archive->setPeriod($oPeriod);
+ $archive->setSite(new Piwik_Site($idSite));
+
+ self::$alreadyBuilt[$idSite][$date][$period] = $archive;
+ }
+
+ return $archive;
+ }
+
+ abstract public function prepareArchive();
+
+ /**
+ * Returns the value of the element $name from the current archive
+ * The value to be returned is a numeric value and is stored in the archive_numeric_* tables
+ *
+ * @param string $name For example Referers_distinctKeywords
+ * @return float|int|false False if no value with the given name
+ */
+ abstract public function getNumeric( $name );
+
+ /**
+ * Returns the value of the element $name from the current archive
+ *
+ * The value to be returned is a blob value and is stored in the archive_numeric_* tables
+ *
+ * It can return anything from strings, to serialized PHP arrays or PHP objects, etc.
+ *
+ * @param string $name For example Referers_distinctKeywords
+ * @return mixed False if no value with the given name
+ */
+ abstract public function getBlob( $name );
+
+ abstract public function getDataTableFromNumeric( $fields );
+
+ /**
+ * This method will build a dataTable from the blob value $name in the current archive.
+ *
+ * For example $name = 'Referers_searchEngineByKeyword' will return a Piwik_DataTable containing all the keywords
+ * If a idSubTable is given, the method will return the subTable of $name
+ *
+ * @param string $name
+ * @param int $idSubTable or null if requesting the parent table
+ * @return Piwik_DataTable
+ * @throws exception If the value cannot be found
+ */
+ abstract public function getDataTable( $name, $idSubTable = null );
+
+ /**
+ * Same as getDataTable() except that it will also load in memory
+ * all the subtables for the DataTable $name.
+ * You can then access the subtables by using the Piwik_DataTable_Manager getTable()
+ *
+ * @param string $name
+ * @param int $idSubTable or null if requesting the parent table
+ * @return Piwik_DataTable
+ */
+ abstract public function getDataTableExpanded($name, $idSubTable = null);
+
+ /**
+ * Sets the site
+ *
+ * @param Piwik_Site $site
+ */
+ public function setSite( Piwik_Site $site )
+ {
+ $this->site = $site;
+ }
+
+ /**
+ * Gets the site
+ *
+ * @param Piwik_Site $site
+ */
+ public function getSite()
+ {
+ return $this->site;
+ }
+
+ /**
+ * Returns the Id site associated with this archive
+ *
+ * @return int
+ */
+ public function getIdSite()
+ {
+ return $this->site->getId();
+ }
+
+}
+
+
+
+
+
diff --git a/core/Archive/Array.php b/core/Archive/Array.php new file mode 100644 index 0000000000..479cbd39ed --- /dev/null +++ b/core/Archive/Array.php @@ -0,0 +1,161 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Request.php 380 2008-03-17 14:59:24Z matt $ + * + * + * @package Piwik_Archive + */ + +require_once "DataTable/Simple.php"; +require_once "DataTable/Array.php"; +/** + * This class is used to store multiple archives, when the user requests a period's archive. + * + */ +abstract class Piwik_Archive_Array extends Piwik_Archive +{ + /** + * This array contains one Piwik_Archive per entry in the period + * + * @var array + */ + protected $archives = array(); + + abstract protected function getIndexName(); + abstract protected function getDataTableLabelValue( $archive ); + + public function prepareArchive() + { + foreach($this->archives as $archive) + { + $archive->prepareArchive(); + } + } + + /** + * Returns a newly created Piwik_DataTable_Array. + * + * @return Piwik_DataTable_Array + */ + protected function getNewDataTableArray() + { + $table = new Piwik_DataTable_Array; + $table->setKeyName($this->getIndexName()); + return $table; + } + + + + /** + * Adds metadata information to the Piwik_DataTable_Array + * using the information given by the Archive + * + * @param Piwik_DataTable_Array $table + * @param unknown_type $archive + */ + protected function loadMetadata(Piwik_DataTable_Array $table, $archive) + { + } + + /** + * Returns a DataTable_Array containing numeric values + * of the element $name from the archives in this Archive_Array. + * + * @param string $name Name of the mysql table field to load eg. Referers_distinctKeywords + * + * @return Piwik_DataTable_Array containing the requested numeric value for each Archive + */ + public function getNumeric( $name ) + { + $table = $this->getNewDataTableArray(); + + foreach($this->archives as $archive) + { + $numeric = $archive->getNumeric( $name ) ; + $subTable = new Piwik_DataTable_Simple(); + $subTable->loadFromArray( array( $numeric ) ); + $table->addTable($subTable, $this->getDataTableLabelValue($archive)); + + $this->loadMetadata($table, $archive); + } + + return $table; + } + + + /** + * Returns a DataTable_Array containing values + * of the element $name from the archives in this Archive_Array. + * + * The value to be returned are blob values (stored in the archive_numeric_* tables in the DB). * + * It can return anything from strings, to serialized PHP arrays or PHP objects, etc. + * + * @param string $name Name of the mysql table field to load eg. Referers_keywordBySearchEngine + * + * @return Piwik_DataTable_Array containing the requested blob values for each Archive + */ + public function getBlob( $name ) + { + $table = $this->getNewDataTableArray(); + + foreach($this->archives as $archive) + { + $blob = $archive->getBlob( $name ) ; + $subTable = new Piwik_DataTable_Simple(); + $subTable->loadFromArray( array('blob' => $blob)); + $table->addTable($subTable, $this->getDataTableLabelValue($archive)); + + $this->loadMetadata($table, $archive); + } + return $table; + } + + /** + * Given a BLOB field name (eg. 'Referers_searchEngineByKeyword'), it will return a Piwik_DataTable_Array + * which is an array of Piwik_DataTable, ordered by chronological order + * + * @param string $name Name of the mysql table field to load + * @param int $idSubTable optional idSubDataTable + * @return Piwik_DataTable_Array + * @throws exception If the value cannot be found + */ + public function getDataTable( $name, $idSubTable = null ) + { + $table = $this->getNewDataTableArray(); + foreach($this->archives as $archive) + { + $subTable = $archive->getDataTable( $name, $idSubTable ) ; + $table->addTable($subTable, $this->getDataTableLabelValue($archive)); + + $this->loadMetadata($table, $archive); + } + return $table; + } + + + /** + * Same as getDataTable() except that it will also load in memory + * all the subtables for the DataTable $name. + * You can then access the subtables by using the Piwik_DataTable_Manager::getInstance()->getTable($idSubTable); + * + * @param string $name Name of the mysql table field to load + * @param int $idSubTable optional idSubDataTable + * @return Piwik_DataTable_Array + */ + public function getDataTableExpanded($name, $idSubTable = null) + { + $table = $this->getNewDataTableArray(); + foreach($this->archives as $archive) + { + $subTable = $archive->getDataTableExpanded( $name, $idSubTable ) ; + $table->addTable($subTable, $this->getDataTableLabelValue($archive)); + + $this->loadMetadata($table, $archive); + } + return $table; + } +} diff --git a/core/Archive/Array/IndexedByDate.php b/core/Archive/Array/IndexedByDate.php new file mode 100644 index 0000000000..87e57491d2 --- /dev/null +++ b/core/Archive/Array/IndexedByDate.php @@ -0,0 +1,122 @@ +<?php +require_once "Archive/Array.php"; + +class Piwik_Archive_Array_IndexedByDate extends Piwik_Archive_Array { + + /** + * Builds an array of Piwik_Archive of a given date range + * + * @param Piwik_Site $oSite + * @param string $strPeriod eg. 'day' 'week' etc. + * @param string $strDate A date range, eg. 'last10', 'previous5' or 'YYYY-MM-DD,YYYY-MM-DD' + */ + function __construct(Piwik_Site $oSite, $strPeriod, $strDate) + { + $rangePeriod = new Piwik_Period_Range($strPeriod, $strDate); + foreach($rangePeriod->getSubperiods() as $subPeriod) + { + $startDate = $subPeriod->getDateStart(); + $archive = Piwik_Archive::build($oSite->getId(), $strPeriod, $startDate ); + $archive->prepareArchive(); + $timestamp = $archive->getTimestampStartDate(); + $this->archives[$timestamp] = $archive; + } + ksort( $this->archives ); + } + + protected function getIndexName() + { + return 'date'; + } + + protected function loadMetadata(Piwik_DataTable_Array $table, $archive) + { + $table->metadata[$archive->getPrettyDate()] = array( + 'timestamp' => $archive->getTimestampStartDate(), + 'site' => $archive->getSite(), + ); + } + protected function getDataTableLabelValue( $archive ) + { + return $archive->getPrettyDate(); + } + + /** + * Given a list of fields defining numeric values, it will return a Piwik_DataTable_Array + * which is an array of Piwik_DataTable_Simple, ordered by chronological order + * + * @param array|string $fields array( fieldName1, fieldName2, ...) Names of the mysql table fields to load + * @return Piwik_DataTable_Array + */ + public function getDataTableFromNumeric( $fields ) + { + if(!is_array($fields)) + { + $fields = array($fields); + } + + $inName = "'" . implode("', '",$fields) . "'"; + + // we select in different shots + // one per distinct table (case we select last 300 days, maybe we will select from 10 different tables) + $queries = array(); + foreach($this->archives as $archive) + { + if(!$archive->isThereSomeVisits) + { + continue; + } + + $table = $archive->archiveProcessing->getTableArchiveNumericName(); + + // for every query store IDs + $queries[$table][] = $archive->getIdArchive(); + } + // we select the requested value + $db = Zend_Registry::get('db'); + + // date => array( 'field1' =>X, 'field2'=>Y) + // date2 => array( 'field1' =>X2, 'field2'=>Y2) + + $arrayValues = array(); + foreach($queries as $table => $aIds) + { + $inIds = implode(', ', $aIds); + $sql = "SELECT value, name, idarchive, UNIX_TIMESTAMP(date1) as timestamp + FROM $table + WHERE idarchive IN ( $inIds ) + AND name IN ( $inName )"; + + $values = $db->fetchAll($sql); + + foreach($values as $value) + { + $arrayValues[$value['timestamp']][$value['name']] = $value['value']; + } + } + + $contentArray = array(); + // we add empty tables so that every requested date has an entry, even if there is nothing + // example: <result date="2007-01-01" /> + foreach($this->archives as $timestamp => $archive) + { + $strDate = $this->archives[$timestamp]->getPrettyDate(); + $contentArray[$timestamp]['table'] = new Piwik_DataTable_Simple(); + $contentArray[$timestamp]['prettyDate'] = $strDate; + } + + foreach($arrayValues as $timestamp => $aNameValues) + { + $contentArray[$timestamp]['table']->loadFromArray($aNameValues); + } + ksort( $contentArray ); + + $tableArray = $this->getNewDataTableArray(); + foreach($contentArray as $timestamp => $aData) + { + $tableArray->addTable($aData['table'], $aData['prettyDate']); + $this->loadMetadata($tableArray, $this->archives[$timestamp]); + } + return $tableArray; + } +}
\ No newline at end of file diff --git a/core/Archive/Array/IndexedBySite.php b/core/Archive/Array/IndexedBySite.php new file mode 100644 index 0000000000..8d1f984809 --- /dev/null +++ b/core/Archive/Array/IndexedBySite.php @@ -0,0 +1,98 @@ +<?php +require_once "Archive/Array.php"; + +class Piwik_Archive_Array_IndexedBySite extends Piwik_Archive_Array { + + /** + * + * @param Piwik_Site $oSite + * @param string $strPeriod eg. 'day' 'week' etc. + * @param string $strDate A date range, eg. 'last10', 'previous5' or 'YYYY-MM-DD,YYYY-MM-DD' + */ + function __construct($sites, $strPeriod, $strDate) + { + foreach($sites as $idSite) + { + $archive = Piwik_Archive::build($idSite, $strPeriod, $strDate ); + $archive->setSite(new Piwik_Site($idSite)); + $archive->prepareArchive(); + $this->archives[$idSite] = $archive; + } + ksort( $this->archives ); + } + + protected function getIndexName() + { + return 'idSite'; + } + + protected function getDataTableLabelValue( $archive ) + { + return $archive->getIdSite(); + } + + /** + * Given a list of fields defining numeric values, it will return a Piwik_DataTable_Array + * which is an array of Piwik_DataTable_Simple, ordered by chronological order + * + * @param array|string $fields array( fieldName1, fieldName2, ...) Names of the mysql table fields to load + * @return Piwik_DataTable_Array + */ + public function getDataTableFromNumeric( $fields ) + { + if(!is_array($fields)) + { + $fields = array($fields); + } + $inName = "'" . implode("', '",$fields) . "'"; + + $numericTable = null; + $aIds = array(); + foreach($this->archives as $archive) + { + if(is_null($numericTable)) + { + $numericTable = $archive->archiveProcessing->getTableArchiveNumericName(); + } + else if( $numericTable != $archive->archiveProcessing->getTableArchiveNumericName()) + { + throw new Exception("Piwik_Archive_Array_IndexedBySite::getDataTableFromNumeric() algorithm won't work if data is stored in different tables"); + } + $aIds[] = $archive->getIdArchive(); + } + + $inIds = implode(', ', $aIds); + $sql = "SELECT value, name, idarchive, idsite + FROM $numericTable + WHERE idarchive IN ( $inIds ) + AND name IN ( $inName )"; + $values = Zend_Registry::get('db')->fetchAll($sql); + + $arrayValues = array(); + foreach($values as $value) + { + $arrayValues[$value['idsite']][$value['name']] = $value['value']; + } + + // we add empty tables so that every requested date has an entry, even if there is nothing + // example: <result idSite="159" /> + $contentArray = array(); + foreach($this->archives as $idSite => $archive) + { + $contentArray[$idSite]['table'] = new Piwik_DataTable_Simple(); + } + + foreach($arrayValues as $idSite => $aNameValues) + { + $contentArray[$idSite]['table']->loadFromArray($aNameValues); + } + ksort( $contentArray ); + + $tableArray = $this->getNewDataTableArray(); + foreach($contentArray as $idSite => $aData) + { + $tableArray->addTable($aData['table'], $idSite); + } + return $tableArray; + } +}
\ No newline at end of file diff --git a/core/Archive/Single.php b/core/Archive/Single.php new file mode 100644 index 0000000000..be4fbde7e4 --- /dev/null +++ b/core/Archive/Single.php @@ -0,0 +1,463 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Request.php 380 2008-03-17 14:59:24Z matt $ + * + * + * @package Piwik_Archive + */ + +/** + * This class is used to store the data of a single archive, + * for example the statistics for the 'day' '2008-02-21' for the website idSite '2' + * + */ +class Piwik_Archive_Single extends Piwik_Archive +{ + /** + * The Piwik_ArchiveProcessing object used to check that the archive is available + * and launch the processing if the archive was not yet processed + * + * @var Piwik_ArchiveProcessing + */ + public $archiveProcessing = null; + + /** + * @var bool Set to true if the archive has at least 1 visit + */ + public $isThereSomeVisits = false; + + /** + * Period of this Archive + * + * @var Piwik_Period + */ + protected $period = null; + + /** + * Set to true will activate numeric value caching for this archive. + * + * @var bool + */ + protected $cacheEnabledForNumeric = true; + + /** + * Array of cached numeric values, used to make requests faster + * when requesting the same value again and again + * + * @var array of numeric + */ + protected $numericCached = array(); + + /** + * Array of cached blob, used to make requests faster when requesting the same blob again and again + * + * @var array of mixed + */ + protected $blobCached = array(); + + /** + * idarchive of this Archive in the database + * + * @var int + */ + protected $idArchive = null; + + /** + * Flag set to true once the archive has been checked (when we make sure it is archived) + * + * @var bool + */ + protected $alreadyChecked = false; + + /** + * Returns the pretty date of this Archive, eg. 'Thursday 20th March 2008' + * + * @return string + */ + public function getPrettyDate() + { + return $this->period->getPrettyString(); + } + + /** + * Returns the idarchive of this Archive used to index this archive in the DB + * + * @return int + */ + public function getIdArchive() + { + if(is_null($this->idArchive)) + { + throw new Exception("idArchive is null"); + } + return $this->idArchive; + } + + /** + * Set the period + * + * @param Piwik_Period $period + */ + public function setPeriod( Piwik_Period $period ) + { + $this->period = $period; + } + + /** + * Returns the timestamp of the first date in the period for this Archive. + * This is used to sort archives by date when working on a Archive_Array + * + * @return int Unix timestamp + */ + public function getTimestampStartDate() + { + if(!is_null($this->archiveProcessing)) + { + return $this->archiveProcessing->getTimestampStartDate(); + } + + return $this->period->getDateStart()->getTimestamp(); + } + + /** + * Prepares the archive. Gets the idarchive from the ArchiveProcessing. + * + * This will possibly launch the archiving process if the archive was not available. + * + * @return void + */ + public function prepareArchive() + { + if(!$this->alreadyChecked) + { + $this->isThereSomeVisits = false; + $this->alreadyChecked = true; + + // if the END of the period is BEFORE the website creation date + // we already know there are no stats for this period + // we add one day to make sure we don't miss the day of the website creation + if( $this->period->getDateEnd()->addDay(2)->isEarlier( $this->site->getCreationDate() ) ) + { + return; + } + + // if the starting date is in the future we know there is no visit + if( $this->period->getDateStart()->subDay(1)->isLater( Piwik_Date::today() ) ) + { + return; + } + + // we make sure the archive is available for the given date + $periodLabel = $this->period->getLabel(); + $archiveProcessing = Piwik_ArchiveProcessing::factory($periodLabel); + $archiveProcessing->setSite($this->site); + $archiveProcessing->setPeriod($this->period); + + $idArchive = $archiveProcessing->loadArchive(); + $this->isThereSomeVisits = $archiveProcessing->isThereSomeVisits; + + $this->archiveProcessing = $archiveProcessing; + + $this->idArchive = $idArchive; + $this->alreadyChecked = true; + } + } + + /** + * Returns a value from the current archive with the name = $name + * Method used by getNumeric or getBlob + * + * @param string $name + * @param string $typeValue numeric|blob + * @return mixed|false if no result + */ + protected function get( $name, $typeValue = 'numeric' ) + { + // values previously "get" and now cached + if($typeValue == 'numeric' + && $this->cacheEnabledForNumeric + && isset($this->numericCached[$name]) + ) + { + return $this->numericCached[$name]; + } + + // During archiving we prefetch the blobs recursively + // and we get them faster from memory after + if($typeValue == 'blob' + && isset($this->blobCached[$name])) + { + return $this->blobCached[$name]; + } + + $this->prepareArchive(); + + if($name == 'idarchive') + { + return $this->idArchive; + } + +// Piwik::log("-- get '$name'"); + + if(!$this->isThereSomeVisits) + { + return false; + } + + // select the table to use depending on the type of the data requested + switch($typeValue) + { + case 'blob': + $table = $this->archiveProcessing->getTableArchiveBlobName(); + break; + + case 'numeric': + default: + $table = $this->archiveProcessing->getTableArchiveNumericName(); + break; + } + + // we select the requested value + $db = Zend_Registry::get('db'); + $value = $db->fetchOne("SELECT value + FROM $table + WHERE idarchive = ? + AND name = ?", + array( $this->idArchive , $name) + ); + + // no result, returns false + if($value === false) + { + if($typeValue == 'numeric' + && $this->cacheEnabledForNumeric) + { + // we cache the results + $this->numericCached[$name] = false; + } + return $value; + } + + // uncompress when selecting from the BLOB table + if($typeValue == 'blob') + { + $value = gzuncompress($value); + } + + if($typeValue == 'numeric' + && $this->cacheEnabledForNumeric) + { + // we cache the results + $this->numericCached[$name] = $value; + } + return $value; + } + + + /** + * This method loads in memory all the subtables for the main table called $name. + * You have to give it the parent table $dataTableToLoad so we can lookup the sub tables ids to load. + * + * If $addMetadataSubtableId set to true, it will add for each row a 'metadata' called 'databaseSubtableId' + * containing the child ID of the subtable associated to this row. + * + * @param string $name + * @param Piwik_DataTable $dataTableToLoad + * @param bool $addMetadataSubtableId + * + * @return void + */ + public function loadSubDataTables($name, Piwik_DataTable $dataTableToLoad, $addMetadataSubtableId = false) + { + // we have to recursively load all the subtables associated to this table's rows + // and update the subtableID so that it matches the newly instanciated table + foreach($dataTableToLoad->getRows() as $row) + { + $subTableID = $row->getIdSubDataTable(); + + if($subTableID !== null) + { + $subDataTableLoaded = $this->getDataTable($name, $subTableID); + + $this->loadSubDataTables($name, $subDataTableLoaded); + + // we edit the subtable ID so that it matches the newly table created in memory + // NB: + // we dont do that in the case we are displaying the table expanded. + // in this case we want the user to see the REAL dataId in the database + if($addMetadataSubtableId) + { + $row->addMetadata('databaseSubtableId', $row->getIdSubDataTable()); + } + $row->setSubtable( $subDataTableLoaded ); + } + } + } + + + /** + * Free the blob cache memory array + * + * @return void + */ + public function freeBlob( $name ) + { + // we delete the blob + $this->blobCached = null; + $this->blobCached = array(); + } + + /** + * Fetches all blob fields name_* at once for the current archive for performance reasons. + * + * @return void + */ + public function preFetchBlob( $name ) + { + if(!$this->isThereSomeVisits) + { + return false; + } + + $tableBlob = $this->archiveProcessing->getTableArchiveBlobName(); + + // we select the requested value + $db = Zend_Registry::get('db'); + $query = $db->query("SELECT value, name + FROM $tableBlob + WHERE idarchive = ? + AND name LIKE '$name%'", + array( $this->idArchive ) + ); + + while($row = $query->fetch()) + { + $value = $row['value']; + $name = $row['name']; + + $this->blobCached[$name] = gzuncompress($value); + } + } + + /** + * Returns a numeric value from this Archive, with the name '$name' + * + * @param string $name + * @return int|float + */ + public function getNumeric( $name ) + { + // we cast the result as float because returns false when no visitors + return (float)$this->get($name, 'numeric'); + } + + + /** + * Returns a blob value from this Archive, with the name '$name' + * Blob values are all values except int and float. + * + * @param string $name + * @return mixed + */ + public function getBlob( $name ) + { + return $this->get($name, 'blob'); + } + + /** + * Given a list of fields defining numeric values, it will return a Piwik_DataTable_Simple + * containing one row per field name. + * + * For example $fields = array( 'max_actions', + * 'nb_uniq_visitors', + * 'nb_visits', + * 'nb_actions', + * 'sum_visit_length', + * 'bounce_count', + * ); + * + * @param string|array $fields Name or array of names of Archive fields + * + * @return Piwik_DataTable_Simple + */ + public function getDataTableFromNumeric( $fields ) + { + require_once "DataTable/Simple.php"; + if(!is_array($fields)) + { + $fields = array($fields); + } + + $values = array(); + foreach($fields as $field) + { + $values[$field] = $this->getNumeric($field); + } + + $table = new Piwik_DataTable_Simple; + $table->loadFromArray($values); + return $table; + } + + /** + * Returns a DataTable that has the name '$name' from the current Archive. + * If $idSubTable is specified, returns the subDataTable called '$name_$idSubTable' + * + * @param string $name + * @param int $idSubTable optional id SubDataTable + * @return Piwik_DataTable + */ + public function getDataTable( $name, $idSubTable = null ) + { + if(!is_null($idSubTable)) + { + $name .= "_$idSubTable"; + } + + $data = $this->get($name, 'blob'); + + $table = new Piwik_DataTable; + + if($data !== false) + { + $table->loadFromSerialized($data); + } + + if($data === false + && $idSubTable !== null) + { + throw new Exception("You are requesting a precise subTable but there is not such data in the Archive."); + } + + return $table; + } + + /** + * Returns a DataTable that has the name '$name' from the current Archive. + * Also loads in memory all subDataTable for this DataTable. + * + * For example, if $name = 'Referers_keywordBySearchEngine' it will load all DataTable + * named 'Referers_keywordBySearchEngine_*' and they will be set as subDataTable to the + * rows. You can then go through the rows + * $rows = DataTable->getRows(); + * and for each row request the subDataTable (in this case the DataTable of the keywords for each search engines) + * $idSubTable = $row->getIdSubDataTable(); + * $subTable = Piwik_DataTable_Manager::getInstance()->getTable($idSubTable); + * + * @param string $name + * @param int $idSubTable Optional subDataTable to load instead of loading the parent DataTable + * @return Piwik_DataTable + */ + public function getDataTableExpanded($name, $idSubTable = null) + { + $this->preFetchBlob($name); + $dataTableToLoad = $this->getDataTable($name, $idSubTable); + $this->loadSubDataTables($name, $dataTableToLoad, $addMetadataSubtableId = true); + return $dataTableToLoad; + } +} +?>
\ No newline at end of file diff --git a/core/ArchiveProcessing.php b/core/ArchiveProcessing.php new file mode 100644 index 0000000000..b0e71a2114 --- /dev/null +++ b/core/ArchiveProcessing.php @@ -0,0 +1,570 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: ArchiveProcessing.php 536 2008-06-27 01:32:25Z matt $ + * + * @package Piwik_ArchiveProcessing + */ + +require_once 'TablePartitioning.php'; +require_once 'ArchiveProcessing/Record.php'; +require_once 'DataTable.php'; + +/** + * The ArchiveProcessing module is a module that reads the Piwik logs from the DB and + * compute all the reports, which are then stored in the database. + * + * The ArchiveProcessing class is used by the Archive object to make sure the given Archive is processed and available in the DB. + * + * A record in the Database for a given report is defined by + * - idarchive = unique ID that is associated to all the data of this archive (idsite+period+date) + * - idsite = the ID of the website + * - date1 = starting day of the period + * - date2 = ending day of the period + * - period = integer that defines the period (day/week/etc.). @see period::getId() + * - ts_archived = timestamp when the archive was processed + * - name = the name of the report (ex: uniq_visitors or search_keywords_by_search_engines) + * - value = the actual data + * + * @package Piwik_ArchiveProcessing + */ +abstract class Piwik_ArchiveProcessing +{ + /** + * Flag stored at the end of the archiving + * + * @var int + */ + const DONE_OK = 1; + + /** + * Flag stored at the start of the archiving + * When requesting an Archive, we make sure that non-finished archive are not considered valid + * + * @var int + */ + const DONE_ERROR = 2; + + /** + * Idarchive in the DB for the requested archive + * + * @var int + */ + protected $idArchive; + + /** + * Period id @see Piwik_Period::getId() + * + * @var int + */ + protected $periodId; + + /** + * Timestamp for the first date of the period + * + * @var int unix timestamp + */ + protected $timestampDateStart = null; + + /** + * Starting date of the archive + * + * @var Piwik_Date + */ + protected $dateStart; + + /** + * Ending date of the archive + * + * @var Piwik_Date + */ + protected $dateEnd; + + /** + * Object used to generate (depending on the $dateStart) the name of the DB table to use to store numeric values + * + * @var Piwik_TablePartitioning + */ + protected $tableArchiveNumeric; + + /** + * Object used to generate (depending on the $dateStart) the name of the DB table to use to store numeric values + * + * @var Piwik_TablePartitioning + */ + protected $tableArchiveBlob; + + /** + * Maximum timestamp above which a given archive is considered out of date + * + * @var int + */ + protected $maxTimestampArchive; + + /** + * Id of the current site + * Can be accessed by plugins (that is why it's public) + * + * @var int + */ + public $idsite = null; + + /** + * Period of the current archive + * Can be accessed by plugins (that is why it's public) + * + * @var Piwik_Period + */ + public $period = null; + + /** + * Site of the current archive + * Can be accessed by plugins (that is why it's public) + * + * @var Piwik_Site + */ + public $site = null; + + /** + * Starting date @see Piwik_Date::toString() + * + * @var string + */ + public $strDateStart; + + /** + * Ending date @see Piwik_Date::toString() + * + * @var string + */ + public $strDateEnd; + + /** + * Name of the DB table _log_visit + * + * @var string + */ + public $logTable; + + /** + * Name of the DB table _log_link_visit_action + * + * @var string + */ + public $logVisitActionTable; + + /** + * Name of the DB table _log_action + * + * @var string + */ + public $logActionTable; + + /** + * When set to true, we always archive, even if the archive is already available. + * You can change this settings automatically in the config/global.ini.php always_archive_data under the [Debug] section + * + * @var bool + */ + protected $debugAlwaysArchive = false; + + /** + * Builds the archive processing object, + * Reads some configuration value from the config file + * + */ + public function __construct() + { + $this->debugAlwaysArchive = Zend_Registry::get('config')->Debug->always_archive_data; + } + + /** + * Returns the Piwik_ArchiveProcessing_Day or Piwik_ArchiveProcessing_Period object + * depending on $name period string + * + * @param string $name day|week|month|year + * @return Piwik_ArchiveProcessing Piwik_ArchiveProcessing_Day|Piwik_ArchiveProcessing_Period + */ + static function factory($name ) + { + switch($name) + { + case 'day': + require_once 'ArchiveProcessing/Day.php'; + $process = new Piwik_ArchiveProcessing_Day; + break; + + case 'week': + case 'month': + case 'year': + require_once 'ArchiveProcessing/Period.php'; + $process = new Piwik_ArchiveProcessing_Period; + break; + + default: + throw new Exception("Unknown period specified $name"); + break; + } + return $process; + } + + /** + * Inits the object + * + * @return void + */ + protected function loadArchiveProperties() + { + $this->idsite = $this->site->getId(); + + $this->periodId = $this->period->getId(); + + $this->dateStart = $this->period->getDateStart(); + $this->dateEnd = $this->period->getDateEnd(); + + $this->tableArchiveNumeric = new Piwik_TablePartitioning_Monthly('archive_numeric'); + $this->tableArchiveNumeric->setTimestamp($this->dateStart->get()); + $this->tableArchiveBlob = new Piwik_TablePartitioning_Monthly('archive_blob'); + $this->tableArchiveBlob->setTimestamp($this->dateStart->get()); + + $this->strDateStart = $this->dateStart->toString(); + $this->strDateEnd = $this->dateEnd->toString(); + + // if the current archive is a DAY and if it's today, + // we set this maxTimestampArchive that defines the lifetime value of today's archive + $this->maxTimestampArchive = 0; + if( $this->period->getNumberOfSubperiods() == 0 + && $this->period->toString() == date("Y-m-d") + ) + { + //TODO this TIMESTAMP should be a mysql NOW()!!!! + $this->maxTimestampArchive = time() - Zend_Registry::get('config')->General->time_before_archive_considered_outdated; + } + // either + // - if the period we're looking for is finished, we look for a ts_archived that + // is greater than the last day of the archive + // - if the period we're looking for is not finished, we look for a recent enough archive + // recent enough means maxTimestampArchive = 00:00:01 this morning + else + { + if($this->period->isFinished()) + { + $this->maxTimestampArchive = $this->period->getDateEnd()->setTime('00:00:00')->addDay(1)->getTimestamp(); + } + else + { + $this->maxTimestampArchive = Piwik_Date::today()->getTimestamp(); + } + } + } + + /** + * This method returns the idArchive ; if necessary, it triggers the archiving process. + * + * If the archive was not processed yet, it will launch the archiving process. + * If the current archive needs sub-archives (eg. a month archive needs all the days archive) + * it will recursively launch the archiving (using this loadArchive() on the sub-periods) + * + * @return int The idarchive of the archive + */ + public function loadArchive() + { + $this->loadArchiveProperties(); + $this->idArchive = $this->isArchived(); + + if($this->idArchive === false + && $this->isArchivingDisabled()) + { + $this->isThereSomeVisits = false; + } + elseif($this->idArchive === false + || $this->debugAlwaysArchive) + { + $this->launchArchiving(); + } + else + { + $this->isThereSomeVisits = true; + } + + return $this->idArchive; + } + + /** + * @see loadArchive() + * + */ + protected function launchArchiving() + { + $this->initCompute(); + $this->compute(); + $this->postCompute(); + // we execute again the isArchived that does some initialization work + $this->idArchive = $this->isArchived(); + } + + /** + * This methods reads the subperiods if necessary, + * and computes the archive of the current period. + */ + abstract protected function compute(); + + /** + * Init the object before launching the real archive processing + * + * @return void + */ + protected function initCompute() + { + $this->loadNextIdarchive(); + + $record = new Piwik_ArchiveProcessing_Record_Numeric('done', Piwik_ArchiveProcessing::DONE_ERROR); + $this->insertRecord($record); + $record->delete(); + + $this->logTable = Piwik::prefixTable('log_visit'); + $this->logVisitActionTable = Piwik::prefixTable('log_link_visit_action'); + $this->logActionTable = Piwik::prefixTable('log_action'); + } + + /** + * Post processing called at the end of the main archive processing. + * Makes sure the new archive is marked as "successful" in the DB + * + * We also try to delete some stuff from memory but really there is still a lot... + * + * @return void + */ + protected function postCompute() + { + // delete the first done = ERROR + Zend_Registry::get('db')->query(" + DELETE FROM ".$this->tableArchiveNumeric->getTableName()." + WHERE idarchive = ? AND name = 'done'", + array($this->idArchive) + ); + + $record = new Piwik_ArchiveProcessing_Record_Numeric('done', Piwik_ArchiveProcessing::DONE_OK); + + // save in the database the records + $records = Piwik_ArchiveProcessing_Record_Manager::getInstance()->getRecords(); + + foreach($records as $record) + { + $this->insertRecord( $record); + } + + // delete the records from the global manager + foreach($records as $record) + { + $record->delete(); + } + unset($records); + + // we delete all tables from the table register + Piwik_ArchiveProcessing_Record_Manager::getInstance()->deleteAll(); + } + + /** + * Returns the name of the numeric table where the archive numeric values are stored + * + * @return string + */ + public function getTableArchiveNumericName() + { + return $this->tableArchiveNumeric->getTableName(); + } + + /** + * Returns the name of the blob table where the archive blob values are stored + * + * @return string + */ + public function getTableArchiveBlobName() + { + return $this->tableArchiveBlob->getTableName(); + } + + /** + * Set the period + * + * @param Piwik_Period $period + */ + public function setPeriod( Piwik_Period $period ) + { + $this->period = $period; + } + + /** + * Set the site + * + * @param Piwik_Site $site + */ + public function setSite( Piwik_Site $site ) + { + $this->site = $site; + } + + /** + * Returns the timestamp of the first date of the period + * + * @return int + */ + public function getTimestampStartDate() + { + // case when archive processing is in the past or the future, the starting date has not been set or processed yet + if(is_null($this->timestampDateStart)) + { + return Piwik_Date::factory($this->strDateStart)->getTimestamp(); + } + return $this->timestampDateStart; + } + + /** + * Returns the idArchive we will use for the current archive + * + * @return int IdArchive to use when saving the current Archive + */ + protected function loadNextIdarchive() + { + $db = Zend_Registry::get('db'); + $id = $db->fetchOne("SELECT max(idarchive) FROM ".$this->tableArchiveNumeric->getTableName()); + if(empty($id)) + { + $id = 0; + } + $this->idArchive = $id + 1; + + } + + /** + * Inserts a record in the right table (either NUMERIC or BLOB) + * + * @param Piwik_ArchiveProcessing_Record $record + */ + protected function insertRecord($record) + { + // table to use to save the data + if(Piwik::isNumeric($record->value)) + { + $table = $this->tableArchiveNumeric; + } + else + { + $table = $this->tableArchiveBlob; + } + + $query = "INSERT INTO ".$table->getTableName()." (idarchive, idsite, date1, date2, period, ts_archived, name, value) + VALUES (?,?,?,?,?,?,?,?)"; + Zend_Registry::get('db')->query($query, + array( $this->idArchive, + $this->idsite, + $this->strDateStart, + $this->strDateEnd, + $this->periodId, + date("Y-m-d H:i:s"), + $record->name, + $record->value, + ) + ); + } + + /** + * Returns the idArchive if the archive is available in the database. + * Returns false if the archive needs to be computed. + * + * An archive is available if + * - for today, the archive was computed less than maxTimestampArchive seconds ago + * - for any other day, if the archive was computed once this day was finished + * - for other periods, if the archive was computed once the period was finished + * + * @return int|false + */ + protected function isArchived() + { + $bindSQL = array( $this->idsite, + $this->strDateStart, + $this->strDateEnd, + $this->periodId, + ); + $timeStampWhere = " AND UNIX_TIMESTAMP(ts_archived) >= ? "; + $bindSQL[] = $this->maxTimestampArchive; + + $sqlQuery = " SELECT idarchive, value, name, UNIX_TIMESTAMP(date1) as timestamp + FROM ".$this->tableArchiveNumeric->getTableName()." + WHERE idsite = ? + AND date1 = ? + AND date2 = ? + AND period = ? + AND ( (name = 'done' AND value = ".Piwik_ArchiveProcessing::DONE_OK.") + OR name = 'nb_visits') + $timeStampWhere + ORDER BY ts_archived DESC"; + + $results = Zend_Registry::get('db')->fetchAll($sqlQuery, $bindSQL ); + if(empty($results)) + { + return false; + } + + $idarchive = false; + // we look for the more recent idarchive + foreach($results as $result) + { + if($result['name'] == 'done') + { + $idarchive = $result['idarchive']; + $this->timestampDateStart = $result['timestamp']; + break; + } + } + + // case when we have a nb_visits entry in the archive, but the process is not finished yet or failed to finish + // therefore we don't have the done=OK + if($idarchive === false) + { + return false; + } + + // we look for the nb_visits result for this more recent archive + foreach($results as $result) + { + if($result['name'] == 'nb_visits' + && $result['idarchive'] == $idarchive) + { + $this->isThereSomeVisits = ($result['value'] != 0); + break; + } + } + return $idarchive; + } + + /** + * Returns true if, for some reasons, triggering the archiving is disabled. + * + * @return bool + */ + protected function isArchivingDisabled() + { + static $archivingIsDisabled = null; + + if(is_null($archivingIsDisabled)) + { + $archivingIsDisabled = false; + + $enableBrowserArchivingTriggering = (bool)Zend_Registry::get('config')->General->enable_browser_archiving_triggering; + if($enableBrowserArchivingTriggering == false) + { + if( !Piwik::isPhpCliMode()) + { + $archivingIsDisabled = true; + } + } + } + + return $archivingIsDisabled; + } +} diff --git a/core/ArchiveProcessing/Day.php b/core/ArchiveProcessing/Day.php new file mode 100644 index 0000000000..e0433820ce --- /dev/null +++ b/core/ArchiveProcessing/Day.php @@ -0,0 +1,348 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Day.php 504 2008-06-01 20:19:28Z matt $ + * + * @package Piwik_ArchiveProcessing + */ + + +/** + * Handles the archiving process for a day. + * The class provides generic methods to manipulate data from the DB, easily create Piwik_DataTable objects. + * + * All the logic of the archiving is done inside the plugins listening to the event 'ArchiveProcessing_Day.compute' + * + * @package Piwik_ArchiveProcessing + * + */ +class Piwik_ArchiveProcessing_Day extends Piwik_ArchiveProcessing +{ + /** + * If the archive has at least 1 visit, this is set to true. + * + * @var bool + */ + public $isThereSomeVisits = false; + + /** + * Constructor + */ + function __construct() + { + parent::__construct(); + $this->db = Zend_Registry::get('db'); + } + + /** + * Main method to process logs for a day. The only logic done here is computing the number of visits, actions, etc. + * All the other reports are computed inside plugins listening to the event 'ArchiveProcessing_Day.compute'. + * See some of the plugins for an example eg. 'Provider' + * + * @return void + */ + protected function compute() + { + $query = "SELECT count(distinct visitor_idcookie) as nb_uniq_visitors, + count(*) as nb_visits, + sum(visit_total_actions) as nb_actions, + max(visit_total_actions) as max_actions, + sum(visit_total_time) as sum_visit_length, + sum(case visit_total_actions when 1 then 1 else 0 end) as bounce_count + FROM ".$this->logTable." + WHERE visit_server_date = ? + AND idsite = ? + GROUP BY visit_server_date + ORDER BY NULL + "; + $row = $this->db->fetchRow($query, array($this->strDateStart,$this->idsite ) ); + + if($row === false) + { + return; + } + $this->isThereSomeVisits = true; + + foreach($row as $name => $value) + { + $record = new Piwik_ArchiveProcessing_Record_Numeric($name, $value); + } + + Piwik_PostEvent('ArchiveProcessing_Day.compute', $this); + } + + /** + * Called at the end of the archiving process. + * Does some cleaning job in the database. + * + * @return void + */ + protected function postCompute() + { + parent::postCompute(); + + // we delete out of date records + // = archives that for day N computed on day N (means they are only partial) + $blobTable = $this->tableArchiveBlob->getTableName(); + $numericTable = $this->tableArchiveNumeric->getTableName(); + + $query = " DELETE + FROM %s + WHERE period = ? + AND date1 = DATE(ts_archived) + AND DATE(ts_archived) <> CURRENT_DATE() + "; + + Zend_Registry::get('db')->query(sprintf($query, $blobTable), $this->periodId); + Zend_Registry::get('db')->query(sprintf($query, $numericTable), $this->periodId); + } + + /** + * Helper function that returns a DataTable containing the $select fields / value pairs. + * IMPORTANT: The $select must return only one row!! + * + * Example $select = "count(distinct( config_os )) as countDistinctOs, + * sum( config_flash ) / count(distinct(idvisit)) as percentFlash " + * $labelCount = "test_column_name" + * will return a dataTable that looks like + * label test_column_name + * CountDistinctOs 9 + * PercentFlash 0.5676 + * + * + * @param string $select + * @param string $labelCount + * @return Piwik_DataTable + */ + public function getSimpleDataTableFromSelect($select, $labelCount) + { + $query = "SELECT $select + FROM ".$this->logTable." + WHERE visit_server_date = ? + AND idsite = ?"; + $data = $this->db->fetchRow($query, array( $this->strDateStart, $this->idsite )); + + foreach($data as $label => &$count) + { + $count = array($labelCount => $count); + } + $table = new Piwik_DataTable; + $table->loadFromArrayLabelIsKey($data); + return $table; + } + + /** + * Helper function that returns common statistics for a given database field distinct values. + * + * The statistics returned are: + * - number of unique visitors + * - number of visits + * - number of actions + * - maximum number of action for a visit + * - sum of the visits' length in sec + * - count of bouncing visits (visits with one page view) + * + * For example if $label = 'config_os' it will return the statistics for every distinct Operating systems + * The returned DataTable will have a row per distinct operating systems, + * and a column per stat (nb of visits, max actions, etc) + * + * label nb_uniq_visitors nb_visits nb_actions max_actions sum_visit_length bounce_count + * Linux 27 66 66 1 660 66 + * Windows XP 12 39 39 1 390 39 + * Mac OS 15 36 36 1 360 36 + * + * @param string $label Table log_visit field name to be use to compute common stats + * @return Piwik_DataTable + */ + public function getDataTableInterestForLabel( $label ) + { + $query = "SELECT $label as label, + count(distinct visitor_idcookie) as nb_uniq_visitors, + count(*) as nb_visits, + sum(visit_total_actions) as nb_actions, + max(visit_total_actions) as max_actions, + sum(visit_total_time) as sum_visit_length, + sum(case visit_total_actions when 1 then 1 else 0 end) as bounce_count + FROM ".$this->logTable." + WHERE visit_server_date = ? + AND idsite = ? + GROUP BY label"; + + $query = $this->db->query($query, array( $this->strDateStart, $this->idsite ) ); + + $interest = array(); + while($rowBefore = $query->fetch()) + { + $row = array( + Piwik_Archive::INDEX_NB_UNIQ_VISITORS => $rowBefore['nb_uniq_visitors'], + Piwik_Archive::INDEX_NB_VISITS => $rowBefore['nb_visits'], + Piwik_Archive::INDEX_NB_ACTIONS => $rowBefore['nb_actions'], + Piwik_Archive::INDEX_MAX_ACTIONS => $rowBefore['max_actions'], + Piwik_Archive::INDEX_SUM_VISIT_LENGTH => $rowBefore['sum_visit_length'], + Piwik_Archive::INDEX_BOUNCE_COUNT => $rowBefore['bounce_count'], + 'label' => $rowBefore['label'] + ); + + if(!isset($interest[$row['label']])) $interest[$row['label']]= $this->getNewInterestRow(); + $this->updateInterestStats( $row, $interest[$row['label']]); + } + + $table = new Piwik_DataTable; + $table->loadFromArrayLabelIsKey($interest); + return $table; + } + + /** + * Generates a dataTable given a multidimensional PHP array that associates LABELS to Piwik_DataTableRows + * This is used for the "Actions" DataTable, where a line is the aggregate of all the subtables + * Example: the category /blog has 3 visits because it has /blog/index (2 visits) + /blog/about (1 visit) + * + * @param array $table + * @return Piwik_DataTable + */ + static public function generateDataTable( $table ) + { + $dataTableToReturn = new Piwik_DataTable; + + foreach($table as $label => $maybeDatatableRow) + { + // case the aInfo is a subtable-like array + // it means that we have to go recursively and process it + // then we build the row that is an aggregate of all the children + // and we associate this row to the subtable + if( !($maybeDatatableRow instanceof Piwik_DataTable_Row) ) + { + $subTable = self::generateDataTable($maybeDatatableRow); + $row = new Piwik_DataTable_Row_DataTableSummary( $subTable ); + $row->addSubtable($subTable); + $row->setColumn('label', $label); + } + // if aInfo is a simple Row we build it + else + { + $row = $maybeDatatableRow; + } + + $dataTableToReturn->addRow($row); + } + + return $dataTableToReturn; + } + + /** + * Helper function that returns the serialized DataTable of the given PHP array. + * The array must have the format of Piwik_DataTable::loadFromArrayLabelIsKey() + * Example: array ( + * LABEL => array(col1 => X, col2 => Y), + * LABEL2 => array(col1 => X, col2 => Y), + * ) + * + * @param array $array at the given format + * @return array Array with one element: the serialized data table string + */ + public function getDataTableSerialized( $array ) + { + $table = new Piwik_DataTable; + $table->loadFromArrayLabelIsKey($array ); + $toReturn = $table->getSerialized(); + return $toReturn; + } + + + /** + * Helper function that returns the multiple serialized DataTable of the given PHP array. + * The DataTable here associates a subtable to every row of the level 0 array. + * This is used for example for search engines. Every search engine (level 0) has a subtable containing the + * keywords. + * + * The $arrayLevel0 must have the format + * Example: array ( + * LABEL => array(col1 => X, col2 => Y), + * LABEL2 => array(col1 => X, col2 => Y), + * ) + * + * The $subArrayLevel1ByKey must have the format + * Example: array( + * LABEL => #Piwik_DataTable_ForLABEL, + * LABEL2 => #Piwik_DataTable_ForLABEL2, + * ) + * + * + * @param array $arrayLevel0 + * @param array of Piwik_DataTable $subArrayLevel1ByKey + * @return array Array with N elements: the strings of the datatable serialized + */ + public function getDataTablesSerialized( $arrayLevel0, $subArrayLevel1ByKey, $maximumRowsInDataTableLevelZero = null, $maximumRowsInSubDataTable = null) + { + $tablesByLabel = array(); + + foreach($arrayLevel0 as $label => $aAllRowsForThisLabel) + { + $table = new Piwik_DataTable; + $table->loadFromArrayLabelIsKey($aAllRowsForThisLabel); + $tablesByLabel[$label] = $table; + } + $parentTableLevel0 = new Piwik_DataTable; + $parentTableLevel0->loadFromArrayLabelIsKey($subArrayLevel1ByKey, $tablesByLabel); + + $toReturn = $parentTableLevel0->getSerialized($maximumRowsInDataTableLevelZero, $maximumRowsInSubDataTable); + return $toReturn; + } + + /** + * Returns an empty row containing default values for the common stat + * + * @return array + */ + public function getNewInterestRow() + { + return array( Piwik_Archive::INDEX_NB_UNIQ_VISITORS => 0, + Piwik_Archive::INDEX_NB_VISITS => 0, + Piwik_Archive::INDEX_NB_ACTIONS => 0, + Piwik_Archive::INDEX_MAX_ACTIONS => 0, + Piwik_Archive::INDEX_SUM_VISIT_LENGTH => 0, + Piwik_Archive::INDEX_BOUNCE_COUNT => 0 + ); + } + + + /** + * Returns a Piwik_DataTable_Row containing default values for common stat, + * plus a column 'label' with the value $label + * + * @param string $label + * @return Piwik_DataTable_Row + */ + public function getNewInterestRowLabeled( $label ) + { + return new Piwik_DataTable_Row( + array( + Piwik_DataTable_Row::COLUMNS => array( 'label' => $label) + + $this->getNewInterestRow() + ) + ); + } + + /** + * Adds the given row $newRowToAdd to the existing $oldRowToUpdate passed by reference + * + * The rows are php arrays Name => value + * + * @param array $newRowToAdd + * @param array $oldRowToUpdate + */ + public function updateInterestStats( $newRowToAdd, &$oldRowToUpdate) + { + $oldRowToUpdate[Piwik_Archive::INDEX_NB_UNIQ_VISITORS] += $newRowToAdd[Piwik_Archive::INDEX_NB_UNIQ_VISITORS]; + $oldRowToUpdate[Piwik_Archive::INDEX_NB_VISITS] += $newRowToAdd[Piwik_Archive::INDEX_NB_VISITS]; + $oldRowToUpdate[Piwik_Archive::INDEX_NB_ACTIONS] += $newRowToAdd[Piwik_Archive::INDEX_NB_ACTIONS]; + $oldRowToUpdate[Piwik_Archive::INDEX_MAX_ACTIONS] = (float)max($newRowToAdd[Piwik_Archive::INDEX_MAX_ACTIONS], $oldRowToUpdate[Piwik_Archive::INDEX_MAX_ACTIONS]); + $oldRowToUpdate[Piwik_Archive::INDEX_SUM_VISIT_LENGTH] += $newRowToAdd[Piwik_Archive::INDEX_SUM_VISIT_LENGTH]; + $oldRowToUpdate[Piwik_Archive::INDEX_BOUNCE_COUNT] += $newRowToAdd[Piwik_Archive::INDEX_BOUNCE_COUNT]; + } +} + + diff --git a/core/ArchiveProcessing/Period.php b/core/ArchiveProcessing/Period.php new file mode 100644 index 0000000000..7e834bec62 --- /dev/null +++ b/core/ArchiveProcessing/Period.php @@ -0,0 +1,270 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Period.php 536 2008-06-27 01:32:25Z matt $ + * + * @package Piwik_ArchiveProcessing + */ + +/** + * Handles the archiving process for a period + * + * This class provides generic methods to archive data for a period (week / month / year). + * + * These methods are called by the plugins that do the logic of archiving their own data. \ + * They hook on the event 'ArchiveProcessing_Period.compute' + * + * @package Piwik_ArchiveProcessing + */ +class Piwik_ArchiveProcessing_Period extends Piwik_ArchiveProcessing +{ + /** + * Sums all values for the given field names $aNames over the period + * See @archiveNumericValuesGeneral for more information + * + * @param string|array + * @return Piwik_ArchiveProcessing_Record_Numeric + * + */ + public function archiveNumericValuesSum( $aNames ) + { + return $this->archiveNumericValuesGeneral($aNames, 'sum'); + } + + /** + * Get the maximum value for all values for the given field names $aNames over the period + * See @archiveNumericValuesGeneral for more information + * + * @param string|array + * @return Piwik_ArchiveProcessing_Record_Numeric + * + */ + public function archiveNumericValuesMax( $aNames ) + { + return $this->archiveNumericValuesGeneral($aNames, 'max'); + } + + /** + * Given a list of fields names, the method will fetch all their values over the period, and archive them using the given operation. + * + * For example if $operationToApply = 'sum' and $aNames = array('nb_visits', 'sum_time_visit') + * it will sum all values of nb_visits for the period (for example give the number of visits for the month by summing the visits of every day) + * + * @param array|string $aNames Array of strings or string containg the field names to select + * @param string $operationToApply Available operations = sum, max, min + * @return Piwik_ArchiveProcessing_Record_Numeric Returns the record if $aNames is a string, + * an array of Piwik_ArchiveProcessing_Record_Numeric indexed by their field names if aNames is an array of strings + */ + private function archiveNumericValuesGeneral($aNames, $operationToApply) + { + if(!is_array($aNames)) + { + $aNames = array($aNames); + } + + // fetch the numeric values and apply the operation on them + $results = array(); + foreach($this->archives as $archive) + { + foreach($aNames as $name) + { + if(!isset($results[$name])) + { + $results[$name] = 0; + } + $valueToSum = $archive->getNumeric($name); + + if($valueToSum !== false) + { + switch ($operationToApply) { + case 'sum': + $results[$name] += $valueToSum; + break; + case 'max': + $results[$name] = max($results[$name], $valueToSum); + break; + case 'min': + $results[$name] = min($results[$name], $valueToSum); + break; + default: + throw new Exception("Operation not applicable."); + break; + } + } + } + } + + // build the Record Numeric objects + $records = array(); + foreach($results as $name => $value) + { + $records[$name] = new Piwik_ArchiveProcessing_Record_Numeric( + $name, + $value + ); + } + + // if asked for only one field to sum + if(count($records) == 1) + { + return $records[$name]; + } + + // returns the array of records once summed + return $records; + } + + + /** + * This powerful method will compute the sum of DataTables over the period for the given fields $aRecordName. + * The resulting DataTable will be then added to queue of data to be recorded in the database. + * It will usually be called in a plugin that listens to the hook 'ArchiveProcessing_Period.compute' + * + * For example if $aRecordName = 'UserCountry_country' the method will select all UserCountry_country DataTable for the period + * (eg. the 31 dataTable of the last month), sum them, and create the Piwik_ArchiveProcessing_Record_BlobArray so that + * the resulting dataTable is AUTOMATICALLY recorded in the database. + * + * + * This method works on recursive dataTable. For example for the 'Actions' it will select all subtables of all dataTable of all the sub periods + * and get the sum. + * + * It returns an array that gives information about the "final" DataTable. The array gives for every field name, the number of rows in the + * final DataTable (ie. the number of distinct LABEL over the period) (eg. the number of distinct keywords over the last month) + * + * @param string|array Field name(s) of DataTable to select so we can get the sum + * @return array array ( + * nameTable1 => number of rows, + * nameTable2 => number of rows, + * ) + */ + public function archiveDataTable( $aRecordName, $maximumRowsInDataTableLevelZero = null, $maximumRowsInSubDataTable = null ) + { + if(!is_array($aRecordName)) + { + $aRecordName = array($aRecordName); + } + + $nameToCount = array(); + foreach($aRecordName as $recordName) + { + $table = $this->getRecordDataTableSum($recordName); + + $nameToCount[$recordName]['level0'] = $table->getRowsCount(); + $nameToCount[$recordName]['recursive'] = $table->getRowsCountRecursive(); + + $record = new Piwik_ArchiveProcessing_Record_BlobArray($recordName, $table->getSerialized( $maximumRowsInDataTableLevelZero, $maximumRowsInSubDataTable )); + } + return $nameToCount; + } + + /** + * This method selects all DataTables that have the name $name over the period. + * It calls the appropriate methods that sum all these tables together. + * The resulting DataTable is returned. + * + * @param string $name + * @return Piwik_DataTable + */ + protected function getRecordDataTableSum( $name ) + { + $table = new Piwik_DataTable; + foreach($this->archives as $archive) + { + $archive->preFetchBlob($name); + $datatableToSum = $archive->getDataTable($name); + $archive->loadSubDataTables($name, $datatableToSum); + $table->addDataTable($datatableToSum); + $archive->freeBlob($name); + } + return $table; + } + + protected function initCompute() + { + parent::initCompute(); + $this->archives = $this->loadSubperiodsArchive(); + } + + /** + * Returns the ID of the archived subperiods. + * + * @return array Array of the idArchive of the subperiods + */ + protected function loadSubperiodsArchive() + { + $periods = array(); + + // we first compute every subperiod of the archive + foreach($this->period->getSubperiods() as $period) + { + $archivePeriod = new Piwik_Archive_Single; + $archivePeriod->setSite( $this->site ); + $archivePeriod->setPeriod( $period ); + $archivePeriod->prepareArchive(); + + $periods[] = $archivePeriod; + } + return $periods; + } + + /** + * Main method to process logs for a period. + * The only logic done here is computing the number of visits, actions, etc. + * + * All the other reports are computed inside plugins listening to the event 'ArchiveProcessing_Period.compute'. + * See some of the plugins for an example. + * + * @return void + */ + protected function compute() + { + $this->archiveNumericValuesMax( 'max_actions' ); + $toSum = array( + 'nb_uniq_visitors', + 'nb_visits', + 'nb_actions', + 'sum_visit_length', + 'bounce_count', + ); + $record = $this->archiveNumericValuesSum($toSum); + + $this->isThereSomeVisits = ($record['nb_visits']->value != 0); + if($this->isThereSomeVisits === false) + { + return; + } + + Piwik_PostEvent('ArchiveProcessing_Period.compute', $this); + } + + /** + * Called at the end of the archiving process. + * Does some cleaning job in the database. + * + * @return void + */ + protected function postCompute() + { + parent::postCompute(); + + // we delete records that are now out of date + // in the case of a period we delete archives that were archived before the end of the period + // and only if they are at least 1 day old (so we don't delete archives computed today that may be stil valid) + $blobTable = $this->tableArchiveBlob->getTableName(); + $numericTable = $this->tableArchiveNumeric->getTableName(); + + $query = " DELETE + FROM %s + WHERE period > ? + AND DATE(ts_archived) <= date2 + AND date(ts_archived) < date_sub(CURRENT_DATE(), INTERVAL 1 DAY) + "; + + Zend_Registry::get('db')->query(sprintf($query, $blobTable), Piwik::$idPeriods['day']); + Zend_Registry::get('db')->query(sprintf($query, $numericTable), Piwik::$idPeriods['day']); + } + +} diff --git a/core/ArchiveProcessing/Record.php b/core/ArchiveProcessing/Record.php new file mode 100644 index 0000000000..04a402bf27 --- /dev/null +++ b/core/ArchiveProcessing/Record.php @@ -0,0 +1,55 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Record.php 444 2008-04-11 13:38:22Z johmathe $ + * + * @package Piwik_ArchiveProcessing + */ + +require_once "ArchiveProcessing/Record/Blob.php"; +require_once "ArchiveProcessing/Record/BlobArray.php"; +require_once "ArchiveProcessing/Record/Numeric.php"; +require_once "ArchiveProcessing/Record/Manager.php"; + + +/** + * A Record is a tuple (name, value) to be saved in the database. + * At its creation, the record registers itself to the RecordManager. + * The record will then be automatically saved in the DB once the Archiving process is finished. + * + * We have two record types available: + * - numeric ; the value will be saved as float in the DB. + * It should be used for INTEGER, FLOAT + * - blob ; the value will be saved in a binary field in the DB + * It should be used for all the other types: PHP variables, STRING, serialized OBJECTS or ARRAYS, etc. + * + * @package Piwik_ArchiveProcessing + * @subpackage Piwik_ArchiveProcessing_Record + */ +abstract class Piwik_ArchiveProcessing_Record +{ + public $name; + public $value; + + function __construct( $name, $value) + { + $this->name = $name; + $this->value = $value; + Piwik_ArchiveProcessing_Record_Manager::getInstance()->registerRecord($this); + } + + public function delete() + { + Piwik_ArchiveProcessing_Record_Manager::getInstance()->unregister($this); + } + + public function __destruct() + { + } +} + + + diff --git a/core/ArchiveProcessing/Record/Blob.php b/core/ArchiveProcessing/Record/Blob.php new file mode 100644 index 0000000000..fd805c31d5 --- /dev/null +++ b/core/ArchiveProcessing/Record/Blob.php @@ -0,0 +1,33 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Record.php 180 2008-01-17 16:32:37Z matt $ + * + * @package Piwik_ArchiveProcessing + */ + +/** + * Blob record. + * Example: $record = new Piwik_ArchiveProcessing_Record_Blob('visitor_names', serialize(array('piwik-fan', 'php', 'stevie-vibes'))); + * The value will be compressed before being saved in the DB. + * + * @package Piwik_ArchiveProcessing + * @subpackage Piwik_ArchiveProcessing_Record + */ +class Piwik_ArchiveProcessing_Record_Blob extends Piwik_ArchiveProcessing_Record +{ + public $name; + public $value; + function __construct( $name, $value) + { + $value = gzcompress($value); + parent::__construct( $name, $value ); + } + public function __toString() + { + return $this->name ." = BLOB";//". gzuncompress($this->value); + } +} diff --git a/core/ArchiveProcessing/Record/BlobArray.php b/core/ArchiveProcessing/Record/BlobArray.php new file mode 100644 index 0000000000..4e3165489e --- /dev/null +++ b/core/ArchiveProcessing/Record/BlobArray.php @@ -0,0 +1,63 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Record.php 180 2008-01-17 16:32:37Z matt $ + * + * @package Piwik_ArchiveProcessing + */ + +/** + * Array of blob records. + * Useful for easily saving splited data in the DB. + * + * Example: $record = new Piwik_ArchiveProcessing_Record_BlobArray( + * 'veryLongBook', + * 0 => serialize( array( '1st chapter very long, 6MB of data we dont want to save' )), + * 1 => serialize( array( '2nd chapter very long, 8MB of data we dont want to save' )), + * 2 => serialize( array( '3rd chapter very long, 7MB of data we dont want to save' )), + * 3 => serialize( array( '4th chapter very long, 10MB of data we dont want to save' )), + * ); + * + * Will be saved in the DB as + * veryLongBook => X + * veryLongBook_1 => Y + * veryLongBook_2 => Z + * veryLongBook_3 => M + * + * @package Piwik_ArchiveProcessing + * @subpackage Piwik_ArchiveProcessing_Record + */ +class Piwik_ArchiveProcessing_Record_BlobArray extends Piwik_ArchiveProcessing_Record +{ + + function __construct( $name, $aValue) + { + foreach($aValue as $id => $value) + { + // for the parent Table we keep the name + // for example for the Table of searchEngines we keep the name 'referer_search_engine' + // but for the child table of 'Google' which has the ID = 9 the name would be 'referer_search_engine_9' + if($id == 0) + { + $newName = $name; + } + else + { + $newName = $name . '_' . $id; + } + $record = new Piwik_ArchiveProcessing_Record_Blob( $newName, $value ); + + } + } + public function __toString() + { + throw new Exception( 'Not valid' ); + } + public function delete() + { + throw new Exception( 'Not valid' ); + } +} diff --git a/core/ArchiveProcessing/Record/Manager.php b/core/ArchiveProcessing/Record/Manager.php new file mode 100644 index 0000000000..1520492273 --- /dev/null +++ b/core/ArchiveProcessing/Record/Manager.php @@ -0,0 +1,109 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Record.php 180 2008-01-17 16:32:37Z matt $ + * + * @package Piwik_ArchiveProcessing + */ + +/** + * Every new Piwik_ArchiveProcessing_Record will be recorded to this manager when created. + * At the end of the archiving process, the ArchiveProcessing will getRecords() to save them in the db. + * This class is singleton. + * + * @package Piwik_ArchiveProcessing + * @subpackage Piwik_ArchiveProcessing_Record + */ +class Piwik_ArchiveProcessing_Record_Manager +{ + // array of Piwik_ArchiveProcessing_Record to be recorded in the DB + protected $records = array(); + + static private $instance = null; + protected function __construct() + {} + + /** + * Singleton, returns instance + * + * @return Piwik_ArchiveProcessing_Record_Manager + */ + static public function getInstance() + { + if (self::$instance == null) + { + $c = __CLASS__; + self::$instance = new $c(); + } + return self::$instance; + } + + /** + * Method called by Record objects to register themselves. + * All records registered here will be saved in the DB at the end of the archiving process. + * @return void + */ + public function registerRecord( $record ) + { + $this->records[$record->name] = $record; + } + + /** + * Removes a record from the Record Manager. + * + * @return void + */ + public function unregister( $deleteRecord ) + { + unset($this->records[$deleteRecord->name]); + } + + /** + * Returns a string containing the "name : value" of the record + * @return string + */ + public function toString() + { + $str = ''; + foreach($this->records as $record) + { + $str .= $record . "<br>\n"; + } + return $str; + } + + /** + * @return string + */ + public function __toString() + { + return $this->toString(); + } + + /** + * Returns the list of all the records that have to created in the database. + * + * @return array of Records + */ + public function getRecords() + { + return $this->records; + } + + /** + * Delete all records saved in the Manager. + * @return void + */ + public function deleteAll() + { + foreach($this->records as $key => $record) + { + unset($this->records[$key]); + } + $this->records = array(); + } +} + diff --git a/core/ArchiveProcessing/Record/Numeric.php b/core/ArchiveProcessing/Record/Numeric.php new file mode 100644 index 0000000000..f7f206e14e --- /dev/null +++ b/core/ArchiveProcessing/Record/Numeric.php @@ -0,0 +1,30 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Record.php 180 2008-01-17 16:32:37Z matt $ + * + * @package Piwik_ArchiveProcessing + */ + +/** + * Numeric record. + * Example: $record = new Piwik_ArchiveProcessing_Record_Numeric('nb_visitors_live', 15); + * + * @package Piwik_ArchiveProcessing + * @subpackage Piwik_ArchiveProcessing_Record + */ +class Piwik_ArchiveProcessing_Record_Numeric extends Piwik_ArchiveProcessing_Record +{ + function __construct( $name, $value) + { + parent::__construct( $name, $value ); + } + + public function __toString() + { + return $this->name ." = ". $this->value; + } +} diff --git a/core/Auth.php b/core/Auth.php new file mode 100644 index 0000000000..b3d99d5d26 --- /dev/null +++ b/core/Auth.php @@ -0,0 +1,52 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Auth.php 581 2008-07-27 23:07:52Z matt $ + * + * @package Piwik + */ + +interface Piwik_Auth { + /** + * @return Piwik_Auth_Result + */ + public function authenticate(); +} + +/** + * + * @package Piwik + */ +class Piwik_Auth_Result extends Zend_Auth_Result +{ + /** + * token_auth parameter used to authenticate in the API + * + * @var string + */ + protected $_token_auth = null; + + const SUCCESS_SUPERUSER_AUTH_CODE = 42; + + public function __construct($code, $login, $token_auth, array $messages = array()) + { + // Piwik_Auth_Result::SUCCESS_SUPERUSER_AUTH_CODE, Piwik_Auth_Result::SUCCESS, Piwik_Auth_Result::FAILURE + $this->_code = (int)$code; + $this->_identity = $login; + $this->_messages = $messages; + $this->_token_auth = $token_auth; + } + + /** + * Returns the token_auth to authenticate the current user in the API + * + * @return string + */ + public function getTokenAuth() + { + return $this->_token_auth; + } +} diff --git a/core/Common.php b/core/Common.php new file mode 100644 index 0000000000..f02c7abde0 --- /dev/null +++ b/core/Common.php @@ -0,0 +1,643 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Common.php 581 2008-07-27 23:07:52Z matt $ + * + * @package Piwik_Helper + */ + +/** + * Static class providing functions used by both the CORE of Piwik and the visitor logging engine. + * + * This is the only external class loaded by the /piwik.php file. + * This class should contain only the functions that are used in + * both the CORE and the piwik.php statistics logging engine. + * + * @package Piwik_Helper + */ +class Piwik_Common +{ + /** + * Const used to map the referer type to an integer in the log_visit table + * + */ + const REFERER_TYPE_DIRECT_ENTRY = 1; + const REFERER_TYPE_SEARCH_ENGINE = 2; + const REFERER_TYPE_WEBSITE = 3; + const REFERER_TYPE_PARTNER = 4; + const REFERER_TYPE_NEWSLETTER = 5; + const REFERER_TYPE_CAMPAIGN = 6; + + /** + * Flag used with htmlspecialchar + * See php.net/htmlspecialchars + * + */ + const HTML_ENCODING_QUOTE_STYLE = ENT_COMPAT; + + + /** + * Returns the path and query part from a URL. + * Eg. http://piwik.org/test/index.php?module=CoreHome will return /test/index.php?module=CoreHome + * + * @param string $url either http://piwik.org/test or / + * @return string + */ + static function getPathAndQueryFromUrl($url) + { + $parsedUrl = parse_url( $url ); + + $result = ''; + + if(isset($parsedUrl['path'])) + { + $result .= substr($parsedUrl['path'], 1); + } + + if(isset($parsedUrl['query'])) + { + $result .= '?'.$parsedUrl['query']; + } + + return $result; + } + + /** + * Returns the value of a GET parameter $parameter in an URL query $urlQuery + * + * @param string $urlQuery result of parse_url()['query'] and htmlentitied (& is &) eg. module=test&action=toto or ?page=test + * @param string $param + * + * @return string|bool Parameter value if found (can be the empty string!), false if not found + */ + static public function getParameterFromQueryString( $urlQuery, $parameter) + { + $nameToValue = self::getArrayFromQueryString($urlQuery); + + if(isset($nameToValue[$parameter])) + { + return $nameToValue[$parameter]; + } + return false; + } + + /** + * Returns an URL query string in an array format + * The input query string should be htmlspecialchar'ed + * + * @param string urlQuery + * @return array array( param1=> value1, param2=>value2) + */ + static public function getArrayFromQueryString( $urlQuery ) + { + if(strlen($urlQuery) == 0) + { + return array(); + } + if($urlQuery[0] == '?') + { + $urlQuery = substr($urlQuery, 1); + } + + $separator = '&'; + + $urlQuery = $separator . $urlQuery; + // $urlQuery = str_replace(array('%20'), ' ', $urlQuery); + $refererQuery = trim($urlQuery); + + $values = explode($separator, $refererQuery); + + $nameToValue = array(); + + foreach($values as $value) + { + if( false !== strpos($value, '=')) + { + $exploded = explode('=',$value); + $nameToValue[$exploded[0]] = $exploded[1]; + } + } + return $nameToValue; + } + + /** + * Returns true if the string is a valid filename + * File names that start with a-Z or 0-9 and contain a-Z, 0-9, underscore(_), dash(-), and dot(.) will be accepted. + * File names beginning with anything but a-Z or 0-9 will be rejected (including .htaccess for example). + * File names containing anything other than above mentioned will also be rejected (file names with spaces won't be accepted). + * + * @param string filename + * @return bool + * + */ + static public function isValidFilename($filename) + { + return (false !== ereg("(^[a-zA-Z0-9]+([a-zA-Z\_0-9\.-]*))$" , $filename)); + } + /** + * Returns true if the string passed may be a URL. + * We don't need a precise test here because the value comes from the website + * tracked source code and the URLs may look very strange. + * + * @param string $url + * @return bool + */ + static function isLookLikeUrl( $url ) + { + return ereg('^(ftp|news|http|https)?://[A-Za-z0-9\/_.-?&]*', $url); + } + + /** + * Returns the variable after cleaning operations. + * NB: The variable still has to be escaped before going into a SQL Query! + * + * If an array is passed the cleaning is done recursively on all the sub-arrays. \ + * The keys of the array are filtered as well! + * + * How this method works: + * - The variable returned has been htmlspecialchars to avoid the XSS security problem. + * - The single quotes are not protected so "Piwik's amazing" will still be "Piwik's amazing". + * + * - Transformations are: + * - '&' (ampersand) becomes '&' + * - '"'(double quote) becomes '"' + * - '<' (less than) becomes '<' + * - '>' (greater than) becomes '>' + * - It handles the magic_quotes setting. + * - A non string value is returned without modification + * + * @param mixed The variable to be cleaned + * @return mixed The variable after cleaning + */ + static public function sanitizeInputValues($value) + { + if(is_numeric($value)) + { + return $value; + } + elseif(is_string($value)) + { + $value = htmlspecialchars($value, Piwik_Common::HTML_ENCODING_QUOTE_STYLE, 'UTF-8'); + + // Undo the damage caused by magic_quotes -- only before php 5.3 as it is now deprecated + if ( version_compare(phpversion(), '5.3') === -1 + && get_magic_quotes_gpc()) + { + $value = stripslashes($value); + } + } + elseif (is_array($value)) + { + foreach (array_keys($value) as $key) + { + $newKey = $key; + $newKey = Piwik_Common::sanitizeInputValues($newKey); + if ($key != $newKey) + { + $value[$newKey] = $value[$key]; + unset($value[$key]); + } + + $value[$newKey] = Piwik_Common::sanitizeInputValues($value[$newKey]); + } + } + elseif( !is_null($value) + && !is_bool($value) + ) + { + throw new Exception("The value to escape has not a supported type. Value = ".var_export($value, true)); + } + return $value; + } + + /** + * Returns a variable from the $_REQUEST superglobal. + * If the variable doesn't have a value or an empty value, returns the defaultValue if specified. + * If the variable doesn't have neither a value nor a default value provided, an exception is raised. + * + * @param string $varName name of the variable + * @param string $varDefault default value. If '', and if the type doesn't match, exit() ! + * @param string $varType Expected type, the value must be one of the following: array, numeric, int, integer, string + * + * @exception if the variable type is not known + * @exception if the variable we want to read doesn't have neither a value nor a default value specified + * + * @return mixed The variable after cleaning + */ + static public function getRequestVar($varName, $varDefault = null, $varType = null, $requestArrayToUse = null) + { + if(is_null($requestArrayToUse)) + { + $requestArrayToUse = $_REQUEST; + } + + $varDefault = self::sanitizeInputValues( $varDefault ); + + if($varType == 'int') + { + // settype accepts only integer + // 'int' is simply a shortcut for 'integer' + $varType = 'integer'; + } + + // there is no value $varName in the REQUEST so we try to use the default value + if(empty($varName) + || !isset($requestArrayToUse[$varName]) + || ( !is_array($requestArrayToUse[$varName]) + && strlen($requestArrayToUse[$varName]) === 0 + ) + ) + { + if( is_null($varDefault)) + { + throw new Exception("\$varName '$varName' doesn't have value in \$_REQUEST and doesn't have a" . + " \$varDefault value"); + } + else + { + if( !is_null($varType) + && in_array($varType, array('string', 'integer', 'array')) + ) + { + settype($varDefault, $varType); + } + return $varDefault; + } + } + + // Normal case, there is a value available in REQUEST for the requested varName + $value = self::sanitizeInputValues( $requestArrayToUse[$varName] ); + + if( !is_null($varType)) + { + $ok = false; + + if($varType == 'string') + { + if(is_string($value)) $ok = true; + } + elseif($varType == 'numeric') + { + if(is_numeric($value) || $value==(int)$value || $value==(float)$value) $ok = true; + } + elseif($varType == 'integer') + { + if(is_int($value) || $value==(int)$value) $ok = true; + } + elseif($varType == 'float') + { + if(is_float($value) || $value==(float)$value) $ok = true; + } + elseif($varType == 'array') + { + if(is_array($value)) $ok = true; + } + else + { + throw new Exception("\$varType specified is not known. It should be one of the following: array, numeric, int, integer, float, string"); + } + + // The type is not correct + if($ok === false) + { + if($varDefault === null) + { + throw new Exception("\$varName '$varName' doesn't have a correct type in \$_REQUEST and doesn't " . + "have a \$varDefault value"); + } + // we return the default value with the good type set + else + { + settype($varDefault, $varType); + return $varDefault; + } + } + } + + return $value; + } + + /** + * Returns a 32 characters long uniq ID + * + * @return string 32 chars + */ + static public function generateUniqId() + { + return md5(uniqid(rand(), true)); + } + + /** + * Returns a 3 letters ID for the operating system part, given a user agent string. + * @see core/DataFiles/OS.php for the list of OS (also available in $GLOBALS['Piwik_Oslist']) + * If the OS cannot be identified in the user agent, returns 'UNK' + * + * @param string $userAgent + * + * @return string + */ + static public function getOs($userAgent) + { + + require_once "core/DataFiles/OS.php"; + $osNameToId = $GLOBALS['Piwik_Oslist']; + + foreach($osNameToId as $key => $value) + { + if ($ok = ereg($key, $userAgent)) + { + return $value; + } + } + return 'UNK'; + } + + /** + * Returns the browser information from the user agent string. + * + * @see core/DataFiles/Browsers.php for the list of OS (also available in $GLOBALS['Piwik_BrowserList']) + * + * @param string $userAgent + * @return array array( 'name' => '', // 2 letters ID or 'UNK' for an unknown browser + * 'major_number' => '', // 2 in firefox 2.0.12 + * 'minor_number' => '', // 0 in firefox 2.0.12 + * 'version' => '' // major_number.minor_number + * ); + */ + static public function getBrowserInfo($userAgent) + { + + require_once "core/DataFiles/Browsers.php"; + + $browsers = $GLOBALS['Piwik_BrowserList']; + + $info = array( + 'name' => 'UNK', + 'major_number' => '', + 'minor_number' => '', + 'version' => '' + ); + + $browser = ''; + foreach($browsers as $key => $value) + { + if(!empty($browser)) $browser .= "|"; + $browser .= $key; + } + + $results = array(); + + // added fix for Mozilla Suite detection + if ((preg_match_all("/(mozilla)[\/\sa-z;.0-9-(]+rv:([0-9]+)([.0-9a-z]+)\) gecko\/[0-9]{8}$/i", $userAgent, $results)) + || (preg_match_all("/($browser)[\/\sa-z(]*([0-9]+)([\.0-9a-z]+)?/i", $userAgent, $results)) + ) + { + $count = count($results[0])-1; + + // browser code + $info['name'] = $browsers[strtolower($results[1][$count])]; + + // majeur version number (7 in mozilla 1.7 + $info['major_number'] = $results[2][$count]; + + // is an minor version number ? If not, 0 + $match = array(); + + preg_match('/([.\0-9]+)?([\.a-z0-9]+)?/i', $results[3][$count], $match); + + if(isset($match[1])) + { + // find minor version number (7 in mozilla 1.7, 9 in firefox 0.9.3) + $info['minor_number'] = substr($match[1], 0, 2); + } + else + { + $info['minor_number'] = '.0'; + } + + $info['version'] = $info['major_number'] . $info['minor_number']; + } + return $info; + } + + + /** + * Returns the best possible IP of the current user, in the format A.B.C.D + * + * @return string ip + */ + static public function getIp() + { + if(isset($_SERVER['HTTP_CLIENT_IP']) + && ($ip = Piwik_Common::getFirstIpFromList($_SERVER['HTTP_CLIENT_IP'])) + && strpos($ip, "unknown") === false) + { + return $ip; + } + elseif(isset($_SERVER['HTTP_X_FORWARDED_FOR']) + && $ip = Piwik_Common::getFirstIpFromList($_SERVER['HTTP_X_FORWARDED_FOR']) + && isset($ip) + && !empty($ip) + && strpos($ip, "unknown")===false ) + { + return $ip; + } + elseif( isset($_SERVER['HTTP_CLIENT_IP']) + && strlen( Piwik_Common::getFirstIpFromList($_SERVER['HTTP_CLIENT_IP']) ) != 0 ) + { + return Piwik_Common::getFirstIpFromList($_SERVER['HTTP_CLIENT_IP']); + } + else if( isset($_SERVER['HTTP_X_FORWARDED_FOR']) + && strlen ($ip = Piwik_Common::getFirstIpFromList($_SERVER['HTTP_X_FORWARDED_FOR'])) != 0) + { + return $ip; + } + elseif(isset($_SERVER['REMOTE_ADDR'])) + { + return Piwik_Common::getFirstIpFromList($_SERVER['REMOTE_ADDR']); + } + else + { + return '0.0.0.0'; + } + } + + + /** + * Returns the first element of a comma separated list of IPs + * + * @param string $ip + * + * @return string first element before ',' + */ + static private function getFirstIpFromList($ip) + { + $p = strpos($ip, ','); + if($p!==false) + { + return trim(Piwik_Common::sanitizeInputValues(substr($ip, 0, $p))); + } + return trim(Piwik_Common::sanitizeInputValues($ip)); + } + + + /** + * Returns the continent of a given country + * + * @param string Country 2 letters isocode + * + * @return string Continent (3 letters code : afr, asi, eur, amn, ams, oce) + */ + static public function getContinent($country) + { + require_once "core/DataFiles/Countries.php"; + + $countryList = $GLOBALS['Piwik_CountryList']; + + if(isset($countryList[$country][0])) + { + return $countryList[$country][0]; + } + else + { + return 'unk'; + } + } + + /** + * Returns the visitor country based only on the Browser 'accepted language' information + * + * @param string $lang browser lang + * + * @return string 2 letters ISO code + */ + static public function getCountry( $lang ) + { + require_once "core/DataFiles/Countries.php"; + + $countryList = $GLOBALS['Piwik_CountryList']; + + $replaceLangCodeByCountryCode = array( + // replace cs language (Serbia Montenegro country code) with czech country code + 'cs' => 'cz', + // replace sv language (El Salvador country code) with sweden country code + 'sv' => 'se', + // replace fa language (Unknown country code) with Iran country code + 'fa' => 'ir', + // replace ja language (Unknown country code) with japan country code + 'ja' => 'jp', + // replace ko language (Unknown country code) with corée country code + 'ko' => 'kr', + // replace he language (Unknown country code) with Israel country code + 'he' => 'il', + // replace da language (Unknown country code) with Danemark country code + 'da' => 'dk', + // replace gb code with UK country code + 'gb' => 'uk', + ); + + + if(empty($lang) || strlen($lang) < 2) + { + return 'xx'; + } + + $lang = str_replace( array_keys($replaceLangCodeByCountryCode), + array_values($replaceLangCodeByCountryCode), + $lang + ); + + // Ex: "fr" + if(strlen($lang) == 2) + { + if(isset($countryList[$lang])) + { + return $lang; + } + } + + // when comma + $offcomma = strpos($lang, ','); + + if($offcomma == 2) + { + // in 'fr,en-us', keep first two chars + $domain = substr($lang, 0, 2); + if(isset($countryList[$domain])) + { + return $domain; + } + + // catch the second language Ex: "fr" in "en,fr" + $domain = substr($lang, 3, 2); + if(isset($countryList[$domain])) + { + return $domain; + } + } + + // detect second code Ex: "be" in "fr-be" + $off = strpos($lang, '-'); + if($off!==false) + { + $domain = substr($lang, $off+1, 2); + + if(isset($countryList[$domain])) + { + return $domain; + } + } + + // catch the second language Ex: "fr" in "en;q=1.0,fr;q=0.9" + if(preg_match("/^[a-z]{2};q=[01]\.[0-9],(?P<domain>[a-z]{2});/", $lang, $parts)) + { + $domain = $parts['domain']; + + if(isset($GLOBALS['countryList'][$domain][0])) + { + return $domain; + } + } + + // finally try with the first ever langage code + $domain = substr($lang, 0, 2); + if(isset($countryList[$domain])) + { + return $domain; + } + + // at this point we really can't guess the country + return 'xx'; + } + + + /** + * Generate random string + * + * @param string $length string length + * @param string $alphabet characters allowed in random string + * + * @return string random string with given length + */ + public static function getRandomString($length = 16, $alphabet = "abcdefghijklmnoprstuvwxyz0123456789") + { + $chars = $alphabet; + $str = ''; + + list($usec, $sec) = explode(" ", microtime()); + $seed = ((float)$sec+(float)$usec)*100000; + mt_srand($seed); + + for($i = 0; $i < $length; $i++) + { + $rand_key = mt_rand(0, strlen($chars)-1); + $str .= substr($chars, $rand_key, 1); + } + return str_shuffle($str); + } +} + + diff --git a/core/Config.php b/core/Config.php new file mode 100644 index 0000000000..3c6d27d9ff --- /dev/null +++ b/core/Config.php @@ -0,0 +1,223 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Config.php 546 2008-07-02 07:13:16Z matt $ + * + * @package Piwik_Helper + */ + +require_once "Zend/Config/Ini.php"; +require_once "Zend/Registry.php"; + +/** + * This class is used to access configuration files values. + * You can also set these values, the updated configuration files will be written at the end of the script execution. + * + * Example reading a value from the configuration file: + * $minValue = Zend_Registry::get('config')->General->minimum_memory_limit; + * + * will read the value minimumMemoryLimit under the [General] section of the config file + * + * @package Piwik_Helper + */ +class Piwik_Config +{ + /** + * When the user modifies the configuration file and there is one value missing, we suggest the default config file + * + * @var string + */ + protected $urlToPiwikHelpMissingValueInConfigurationFile = + 'http://dev.piwik.org/trac/browser/trunk/config/global.ini.php?format=raw'; + + protected $defaultConfig = null; + protected $userConfig = null; + protected $pathIniFileUserConfig = null; + protected $pathIniFileDefaultConfig = null; + protected $configFileUpdated = false; + public $doWriteFileWhenUpdated = true; + + /** + * Storing the correct cwd() because the value is not correct in the destructor + * "The working directory in the script shutdown phase can be different with some SAPIs (e.g. Apache)." + * + * @see http://bugs.php.net/bug.php?id=34206 + */ + protected $correctCwd; + + /** + * Returns default relative path for configuration file + * + * @return string + */ + static public function getDefaultUserConfigPath() + { + return 'config/config.ini.php'; + } + + /** + * Builds the Config object, given the optional path for the user INI file + * If not specified, it will use the default path + * + * @param string $pathIniFileUserConfig + */ + function __construct($pathIniFileUserConfig = null) + { + Zend_Registry::set('config', $this); + + $this->pathIniFileDefaultConfig = 'config/global.ini.php'; + if(is_null($pathIniFileUserConfig)) + { + $this->pathIniFileUserConfig = self::getDefaultUserConfigPath(); + } + else + { + $this->pathIniFileUserConfig = $pathIniFileUserConfig; + } + + $this->defaultConfig = new Zend_Config_Ini($this->pathIniFileDefaultConfig, null, true); + + if(!Zend_Loader::isReadable($this->pathIniFileUserConfig)) + { + throw new Exception("The configuration file {$this->pathIniFileUserConfig} has not been found."); + } + $this->userConfig = new Zend_Config_Ini($this->pathIniFileUserConfig, null, true); + + // see http://bugs.php.net/bug.php?id=34206 + $this->correctCwd = getcwd(); + } + + /** + * At the script shutdown, we save the new configuration file, if the user has set some values + * + */ + function __destruct() + { + if($this->configFileUpdated === true + && $this->doWriteFileWhenUpdated === true) + { + $configFile = "; <?php exit; ?> DO NOT REMOVE THIS LINE\n"; + $configFile .= "; file automatically generated during the piwik installation process (and updated later by some other plugins)\n"; + + foreach($this->userConfig as $section => $arraySection) + { + $arraySection = $arraySection->toArray(); + $configFile .= "[$section]\n"; + foreach($arraySection as $name => $value) + { + if(is_numeric($name)) + { + $name = $section; + $value = array($value); + } + + if(is_array($value)) + { + foreach($value as $currentValue) + { + $configFile .= $name."[] = $currentValue\n"; + } + } + else + { + // hack: we add " " around the password because when requesting this data using Zend_Config + // the toArray removes the " around the value + if( ($section == 'database' || $section == 'database_tests') + && $name == 'password') + { + $value = '"'.$value.'"'; + } + + $configFile .= $name." = $value\n"; + } + } + $configFile .= "\n"; + } + + chdir($this->correctCwd); + file_put_contents($this->getDefaultUserConfigPath(), $configFile ); + } + } + + /** + * If called, we use the "testing" environment, which means using the database_tests and log_tests sections + * for DB & Log configuration. + * + * @return void + * + */ + public function setTestEnvironment() + { + $this->database = $this->database_tests; + $this->log = $this->log_tests; + } + + /** + * Called when setting configuration values eg. + * Zend_Registry::get('config')->superuser = $_SESSION['superuser_infos']; + * + * The values will be saved in the configuration file at the end of the script @see __destruct() + * + * @param string $name + * @param mixed $value + */ + public function __set($name, $value) + { + $this->checkWritePermissionOnFile(); + if(!is_null($this->userConfig)) + { + if($this->userConfig->$name != $value) + { + $this->configFileUpdated = true; + } + $this->userConfig->$name = $value; + } + else + { + $this->defaultConfig->$name = $value; + } + } + + protected function checkWritePermissionOnFile() + { + static $enoughPermission = null; + + if(is_null($enoughPermission)) + { + if($this->doWriteFileWhenUpdated) + { + Piwik_FrontController::checkDirectoriesWritableOrDie( array('/config') ); + } + $enoughPermission = true; + } + return $enoughPermission; + } + + /** + * Called when getting a configuration value, eg. Zend_Registry::get('config')->superuser->login + * + * @param string $name + * @return mixed value + * + * @throws exception if the value was not found in the configuration file + */ + public function __get($name) + { + if( !is_null($this->userConfig) + && null !== ($valueInUserConfig = $this->userConfig->$name)) + { + return $valueInUserConfig; + } + if(null !== ($valueInDefaultConfig = $this->defaultConfig->$name)) + { + return $valueInDefaultConfig; + } + + throw new Exception("The configuration parameter $name couldn't be found in your configuration file. + <br>Try to replace your default configuration file ({$this->pathIniFileDefaultConfig}) with + the <a href='".$this->urlToPiwikHelpMissingValueInConfigurationFile."'>default piwik configuration file</a> "); + } +} diff --git a/core/Controller.php b/core/Controller.php new file mode 100644 index 0000000000..de1a1b2d53 --- /dev/null +++ b/core/Controller.php @@ -0,0 +1,270 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Controller.php 561 2008-07-21 00:00:35Z matt $ + * + * @package Piwik + */ + +/** + * Parent class of all plugins Controllers (located in /plugins/PluginName/Controller.php + * It defines some helper functions controllers can use. + * + * @package Piwik + */ +abstract class Piwik_Controller +{ + /** + * Plugin name, eg. Referers + * @var string + */ + protected $pluginName; + + /** + * Date string + * + * @var string + */ + protected $strDate; + + /** + * Piwik_Date object or null if the requested date is a range + * + * @var Piwik_Date|null + */ + protected $date; + + /** + * Builds the controller object, reads the date from the request, extracts plugin name from + * + */ + function __construct() + { + $aPluginName = explode('_', get_class($this)); + $this->pluginName = $aPluginName[1]; + $this->strDate = Piwik_Common::getRequestVar('date', 'yesterday','string'); + + // the date looks like YYYY-MM-DD we can build it + try{ + $this->date = Piwik_Date::factory($this->strDate); + $this->strDate = $this->date->toString(); + } catch(Exception $e){ + // the date looks like YYYY-MM-DD,YYYY-MM-DD or other format + // case the date looks like a range + $this->date = null; + } + } + + /** + * Returns the name of the default method that will be called + * when visiting: index.php?module=PluginName without the action parameter + * + * @return string + */ + function getDefaultAction() + { + return 'index'; + } + + /** + * Given an Object implementing Piwik_iView interface, we either: + * - echo the output of the rendering if fetch = false + * - returns the output of the rendering if fetch = true + * + * @param Piwik_ViewDataTable $view + * @param bool $fetch + * @return string|void + */ + protected function renderView( Piwik_ViewDataTable $view, $fetch) + { + $view->main(); + $rendered = $view->getView()->render(); + if($fetch) + { + return $rendered; + } + echo $rendered; + } + + /** + * Returns a ViewDataTable object of an Evolution graph + * for the last30 days/weeks/etc. of the current period, relative to the current date. + * + * @param string $currentModuleName + * @param string $currentControllerAction + * @param string $apiMethod + * @return Piwik_ViewDataTable_GenerateGraphHTML_ChartEvolution + */ + protected function getLastUnitGraph($currentModuleName, $currentControllerAction, $apiMethod) + { + require_once "ViewDataTable/GenerateGraphHTML.php"; + $view = Piwik_ViewDataTable::factory('graphEvolution'); + $view->init( $currentModuleName, $currentControllerAction, $apiMethod ); + + // if the date is not yet a nicely formatted date range ie. YYYY-MM-DD,YYYY-MM-DD we build it + // otherwise the current controller action is being called with the good date format already so it's fine + // see constructor + if( !is_null($this->date)) + { + $view->setParametersToModify( + $this->getGraphParamsModified( array('date'=>$this->strDate)) + ); + } + + return $view; + } + + + /** + * Returns the array of new processed parameters once the parameters are applied. + * For example: if you set range=last30 and date=2008-03-10, + * the date element of the returned array will be "2008-02-10,2008-03-10" + * + * Parameters you can set: + * - range: last30, previous10, etc. + * - date: YYYY-MM-DD, today, yesterday + * - period: day, week, month, year + * + * @param array paramsToSet = array( 'date' => 'last50', 'viewDataTable' =>'sparkline' ) + */ + protected function getGraphParamsModified($paramsToSet = array()) + { + if(!isset($paramsToSet['range'])) + { + $range = 'last30'; + } + else + { + $range = $paramsToSet['range']; + } + + if(!isset($paramsToSet['date'])) + { + $endDate = $this->strDate; + } + else + { + $endDate = $paramsToSet['date']; + } + + if(!isset($paramsToSet['period'])) + { + $period = Piwik_Common::getRequestVar('period'); + } + else + { + $period = $paramsToSet['period']; + } + + $last30Relative = new Piwik_Period_Range($period, $range ); + + $last30Relative->setDefaultEndDate(Piwik_Date::factory($endDate)); + + $paramDate = $last30Relative->getDateStart()->toString() . "," . $last30Relative->getDateEnd()->toString(); + + $params = array_merge($paramsToSet , array( 'date' => $paramDate ) ); + + return $params; + } + + /** + * Returns a numeric value from the API. + * Works only for API methods that originally returns numeric values (there is no cast here) + * + * @param string $methodToCall, eg. Referers.getNumberOfDistinctSearchEngines + * @return int|float + */ + protected function getNumericValue( $methodToCall ) + { + $requestString = 'method='.$methodToCall.'&format=original'; + $request = new Piwik_API_Request($requestString); + return $request->process(); + } + + /** + * Returns the current URL to use in a <img src=X> to display a sparkline. + * $action must be the name of a Controller method that requests data using the Piwik_ViewDataTable::factory + * It will automatically build a sparkline by setting the viewDataTable=sparkline parameter in the URL. + * It will also computes automatically the 'date' for the 'last30' days/weeks/etc. + * + * @param string $action, eg. method name of the controller to call in the img src + * @return string the generated URL + */ + protected function getUrlSparkline( $action ) + { + $params = $this->getGraphParamsModified( + array( 'viewDataTable' => 'sparkline', + 'action' => $action, + 'module' => $this->pluginName) + ); + $url = Piwik_Url::getCurrentQueryStringWithParametersModified($params); + return $url; + } + + protected function setGeneralVariablesView($view) + { + $oDate = Piwik_Date::factory($this->strDate); + $localizedDateFormat = Piwik_Translate('CoreHome_LocalizedDateFormat'); + $view->prettyDate = $oDate->getLocalized($localizedDateFormat); + $view->date = $this->strDate; + + try { + $currentPeriod = Piwik_Common::getRequestVar('period'); + $view->idSite = Piwik_Common::getRequestVar('idSite'); + } catch(Exception $e) { + self::redirectToIndex(Piwik::getModule(), Piwik::getAction()); + } + $otherPeriodsAvailable = array('day', 'week', 'month', 'year'); + $otherPeriodsNames = array( + 'day' => Piwik_Translate('CoreHome_PeriodDay'), + 'week' => Piwik_Translate('CoreHome_PeriodWeek'), + 'month' => Piwik_Translate('CoreHome_PeriodMonth'), + 'year' => Piwik_Translate('CoreHome_PeriodYear') + ); + + $found = array_search($currentPeriod,$otherPeriodsAvailable); + if($found !== false) + { + unset($otherPeriodsAvailable[$found]); + } + + $view->period = $currentPeriod; + $view->otherPeriods = $otherPeriodsAvailable; + $view->periodsNames = $otherPeriodsNames; + } + + function redirectToIndex($moduleToRedirect = 'CoreHome', $actionToRedirect = 'index') + { + $sitesId = Piwik_SitesManager_API::getSitesIdWithAtLeastViewAccess(); + if(!empty($sitesId)) + { + $firstSiteId = $sitesId[0]; + $firstSite = new Piwik_Site($firstSiteId); + if ($firstSite->getCreationDate()->isToday()) + { + $defaultDate = 'today'; + } + else + { + $defaultDate = Zend_Registry::get('config')->General->default_day; + } + header("Location:index.php?module=".$moduleToRedirect."&action=".$actionToRedirect."&idSite=$firstSiteId&period=day&date=$defaultDate"); + } + else + { + if(($currentLogin = Piwik::getCurrentUserLogin()) != 'anonymous') + { + Piwik_ExitWithMessage( sprintf(Piwik_Translate('CoreHome_NoPrivileges'),$currentLogin). + "<br /><br /> <b><a href='?module=Login&action=logout'>› ".Piwik_Translate('General_Logout')."</a></b><br />"); + } + else + { + Piwik_FrontController::dispatch('Login'); + } + } + exit; + } +}
\ No newline at end of file diff --git a/core/Cookie.php b/core/Cookie.php new file mode 100644 index 0000000000..7bcbe7b92c --- /dev/null +++ b/core/Cookie.php @@ -0,0 +1,282 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Cookie.php 444 2008-04-11 13:38:22Z johmathe $ + * + * @package Piwik_Helper + */ + + +/** + * Simple class to handle the cookies: + * - read a cookie values + * - edit an existing cookie and save it + * - create a new cookie, set values, expiration date, etc. and save it + * + * @package Piwik_Helper + */ +class Piwik_Cookie +{ + /** + * The name of the cookie + */ + protected $name = null; + + /** + * The expire time for the cookie (expressed in UNIX Timestamp) + */ + protected $expire = null; + + /** + * The content of the cookie + */ + protected $value = array(); + + /** + * The character used to separate the tuple name=value in the cookie + */ + const VALUE_SEPARATOR = ':'; + + /** + * Instanciate a new Cookie object and tries to load the cookie content if the cookie + * exists already. + * + * @param string cookie Name + * @param int The timestamp after which the cookie will expire, eg time() + 86400 + */ + public function __construct( $cookieName, $expire = null) + { + $this->name = $cookieName; + + if(is_null($expire) + || !is_numeric($expire) + || $expire <= 0) + { + $this->expire = $this->getDefaultExpire(); + } + + if($this->isCookieFound()) + { + $this->loadContentFromCookie(); + } + } + + /** + * Returns true if the visitor already has the cookie. + * @return bool + */ + public function isCookieFound() + { + return isset($_COOKIE[$this->name]); + } + + /** + * Returns the default expiry time, 10 years + * @return int Timestamp in 10 years + */ + protected function getDefaultExpire() + { + return time() + 86400*365*10; + } + + /** + * We don't use the setcookie function because it is buggy for some PHP versions. + * + * Taken from http://php.net/setcookie + */ + protected function setCookie($Name, $Value, $Expires, $Path = '', $Domain = '', $Secure = false, $HTTPOnly = false) + { + if (!empty($Domain)) + { + // Fix the domain to accept domains with and without 'www.'. + if (strtolower(substr($Domain, 0, 4)) == 'www.') $Domain = substr($Domain, 4); + + $Domain = '.' . $Domain; + + // Remove port information. + $Port = strpos($Domain, ':'); + if ($Port !== false) $Domain = substr($Domain, 0, $Port); + } + + $header = 'Set-Cookie: ' . rawurlencode($Name) . '=' . rawurlencode($Value) + . (empty($Expires) ? '' : '; expires=' . gmdate('D, d-M-Y H:i:s', $Expires) . ' GMT') + . (empty($Path) ? '' : '; path=' . $Path) + . (empty($Domain) ? '' : '; domain=' . $Domain) + . (!$Secure ? '' : '; secure') + . (!$HTTPOnly ? '' : '; HttpOnly'); + + header($header, false); + } + + /** + * We set the privacy policy header + * + * @return void + */ + protected function setP3PHeader() + { + header("P3P: CP='OTI DSP COR NID STP UNI OTPa OUR'"); + } + + /** + * Delete the cookie + * + * @return void + */ + public function delete() + { + $this->setP3PHeader(); + setcookie($this->name, false, time() - 86400); + } + + /** + * Saves the cookie (set the Cookie header). + * You have to call this method before sending any text to the browser or you would get the + * "Header already sent" error. + */ + public function save() + { + $this->setP3PHeader(); + $this->setCookie( $this->name, $this->generateContentString(), $this->expire); + } + + /** + * Load the cookie content into a php array. + * Parses the cookie string to extract the different variables. + * Unserialize the array when necessary. + * Decode the non numeric values that were base64 encoded. + * + * @return void + */ + protected function loadContentFromCookie() + { + $cookieStr = $_COOKIE[$this->name]; + + $values = explode( self::VALUE_SEPARATOR, $cookieStr); + foreach($values as $nameValue) + { + $equalPos = strpos($nameValue, '='); + $varName = substr($nameValue,0,$equalPos); + $varValue = substr($nameValue,$equalPos+1); + + // no numeric value are base64 encoded so we need to decode them + if(!is_numeric($varValue)) + { + $varValue = base64_decode($varValue); + + // some of the values may be serialized array so we try to unserialize it + if( ($arrayValue = @unserialize($varValue)) !== false + // we set the unserialized version only for arrays as you can have set a serialized string on purpose + && is_array($arrayValue) + ) + { + $varValue = $arrayValue; + } + } + + $this->set($varName, $varValue); + } + } + + /** + * Returns the string to save in the cookie from the $this->value array of values. + * It goes through the array and generates the cookie content string. + * @return string Cookie content + */ + protected function generateContentString() + { + $cookieStr = ''; + foreach($this->value as $name=>$value) + { + if(is_array($value)) + { + $value = serialize($value); + } + $value = base64_encode($value); + + $cookieStr .= "$name=$value" . self::VALUE_SEPARATOR; + } + $cookieStr = substr($cookieStr, 0, strlen($cookieStr)-1); + return $cookieStr; + } + + /** + * Registers a new name => value association in the cookie. + * + * Registering new values is optimal if the value is a numeric value. + * If the value is a string, it will be saved as a base64 encoded string. + * If the value is an array, it will be saved as a serialized and base64 encoded + * string which is not very good in terms of bytes usage. + * You should save arrays only when you are sure about their maximum data size. + * A cookie has to stay small and its size shouldn't increase over time! + * + * @param string Name of the value to save; the name will be used to retrieve this value + * @param string|array|numeric Value to save + * + */ + public function set( $name, $value ) + { + $name = self::escapeValue($name); + $this->value[$name] = $value; + } + + /** + * Returns the value defined by $name from the cookie. + * + * @param string|integer Index name of the value to return + * @return mixed The value if found, false if the value is not found + */ + public function get( $name ) + { + $name = self::escapeValue($name); + return isset($this->value[$name]) ? self::escapeValue($this->value[$name]) : false; + } + + /** + * Returns an easy to read cookie dump + * + * @return string The cookie dump + */ + public function __toString() + { + $str = "<-- Content of the cookie '{$this->name}' <br>\n"; + foreach($this->value as $name => $value ) + { + $str .= $name . " = " . var_export($this->get($name), true) . "<br>\n"; + } + $str .= "--> <br>\n"; + return $str; + } + + /** + * Escape values from the cookie before sending them back to the client + * (when using the get() method). + * + * @return mixed The value once cleaned. + */ + static protected function escapeValue( $value ) + { + return Piwik_Common::sanitizeInputValues($value); + } +} + + +//$c = new Piwik_Cookie( 'piwik_logstats', 86400); +//echo $c; +//$c->set(1,1); +//$c->set('test',1); +//$c->set('test2','test=432:gea785'); +//$c->set('test3',array('test=432:gea785')); +//$c->set('test4',array(array(0=>1),1=>'test')); +//echo $c; +//echo "<br>"; +//$v=$c->get('more!'); +//if(empty($v)) $c->set('more!',1); +//$c->set('more!', array($c->get('more!'))); +//$c->save(); +//$c->delete(); + + diff --git a/core/DataFiles/Browsers.php b/core/DataFiles/Browsers.php new file mode 100644 index 0000000000..9744f7f6e5 --- /dev/null +++ b/core/DataFiles/Browsers.php @@ -0,0 +1,59 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Browsers.php 444 2008-04-11 13:38:22Z johmathe $ + * + * @package Piwik_UserSettings + */ + +/** + * Browser list. + * If you want to add a new entry, please email us at hello at piwik.org + * + */ +if(!isset($GLOBALS['Piwik_BrowserList'] )) +{ + $GLOBALS['Piwik_BrowserList'] = array( + 'msie' => 'IE', + 'microsoft internet explorer' => 'IE', + 'internet explorer' => 'IE', + 'netscape6' => 'NS', + 'netscape' => 'NS', + 'galeon' => 'GA', + 'phoenix' => 'PX', + 'firefox' => 'FF', + 'mozilla firebird' => 'FB', + 'firebird' => 'FB', + 'seamonkey' => 'SM', + 'chimera' => 'CH', + 'camino' => 'CA', + 'safari' => 'SF', + 'k-meleon' => 'KM', + 'mozilla' => 'MO', + 'opera' => 'OP', + 'konqueror' => 'KO', + 'icab' => 'IC', + 'lynx' => 'LX', + 'links' => 'LI', + 'ncsa mosaic' => 'MC', + 'amaya' => 'AM', + 'omniweb' => 'OW', + 'hotjava' => 'HJ', + 'browsex' => 'BX', + 'amigavoyager' => 'AV', + 'amiga-aweb' => 'AW', + 'ibrowse' => 'IB', + 'unknown' => 'UNK' + ); + + + $GLOBALS['Piwik_BrowserList_IdToLabel'] + = array_map('ucwords',array_flip($GLOBALS['Piwik_BrowserList'])); + + $GLOBALS['Piwik_BrowserList_IdToShortLabel'] = $GLOBALS['Piwik_BrowserList_IdToLabel']; + $GLOBALS['Piwik_BrowserList_IdToShortLabel']['IE'] = "IE"; + $GLOBALS['Piwik_BrowserList_IdToShortLabel']['FB'] = "Firebird"; +} diff --git a/core/DataFiles/Countries.php b/core/DataFiles/Countries.php new file mode 100644 index 0000000000..af02827723 --- /dev/null +++ b/core/DataFiles/Countries.php @@ -0,0 +1,240 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Countries.php 444 2008-04-11 13:38:22Z johmathe $ + * + * @package Piwik_UserCountry + */ + +/** + * Country code and continent database. + * If you want to add a new entry, please email us at hello at piwik.org + * + */ +if(!isset($GLOBALS['Piwik_CountryList'])) +{ + $GLOBALS['Piwik_CountryList'] = array( + 'xx' => array('unk'), + 'ac' => array('afr'), + 'ad' => array('eur'), + 'ae' => array('asi'), + 'af' => array('asi'), + 'ag' => array('ams'), + 'ai' => array('ams'), + 'al' => array('eur'), + 'am' => array('asi'), + 'an' => array('ams'), + 'ao' => array('afr'), + 'aq' => array('aut'), + 'ar' => array('ams'), + 'as' => array('oce'), + 'at' => array('eur'), + 'au' => array('oce'), + 'aw' => array('ams'), + 'az' => array('asi'), + 'ba' => array('eur'), + 'bb' => array('ams'), + 'bd' => array('asi'), + 'be' => array('eur'), + 'bf' => array('afr'), + 'bg' => array('eur'), + 'bh' => array('asi'), + 'bi' => array('afr'), + 'bj' => array('afr'), + 'bm' => array('ams'), + 'bn' => array('asi'), + 'bo' => array('ams'), + 'br' => array('ams'), + 'bs' => array('ams'), + 'bt' => array('asi'), + 'bw' => array('afr'), + 'by' => array('eur'), + 'bz' => array('ams'), + 'ca' => array('amn'), + 'cc' => array('oce'), + 'cd' => array('afr'), + 'cf' => array('afr'), + 'cg' => array('afr'), + 'ch' => array('eur'), + 'ci' => array('afr'), + 'ck' => array('asi'), + 'cl' => array('ams'), + 'cm' => array('afr'), + 'cn' => array('asi'), + 'co' => array('ams'), + 'cs' => array('eur'), + 'cr' => array('ams'), + 'cu' => array('ams'), + 'cv' => array('afr'), + 'cy' => array('eur'), + 'cz' => array('eur'), + 'de' => array('eur'), + 'dj' => array('afr'), + 'dk' => array('eur'), + 'dm' => array('ams'), + 'do' => array('ams'), + 'dz' => array('afr'), + 'ec' => array('ams'), + 'ee' => array('eur'), + 'eg' => array('afr'), + 'eh' => array('afr'), + 'er' => array('afr'), + 'es' => array('eur'), + 'et' => array('afr'), + 'fi' => array('eur'), + 'fj' => array('oce'), + 'fk' => array('ams'), + 'fm' => array('oce'), + 'fr' => array('eur'), + 'ga' => array('afr'), + 'gb' => array('eur'), + 'gd' => array('ams'), + 'ge' => array('asi'), + 'gf' => array('ams'), + 'gg' => array('eur'), + 'gh' => array('afr'), + 'gi' => array('afr'), + 'gl' => array('amn'), + 'gm' => array('afr'), + 'gn' => array('afr'), + 'gp' => array('ams'), + 'gq' => array('afr'), + 'gr' => array('eur'), + 'gs' => array('eur'), + 'gt' => array('ams'), + 'gw' => array('afr'), + 'gy' => array('ams'), + 'hk' => array('asi'), + 'hn' => array('ams'), + 'hr' => array('eur'), + 'ht' => array('ams'), + 'hu' => array('eur'), + 'id' => array('asi'), + 'ie' => array('eur'), + 'il' => array('asi'), + 'in' => array('asi'), + 'iq' => array('asi'), + 'ir' => array('asi'), + 'is' => array('eur'), + 'it' => array('eur'), + 'jm' => array('ams'), + 'jo' => array('asi'), + 'jp' => array('asi'), + 'ke' => array('afr'), + 'kg' => array('asi'), + 'kh' => array('asi'), + 'ki' => array('oce'), + 'km' => array('afr'), + 'kp' => array('asi'), + 'kr' => array('asi'), + 'kw' => array('asi'), + 'ky' => array('ams'), + 'kz' => array('asi'), + 'la' => array('asi'), + 'lb' => array('asi'), + 'li' => array('eur'), + 'lk' => array('asi'), + 'lr' => array('afr'), + 'ls' => array('afr'), + 'lt' => array('eur'), + 'lu' => array('eur'), + 'lv' => array('eur'), + 'ly' => array('afr'), + 'ma' => array('afr'), + 'mc' => array('eur'), + 'md' => array('eur'), + 'mg' => array('afr'), + 'mh' => array('oce'), + 'mk' => array('eur'), + 'ml' => array('afr'), + 'mm' => array('asi'), + 'mn' => array('asi'), + 'mo' => array('asi'), + 'mq' => array('ams'), + 'mr' => array('afr'), + 'mt' => array('eur'), + 'mu' => array('afr'), + 'mv' => array('asi'), + 'mw' => array('afr'), + 'mx' => array('ams'), + 'my' => array('asi'), + 'mz' => array('afr'), + 'na' => array('afr'), + 'nc' => array('oce'), + 'ne' => array('afr'), + 'ng' => array('afr'), + 'ni' => array('ams'), + 'nl' => array('eur'), + 'no' => array('eur'), + 'np' => array('asi'), + 'nr' => array('oce'), + 'nz' => array('oce'), + 'om' => array('asi'), + 'pa' => array('ams'), + 'pe' => array('ams'), + 'pf' => array('oce'), + 'pg' => array('oce'), + 'ph' => array('asi'), + 'pk' => array('asi'), + 'pl' => array('eur'), + 'pm' => array('amn'), + 'pr' => array('ams'), + 'pt' => array('eur'), + 'pw' => array('oce'), + 'py' => array('ams'), + 'qa' => array('asi'), + 're' => array('afr'), + 'ro' => array('eur'), + 'ru' => array('asi'), + 'rs' => array('asi'), + 'rw' => array('afr'), + 'sa' => array('asi'), + 'sb' => array('oce'), + 'sc' => array('afr'), + 'sd' => array('afr'), + 'se' => array('eur'), + 'sg' => array('asi'), + 'si' => array('eur'), + 'sk' => array('eur'), + 'sl' => array('afr'), + 'sm' => array('eur'), + 'sn' => array('afr'), + 'so' => array('afr'), + 'sr' => array('ams'), + 'sv' => array('ams'), + 'sy' => array('asi'), + 'sz' => array('afr'), + 'td' => array('afr'), + 'tg' => array('afr'), + 'th' => array('asi'), + 'tj' => array('asi'), + 'tm' => array('asi'), + 'tn' => array('afr'), + 'to' => array('oce'), + 'tp' => array('oce'), + 'tr' => array('eur'), + 'tt' => array('ams'), + 'tw' => array('asi'), + 'tz' => array('afr'), + 'ua' => array('eur'), + 'ug' => array('afr'), + 'uk' => array('eur'), + 'us' => array('amn'), + 'uy' => array('ams'), + 'uz' => array('asi'), + 'va' => array('eur'), + 've' => array('ams'), + 'vn' => array('asi'), + 'vu' => array('oce'), + 'wf' => array('oce'), + 'ye' => array('asi'), + 'yu' => array('eur'), + 'za' => array('afr'), + 'zm' => array('afr'), + 'zw' => array('afr'), + ); +} + diff --git a/core/DataFiles/OS.php b/core/DataFiles/OS.php new file mode 100644 index 0000000000..f3e5e816da --- /dev/null +++ b/core/DataFiles/OS.php @@ -0,0 +1,86 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: OS.php 444 2008-04-11 13:38:22Z johmathe $ + * + * @package Piwik_UserSettings + */ + +/** + * Operating systems database. + * If you want to add a new entry, please email us at hello at piwik.org + * + */ +if(!isset($GLOBALS['Piwik_Oslist'])) +{ + $GLOBALS['Piwik_Oslist'] = array( + 'Nintendo Wii' => 'WII', + 'PlayStation Portable' => 'PSP', + 'PLAYSTATION 3' => 'PS3', + 'Windows NT 6.0' => 'WVI', + 'Windows Vista' => 'WVI', + 'Windows NT 5.2' => 'WS3', + 'Windows Server 2003' => 'WS3', + 'Windows NT 5.1' => 'WXP', + 'Windows XP' => 'WXP', + 'Win98' => 'W98', + 'Windows 98' => 'W98', + 'Windows NT 5.0' => 'W2K', + 'Windows 2000' => 'W2K', + 'Windows NT 4.0' => 'WNT', + 'WinNT' => 'WNT', + 'Windows NT' => 'WNT', + 'Win 9x 4.90' => 'WME', + 'Win 9x 4.90' => 'WME', + 'Windows Me' => 'WME', + 'Win32' => 'W95', + 'Win95' => 'W95', + 'Windows 95' => 'W95', + 'Mac_PowerPC' => 'MAC', + 'Mac PPC' => 'MAC', + 'PPC' => 'MAC', + 'Mac PowerPC' => 'MAC', + 'Mac OS' => 'MAC', + 'Linux' => 'LIN', + 'SunOS' => 'SOS', + 'FreeBSD' => 'BSD', + 'AIX' => 'AIX', + 'IRIX' => 'IRI', + 'HP-UX' => 'HPX', + 'OS/2' => 'OS2', + 'NetBSD' => 'NBS', + 'Unknown' => 'XXX' + ); + + + $GLOBALS['Piwik_Oslist_IdToLabel'] = array_flip($GLOBALS['Piwik_Oslist']); + + $GLOBALS['Piwik_Oslist_IdToShortLabel'] = array( + 'PS3' => 'PS3', + 'PSP' => 'PSP', + 'WII' => 'WII', + 'WVI' => 'Win Vista', + 'WS3' => 'Win S2003', + 'WXP' => 'Win XP', + 'W98' => 'Win 98', + 'W2K' => 'Win 2000', + 'WNT' => 'Win NT', + 'WME' => 'Win Me', + 'W95' => 'Win 95', + 'WCE' => 'Win CE', + 'MAC' => 'Mac OS', + 'LIN' => 'Linux', + 'INC' => 'Inconnu', + 'SOS' => 'SunOS', + 'BSD' => 'FreeBSD', + 'AIX' => 'AIX', + 'IRI' => 'IRIX', + 'HPX' => 'HPX', + 'OS2' => 'OS/2', + 'NBS' => 'NetBSD', + 'XXX' => 'Unknown', + ); +} diff --git a/core/DataFiles/SearchEngines.php b/core/DataFiles/SearchEngines.php new file mode 100644 index 0000000000..c3e9f97b21 --- /dev/null +++ b/core/DataFiles/SearchEngines.php @@ -0,0 +1,1080 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: SearchEngines.php 569 2008-07-22 23:12:58Z matt $ + * + * @package Piwik_Referers + */ +/** + * Search Engine database + * + * ====================================== + * HOW TO ADD A SEARCH ENGINE TO THE LIST + * ====================================== + * If you want to add a new entry, please email us the information + icon at hello at piwik.org + * + * Detail of a line: + * Url => array( SearchEngineName, VariableKeyword, [charset used by the search engine]) + * + * The main search engine URL has to be at the top of the list for the given search Engine. + * + * You can add new search engines icons by adding the icon + * in the plugins/Referers/images/SearchEngines directory + * using the format "mainSearchEngineUrl.png". Example: www.google.com.png + * + * + */ +if(!isset($GLOBALS['Piwik_SearchEngines'] )) +{ + $GLOBALS['Piwik_SearchEngines'] = array( + + //" " => array(" ", " " [, " "]), + + // 1 + "1.cz" => array("1.cz", "q", "iso-8859-2"), + "www.1.cz" => array("1.cz", "q", "iso-8859-2"), + + // 1und1 + "portal.1und1.de" => array("1und1", "search"), + + // 3271 + "nmsearch.3721.com" => array("3271", "p"), + "seek.3721.com" => array("3271", "p"), + + // A9 + "www.a9.com" => array("A9", ""), + "a9.com" => array("A9", ""), + + // Abacho + "search.abacho.com" => array("Abacho", "q"), + + // about + "search.about.com" => array("About", "terms"), + + //Acoon + "www.acoon.de" => array("Acoon", "begriff"), + + //Acont + "acont.de" => array("Acont", "query"), + + //Alexa + "www.alexa.com" => array("Alexa", "q"), + "alexa.com" => array("Alexa", "q"), + + //Alice Adsl + "rechercher.aliceadsl.fr" => array("Alice Adsl", "qs"), + "search.alice.it" => array("Alice (Virgilio)", "qt"), + + //Allesklar + "www.allesklar.de" => array("Allesklar", "words"), + + // AllTheWeb + "www.alltheweb.com" => array("AllTheWeb", "q"), + + // all.by + "all.by" => array("All.by", "query"), + + // Altavista + "www.altavista.com" => array("AltaVista", "q"), + "listings.altavista.com" => array("AltaVista", "q"), + "www.altavista.de" => array("AltaVista", "q"), + "altavista.fr" => array("AltaVista", "q"), + "de.altavista.com" => array("AltaVista", "q"), + "fr.altavista.com" => array("AltaVista", "q"), + "es.altavista.com" => array("AltaVista", "q"), + "www.altavista.fr" => array("AltaVista", "q"), + "search.altavista.com" => array("AltaVista", "q"), + "search.fr.altavista.com" => array("AltaVista", "q"), + "se.altavista.com" => array("AltaVista", "q"), + "be-nl.altavista.com" => array("AltaVista", "q"), + "be-fr.altavista.com" => array("AltaVista", "q"), + "it.altavista.com" => array("AltaVista", "q"), + "us.altavista.com" => array("AltaVista", "q"), + "nl.altavista.com" => array("Altavista", "q"), + "ch.altavista.com" => array("AltaVista", "q"), + + // APOLLO7 + "www.apollo7.de" => array("Apollo7", "query"), + "apollo7.de" => array("Apollo7", "query"), + + // AOL + "search.aol.com" => array("AOL", "query"), + "aolsearch.aol.com" => array("AOL", "query"), + "www.aolrecherche.aol.fr" => array("AOL", "q"), + "www.aolrecherches.aol.fr" => array("AOL", "query"), + "www.aolimages.aol.fr" => array("AOL", "query"), + "www.recherche.aol.fr" => array("AOL", "q"), + "aolsearcht.aol.com" => array("AOL", "query"), + "find.web.aol.com" => array("AOL", "query"), + "recherche.aol.ca" => array("AOL", "query"), + "aolsearch.aol.co.uk" => array("AOL", "query"), + "search.aol.co.uk" => array("AOL", "query"), + "aolrecherche.aol.fr" => array("AOL", "q"), + "sucheaol.aol.de" => array("AOL", "q"), + "suche.aol.de" => array("AOL", "q"), + "suche.aolsvc.de" => array("AOL", "q"), + + "aolbusqueda.aol.com.mx" => array("AOL", "query"), + + // Aport + "sm.aport.ru" => array("Aport", "r"), + + // Arcor + "www.arcor.de" => array("Arcor", "Keywords"), + + // Arianna (Libero.it) + "arianna.libero.it" => array("Arianna", "query"), + + // Ask + "www.ask.com" => array("Ask", "ask"), + "web.ask.com" => array("Ask", "ask"), + "www.ask.co.uk" => array("Ask", "q"), + "uk.ask.com" => array("Ask", "q"), + "fr.ask.com" => array("Ask", "q"), + "de.ask.com" => array("Ask", "q"), + "es.ask.com" => array("Ask", "q"), + "it.ask.com" => array("Ask", "q"), + "nl.ask.com" => array("Ask", "q"), + "ask.jp" => array("Ask", "q"), + + // Atlas + "search.atlas.cz" => array("Atlas", "q", "windows-1250"), + + // Austronaut + "www2.austronaut.at" => array("Austronaut", "begriff"), + + // Baidu + "www.baidu.com" => array("Baidu", "wd"), + "www1.baidu.com" => array("Baidu", "wd"), + + // BBC + "search.bbc.co.uk" => array("BBC", "q"), + + // Bellnet + "www.suchmaschine.com" => array("Bellnet", "suchstr"), + + // Biglobe + "cgi.search.biglobe.ne.jp" => array("Biglobe", "q"), + + // Bild + "www.bild.t-online.de" => array("Bild.de (enhanced by Google)", "query"), + + //Blogdigger + "www.blogdigger.com" => array("Blogdigger","q"), + + //Bloglines + "www.bloglines.com" => array("Bloglines","q"), + + //Blogpulse + "www.blogpulse.com" => array("Blogpulse","query"), + + //Bluewin + "search.bluewin.ch" => array("Bluewin","query"), + + // Caloweb + "www.caloweb.de" => array("Caloweb", "q"), + + // Cegetel (Google) + "www.cegetel.net" => array("Cegetel (Google)", "q"), + + // Centrum + "fulltext.centrum.cz" => array("Centrum", "q", "windows-1250"), + "morfeo.centrum.cz" => array("Centrum", "q", "windows-1250"), + "search.centrum.cz" => array("Centrum", "q", "windows-1250"), + + // Chello + "www.chello.fr" => array("Chello", "q1"), + + // Club Internet + "recherche.club-internet.fr" => array("Club Internet", "q"), + + // Comcast + "www.comcast.net" => array("Comcast", "query"), + + // Comet systems + "search.cometsystems.com" => array("CometSystems", "q"), + + // Compuserve + "suche.compuserve.de" => array("Compuserve.de (Powered by Google)", "q"), + "websearch.cs.com" => array("Compuserve.com (Enhanced by Google)", "query"), + + // Copernic + "metaresults.copernic.com" => array("Copernic", " "), + + // Crossbot + "www.crossbot.de" => array("Crossbot", "q"), + + // DasOertliche + "www.dasoertliche.de" => array("DasOertliche", "kw"), + + // DasTelefonbuch + "www.4call.dastelefonbuch.de" => array("DasTelefonbuch", "kw"), + + // Defind.de + "suche.defind.de" => array("Defind.de", "search"), + + // Deskfeeds + "www.deskfeeds.com" => array("Deskfeeds", "sx"), + + // Dino + "www.dino-online.de" => array("Dino", "query"), + + // dir.com + "fr.dir.com" => array("dir.com", "req"), + + // dmoz + "dmoz.org" => array("dmoz", "search"), + "editors.dmoz.org" => array("dmoz", "search"), + "search.dmoz.org" => array("dmoz", "search"), + "www.dmoz.org" => array("dmoz", "search"), + + // Dogpile + "search.dogpile.com" => array("Dogpile", "q"), + "nbci.dogpile.com" => array("Dogpile", "q"), + + // earthlink + "search.earthlink.net" => array("Earthlink", "q"), + + // Eniro + "www.eniro.se" => array("Eniro", "q"), + + // Espotting + "affiliate.espotting.fr" => array("Espotting", "keyword"), + + // Eudip + "www.eudip.com" => array("Eudip", " "), + + // Eurip + "www.eurip.com" => array("Eurip", "q"), + + // Euroseek + "www.euroseek.com" => array("Euroseek", "string"), + + // Excite + "www.excite.it" => array("Excite", "q"), + "msxml.excite.com" => array("Excite", "qkw"), + "www.excite.fr" => array("Excite", "search"), + + // Exalead + "www.exalead.fr" => array("Exalead", "q"), + "www.exalead.com" => array("Exalead", "q"), + + // eo + "eo.st" => array("eo", "q"), + + // Feedminer + "www.feedminer.com" => array("Feedminer", "q"), + + // Feedster + "www.feedster.com" => array("Feedster", ""), + + // Francite + "recherche.francite.com" => array("Francite", "name"), + "antisearch.francite.com" => array("Francite", "KEYWORDS"), + + // Fireball + "suche.fireball.de" => array("Fireball", "query"), + + + // Firstfind + "www.firstsfind.com" => array("Firstsfind", "qry"), + + // Fixsuche + "www.fixsuche.de" => array("Fixsuche", "q"), + + // Flix + "www.flix.de" => array("Flix.de", "keyword"), + + // Free + "search.free.fr" => array("Free", "q"), + "search1-2.free.fr" => array("Free", "q"), + "search1-1.free.fr" => array("Free", "q"), + + // Freenet + "suche.freenet.de" => array("Freenet", "query"), + + //Froogle + "froogle.google.com" => array("Google (Froogle)", "q"), + "froogle.google.de" => array("Google (Froogle)", "q"), + "froogle.google.co.uk" => array("Google (Froogle)", "q"), + + //GAIS + "gais.cs.ccu.edu.tw" => array("GAIS)", "query"), + + // Gigablast + "www.gigablast.com" => array("Gigablast", "q"), + "blogs.gigablast.com" => array("Gigablast (Blogs)", "q"), + "travel.gigablast.com" => array("Gigablast (Travel)", "q"), + "dir.gigablast.com" => array("Gigablast (Directory)", "q"), + "gov.gigablast.com" => array("Gigablast (Gov)", "q"), + + // GMX + "suche.gmx.net" => array("GMX", "su"), + "www.gmx.net" => array("GMX", "su"), + + // goo + "search.goo.ne.jp" => array("goo", "mt"), + "ocnsearch.goo.ne.jp" => array("goo", "mt"), + + + // Google + "www.google.com" => array("Google", "q"), + "gogole.fr" => array("Google", "q"), + "www.gogole.fr" => array("Google", "q"), + "wwwgoogle.fr" => array("Google", "q"), + "ww.google.fr" => array("Google", "q"), + "w.google.fr" => array("Google", "q"), + "www.google.fr" => array("Google", "q"), + "www.google.fr." => array("Google", "q"), + "google.fr" => array("Google", "q"), + "www2.google.com" => array("Google", "q"), + "w.google.com" => array("Google", "q"), + "ww.google.com" => array("Google", "q"), + "wwwgoogle.com" => array("Google", "q"), + "www.gogole.com" => array("Google", "q"), + "www.gppgle.com" => array("Google", "q"), + "go.google.com" => array("Google", "q"), + "www.google.ae" => array("Google", "q"), + "www.google.as" => array("Google", "q"), + "www.google.at" => array("Google", "q"), + "wwwgoogle.at" => array("Google", "q"), + "ww.google.at" => array("Google", "q"), + "w.google.at" => array("Google", "q"), + "www.google.az" => array("Google", "q"), + "www.google.be" => array("Google", "q"), + "www.google.bg" => array("Google", "q"), + "www.google.ba" => array("Google", "q"), + "google.bg" => array("Google", "q"), + "www.google.bi" => array("Google", "q"), + "www.google.ca" => array("Google", "q"), + "ww.google.ca" => array("Google", "q"), + "w.google.ca" => array("Google", "q"), + "www.google.cc" => array("Google", "q"), + "www.google.cd" => array("Google", "q"), + "www.google.cg" => array("Google", "q"), + "www.google.ch" => array("Google", "q"), + "ww.google.ch" => array("Google", "q"), + "w.google.ch" => array("Google", "q"), + "www.google.ci" => array("Google", "q"), + "www.google.cl" => array("Google", "q"), + "www.google.cn" => array("Google", "q"), + "www.google.co" => array("Google", "q"), + "www.google.cz" => array("Google", "q"), + "wwwgoogle.cz" => array("Google", "q"), + "www.google.de" => array("Google", "q"), + "ww.google.de" => array("Google", "q"), + "w.google.de" => array("Google", "q"), + "wwwgoogle.de" => array("Google", "q"), + "www.googleearth.de" => array("Google", "q"), + "googleearth.de" => array("Google", "q"), + "google.gr" => array("Google", "q"), + "google.hr" => array("Google", "q"), + "www.google.dj" => array("Google", "q"), + "www.google.dk" => array("Google", "q"), + "www.google.es" => array("Google", "q"), + "www.google.fi" => array("Google", "q"), + "www.google.fm" => array("Google", "q"), + "www.google.gg" => array("Google", "q"), + "www.googel.fi" => array("Google", "q"), + "www.googleearth.fr" => array("Google", "q"), + "www.google.gl" => array("Google", "q"), + "www.google.gm" => array("Google", "q"), + "www.google.gr" => array("Google", "q"), + "www.google.hn" => array("Google", "q"), + "www.google.hr" => array("Google", "q"), + "www.google.hu" => array("Google", "q"), + "www.google.ie" => array("Google", "q"), + "www.google.is" => array("Google", "q"), + "www.google.it" => array("Google", "q"), + "www.google.jo" => array("Google", "q"), + "www.google.kz" => array("Google", "q"), + "www.google.li" => array("Google", "q"), + "www.google.lt" => array("Google", "q"), + "www.google.lu" => array("Google", "q"), + "www.google.lv" => array("Google", "q"), + "www.google.ms" => array("Google", "q"), + "www.google.mu" => array("Google", "q"), + "www.google.mw" => array("Google", "q"), + "www.google.md" => array("Google", "q"), + "www.google.nl" => array("Google", "q"), + "www.google.no" => array("Google", "q"), + "www.google.pl" => array("Google", "q"), + "www.google.sk" => array("Google", "q"), + "www.google.pn" => array("Google", "q"), + "www.google.pt" => array("Google", "q"), + "www.google.dk" => array("Google", "q"), + "www.google.ro" => array("Google", "q"), + "www.google.ru" => array("Google", "q"), + "www.google.rw" => array("Google", "q"), + "www.google.se" => array("Google", "q"), + "www.google.sn" => array("Google", "q"), + "www.google.sh" => array("Google", "q"), + "www.google.si" => array("Google", "q"), + "www.google.sm" => array("Google", "q"), + "www.google.td" => array("Google", "q"), + "www.google.tt" => array("Google", "q"), + "www.google.uz" => array("Google", "q"), + "www.google.vg" => array("Google", "q"), + "www.google.com.ar" => array("Google", "q"), + "www.google.com.au" => array("Google", "q"), + "www.google.com.bo" => array("Google", "q"), + "www.google.com.br" => array("Google", "q"), + "www.google.com.co" => array("Google", "q"), + "www.google.com.cu" => array("Google", "q"), + "www.google.com.ec" => array("Google", "q"), + "www.google.com.eg" => array("Google", "q"), + "www.google.com.do" => array("Google", "q"), + "www.google.com.fj" => array("Google", "q"), + "www.google.com.gr" => array("Google", "q"), + "www.google.com.gt" => array("Google", "q"), + "www.google.com.hk" => array("Google", "q"), + "www.google.com.ly" => array("Google", "q"), + "www.google.com.mt" => array("Google", "q"), + "www.google.com.mx" => array("Google", "q"), + "www.google.com.my" => array("Google", "q"), + "www.google.com.nf" => array("Google", "q"), + "www.google.com.ni" => array("Google", "q"), + "www.google.com.np" => array("Google", "q"), + "www.google.com.pa" => array("Google", "q"), + "www.google.com.pe" => array("Google", "q"), + "www.google.com.ph" => array("Google", "q"), + "www.google.com.pk" => array("Google", "q"), + "www.google.com.pl" => array("Google", "q"), + "www.google.com.pr" => array("Google", "q"), + "www.google.com.py" => array("Google", "q"), + "www.google.com.qa" => array("Google", "q"), + "www.google.com.om" => array("Google", "q"), + "www.google.com.ru" => array("Google", "q"), + "www.google.com.sg" => array("Google", "q"), + "www.google.com.sa" => array("Google", "q"), + "www.google.com.sv" => array("Google", "q"), + "www.google.com.tr" => array("Google", "q"), + "www.google.com.tw" => array("Google", "q"), + "www.google.com.ua" => array("Google", "q"), + "www.google.com.uy" => array("Google", "q"), + "www.google.com.vc" => array("Google", "q"), + "www.google.com.vn" => array("Google", "q"), + "www.google.co.cr" => array("Google", "q"), + "www.google.co.gg" => array("Google", "q"), + "www.google.co.hu" => array("Google", "q"), + "www.google.co.id" => array("Google", "q"), + "www.google.co.il" => array("Google", "q"), + "www.google.co.in" => array("Google", "q"), + "www.google.co.je" => array("Google", "q"), + "www.google.co.jp" => array("Google", "q"), + "www.google.co.ls" => array("Google", "q"), + "www.google.co.ke" => array("Google", "q"), + "www.google.co.kr" => array("Google", "q"), + "www.google.co.nz" => array("Google", "q"), + "www.google.co.th" => array("Google", "q"), + "www.google.co.uk" => array("Google", "q"), + "www.google.co.ve" => array("Google", "q"), + "www.google.co.za" => array("Google", "q"), + "www.google.co.ma" => array("Google", "q"), + "www.goggle.com" => array("Google", "q"), + + + // Powered by Google + "www.charter.net" => array("Google", "q"), + "brisbane.t-online.de" => array("Google", "q"), + "miportal.bellsouth.net" => array("Google", "string"), + "home.bellsouth.net" => array("Google", "string"), + "pesquisa.clix.pt" => array("Google", "q"), + "google.startsiden.no" => array("Google", "q"), + "google.startpagina.nl" => array("Google", "q"), + "search.peoplepc.com" => array("Google", "q"), + "www.google.interia.pl" => array("Google", "q"), + "buscador.terra.es" => array("Google", "query"), + "buscador.terra.cl" => array("Google", "query"), + "buscador.terra.com.br" => array("Google", "query"), + "www.icq.com" => array("Google", "q"), + "www.adelphia.net" => array("Google", "q"), + "so.qq.com" => array("Google", "word"), + "misc.skynet.be" => array("Google", "keywords"), + "www.start.no" => array("Google", "q"), + "verden.abcsok.no" => array("Google", "q"), + "search.sweetim.com" => array("Google", "q"), + + + //Google Blogsearch + "blogsearch.google.com" => array("Google Blogsearch", "q"), + "blogsearch.google.de" => array("Google Blogsearch", "q"), + "blogsearch.google.fr" => array("Google Blogsearch", "q"), + "blogsearch.google.co.uk" => array("Google Blogsearch", "q"), + "blogsearch.google.it" => array("Google Blogsearch", "q"), + "blogsearch.google.net" => array("Google Blogsearch", "q"), + "blogsearch.google.es" => array("Google Blogsearch", "q"), + "blogsearch.google.ru" => array("Google Blogsearch", "q"), + "blogsearch.google.be" => array("Google Blogsearch", "q"), + "blogsearch.google.nl" => array("Google Blogsearch", "q"), + "blogsearch.google.at" => array("Google Blogsearch", "q"), + "blogsearch.google.ch" => array("Google Blogsearch", "q"), + "blogsearch.google.pl" => array("Google Blogsearch", "q"), + + + // Google translation + "translate.google.com" => array("Google Translations", "q"), + + // Google Directory + "directory.google.com" => array("Google Directory", " "), + + // Google Images + "images.google.fr" => array("Google Images", "q"), + "images.google.be" => array("Google Images", "q"), + "images.google.ca" => array("Google Images", "q"), + "images.google.co.uk" => array("Google Images", "q"), + "images.google.de" => array("Google Images", "q"), + "images.google.it" => array("Google Images", "q"), + "images.google.at" => array("Google Images", "q"), + "images.google.bg" => array("Google Images", "q"), + "images.google.ch" => array("Google Images", "q"), + "images.google.ci" => array("Google Images", "q"), + "images.google.com.au" => array("Google Images", "q"), + "images.google.com.cu" => array("Google Images", "q"), + "images.google.co.id" => array("Google Images", "q"), + "images.google.co.il" => array("Google Images", "q"), + "images.google.co.in" => array("Google Images", "q"), + "images.google.co.jp" => array("Google Images", "q"), + "images.google.co.hu" => array("Google Images", "q"), + "images.google.co.kr" => array("Google Images", "q"), + "images.google.co.nz" => array("Google Images", "q"), + "images.google.co.th" => array("Google Images", "q"), + "images.google.co.tw" => array("Google Images", "q"), + "images.google.co.ve" => array("Google Images", "q"), + "images.google.com.ar" => array("Google Images", "q"), + "images.google.com.br" => array("Google Images", "q"), + "images.google.com.cu" => array("Google Images", "q"), + "images.google.com.do" => array("Google Images", "q"), + "images.google.com.gr" => array("Google Images", "q"), + "images.google.com.hk" => array("Google Images", "q"), + "images.google.com.mx" => array("Google Images", "q"), + "images.google.com.my" => array("Google Images", "q"), + "images.google.com.pe" => array("Google Images", "q"), + "images.google.com.tr" => array("Google Images", "q"), + "images.google.com.tw" => array("Google Images", "q"), + "images.google.com.ua" => array("Google Images", "q"), + "images.google.com.vn" => array("Google Images", "q"), + "images.google.dk" => array("Google Images", "q"), + "images.google.es" => array("Google Images", "q"), + "images.google.fi" => array("Google Images", "q"), + "images.google.gg" => array("Google Images", "q"), + "images.google.gr" => array("Google Images", "q"), + "images.google.it" => array("Google Images", "q"), + "images.google.ms" => array("Google Images", "q"), + "images.google.nl" => array("Google Images", "q"), + "images.google.no" => array("Google Images", "q"), + "images.google.pl" => array("Google Images", "q"), + "images.google.pt" => array("Google Images", "q"), + "images.google.ro" => array("Google Images", "q"), + "images.google.ru" => array("Google Images", "q"), + "images.google.se" => array("Google Images", "q"), + "images.google.sk" => array("Google Images", "q"), + "images.google.com" => array("Google Images", "q"), + + // Google News + "news.google.com" => array("Google News", "q"), + "news.google.se" => array("Google News", "q"), + "news.google.com" => array("Google News", "q"), + "news.google.es" => array("Google News", "q"), + "news.google.ch" => array("Google News", "q"), + "news.google.lt" => array("Google News", "q"), + "news.google.ie" => array("Google News", "q"), + "news.google.de" => array("Google News", "q"), + "news.google.cl" => array("Google News", "q"), + "news.google.com.ar" => array("Google News", "q"), + "news.google.fr" => array("Google News", "q"), + "news.google.ca" => array("Google News", "q"), + "news.google.co.uk" => array("Google News", "q"), + "news.google.co.jp" => array("Google News", "q"), + "news.google.com.pe" => array("Google News", "q"), + "news.google.com.au" => array("Google News", "q"), + "news.google.com.mx" => array("Google News", "q"), + "news.google.com.hk" => array("Google News", "q"), + "news.google.co.in" => array("Google News", "q"), + "news.google.at" => array("Google News", "q"), + "news.google.com.tw" => array("Google News", "q"), + "news.google.com.co" => array("Google News", "q"), + "news.google.co.ve" => array("Google News", "q"), + "news.google.lu" => array("Google News", "q"), + "news.google.com.ly" => array("Google News", "q"), + "news.google.it" => array("Google News", "q"), + "news.google.sm" => array("Google News", "q"), + + // Goyellow.de + "www.goyellow.de" => array("GoYellow.de", "MDN"), + + // HighBeam + "www.highbeam.com" => array("HighBeam", "Q"), + + // Hit-Parade + "recherche.hit-parade.com" => array("Hit-Parade", "p7"), + "class.hit-parade.com" => array("Hit-Parade", "p7"), + + // Hotbot via Lycos + "hotbot.lycos.com" => array("Hotbot (Lycos)", "query"), + "search.hotbot.de" => array("Hotbot", "query"), + "search.hotbot.fr" => array("Hotbot", "query"), + "www.hotbot.com" => array("Hotbot", "query"), + + // 1stekeuze + "zoek.1stekeuze.nl" => array("1stekeuze", "terms"), + + // Infoseek + "search.www.infoseek.co.jp" => array("Infoseek", "qt"), + + // Icerocket + "blogs.icerocket.com" => array("Icerocket", "qt"), + + // ICQ + "www.icq.com" => array("ICQ", "q"), + "search.icq.com" => array("ICQ", "q"), + + // Ilse + "spsearch.ilse.nl" => array("Startpagina", "search_for"), + "be.ilse.nl" => array("Ilse BE", "query"), + "search.ilse.nl" => array("Ilse NL", "search_for"), + + // Iwon + "search.iwon.com" => array("Iwon", "searchfor"), + + // Ixquick + "ixquick.com" => array("Ixquick", "query"), + "www.eu.ixquick.com" => array("Ixquick", "query"), + "us.ixquick.com" => array("Ixquick", "query"), + "s1.us.ixquick.com" => array("Ixquick", "query"), + "s2.us.ixquick.com" => array("Ixquick", "query"), + "s3.us.ixquick.com" => array("Ixquick", "query"), + "s4.us.ixquick.com" => array("Ixquick", "query"), + "s5.us.ixquick.com" => array("Ixquick", "query"), + "eu.ixquick.com" => array("Ixquick","query"), + + // Jyxo + "jyxo.cz" => array("Jyxo", "q"), + + // Jungle Spider + "www.jungle-spider.de" => array("Jungle Spider", "qry"), + + // Kartoo + "kartoo.com" => array("Kartoo", ""), + "kartoo.de" => array("Kartoo", ""), + "kartoo.fr" => array("Kartoo", ""), + + + // Kataweb + "www.kataweb.it" => array("Kataweb", "q"), + + // Klug suchen + "www.klug-suchen.de" => array("Klug suchen!", "query"), + + // La Toile Du Québec via Google + "google.canoe.com" => array("La Toile Du Québec (Google)", "q"), + "www.toile.com" => array("La Toile Du Québec (Google)", "q"), + "web.toile.com" => array("La Toile Du Québec (Google)", "q"), + + // La Toile Du Québec + "recherche.toile.qc.ca" => array("La Toile Du Québec", "query"), + + // Live.com + "www.live.com" => array("Live", "q"), + "beta.search.live.com" => array("Live", "q"), + "search.live.com" => array("Live", "q"), + "g.msn.com" => array("Live", " "), + + // Looksmart + "www.looksmart.com" => array("Looksmart", "key"), + + // Lycos + "search.lycos.com" => array("Lycos", "query"), + "vachercher.lycos.fr" => array("Lycos", "query"), + "www.lycos.fr" => array("Lycos", "query"), + "suche.lycos.de" => array("Lycos", "query"), + "search.lycos.de" => array("Lycos", "query"), + "sidesearch.lycos.com" => array("Lycos", "query"), + "www.multimania.lycos.fr" => array("Lycos", "query"), + "buscador.lycos.es" => array("Lycos", "query"), + + // Mail.ru + "go.mail.ru" => array("Mailru", "q"), + + // Mamma + "mamma.com" => array("Mamma", "query"), + "mamma75.mamma.com" => array("Mamma", "query"), + "www.mamma.com" => array("Mamma", "query"), + + // Meceoo + "www.meceoo.fr" => array("Meceoo", "kw"), + + // Mediaset + "servizi.mediaset.it" => array("Mediaset", "searchword"), + + // Metacrawler + "search.metacrawler.com" => array("Metacrawler", "general"), + + // Metager + "mserv.rrzn.uni-hannover.de" => array("Metager", "eingabe"), + "www.metager.de" => array("Metager", "eingabe"), + + // Metager2 + "www.metager2.de" => array("Metager2", "q"), + "metager2.de" => array("Metager2", "q"), + + // Meinestadt + "www.meinestadt.de" => array("Meinestadt.de", "words"), + + // Monstercrawler + "www.monstercrawler.com" => array("Monstercrawler", "qry"), + + // Mozbot + "www.mozbot.fr" => array("mozbot", "q"), + "www.mozbot.co.uk" => array("mozbot", "q"), + "www.mozbot.com" => array("mozbot", "q"), + + // MSN + "search.msn.com" => array("MSN", "q"), + "beta.search.msn.fr" => array("MSN", "q"), + "search.msn.fr" => array("MSN", "q"), + "search.msn.es" => array("MSN", "q"), + "search.msn.se" => array("MSN", "q"), + "search.latam.msn.com" => array("MSN", "q"), + "search.msn.nl" => array("MSN", "q"), + "leguide.fr.msn.com" => array("MSN", "s"), + "leguide.msn.fr" => array("MSN", "s"), + "search.msn.co.jp" => array("MSN", "q"), + "search.msn.no" => array("MSN", "q"), + "search.msn.at" => array("MSN", "q"), + "search.msn.com.hk" => array("MSN", "q"), + "search.t1msn.com.mx" => array("MSN", "q"), + "fr.ca.search.msn.com" => array("MSN", "q"), + "search.msn.be" => array("MSN", "q"), + "search.fr.msn.be" => array("MSN", "q"), + "search.msn.it" => array("MSN", "q"), + "sea.search.msn.it" => array("MSN", "q"), + "sea.search.msn.fr" => array("MSN", "q"), + "sea.search.msn.de" => array("MSN", "q"), + "sea.search.msn.com" => array("MSN", "q"), + "sea.search.fr.msn.be" => array("MSN", "q"), + "search.msn.com.tw" => array("MSN", "q"), + "search.msn.de" => array("MSN", "q"), + "search.msn.co.uk" => array("MSN", "q"), + "search.msn.co.za" => array("MSN", "q"), + "search.msn.ch" => array("MSN", "q"), + "search.msn.es" => array("MSN", "q"), + "search.msn.com.br" => array("MSN", "q"), + "search.ninemsn.com.au" => array("MSN", "q"), + "search.msn.dk" => array("MSN", "q"), + "search.arabia.msn.com" => array("MSN", "q"), + "search.prodigy.msn.com" => array("MSN", "q"), + + // El Mundo + "ariadna.elmundo.es" => array("El Mundo", "q"), + + // MyWebSearch + "kf.mysearch.myway.com" => array("MyWebSearch", "searchfor"), + "ms114.mysearch.com" => array("MyWebSearch", "searchfor"), + "ms146.mysearch.com" => array("MyWebSearch", "searchfor"), + "mysearch.myway.com" => array("MyWebSearch", "searchfor"), + "searchfr.myway.com" => array("MyWebSearch", "searchfor"), + "ki.mysearch.myway.com" => array("MyWebSearch", "searchfor"), + "search.mywebsearch.com" => array("MyWebSearch", "searchfor"), + "www.mywebsearch.com" => array("MyWebSearch", "searchfor"), + + // Najdi + "www.najdi.si" => array("Najdi.si", "q"), + + // Needtofind + "ko.search.need2find.com" => array("Needtofind", "searchfor"), + + // Netster + "www.netster.com" => array("Netster", "keywords"), + + // Netscape + "search-intl.netscape.com" => array("Netscape", "search"), + "www.netscape.fr" => array("Netscape", "q"), + "suche.netscape.de" => array("Netscape", "q"), + "search.netscape.com" => array("Netscape", "query"), + + // Nomade + "ie4.nomade.fr" => array("Nomade", "s"), + "rechercher.nomade.aliceadsl.fr"=> array("Nomade (AliceADSL)", "s"), + "rechercher.nomade.fr" => array("Nomade", "s"), + + // Northern Light + "www.northernlight.com" => array("Northern Light", "qr"), + + // Numéricable + "www.numericable.fr" => array("Numéricable", "query"), + + // Onet + "szukaj.onet.pl" => array("Onet.pl", "qt"), + + // Opera + "search.opera.com" => array("Opera", "search"), + + // Openfind + "wps.openfind.com.tw" => array("Openfind (Websearch)", "query"), + "bbs2.openfind.com.tw" => array("Openfind (BBS)", "query"), + "news.openfind.com.tw" => array("Openfind (News)", "query"), + + // Overture + "www.overture.com" => array("Overture", "Keywords"), + "www.fr.overture.com" => array("Overture", "Keywords"), + + // Paperball + "suche.paperball.de" => array("Paperball", "query"), + + // Picsearch + "www.picsearch.com" => array("Picsearch", "q"), + + // Plazoo + "www.plazoo.com" => array("Plazoo", "q"), + + // Postami + "www.postami.com" => array("Postami", "query"), + + // Quick searches + "data.quicksearches.net" => array("QuickSearches", "q"), + + // Qualigo + "www.qualigo.de" => array("Qualigo", "q"), + "www.qualigo.ch" => array("Qualigo", "q"), + "www.qualigo.at" => array("Qualigo", "q"), + "www.qualigo.nl" => array("Qualigo", "q"), + + // Rambler + "search.rambler.ru" => array("Rambler", "words"), + + // Reacteur.com + "www.reacteur.com" => array("Reacteur", "kw"), + + // Sapo + "pesquisa.sapo.pt" => array("Sapo","q"), + + // Search.com + "www.search.com" => array("Search.com", "q"), + + // Search.ch + "www.search.ch" => array("Search.ch", "q"), + + // Search a lot + "www.searchalot.com" => array("Searchalot", "query"), + + // Seek + "www.seek.fr" => array("Searchalot", "qry_str"), + + // Seekport + "www.seekport.de" => array("Seekport", "query"), + "www.seekport.co.uk" => array("Seekport", "query"), + "www.seekport.fr" => array("Seekport", "query"), + "www.seekport.at" => array("Seekport", "query"), + "www.seekport.es" => array("Seekport", "query"), + "www.seekport.it" => array("Seekport", "query"), + + // Seekport (blogs) + "blogs.seekport.de" => array("Seekport (Blogs)", "query"), + "blogs.seekport.co.uk" => array("Seekport (Blogs)", "query"), + "blogs.seekport.fr" => array("Seekport (Blogs)", "query"), + "blogs.seekport.at" => array("Seekport (Blogs)", "query"), + "blogs.seekport.es" => array("Seekport (Blogs)", "query"), + "blogs.seekport.it" => array("Seekport (Blogs)", "query"), + + // Seekport (news) + "news.seekport.de" => array("Seekport (News)", "query"), + "news.seekport.co.uk" => array("Seekport (News)", "query"), + "news.seekport.fr" => array("Seekport (News)", "query"), + "news.seekport.at" => array("Seekport (News)", "query"), + "news.seekport.es" => array("Seekport (News)", "query"), + "news.seekport.it" => array("Seekport (News)", "query"), + + // Searchscout + "www.searchscout.com" => array("Search Scout", "gt_keywords"), + + // Searchy + "www.searchy.co.uk" => array("Searchy", "search_term"), + + // Seznam + "search1.seznam.cz" => array("Seznam", "q"), + "search2.seznam.cz" => array("Seznam", "q"), + "search.seznam.cz" => array("Seznam", "q"), + + // Sharelook + "www.sharelook.fr" => array("Sharelook", "keyword"), + "www.sharelook.de" => array("Sharelook", "keyword"), + + // Skynet + "search.skynet.be" => array("Skynet", "keywords"), + + // Sphere + "www.sphere.com" => array("Sphere", "q"), + + // Startpagina + "startgoogle.startpagina.nl" => array("Startpagina (Google)", "q"), + + // Suchnase + "www.suchnase.de" => array("Suchnase", "qkw"), + + // Supereva + "search.supereva.com" => array("Supereva", "q"), + + // Sympatico + "search.sympatico.msn.ca" => array("Sympatico", "q"), + "search.sli.sympatico.ca" => array("Sympatico", "q"), + "search.fr.sympatico.msn.ca" => array("Sympatico", "q"), + "sea.search.fr.sympatico.msn.ca"=> array("Sympatico", "q"), + + // Suchmaschine.com + "www.suchmaschine.com" => array("Suchmaschine.com", "suchstr"), + + //Technorati + "www.technorati.com" => array("Technorati", " "), + + // Teoma + "www.teoma.com" => array("Teoma", "t"), + + // Tiscali + "rechercher.nomade.tiscali.fr" => array("Tiscali", "s"), + "search-dyn.tiscali.it" => array("Tiscali", "key"), + "www.tiscali.co.uk" => array("Tiscali", "query"), + "search-dyn.tiscali.de" => array("Tiscali", "key"), + "hledani.tiscali.cz" => array("Tiscali", "query", "windows-1250"), + + // T-Online + "suche.t-online.de" => array("T-Online", "q"), + + // Trouvez.com + "www.trouvez.com" => array("Trouvez.com", "query"), + + // Trusted-Search + + "www.trusted--search.com" => array("Trusted Search", "w"), + + // Vinden + "zoek.vinden.nl" => array("Vinden", "query"), + + // Vindex + "www.vindex.nl" => array("Vindex","search_for"), + + // Virgilio + "search.virgilio.it" => array("Virgilio", "qs"), + + // Voila + "search.voila.com" => array("Voila", "kw"), + "search.ke.voila.fr" => array("Voila", "rdata"), + "moteur.voila.fr" => array("Voila", "kw"), + "search.voila.fr" => array("Voila", "kw"), + "beta.voila.fr" => array("Voila", "kw"), + + // Volny + "web.volny.cz" => array("Volny", "search", "windows-1250"), + + // Wanadoo + "search.ke.wanadoo.fr" => array("Wanadoo", "kw"), + "busca.wanadoo.es" => array("Wanadoo", "buscar"), + + // Web.de + "suche.web.de" => array("Web.de (Websuche)", "su"), + "dir.web.de" => array("Web.de (Directory)", "su"), + + // Webtip + "www.webtip.de" => array("Webtip", "keyword"), + + // X-recherche + "www.x-recherche.com" => array("X-Recherche", "mots"), + + // Yahoo + "search.yahoo.com" => array("Yahoo!", "p"), + "ink.yahoo.com" => array("Yahoo!", "p"), + "ink.yahoo.fr" => array("Yahoo!", "p"), + "fr.ink.yahoo.com" => array("Yahoo!", "p"), + "search.yahoo.co.jp" => array("Yahoo!", "p"), + "search.yahoo.fr" => array("Yahoo!", "p"), + "ar.search.yahoo.com" => array("Yahoo!", "p"), + "br.search.yahoo.com" => array("Yahoo!", "p"), + "ch.search.yahoo.com" => array("Yahoo!", "p"), + "de.search.yahoo.com" => array("Yahoo!", "p"), + "ca.search.yahoo.com" => array("Yahoo!", "p"), + "cf.search.yahoo.com" => array("Yahoo!", "p"), + "fr.search.yahoo.com" => array("Yahoo!", "p"), + "espanol.search.yahoo.com" => array("Yahoo!", "p"), + "es.search.yahoo.com" => array("Yahoo!", "p"), + "id.search.yahoo.com" => array("Yahoo!", "p"), + "it.search.yahoo.com" => array("Yahoo!", "p"), + "kr.search.yahoo.com" => array("Yahoo!", "p"), + "mx.search.yahoo.com" => array("Yahoo!", "p"), + "nl.search.yahoo.com" => array("Yahoo!", "p"), + "uk.search.yahoo.com" => array("Yahoo!", "p"), + "cade.search.yahoo.com" => array("Yahoo!", "p"), + "tw.search.yahoo.com" => array("Yahoo!", "p"), + "www.yahoo.com.cn" => array("Yahoo!", "p"), + + "de.dir.yahoo.com" => array("Yahoo! Webverzeichnis", ""), + "cf.dir.yahoo.com" => array("Yahoo! Directory", ""), + "fr.dir.yahoo.com" => array("Yahoo! Directory", ""), + + // Yandex + "www.yandex.ru" => array("Yandex", "text"), + "yandex.ru" => array("Yandex", "text"), + "search.yaca.yandex.ru" => array("Yandex", "text"), + "ya.ru" => array("Yandex", "text"), + "www.ya.ru" => array("Yandex", "text"), + "images.yandex.ru" => array("Yandex Images","text"), + + //Yellowmap + + "www.yellowmap.de" => array("Yellowmap", " "), + "yellowmap.de" => array("Yellowmap", " "), + + // Wanadoo + "search.ke.wanadoo.fr" => array("Wanadoo", "kw"), + "busca.wanadoo.es" => array("Wanadoo", "buscar"), + + // Wedoo + "fr.wedoo.com" => array("Wedoo", "keyword"), + + // Web.nl + "www.web.nl" => array("Web.nl","query"), + + // Weborama + "www.weborama.fr" => array("weborama", "query"), + + // WebSearch + "is1.websearch.com" => array("WebSearch", "qkw"), + "www.websearch.com" => array("WebSearch", "qkw"), + "websearch.cs.com" => array("WebSearch", "query"), + + // Witch + "www.witch.de" => array("Witch", "search"), + + // WXS + "wxsl.nl" => array("Planet Internet","q"), + + // Zoek + "www3.zoek.nl" => array("Zoek","q"), + + // Zhongsou + "p.zhongsou.com" => array("Zhongsou","w"), + + // Zoeken + "www.zoeken.nl" => array("Zoeken","query"), + + // Zoohoo + "zoohoo.cz" => array("Zoohoo", "q", "windows-1250"), + "www.zoohoo.cz" => array("Zoohoo", "q", "windows-1250"), + + // Zoznam + "www.zoznam.sk" => array("Zoznam", "s"), + ); + + $GLOBALS['Piwik_SearchEngines_NameToUrl'] = array(); + foreach($GLOBALS['Piwik_SearchEngines'] as $url => $info) + { + if(!isset($GLOBALS['Piwik_SearchEngines_NameToUrl'][$info[0]])) + { + $GLOBALS['Piwik_SearchEngines_NameToUrl'][$info[0]] = $url; + } + } + +} + diff --git a/core/DataTable.php b/core/DataTable.php new file mode 100644 index 0000000000..4ed4226944 --- /dev/null +++ b/core/DataTable.php @@ -0,0 +1,1014 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: DataTable.php 578 2008-07-27 00:15:21Z matt $ + * + * @package Piwik_DataTable + */ + +require_once "DataTable/Renderer.php"; +require_once "DataTable/Renderer/Console.php"; +require_once "DataTable/Filter.php"; +require_once "DataTable/Row.php"; +require_once "DataTable/Manager.php"; + +/** + * + * ---- DataTable + * A DataTable is a data structure used to store complex tables of data. + * + * A DataTable is composed of multiple DataTable_Row. + * A DataTable can be applied one or several DataTable_Filter. + * A DataTable can be given to a DataTable_Renderer that would export the data under a given format (XML, HTML, etc.). + * + * A DataTable has the following features: + * - serializable to be stored in the DB + * - loadable from the serialized version + * - efficient way of loading data from an external source (from a PHP array structure) + * - very simple interface to get data from the table + * + * ---- DataTable_Row + * A DataTableRow in the table is defined by + * - multiple columns (a label, multiple values, ...) + * - optional metadata + * - optional - a sub DataTable associated to this row + * + * Simple row example: + * - columns = array( 'label' => 'Firefox', + * 'visitors' => 155, + * 'pages' => 214, + * 'bounce_rate' => 67) + * - metadata = array('logo' => '/img/browsers/FF.png') + * - no sub DataTable + * + * A more complex example would be a DataTable_Row that is associated to a sub DataTable. + * For example, for the row of the search engine Google, + * we want to get the list of keywords associated, with their statistics. + * - columns = array( 'label' => 'Google', + * 'visits' => 1550, + * 'visits_length' => 514214, + * 'returning_visits' => 77) + * - metadata = array( 'logo' => '/img/search/google.png', + * 'url' => 'http://google.com') + * - DataTable = DataTable containing several DataTable_Row containing the keywords information for this search engine + * Example of one DataTable_Row + * - the keyword columns specific to this search engine = + * array( 'label' => 'Piwik', // the keyword + * 'visitors' => 155, // Piwik has been searched on Google by 155 visitors + * 'pages' => 214 // Visitors coming from Google with the kwd Piwik have seen 214 pages + * ) + * - the keyword metadata = array() // nothing here, but we could imagining storing the URL of the search in Google for example + * - no subTable + * + * + * ---- DataTable_Filter + * A DataTable_Filter is a applied to a DataTable and so + * can filter information in the multiple DataTable_Row. + * + * For example a DataTable_Filter can: + * - remove rows from the table, + * for example the rows' labels that do not match a given searched pattern + * for example the rows' values that are less than a given percentage (low population) + * - return a subset of the DataTable + * for example a function that apply a limit: $offset, $limit + * - add / remove columns + * for example adding a column that gives the percentage of a given value + * - add some metadata + * for example the 'logo' path if the filter detects the logo + * - edit the value, the label + * - change the rows order + * for example if we want to sort by Label alphabetical order, or by any column value + * + * When several DataTable_Filter are to be applied to a DataTable they are applied sequentially. + * A DataTable_Filter is assigned a priority. + * For example, filters that + * - sort rows should be applied with the highest priority + * - remove rows should be applied with a high priority as they prune the data and improve performance. + * + * ---- Code example + * + * $table = new DataTable; + * $table->loadFromArray( array(...) ); + * + * # sort the table by visits asc + * $filter = new DataTable_Filter_Sort( $table, 'visits', 'asc'); + * $tableFiltered = $filter->getTableFiltered(); + * + * # add a filter to select only the website with a label matching '*.com' (regular expression) + * $filter = new DataTable_Filter_Pattern( $table, 'label', '*(.com)'); + * $tableFiltered = $filter->getTableFiltered(); + * + * # keep the 20 elements from offset 15 + * $filter = new DataTable_Filter_Limit( $tableFiltered, 15, 20); + * $tableFiltered = $filter->getTableFiltered(); + * + * # add a column computing the percentage of visits + * # params = table, column containing the value, new column name to add, number of total visits to use to compute the % + * $filter = new DataTable_Filter_AddColumnPercentage( $tableFiltered, 'visits', 'visits_percentage', 2042); + * $tableFiltered = $filter->getTableFiltered(); + * + * # we get the table as XML + * $xmlOutput = new DataTable_Exporter_Xml( $table ); + * $xmlOutput->setHeader( ... ); + * $xmlOutput->setColumnsToExport( array('visits', 'visits_percent', 'label') ); + * $XMLstring = $xmlOutput->getOutput(); + * + * + * ---- Other (ideas) + * We can also imagine building a DataTable_Compare which would take N DataTable that have the same + * structure and would compare them, by computing the percentages of differences, etc. + * + * For example + * DataTable1 = [ keyword1, 1550 visits] + * [ keyword2, 154 visits ] + * DataTable2 = [ keyword1, 1004 visits ] + * [ keyword3, 659 visits ] + * DataTable_Compare = result of comparison of table1 with table2 + * [ keyword1, +154% ] + * [ keyword2, +1000% ] + * [ keyword3, -430% ] + * + * @see Piwik_DataTable_Row A Piwik_DataTable is composed of Piwik_DataTable_Row + * + * @package Piwik + * @subpackage Piwik_DataTable + * + */ + +class Piwik_DataTable +{ + /** + * Array of Piwik_DataTable_Row + * + * @var array + */ + protected $rows = array(); + + /** + * Id assigned to the DataTable, used to lookup the table using the DataTable_Manager + * + * @var int + */ + protected $currentId; + + /** + * Current depth level of this data table + * 0 is the parent data table + * + * @var int + */ + protected $depthLevel = 0; + + /** + * This flag is set to false once we modify the table in a way that outdates the index + * + * @var bool + */ + protected $indexNotUpToDate = false; + + /** + * List of Piwik_DataTable_Filter queued to this table + * + * @var array + */ + protected $queuedFilters = array(); + + /** + * We keep track of the number of rows before applying the LIMIT filter that deletes some rows + * + * @var int + */ + protected $rowsCountBeforeLimitFilter = 0; + + /** + * Defaults to false for performance reasons (most of the time we don't need recursive sorting so we save a looping over the dataTable) + * + * @var bool + */ + protected $enableRecursiveSort = false; + + /* + * @var Piwik_DataTable_Row + */ + protected $summaryRow = null; + + const ID_SUMMARY_ROW = -1; + const LABEL_SUMMARY_ROW = -1; + + /** + * Maximum nesting level + * + * @var int + */ + const MAXIMUM_DEPTH_LEVEL_ALLOWED = 20; + + /** + * Builds the DataTable, registers itself to the manager + * + */ + public function __construct() + { + $this->currentId = Piwik_DataTable_Manager::getInstance()->addTable($this); + } + + /** + * Sort the dataTable rows using the php callback function + * + * @param string $functionCallback + */ + public function sort( $functionCallback ) + { + $this->indexNotUpToDate = true; + usort( $this->rows, $functionCallback ); + + if($this->enableRecursiveSort === true) + { + foreach($this->getRows() as $row) + { + if(($idSubtable = $row->getIdSubDataTable()) !== null) + { + $table = Piwik_DataTable_Manager::getInstance()->getTable($idSubtable); + $table->enableRecursiveSort(); + $table->sort($functionCallback); + } + } + } + } + + /** + * Enables the recursive sort. Means that when using $table->sort() + * it will also sort all subtables using the same callback + * + * @return void + */ + public function enableRecursiveSort() + { + $this->enableRecursiveSort = true; + } + + /** + * Returns the number of rows before we applied the limit filter + * + * @return int + */ + public function getRowsCountBeforeLimitFilter() + { + $toReturn = $this->rowsCountBeforeLimitFilter; + if($toReturn == 0) + { + return $this->getRowsCount(); + } + return $toReturn; + } + + /** + * Saves the current number of rows + * + * @return void + * + */ + function setRowsCountBeforeLimitFilter() + { + $this->rowsCountBeforeLimitFilter = $this->getRowsCount(); + } + + /** + * Queue a DataTable_Filter that will be applied at the end of the process + * (just before sending the datatable back to the browser (or API, etc.) + * + * @param string $className The class name of the filter, eg. Piwik_DataTable_Filter_Limit + * @param array $parameters The parameters to give to the filter, eg. array( $offset, $limit) for the filter Piwik_DataTable_Filter_Limit + */ + public function queueFilter( $className, $parameters = array() ) + { + if(!is_array($parameters)) + { + $parameters = array($parameters); + } + $this->queuedFilters[] = array('className' => $className, 'parameters' => $parameters); + } + + /** + * Apply all filters that were previously queued to this table + * @see queueFilter() + * @return void + */ + public function applyQueuedFilters() + { + foreach($this->queuedFilters as $filter) + { + if($filter['className'] == 'Piwik_DataTable_Filter_Limit') + { + $this->setRowsCountBeforeLimitFilter(); + } + + $reflectionObj = new ReflectionClass($filter['className']); + + // the first parameter of a filter is the DataTable + // we add the current datatable as the parameter + $filter['parameters'] = array_merge(array($this), $filter['parameters']); + + $filter = $reflectionObj->newInstanceArgs($filter['parameters']); + } + $this->queuedFilters = array(); + } + + /** + * Adds a new DataTable to this DataTable + * Go through all the rows of the new DataTable and applies the algorithm: + * - if a row in $table doesnt exist in $this we add the new row to $this + * - if a row exists in both $table and $this we sum the columns values into $this + * - if a row in $this doesnt exist in $table we keep the row of $this without modification + * + * A common row to 2 DataTable is defined by the same label + * + * @example @see tests/plugins/DataTable.test.php + */ + public function addDataTable( Piwik_DataTable $tableToSum ) + { + foreach($tableToSum->getRows() as $row) + { + $labelToLookFor = $row->getColumn('label'); + $rowFound = $this->getRowFromLabel( $labelToLookFor ); + if($rowFound === false) + { + if( $labelToLookFor === self::LABEL_SUMMARY_ROW ) + { + $this->addSummaryRow($row ); + } + else + { + $this->addRow( $row ); + } + } + else + { + $rowFound->sumRow( $row ); + + // if the row to add has a subtable whereas the current row doesn't + // we simply add it (cloning the subtable) + // if the row has the subtable already + // then we have to recursively sum the subtables + if(($idSubTable = $row->getIdSubDataTable()) !== null) + { + $rowFound->sumSubtable( Piwik_DataTable_Manager::getInstance()->getTable($idSubTable) ); + } + } + } + } + + /** + * Returns the Piwik_DataTable_Row that has a column 'label' with the value $label + * + * @param string $label Value of the column 'label' of the row to return + * @return Piwik_DataTable_Row|false The row if found, false otherwise + */ + public function getRowFromLabel( $label ) + { + if($this->indexNotUpToDate) + { + $this->rebuildIndex(); + } + + if($label === self::LABEL_SUMMARY_ROW + && !is_null($this->summaryRow)) + { + return $this->summaryRow; + } + + $label = (string)$label; + if(!isset($this->rowsIndexByLabel[$label])) + { + return false; + } + return $this->rows[$this->rowsIndexByLabel[$label]]; + } + + /** + * Rebuilds the index used to lookup a row by label + * + * @return void + */ + private function rebuildIndex() + { + foreach($this->rows as $id => $row) + { + $label = $row->getColumn('label'); + + if($label !== false) + { + $this->rowsIndexByLabel[$label] = $id; + } + } + $this->indexNotUpToDate = false; + } + + /** + * Returns the ith row in the array + * + * @param int $id + * @return Piwik_DataTable_Row or false if not found + */ + public function getRowFromId($id) + { + if(!isset($this->rows[$id])) + { + if($id == self::ID_SUMMARY_ROW + && !is_null($this->summaryRow)) + { + return $this->summaryRow; + } + return false; + } + return $this->rows[$id]; + } + + /** + * Shortcut function used for performance reasons + * + * @param Piwik_DataTable_Row $row to add at the end of the array + */ + public function addRow( Piwik_DataTable_Row $row ) + { + $this->rows[] = $row; + $this->indexNotUpToDate = true; + } + + /** + * Sets the summary row (a dataTable can have only one summary row) + * + * @param Piwik_DataTable_Row $row + */ + public function addSummaryRow( Piwik_DataTable_Row $row ) + { + $this->summaryRow = $row; + } + + /** + * Returns the dataTable ID + * + * @return int + */ + public function getId() + { + return $this->currentId; + } + + /** + * Adds a new row from a PHP array data structure + * + * @param array $row, eg. array(Piwik_DataTable_Row::COLUMNS => array( 'visits' => 13, 'test' => 'toto'),) + */ + public function addRowFromArray( $row ) + { + $this->loadFromArray(array($row)); + } + + /** + * Adds a new row a PHP array data structure + * + * @param array $row, eg. array('name' => 'google analytics', 'license' => 'commercial') + */ + public function addRowFromSimpleArray( $row ) + { + $this->loadFromSimpleArray(array($row)); + } + + /** + * Returns the array of Piwik_DataTable_Row + * + * @return array of Piwik_DataTable_Row + */ + public function getRows() + { + if(is_null($this->summaryRow)) + { + return $this->rows; + } + else + { + return $this->rows + array(self::ID_SUMMARY_ROW => $this->summaryRow); + } + } + + /** + * Returns the number of rows in the table + * + * @return int + */ + public function getRowsCount() + { + $count = count($this->rows); + if(is_null($this->summaryRow)) + { + return $count; + } + else + { + return $count + 1; + } + } + + /** + * Returns the first row of the DataTable + * + * @return Piwik_DataTable_Row + */ + public function getFirstRow() + { + if(count($this->rows) == 0) + { + if(!is_null($this->summaryRow)) + { + return $this->summaryRow; + } + return false; + } + $row = array_slice($this->rows, 0, 1); + return $row[0]; + } + + /** + * Returns the last row of the DataTable + * + * @return Piwik_DataTable_Row + */ + public function getLastRow() + { + if(!is_null($this->summaryRow)) + { + return $this->summaryRow; + } + + if(count($this->rows) == 0) + { + return false; + } + $row = array_slice($this->rows, -1); + return $row[0]; + } + + /** + * Returns the sum of the number of rows of all the subtables + * + the number of rows in the parent table + * + * @return int + */ + public function getRowsCountRecursive() + { + $totalCount = 0; + foreach($this->rows as $row) + { + if(($idSubTable = $row->getIdSubDataTable()) !== null) + { + $subTable = Piwik_DataTable_Manager::getInstance()->getTable($idSubTable); + $count = $subTable->getRowsCountRecursive(); + $totalCount += $count; + } + } + + $totalCount += $this->getRowsCount(); + return $totalCount; + } + + /** + * Delete a given column $name in all the rows + * + * @param string $name + */ + public function deleteColumn( $name ) + { + foreach($this->getRows() as $row) + { + $row->deleteColumn($name); + } + if(!is_null($this->summaryRow)) + { + $this->summaryRow->deleteColumn($name); + } + } + + /** + * Deletes the ith row + * + * @param int $key + * @throws Exception if the row $id cannot be found + */ + public function deleteRow( $id ) + { + if($id === self::ID_SUMMARY_ROW) + { + $this->summaryRow = null; + return; + } + if(!isset($this->rows[$id])) + { + throw new Exception("Trying to delete unknown row with idkey = $id"); + } + unset($this->rows[$id]); + } + + /** + * Deletes all row from offset, offset + limit. + * If limit is null then limit = $table->getRowsCount() + * + * @param int $offset + * @param int $limit + */ + public function deleteRowsOffset( $offset, $limit = null ) + { + if($limit === 0) + { + return; + } + + $count = $this->getRowsCount(); + if($offset >= $count) + { + return; + } + + // if we delete until the end, we delete the summary row as well + if( is_null($limit) + || $limit >= $count ) + { + $this->summaryRow = null; + } + + if(is_null($limit)) + { + array_splice($this->rows, $offset); + } + else + { + array_splice($this->rows, $offset, $limit); + } + } + + /** + * Deletes the rows from the list of rows ID + * + * @param array $aKeys ID of the rows to delete + * @throws Exception if any of the row to delete couldn't be found + */ + public function deleteRows( array $aKeys ) + { + foreach($aKeys as $key) + { + $this->deleteRow($key); + } + } + + /** + * Returns a simple output of the DataTable for easy visualization + * Example: echo $datatable; + * + * @return string + */ + public function __toString() + { + $renderer = new Piwik_DataTable_Renderer_Console($this); + return (string)$renderer; + } + + /** + * Returns true if both DataTable are exactly the same. + * Used in unit tests. + * + * @param Piwik_DataTable $table1 + * @param Piwik_DataTable $table2 + * @return bool + */ + static public function isEqual(Piwik_DataTable $table1, Piwik_DataTable $table2) + { + $rows1 = $table1->getRows(); + $rows2 = $table2->getRows(); + + $table1->rebuildIndex(); + $table2->rebuildIndex(); + + $countrows1 = $table1->getRowsCount(); + $countrows2 = $table2->getRowsCount(); + + if($countrows1 != $countrows2) + { + return false; + } + + foreach($rows1 as $row1) + { + $row2 = $table2->getRowFromLabel($row1->getColumn('label')); + if($row2 === false) + { + return false; + } + if( !Piwik_DataTable_Row::isEqual($row1,$row2) ) + { + return false; + } + } + + return true; + } + + /** + * The serialization returns a one dimension array containing all the + * serialized DataTable contained in this DataTable. + * We save DataTable in serialized format in the Database. + * Each row of this returned PHP array will be a row in the DB table. + * + * The keys of the array are very important as they are used to define the DataTable + * + * IMPORTANT: The main table (level 0, parent of all tables) will always be indexed by 0 + * even it was created after some other tables. + * It also means that all the parent tables (level 0) will be indexed with 0 in their respective + * serialized arrays. You should never lookup a parent table using the getTable( $id = 0) as it + * won't work. + * + * @throws Exception if an infinite recursion is found (a table row's has a subtable that is one of its parent table) + * @param int If not null, defines the number of rows maximum of the serialized dataTable + * If $addSummaryRowAfterNRows is less than the size of the table, a SummaryRow will be added at the end of the table, that + * is the sum of the values of all the rows after the Nth row. All the rows after the Nth row will be deleted. + * + * @return array Serialized arrays + * array( // Datatable level0 + * 0 => 'eghuighahgaueytae78yaet7yaetae', + * + * // first Datatable level1 + * 1 => 'gaegae gh gwrh guiwh uigwhuige', + * + * //second Datatable level1 + * 2 => 'gqegJHUIGHEQjkgneqjgnqeugUGEQHGUHQE', + * + * //first Datatable level3 (child of second Datatable level1 for example) + * 3 => 'eghuighahgaueytae78yaet7yaetaeGRQWUBGUIQGH&QE', + * ); + * + */ + public function getSerialized( $maximumRowsInDataTable = null, $maximumRowsInSubDataTable = null ) + { + static $depth = 0; + + if($depth > self::MAXIMUM_DEPTH_LEVEL_ALLOWED) + { + throw new Exception("Maximum recursion level of ".self::MAXIMUM_DEPTH_LEVEL_ALLOWED. " reached. You have probably set a DataTable_Row with an associated DataTable which belongs already to its parent hierarchy."); + } + + if( !is_null($maximumRowsInDataTable) ) + { + $filter = new Piwik_DataTable_Filter_AddSummaryRow($this, $maximumRowsInDataTable - 1); + } + + // For each row, get the serialized row + // If it is associated to a sub table, get the serialized table recursively ; + // but returns all serialized tables and subtable in an array of 1 dimension! + $aSerializedDataTable = array(); + foreach($this->rows as $row) + { + if(($idSubTable = $row->getIdSubDataTable()) !== null) + { + $subTable = Piwik_DataTable_Manager::getInstance()->getTable($idSubTable); + $depth++; + $aSerializedDataTable = $aSerializedDataTable + $subTable->getSerialized( $maximumRowsInSubDataTable, $maximumRowsInSubDataTable ); + $depth--; + } + } + // we load the current Id of the DataTable + $forcedId = $this->getId(); + + // if the datatable is the parent we force the Id at 0 (this is part of the specification) + if($depth == 0) + { + $forcedId = 0; + } + + // we then serialize the rows and store them in the serialized dataTable + $aSerializedDataTable[$forcedId] = serialize($this->rows + array( self::ID_SUMMARY_ROW => $this->summaryRow)); + + return $aSerializedDataTable; + } + + /** + * Load a serialized string of a datatable. + * + * Does not load recursively all the sub DataTable. + * They will be loaded only when requesting them specifically. + * + * The function creates all the necessary DataTable_Row + * + * @param string Serialized string of a datatable + * @return void + */ + public function loadFromSerialized( $stringSerialized ) + { + $serialized = unserialize($stringSerialized); + if($serialized === false) + { + throw new Exception("The unserialization has failed!"); + } + $this->loadFromArray($serialized); + } + + /** + * Loads the DataTable from a PHP array data structure + * + * @param array Array with the following structure + * array( + * // row1 + * array( + * Piwik_DataTable_Row::COLUMNS => array( col1_name => value1, col2_name => value2, ...), + * Piwik_DataTable_Row::METADATA => array( metadata1_name => value1, ...), // see Piwik_DataTable_Row + * + * ), + * + * // row2 + * array( ... ), + * + * ) + * @return void + */ + public function loadFromArray( $array ) + { + foreach($array as $id => $row) + { + if(is_array($row)) + { + $row = new Piwik_DataTable_Row($row); + } + if($id == self::ID_SUMMARY_ROW) + { + $this->summaryRow = $row; + } + else + { + $this->addRow($row); + } + } + } + + /** + * Loads the data from a simple php array. + * Basically maps a simple multidimensional php array to a DataTable. + * Not recursive (if a row contains a php array itself, it won't be loaded) + * + * @param array Array with the simple structure: + * array( + * array( col1_name => valueA, col2_name => valueC, ...), + * array( col1_name => valueB, col2_name => valueD, ...), + * ) + */ + public function loadFromSimpleArray( $array ) + { + if(count($array) === 0) + { + return; + } + + // we define an exception we may throw if at one point we notice that we cannot handle the data structure + $e = new Exception(" Data structure returned is not convertible in the requested format.". + " Try to call this method with the parameters '&format=original&serialize=1'". + "; you will get the original php data structure serialized.". + " The data structure looks like this: \n \$data = " . var_export($array, true) . "; "); + + + // first pass to see if the array has the structure + // array(col1_name => val1, col2_name => val2, etc.) + // with val* that are never arrays (only strings/numbers/bool/etc.) + // if we detect such a "simple" data structure we convert it to a row with the correct columns' names + $thisIsNotThatSimple = false; + + foreach($array as $columnName => $columnValue ) + { + if(is_array($columnValue) || is_object($columnValue)) + { + $thisIsNotThatSimple = true; + break; + } + } + if($thisIsNotThatSimple === false) + { + // case when the array is indexed by the default numeric index + if( array_keys($array) == array_keys(array_fill(0, count($array), true)) ) + { + foreach($array as $row) + { + $this->addRow( new Piwik_DataTable_Row( array( Piwik_DataTable_Row::COLUMNS => array($row) ) ) ); + } + } + else + { + $this->addRow( new Piwik_DataTable_Row( array( Piwik_DataTable_Row::COLUMNS => $array ) ) ); + } + // we have converted our simple array to one single row + // => we exit the method as the job is now finished + return; + } + + + foreach($array as $key => $row) + { + // stuff that looks like a line + if(is_array($row)) + { + /** + * We make sure we can convert this PHP array without losing information. + * We are able to convert only simple php array (no strings keys, no sub arrays, etc.) + * + */ + + // if the key is a string it means that some information was contained in this key. + // it cannot be lost during the conversion. Because we are not able to handle properly + // this key, we throw an explicit exception. + if(is_string($key)) + { + throw $e; + } + // if any of the sub elements of row is an array we cannot handle this data structure... + foreach($row as $subRow) + { + if(is_array($subRow)) + { + throw $e; + } + } + $row = new Piwik_DataTable_Row( array( Piwik_DataTable_Row::COLUMNS => $row ) ); + } + // other (string, numbers...) => we build a line from this value + else + { + $row = new Piwik_DataTable_Row( array( Piwik_DataTable_Row::COLUMNS => array($key => $row)) ); + } + $this->addRow($row); + } + } + + /** + * Rewrites the input $array + * array ( + * LABEL => array(col1 => X, col2 => Y), + * LABEL2 => array(col1 => X, col2 => Y), + * ) + * + * to the structure + * array ( + * array( Piwik_DataTable_Row::COLUMNS => array('label' => LABEL, col1 => X, col2 => Y)), + * array( Piwik_DataTable_Row::COLUMNS => array('label' => LABEL2, col1 => X, col2 => Y)), + * ) + * + * The optional parameter $subtablePerLabel is an array of subTable associated to the rows of the $array + * For example if $subtablePerLabel is given + * array( + * LABEL => #Piwik_DataTable_ForLABEL, + * LABEL2 => #Piwik_DataTable_ForLABEL2, + * ) + * + * the $array would become + * array ( + * array( Piwik_DataTable_Row::COLUMNS => array('label' => LABEL, col1 => X, col2 => Y), + * Piwik_DataTable_Row::DATATABLE_ASSOCIATED => #ID DataTable For LABEL + * ), + * array( Piwik_DataTable_Row::COLUMNS => array('label' => LABEL2, col1 => X, col2 => Y) + * Piwik_DataTable_Row::DATATABLE_ASSOCIATED => #ID2 DataTable For LABEL2 + * ), + * ) + * + * @param array $array See method description + * @param array|null $subtablePerLabel see method description + * + * @return void + */ + public function loadFromArrayLabelIsKey( $array, $subtablePerLabel = null) + { + $cleanRow = array(); + foreach($array as $label => $row) + { + // we make sure that the label column is first in the list! + // important for the UI javascript mainly... + // array_merge doesn't work here as it reindex the numeric value + // see the test testMergeArray in PHP_Related.test.php + $cleanRow[Piwik_DataTable_Row::COLUMNS] = array('label' => $label) + $row; + if(!is_null($subtablePerLabel) + // some rows of this table don't have subtables + // (for examplecase of the campaign without keywords ) + && isset($subtablePerLabel[$label]) + ) + { + $cleanRow[Piwik_DataTable_Row::DATATABLE_ASSOCIATED] = $subtablePerLabel[$label]; + } + $this->addRow( new Piwik_DataTable_Row($cleanRow) ); + } + } + + /** + * At destruction we try to free memory + * But php doesn't give us much control on this + */ + public function __destruct() + { + unset($this->rows); + } + +} diff --git a/core/DataTable/Array.php b/core/DataTable/Array.php new file mode 100644 index 0000000000..c4624d0840 --- /dev/null +++ b/core/DataTable/Array.php @@ -0,0 +1,152 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Simple.php 168 2008-01-14 05:26:43Z matt $ + * + * @package Piwik_DataTable + */ + +/** + * The DataTable_Array is a way to store an array of dataTable. + * The Piwik_DataTable_Array implements some of the features of the Piwik_DataTable such as queueFilter, getRowsCount. + * + * @package Piwik_DataTable + */ +class Piwik_DataTable_Array +{ + /** + * Used to store additional information about the DataTable Array. + * For example if the Array is used to store multiple DataTable of UserCountry, + * we can add the metadata of the 'idSite' they refer to, so we can access it later if necessary. + * + * @var array of mixed + */ + public $metadata = array(); + + /** + * Array containing the DataTable withing this Piwik_DataTable_Array + * + * @var array of Piwik_DataTable + */ + protected $array = array(); + + /** + * This is the label used to index the tables. + * For example if the tables are indexed using the timestamp of each period + * eg. $this->array[1045886960] = new Piwik_DataTable; + * the keyName would be 'timestamp'. + * + * This label is used in the Renderer (it becomes a column name or the XML description tag) + * + * @var string + */ + protected $keyName = 'defaultKeyName'; + + /** + * Returns the keyName string @see self::$keyName + * + * @return string + */ + public function getKeyName() + { + return $this->keyName; + } + + /** + * Set the keyName @see self::$keyName + * + * @param string $name + */ + public function setKeyName($name) + { + $this->keyName = $name; + } + + /** + * Returns the number of DataTable in this DataTable_Array + * + * @return int + */ + public function getRowsCount() + { + return count($this->array); + } + + /** + * Queue a filter to the DataTable_Array will queue this filter to every DataTable of the DataTable_Array. + * + * @param string $className Filter name, eg. Piwik_DataTable_Filter_Limit + * @param array $parameters Filter parameters, eg. array( 50, 10 ) + * + * @return void + */ + public function queueFilter( $className, $parameters = array() ) + { + foreach($this->array as $table) + { + $table->queueFilter($className, $parameters); + } + } + + /** + * Apply the filters previously queued to each of the DataTable of this DataTable_Array. + * + * @return void + */ + public function applyQueuedFilters() + { + foreach($this->array as $table) + { + $table->applyQueuedFilters(); + } + } + + /** + * Returns the array of DataTable + * + * @return array of Piwik_DataTable + */ + public function getArray() + { + return $this->array; + } + + /** + * Adds a new DataTable to the DataTable_Array + * + * @param Piwik_DataTable $table + * @param string $label Label used to index this table in the array + */ + public function addTable( $table, $label ) + { + $this->array[$label] = $table; + } + + /** + * Returns a string output of this DataTable_Array (applying the default renderer to every DataTable + * of this DataTable_Array). + * + * @return string + */ + public function __toString() + { + $renderer = new Piwik_DataTable_Renderer_Console($this); + return (string)$renderer; + } + + /** + * @see Piwik_DataTable::enableRecursiveSort() + */ + public function enableRecursiveSort() + { + foreach($this->array as $table) + { + $table->enableRecursiveSort(); + } + } +} + + diff --git a/core/DataTable/Filter.php b/core/DataTable/Filter.php new file mode 100644 index 0000000000..9394b88be1 --- /dev/null +++ b/core/DataTable/Filter.php @@ -0,0 +1,58 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Filter.php 519 2008-06-09 01:59:24Z matt $ + * + * @package Piwik_DataTable + */ + +/** + * A filter is applied instantly to a given DataTable and can + * - remove rows + * - change columns values (lowercase the strings, truncate, etc.) + * - add/remove columns or metadata (compute percentage values, add an 'icon' metadata based on the label, etc.) + * - add/remove/edit sub DataTable associated to some rows + * - whatever you can imagine + * + * The concept is very simple: the filter is given the DataTable + * and can do whatever is necessary on the data (in the filter() method). + * + * @package Piwik_DataTable + * @subpackage Piwik_DataTable_Filter + */ +abstract class Piwik_DataTable_Filter +{ + /* + * @var Piwik_DataTable + */ + protected $table; + + public function __construct($table) + { + if(!($table instanceof Piwik_DataTable)) + { + throw new Exception("The filter accepts only a Piwik_DataTable object."); + } + $this->table = $table; + } + + abstract protected function filter(); +} + +require_once "DataTable/Filter/ColumnCallbackDeleteRow.php"; +require_once "DataTable/Filter/ColumnCallbackAddMetadata.php"; +require_once "DataTable/Filter/ColumnCallbackReplace.php"; +require_once "DataTable/Filter/MetadataCallbackAddMetadata.php"; +require_once "DataTable/Filter/AddConstantMetadata.php"; +require_once "DataTable/Filter/Null.php"; +require_once "DataTable/Filter/ExcludeLowPopulation.php"; +require_once "DataTable/Filter/Limit.php"; +require_once "DataTable/Filter/Pattern.php"; +require_once "DataTable/Filter/PatternRecursive.php"; +require_once "DataTable/Filter/ReplaceColumnNames.php"; +require_once "DataTable/Filter/Sort.php"; +require_once "DataTable/Filter/AddSummaryRow.php"; +require_once "DataTable/Filter/ReplaceSummaryRowLabel.php"; diff --git a/core/DataTable/Filter/AddConstantMetadata.php b/core/DataTable/Filter/AddConstantMetadata.php new file mode 100644 index 0000000000..d306ff9950 --- /dev/null +++ b/core/DataTable/Filter/AddConstantMetadata.php @@ -0,0 +1,43 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: AddConstantMetadata.php 519 2008-06-09 01:59:24Z matt $ + * + * @package Piwik_DataTable + */ + +/** + * Add a new metadata column to the table. + * + * This is used to add a column containing the logo width and height of the countries flag icons. + * This value is fixed for all icons so we simply add the same value for all rows. + * + * @package Piwik_DataTable + * @subpackage Piwik_DataTable_Filter + */ +class Piwik_DataTable_Filter_AddConstantMetadata extends Piwik_DataTable_Filter +{ + private $metadataToRead; + private $functionToApply; + private $metadataToAdd; + + public function __construct( $table, $metadataName, $metadataValue ) + { + parent::__construct($table); + $this->name = $metadataName; + $this->value = $metadataValue; + $this->filter(); + } + + protected function filter() + { + foreach($this->table->getRows() as $row) + { + $row->addMetadata($this->name, $this->value); + } + } +} + diff --git a/core/DataTable/Filter/AddSummaryRow.php b/core/DataTable/Filter/AddSummaryRow.php new file mode 100644 index 0000000000..b8d956dd43 --- /dev/null +++ b/core/DataTable/Filter/AddSummaryRow.php @@ -0,0 +1,67 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Limit.php 168 2008-01-14 05:26:43Z matt $ + * + * @package Piwik_DataTable + */ + +/** + * Add a new row to the table containing a summary + * of the rows from StartRowToSummarize to EndRowToSummarize. + * It then deletes the rows from StartRowToSummarize to EndRowToSummarize. + * The new row created has a label = 'other' + * + * This filter is useful to build a more compact view of a table, + * keeping the first records unchanged. + * + * For example we use this for the pie chart, to build the last pie part + * which is the sum of all the remaining data after the top 5 data. + * This row is assigned a label of 'Others'. + * + * @package Piwik_DataTable + * @subpackage Piwik_DataTable_Filter + */ +class Piwik_DataTable_Filter_AddSummaryRow extends Piwik_DataTable_Filter +{ + public function __construct( $table, $startRowToSummarize, $labelSummaryRow = Piwik_DataTable::LABEL_SUMMARY_ROW, $columnToSortByBeforeTruncating = Piwik_Archive::INDEX_NB_VISITS ) + { + parent::__construct($table); + $this->startRowToSummarize = $startRowToSummarize; + $this->labelSummaryRow = $labelSummaryRow; + $this->columnToSortByBeforeTruncating = $columnToSortByBeforeTruncating; + + if($table->getRowsCount() > $startRowToSummarize + 1) + { + $this->filter(); + } + } + + protected function filter() + { + $filter = new Piwik_DataTable_Filter_Sort($this->table, $this->columnToSortByBeforeTruncating, 'desc'); + + $rows = $this->table->getRows(); + $count = $this->table->getRowsCount(); + $newRow = new Piwik_DataTable_Row(); + for($i = $this->startRowToSummarize; $i < $count; $i++) + { + if(!isset($rows[$i])) + { + // case when the last row is a summary row, it is not indexed by $cout but by Piwik_DataTable::ID_SUMMARY_ROW + $summaryRow = $this->table->getRowFromId(Piwik_DataTable::ID_SUMMARY_ROW); + $newRow->sumRow($summaryRow); + } + else + { + $newRow->sumRow($rows[$i]); + } + } + $newRow->addColumn('label', $this->labelSummaryRow); + $filter = new Piwik_DataTable_Filter_Limit($this->table, 0, $this->startRowToSummarize); + $this->table->addSummaryRow($newRow); + } +} diff --git a/core/DataTable/Filter/ColumnCallbackAddMetadata.php b/core/DataTable/Filter/ColumnCallbackAddMetadata.php new file mode 100644 index 0000000000..36558c357f --- /dev/null +++ b/core/DataTable/Filter/ColumnCallbackAddMetadata.php @@ -0,0 +1,49 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: ColumnCallbackAddMetadata.php 515 2008-06-08 20:03:21Z matt $ + * + * @package Piwik_DataTable + */ + + +/** + * Add a new 'metadata' column to the table based on the value resulting + * from a callback function with the parameter being another column's value + * + * For example from the "label" column we can to create an "icon" 'metadata' column + * with the icon URI built from the label (LINUX => UserSettings/icons/linux.png) + * + * @package Piwik_DataTable + * @subpackage Piwik_DataTable_Filter + */ + +class Piwik_DataTable_Filter_ColumnCallbackAddMetadata extends Piwik_DataTable_Filter +{ + private $columnToRead; + private $functionToApply; + private $metadataToAdd; + + public function __construct( $table, $columnToRead, $metadataToAdd, $functionToApply ) + { + parent::__construct($table); + $this->functionToApply = $functionToApply; + $this->columnToRead = $columnToRead; + $this->metadataToAdd = $metadataToAdd; + $this->filter(); + } + + protected function filter() + { + foreach($this->table->getRows() as $key => $row) + { + $oldValue = $row->getColumn($this->columnToRead); + $newValue = call_user_func( $this->functionToApply, $oldValue); + $row->addMetadata($this->metadataToAdd, $newValue); + } + } +} + diff --git a/core/DataTable/Filter/ColumnCallbackDeleteRow.php b/core/DataTable/Filter/ColumnCallbackDeleteRow.php new file mode 100644 index 0000000000..33c743c890 --- /dev/null +++ b/core/DataTable/Filter/ColumnCallbackDeleteRow.php @@ -0,0 +1,44 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: ColumnCallbackDeleteRow.php 444 2008-04-11 13:38:22Z johmathe $ + * + * @package Piwik_DataTable + */ + +/** + * Delete all rows for which a given function returns false for a given column. + * + * @package Piwik_DataTable + * @subpackage Piwik_DataTable_Filter + */ +class Piwik_DataTable_Filter_ColumnCallbackDeleteRow extends Piwik_DataTable_Filter +{ + private $columnToFilter; + private $function; + + public function __construct( $table, $columnToFilter, $function ) + { + parent::__construct($table); + $this->function = $function; + $this->columnToFilter = $columnToFilter; + $this->filter(); + } + + protected function filter() + { + foreach($this->table->getRows() as $key => $row) + { + $columnValue = $row->getColumn($this->columnToFilter); + if( $columnValue !== false + && !call_user_func( $this->function, $columnValue)) + { + $this->table->deleteRow($key); + } + } + } +} + diff --git a/core/DataTable/Filter/ColumnCallbackReplace.php b/core/DataTable/Filter/ColumnCallbackReplace.php new file mode 100644 index 0000000000..63d3671f24 --- /dev/null +++ b/core/DataTable/Filter/ColumnCallbackReplace.php @@ -0,0 +1,42 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: ColumnCallbackReplace.php 444 2008-04-11 13:38:22Z johmathe $ + * + * @package Piwik_DataTable + */ + +/** + * Replace a column value with a new value resulting + * from the function called with the column's value + * + * @package Piwik_DataTable + * @subpackage Piwik_DataTable_Filter + */ +class Piwik_DataTable_Filter_ColumnCallbackReplace extends Piwik_DataTable_Filter +{ + private $columnToFilter; + private $functionToApply; + + public function __construct( $table, $columnToFilter, $functionToApply ) + { + parent::__construct($table); + $this->functionToApply = $functionToApply; + $this->columnToFilter = $columnToFilter; + $this->filter(); + } + + protected function filter() + { + foreach($this->table->getRows() as $key => $row) + { + $oldValue = $row->getColumn($this->columnToFilter); + $newValue = call_user_func( $this->functionToApply, $oldValue); + $row->setColumn($this->columnToFilter, $newValue); + } + } +} + diff --git a/core/DataTable/Filter/ExcludeLowPopulation.php b/core/DataTable/Filter/ExcludeLowPopulation.php new file mode 100644 index 0000000000..cb038d6d78 --- /dev/null +++ b/core/DataTable/Filter/ExcludeLowPopulation.php @@ -0,0 +1,50 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: ExcludeLowPopulation.php 519 2008-06-09 01:59:24Z matt $ + * + * @package Piwik_DataTable + */ + +/** + * Delete all rows that have a $columnToFilter value less than the $minimumValue + * + * For example we delete from the countries report table all countries that have less than 3 visits. + * It is very useful to exclude noise from the reports. + * You can obviously apply this filter on a percentaged column, eg. remove all countries with the column 'percent_visits' less than 0.05 + * + * @package Piwik_DataTable + * @subpackage Piwik_DataTable_Filter + */ +class Piwik_DataTable_Filter_ExcludeLowPopulation extends Piwik_DataTable_Filter +{ + static public $minimumValue; + public function __construct( $table, $columnToFilter, $minimumValue ) + { + $this->columnToFilter = $columnToFilter; + self::$minimumValue = $minimumValue; + parent::__construct($table); + $this->filter(); + } + + function filter() + { + $function = array("Piwik_DataTable_Filter_ExcludeLowPopulation", + "excludeLowPopulation"); + + $filter = new Piwik_DataTable_Filter_ColumnCallbackDeleteRow( + $this->table, + $this->columnToFilter, + $function + ); + } + + static public function excludeLowPopulation($value) + { + return $value >= self::$minimumValue; + } +} + diff --git a/core/DataTable/Filter/Limit.php b/core/DataTable/Filter/Limit.php new file mode 100644 index 0000000000..b292a39aeb --- /dev/null +++ b/core/DataTable/Filter/Limit.php @@ -0,0 +1,58 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Limit.php 503 2008-06-01 19:16:56Z matt $ + * + * @package Piwik_DataTable + */ + +/** + * Delete all rows from the table that are not in the offset,offset+limit range + * + * @package Piwik_DataTable + * @subpackage Piwik_DataTable_Filter + */ + +class Piwik_DataTable_Filter_Limit extends Piwik_DataTable_Filter +{ + /** + * Filter constructor. + * + * @param Piwik_DataTable $table + * @param int $offset Starting row (indexed from 0) + * @param int $limit Number of rows to keep (specify -1 to keep all rows) + */ + public function __construct( $table, $offset, $limit = null ) + { + parent::__construct($table); + $this->offset = $offset; + + if(is_null($limit)) + { + $limit = -1; + } + $this->limit = $limit; + + $this->filter(); + } + + protected function filter() + { + $table = $this->table; + $rowsCount = $table->getRowsCount(); + + // we delete from 0 to offset + $table->deleteRowsOffset( 0, $this->offset ); + + // at this point the array has offset less elements. We delete from limit to the end + if( $this->limit >= 0 ) + { + $table->deleteRowsOffset( $this->limit ); + } + } +} + + diff --git a/core/DataTable/Filter/MetadataCallbackAddMetadata.php b/core/DataTable/Filter/MetadataCallbackAddMetadata.php new file mode 100644 index 0000000000..8e133923a7 --- /dev/null +++ b/core/DataTable/Filter/MetadataCallbackAddMetadata.php @@ -0,0 +1,48 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: MetadataCallbackAddMetadata.php 515 2008-06-08 20:03:21Z matt $ + * + * @package Piwik_DataTable + */ + +/** + * Add a new metadata to the table based on the value resulting + * from a callback function with the parameter being another metadata value + * + * For example for the searchEngine we have a "metadata" information that gives + * the URL of the search engine. We use this URL to add a new "metadata" that gives + * the path of the logo for this search engine URL (which has the format URL.png). + * + * @package Piwik_DataTable + * @subpackage Piwik_DataTable_Filter + */ +class Piwik_DataTable_Filter_MetadataCallbackAddMetadata extends Piwik_DataTable_Filter +{ + private $metadataToRead; + private $functionToApply; + private $metadataToAdd; + + public function __construct( $table, $metadataToRead, $metadataToAdd, $functionToApply ) + { + parent::__construct($table); + $this->functionToApply = $functionToApply; + $this->metadataToRead = $metadataToRead; + $this->metadataToAdd = $metadataToAdd; + $this->filter(); + } + + protected function filter() + { + foreach($this->table->getRows() as $key => $row) + { + $oldValue = $row->getMetadata($this->metadataToRead); + $newValue = call_user_func( $this->functionToApply, $oldValue); + $row->addMetadata($this->metadataToAdd, $newValue); + } + } +} + diff --git a/core/DataTable/Filter/Null.php b/core/DataTable/Filter/Null.php new file mode 100644 index 0000000000..723c1a4529 --- /dev/null +++ b/core/DataTable/Filter/Null.php @@ -0,0 +1,35 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Null.php 482 2008-05-18 17:22:35Z matt $ + * + * @package Piwik_DataTable + */ + +/** + * Filter template. + * You can use it if you want to create a new filter. + * + * @package Piwik_DataTable + * @subpackage Piwik_DataTable_Filter + */ +class Piwik_DataTable_Filter_Null extends Piwik_DataTable_Filter +{ + + public function __construct( $table ) + { + parent::__construct($table); + $this->filter(); + } + + protected function filter() + { + foreach($this->table->getRows() as $key => $row) + { + } + } +} + diff --git a/core/DataTable/Filter/Pattern.php b/core/DataTable/Filter/Pattern.php new file mode 100644 index 0000000000..aabccd1911 --- /dev/null +++ b/core/DataTable/Filter/Pattern.php @@ -0,0 +1,44 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Pattern.php 444 2008-04-11 13:38:22Z johmathe $ + * + * @package Piwik_DataTable + */ + +/** + * Delete all rows for which the given $columnToFilter do not contain the $patternToSearch + * This filter is to be used on columns containing strings. + * Exemple: fron the keyword report, keep only the rows for which the label contains "piwik" + * + * @package Piwik_DataTable + * @subpackage Piwik_DataTable_Filter + */ +class Piwik_DataTable_Filter_Pattern extends Piwik_DataTable_Filter +{ + private $columnToFilter; + private $patternToSearch; + + public function __construct( $table, $columnToFilter, $patternToSearch ) + { + parent::__construct($table); + $this->patternToSearch = $patternToSearch; + $this->columnToFilter = $columnToFilter; + $this->filter(); + } + + protected function filter() + { + foreach($this->table->getRows() as $key => $row) + { + if( stripos($row->getColumn($this->columnToFilter), $this->patternToSearch) === false) + { + $this->table->deleteRow($key); + } + } + } +} + diff --git a/core/DataTable/Filter/PatternRecursive.php b/core/DataTable/Filter/PatternRecursive.php new file mode 100644 index 0000000000..a0743aa116 --- /dev/null +++ b/core/DataTable/Filter/PatternRecursive.php @@ -0,0 +1,77 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: PatternRecursive.php 519 2008-06-09 01:59:24Z matt $ + * + * @package Piwik_DataTable + */ + +/** + * Delete all rows for which + * - the given $columnToFilter do not contain the $patternToSearch + * - AND all the subTables associated to this row do not contain the $patternToSearch + * + * This filter is to be used on columns containing strings. + * Exemple: from the pages viewed report, keep only the rows that contain "piwik" or for which a subpage contains "piwik". + * + * @package Piwik_DataTable + * @subpackage Piwik_DataTable_Filter + */ +class Piwik_DataTable_Filter_PatternRecursive extends Piwik_DataTable_Filter +{ + private $columnToFilter; + private $patternToSearch; + + public function __construct( $table, $columnToFilter, $patternToSearch ) + { + parent::__construct($table); + $this->patternToSearch = $patternToSearch;//preg_quote($patternToSearch); + $this->columnToFilter = $columnToFilter; + $this->filter(); + } + + protected function filter( $table = null ) + { + if(is_null($table)) + { + $table = $this->table; + } + $rows = $table->getRows(); + + foreach($rows as $key => $row) + { + // A row is deleted if + // 1 - its label doesnt contain the pattern + // AND 2 - the label is not found in the children + $patternNotFoundInChildren = false; + + try{ + $idSubTable = $row->getIdSubDataTable(); + $subTable = Piwik_DataTable_Manager::getInstance()->getTable($idSubTable); + + // we delete the row if we couldn't find the pattern in any row in the + // children hierarchy + if( $this->filter($subTable) == 0 ) + { + $patternNotFoundInChildren = true; + } + } catch(Exception $e) { + // there is no subtable loaded for example + $patternNotFoundInChildren = true; + } + + if( $patternNotFoundInChildren + && (stripos($row->getColumn($this->columnToFilter), $this->patternToSearch) === false) + ) + { + $table->deleteRow($key); + } + } + + return $table->getRowsCount(); + } +} + diff --git a/core/DataTable/Filter/ReplaceColumnNames.php b/core/DataTable/Filter/ReplaceColumnNames.php new file mode 100644 index 0000000000..1e5d41a065 --- /dev/null +++ b/core/DataTable/Filter/ReplaceColumnNames.php @@ -0,0 +1,93 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: ReplaceColumnNames.php 519 2008-06-09 01:59:24Z matt $ + * + * @package Piwik_DataTable + */ + +/** + * This filter replaces column names using a mapping table that maps from the old name to the new name. + * + * Why this filter? + * For saving bytes in the database, you can change all the columns labels by an integer value. + * Exemple instead of saving 10000 rows with the column name 'nb_uniq_visitors' which would cost a lot of memory, + * we map it to the integer 1 before saving in the DB. + * After selecting the DataTable from the DB though, you need to restore back the real names so that + * it shows nicely in the report (XML for example). + * + * You can specify the mapping array to apply in the constructor. + * + * @package Piwik_DataTable + * @subpackage Piwik_DataTable_Filter + */ +class Piwik_DataTable_Filter_ReplaceColumnNames extends Piwik_DataTable_Filter +{ + /* + * Old column name => new column name + */ + protected $mappingToApply = array( + Piwik_Archive::INDEX_NB_UNIQ_VISITORS => 'nb_uniq_visitors', + Piwik_Archive::INDEX_NB_VISITS => 'nb_visits', + Piwik_Archive::INDEX_NB_ACTIONS => 'nb_actions', + Piwik_Archive::INDEX_MAX_ACTIONS => 'max_actions', + Piwik_Archive::INDEX_SUM_VISIT_LENGTH => 'sum_visit_length', + Piwik_Archive::INDEX_BOUNCE_COUNT => 'bounce_count', + ); + + /** + * @param DataTable Table + * @param array Mapping to apply. Must have the format + * array( OLD_COLUMN_NAME => NEW_COLUMN NAME, + * OLD_COLUMN_NAME2 => NEW_COLUMN NAME2, + * ) + */ + public function __construct( $table, $mappingToApply = null ) + { + parent::__construct($table); + if(!is_null($mappingToApply)) + { + $this->mappingToApply = $mappingToApply; + } + + $this->filter(); + } + + protected function filter() + { + $this->filterTable($this->table); + } + + protected function filterTable($table) + { + foreach($table->getRows() as $key => $row) + { + $this->renameColumns($row); + + try { + $subTable = Piwik_DataTable_Manager::getInstance()->getTable( $row->getIdSubDataTable() ); + $this->filterTable($subTable); + } catch(Exception $e){ + // case idSubTable == null, or if the table is not loaded in memory + } + } + } + + protected function renameColumns($row) + { + $columns = $row->getColumns(); + foreach($this->mappingToApply as $oldName => $newName) + { + if(isset($columns[$oldName])) + { + $columns[$newName] = $columns[$oldName]; + unset($columns[$oldName]); + } + } + $row->setColumns($columns); + } +} + diff --git a/core/DataTable/Filter/ReplaceSummaryRowLabel.php b/core/DataTable/Filter/ReplaceSummaryRowLabel.php new file mode 100644 index 0000000000..ed3b627ddc --- /dev/null +++ b/core/DataTable/Filter/ReplaceSummaryRowLabel.php @@ -0,0 +1,42 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: ReplaceColumnNames.php 482 2008-05-18 17:22:35Z matt $ + * + * @package Piwik_DataTable + */ + +/** + * + * @package Piwik_DataTable + * @subpackage Piwik_DataTable_Filter + */ +class Piwik_DataTable_Filter_ReplaceSummaryRowLabel extends Piwik_DataTable_Filter +{ + public function __construct( $table, $newLabel = null) + { + parent::__construct($table); + if(is_null($newLabel)) + { + $newLabel = Piwik_Translate('General_Others'); + } + $this->newLabel = $newLabel; + $this->filter(); + } + + protected function filter() + { + foreach($this->table->getRows() as $row) + { + if($row->getColumn('label') === Piwik_DataTable::LABEL_SUMMARY_ROW) + { + $row->setColumn('label', $this->newLabel); + break; + } + } + } +} + diff --git a/core/DataTable/Filter/Sort.php b/core/DataTable/Filter/Sort.php new file mode 100644 index 0000000000..f84ca21b1f --- /dev/null +++ b/core/DataTable/Filter/Sort.php @@ -0,0 +1,121 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Sort.php 519 2008-06-09 01:59:24Z matt $ + * + * @package Piwik_DataTable + */ + +/** + * Sort the DataTable based on the value of column $columnToSort ordered by $order. + * Possible to specify a natural sorting (see php.net/natsort for details) + * @package Piwik_DataTable + * @subpackage Piwik_DataTable_Filter + */ +class Piwik_DataTable_Filter_Sort extends Piwik_DataTable_Filter +{ + protected $columnToSort; + protected $order; + + public function __construct( $table, $columnToSort, $order = 'desc', $naturalSort = false ) + { + parent::__construct($table); + + // hack... But I can't see how to do properly + if($columnToSort == '0') + { + $columnToSort = 'label'; + } + + $this->columnToSort = $columnToSort; + $this->naturalSort = $naturalSort; + $this->setOrder($order); + $this->filter(); + } + + function setOrder($order) + { + if($order == 'asc') + { + $this->order = 'asc'; + $this->sign = 1; + } + else + { + $this->order = 'desc'; + $this->sign = -1; + } + } + + function sort($a, $b) + { + return $this->sign * + ($a->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort] + < $b->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort] + ? -1 + : 1 + ); + } + + function naturalSort($a, $b) + { + return $this->sign * strnatcasecmp( + $a->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort], + $b->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort] + ); + } + + + function sortString($a, $b) + { + return $this->sign * + strcasecmp($a->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort], + $b->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort] + ); + } + + protected function filter() + { + if($this->table instanceof Piwik_DataTable_Simple) + { + return; + } + $rows = $this->table->getRows(); + + if(count($rows) == 0) + { + return; + } + $row = current($rows); + $value = $row->getColumn($this->columnToSort); + + if($value === false) + { + // we don't throw the exception because we sometimes export a DataTable without a column labelled '2' + // and when the generic filters tries to sort by default using this column 2, this shouldnt raise an exception... + //throw new Exception("The column to sort by '".$this->columnToSort."' is unknown in the row ". implode(array_keys($row->getColumns()), ',')); + return; + } + + if( Piwik::isNumeric($value)) + { + $methodToUse = "sort"; + } + else + { + if($this->naturalSort) + { + $methodToUse = "naturalSort"; + } + else + { + $methodToUse = "sortString"; + } + } + $this->table->sort( array($this,$methodToUse) ); + } +} + diff --git a/core/DataTable/Manager.php b/core/DataTable/Manager.php new file mode 100644 index 0000000000..5811cb6068 --- /dev/null +++ b/core/DataTable/Manager.php @@ -0,0 +1,101 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Manager.php 519 2008-06-09 01:59:24Z matt $ + * + * @package Piwik_DataTable + */ + +/** + * The DataTable_Manager registers all the instanciated DataTable and provides an + * easy way to access them. This is used to store all the DataTable during the archiving process. + * At the end of archiving, the ArchiveProcessing will read the stored datatable and record them in the DB. + * + * @package Piwik_DataTable + */ +class Piwik_DataTable_Manager +{ + static private $instance = null; + /** + * Returns instance + * + * @return Piwik_DataTable_Manager + */ + static public function getInstance() + { + if (self::$instance == null) + { + $c = __CLASS__; + self::$instance = new $c(); + } + return self::$instance; + } + + /** + * Array used to store the DataTable + * + * @var array + */ + protected $tables = array(); + + /** + * Add a DataTable to the registry + * + * @param Piwik_DataTable + * @return int Number of tables registered in the manager (including the one just added) + */ + public function addTable( $table ) + { + $this->tables[] = $table; + return count($this->tables) - 1; + } + + /** + * Returns the DataTable associated to the ID $idTable. + * NB: The datatable has to have been instanciated before! + * This method will not fetch the DataTable from the DB. + * + * @exception If the table can't be found + * @return Piwik_DataTable The table + */ + public function getTable( $idTable ) + { + if(!isset($this->tables[$idTable])) + { + throw new Exception(sprintf("The requested table (id = %d) couldn't be found in the DataTable Manager", $idTable)); + } + return $this->tables[$idTable]; + } + + /** + * Delete all the registered DataTables from the manager + * + * @return void + */ + public function deleteAll() + { + $this->tables = array(); + } + + public function deleteTable( $id ) + { + if(isset($this->tables[$id])) + { + $this->tables[$id] = null; + } + } + + /** + * Returns the number of DataTable currently registered. + * + * @return int + */ + public function count() + { + return count($this->tables); + } +} + diff --git a/core/DataTable/Renderer.php b/core/DataTable/Renderer.php new file mode 100644 index 0000000000..942c30d8bb --- /dev/null +++ b/core/DataTable/Renderer.php @@ -0,0 +1,104 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Renderer.php 519 2008-06-09 01:59:24Z matt $ + * + * @package Piwik_DataTable + */ + +/** + * A DataTable Renderer can produce an output given a DataTable object. + * All new Renderers must be copied in DataTable/Renderer and added to the factory() method. + * To use a renderer, simply do: + * $render = new Piwik_DataTable_Renderer_Xml( $myTable ); + * echo $render; + * + * @package Piwik_DataTable + * @subpackage Piwik_DataTable_Renderer + */ +abstract class Piwik_DataTable_Renderer +{ + protected $table; + protected $renderSubTables; + + /** + * Builds the renderer. + * Works with any kind of DataTable if the renderer used handles this DataTable. + * + * @param Piwik_DataTable|Piwik_DataTable_Simple|Piwik_DataTable_Array $table to be rendered + */ + function __construct($table = null, $renderSubTables = null) + { + if(!is_null($table)) + { + $this->setTable($table); + } + if(is_null($renderSubTables)) + { + $this->renderSubTables = (bool)Piwik_Common::getRequestVar('expanded', false); + } + else + { + $this->renderSubTables = $renderSubTables; + } + } + + /** + * Computes the dataTable output and returns the string/binary + * + * @return string + */ + abstract public function render(); + + /** + * @see render() + * @return string + */ + public function __toString() + { + return $this->render(); + } + + /** + * Set the DataTable to be rendered + * + * @param Piwik_DataTable|Piwik_DataTable_Simple|Piwik_DataTable_Array $table to be rendered + */ + public function setTable($table) + { + if(!($table instanceof Piwik_DataTable) + && !($table instanceof Piwik_DataTable_Array)) + { + throw new Exception("The renderer accepts only a Piwik_DataTable or an array of DataTable (Piwik_DataTable_Array) object."); + } + $this->table = $table; + } + + /** + * Returns the DataTable associated to the output format $name + * + * @throws exception If the renderer is unknown + * @return Piwik_DataTable_Renderer + */ + static public function factory( $name ) + { + $name = ucfirst(strtolower($name)); + $path = "core/DataTable/Renderer/".$name.".php"; + $className = 'Piwik_DataTable_Renderer_' . $name; + + if( Piwik_Common::isValidFilename($name) + && Zend_Loader::isReadable($path) ) + { + require_once $path; + return new $className; + } + else + { + throw new Exception("Renderer format '$name' not valid. Try 'xml' or 'json' or 'csv' or 'html' or 'php' or 'original' instead."); + } + } +} + diff --git a/core/DataTable/Renderer/Console.php b/core/DataTable/Renderer/Console.php new file mode 100644 index 0000000000..59ea9bc55f --- /dev/null +++ b/core/DataTable/Renderer/Console.php @@ -0,0 +1,125 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Console.php 525 2008-06-25 23:49:13Z matt $ + * + * @package Piwik_DataTable + */ + +/** + * Simple output + * + * @package Piwik_DataTable + * @subpackage Piwik_DataTable_Renderer + */ +class Piwik_DataTable_Renderer_Console extends Piwik_DataTable_Renderer +{ + protected $prefixRows; + function __construct($table = null) + { + parent::__construct($table); + $this->setPrefixRow('#'); + } + + function render() + { + return $this->renderTable($this->table); + } + + function setPrefixRow($str) + { + $this->prefixRows = $str; + } + + protected function renderDataTableArray(Piwik_DataTable_Array $table, $prefix ) + { + $output = "Piwik_DataTable_Array<hr>"; + $prefix = $prefix . ' '; + foreach($table->getArray() as $descTable => $table) + { + $output .= $prefix . "<b>". $descTable. "</b><br>"; + $output .= $prefix . $this->renderTable($table, $prefix . ' '); + $output .= "<hr>"; + } + return $output; + } + + protected function renderTable($table, $prefix = "") + { + if($table instanceof Piwik_DataTable_Array) + { + return $this->renderDataTableArray($table, $prefix); + } + + if($table->getRowsCount() == 0) + { + return "Empty table <br>\n"; + } + + static $depth=0; + $output = ''; + $i = 1; + foreach($table->getRows() as $row) + { + $dataTableArrayBreak = false; + $columns=array(); + foreach($row->getColumns() as $column => $value) + { + if($value instanceof Piwik_DataTable_Array ) + { + $output .= $this->renderDataTableArray($value, $prefix); + $dataTableArrayBreak = true; + break; + } + if(is_string($value)) $value = "'$value'"; + + $columns[] = "'$column' => $value"; + } + if($dataTableArrayBreak === true) + { + continue; + } + $columns = implode(", ", $columns); + + $metadata = array(); + foreach($row->getMetadata() as $name => $value) + { + if(is_string($value)) + { + $value = "'$value'"; + } + $metadata[] = "'$name' => $value"; + } + $metadata = implode(", ", $metadata); + + $output.= str_repeat($this->prefixRows, $depth) + . "- $i [".$columns."] [".$metadata."] [idsubtable = " + . $row->getIdSubDataTable()."]<br>\n"; + + if($row->getIdSubDataTable() !== null) + { + $depth++; + try{ + $output.= $this->renderTable( + Piwik_DataTable_Manager::getInstance()->getTable( + $row->getIdSubDataTable() + ), + $prefix . ' ' + ); + } catch(Exception $e) { + $output.= "-- Sub DataTable not loaded<br>\n"; + } + $depth--; + } + $i++; + } + + return $output; + + } +} + + diff --git a/core/DataTable/Renderer/Csv.php b/core/DataTable/Renderer/Csv.php new file mode 100644 index 0000000000..e686ba9023 --- /dev/null +++ b/core/DataTable/Renderer/Csv.php @@ -0,0 +1,237 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Csv.php 558 2008-07-20 23:10:38Z matt $ + * + * @package Piwik_DataTable + */ + +require_once "DataTable/Renderer/Php.php"; +/** + * CSV export + * + * When rendered using the default settings, a CSV report has the following characteristics: + * The first record contains headers for all the columns in the report. + * All rows have the same number of columns. + * The default field delimiter string is a comma (,). + * Formatting and layout are ignored. + * + * Note that CSV output doesn't handle recursive dataTable. It will output only the first parent level of the tables. + * + * @package Piwik_DataTable + * @subpackage Piwik_DataTable_Renderer + * + */ + +class Piwik_DataTable_Renderer_Csv extends Piwik_DataTable_Renderer +{ + /** + * Column separator + * + * @var string + */ + public $separator = ','; + + /** + * Line end + * + * @var string + */ + public $lineEnd = "\n"; + + /** + * 'metadata' columns will be exported, prefixed by 'metadata_' + * + * @var bool + */ + public $exportMetadata = true; + + /** + * Converts the content to unicode so that UTF8 characters (eg. chinese) can be imported in Excel + * + * @var bool + */ + public $convertToUnicode = true; + + /** + * idSubtable will be exported in a column called 'idsubdatatable' + * + * @var bool + */ + public $exportIdSubtable = true; + + function __construct($table = null) + { + parent::__construct($table); + } + + function render() + { + return $this->renderTable($this->table); + } + + protected function renderTable($table) + { + if($table instanceof Piwik_DataTable_Array) + { + $str = $header = ''; + $prefixColumns = $table->getKeyName() . $this->separator; + foreach($table->getArray() as $currentLinePrefix => $dataTable) + { + $returned = explode("\n",$this->renderTable($dataTable)); + // get the columns names + if(empty($header)) + { + $header = $returned[0]; + } + $returned = array_slice($returned,1); + + // case empty datatable we dont print anything in the CSV export + // when in xml we would output <result date="2008-01-15" /> + if(!empty($returned)) + { + foreach($returned as &$row) + { + $row = $currentLinePrefix . $this->separator . $row; + } + $str .= "\n" . implode("\n", $returned); + } + } + if(!empty($header)) + { + $str = $prefixColumns . $header . $str; + } + } + else + { + $str = $this->renderDataTable($table); + } + + return $this->output($str); + } + + protected function renderDataTable( $table ) + { + if($table instanceof Piwik_DataTable_Simple + && $table->getRowsCount() == 1) + { + $str = 'value' . $this->lineEnd . $table->getRowFromId(0)->getColumn('value'); + return $str; + } + + $csv = array(); + + $allColumns = array(); + foreach($table->getRows() as $row) + { + $csvRow = array(); + + $columns = $row->getColumns(); + foreach($columns as $name => $value) + { + if(!isset($allColumns[$name])) + { + $allColumns[$name] = true; + } + $csvRow[$name] = $value; + } + + if($this->exportMetadata) + { + $metadata = $row->getMetadata(); + foreach($metadata as $name => $value) + { + //if a metadata and a column have the same name make sure they dont overwrite + $name = 'metadata_'.$name; + + $allColumns[$name] = true; + $csvRow[$name] = $value; + } + } + + if($this->exportIdSubtable) + { + $idsubdatatable = $row->getIdSubDataTable(); + if($idsubdatatable !== false) + { + $csvRow['idsubdatatable'] = $idsubdatatable; + } + } + + $csv[] = $csvRow; + } + + // now we make sure that all the rows in the CSV array have all the columns + foreach($csv as &$row) + { + foreach($allColumns as $columnName => $true) + { + if(!isset($row[$columnName])) + { + $row[$columnName] = ''; + } + } + } + $str = ''; + + // specific case, we have only one column and this column wasn't named properly (indexed by a number) + // we don't print anything in the CSV file => an empty line + if(sizeof($allColumns) == 1 + && reset($allColumns) + && !is_string(key($allColumns))) + { + $str .= ''; + } + else + { + $keys = array_keys($allColumns); + $str .= implode($this->separator, $keys); + $str .= $this->lineEnd; + } + + // we render the CSV + foreach($csv as $theRow) + { + $rowStr = ''; + foreach($allColumns as $columnName => $true) + { + $rowStr .= $this->formatValue($theRow[$columnName]) . $this->separator; + } + // remove the last separator + $rowStr = substr_replace($rowStr,"",-strlen($this->separator)); + $str .= $rowStr . $this->lineEnd; + } + $str = substr($str, 0, -strlen($this->lineEnd)); + return $str; + } + + protected function formatValue($value) + { + if(is_string($value) + && !is_numeric($value)) + { + $value = html_entity_decode($value, ENT_COMPAT, 'UTF-8'); + } + return $value; + } + + protected function output( $str ) + { + if(empty($str)) + { + return 'No data available'; + } + // silent fail otherwise unit tests fail + @header("Content-type: application/vnd.ms-excel"); + @header("Content-Disposition: attachment; filename=piwik-report-export.csv"); + if($this->convertToUnicode + && function_exists('mb_convert_encoding')) + { + $str = chr(255) . chr(254) . mb_convert_encoding($str, 'UTF-16LE', 'UTF-8'); + } + return $str; + } +}
\ No newline at end of file diff --git a/core/DataTable/Renderer/Html.php b/core/DataTable/Renderer/Html.php new file mode 100644 index 0000000000..cea46596fc --- /dev/null +++ b/core/DataTable/Renderer/Html.php @@ -0,0 +1,186 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Html.php 519 2008-06-09 01:59:24Z matt $ + * + * @package Piwik_DataTable + */ + +/** + * Simple HTML output + * Works with recursive DataTable (when a row can be associated with a subDataTable). + * + * @package Piwik_DataTable + * @subpackage Piwik_DataTable_Renderer + */ +class Piwik_DataTable_Renderer_Html extends Piwik_DataTable_Renderer +{ + protected $prefixRows; + function __construct($table = null) + { + parent::__construct($table); + } + + function render() + { + return $this->renderTable($this->table); + } + + protected function renderTable($table) + { + if($table instanceof Piwik_DataTable_Array) + { + $columnPrefixToAdd = $table->getKeyName(); + $out = "<table border=1>"; + foreach($table->getArray() as $date => $subtable ) + { + $out .= "<tr><td><h2>$columnPrefixToAdd = $date</h2>"; + $out .= $this->renderDataTable($subtable); + $out .= "</td></tr>"; + } + $out .= "</table>"; + } + else + { + $out = $this->renderDataTable($table); + } + return $out; + } + + protected function renderDataTable($table) + { + if($table->getRowsCount() == 0) + { + return "<b><i>Empty table</i></b> <br>\n"; + } + if($table instanceof Piwik_DataTable_Simple + && $table->getRowsCount() ==1) + { + $table->deleteColumn('label'); + } + + static $depth=0; + $i = 1; + $someMetadata = false; + $someIdSubTable = false; + + $tableStructure = array(); + + /* + * table = array + * ROW1 = col1 | col2 | col3 | metadata | idSubTable + * ROW2 = col1 | col2 (no value but appears) | col3 | metadata | idSubTable + * subtable here + */ + $allColumns = array(); + foreach($table->getRows() as $row) + { + foreach($row->getColumns() as $column => $value) + { + $allColumns[$column] = true; + $tableStructure[$i][$column] = $value; + } + + $metadata=array(); + foreach($row->getMetadata() as $name => $value) + { + if(is_string($value)) $value = "'$value'"; + $metadata[] = "'$name' => $value"; + } + + if(count($metadata) != 0) + { + $someMetadata = true; + $metadata = implode("<br>", $metadata); + $tableStructure[$i]['_metadata'] = $metadata; + } + + $idSubtable = $row->getIdSubDataTable(); + if(!is_null($idSubtable)) + { + $someIdSubTable = true; + $tableStructure[$i]['_idSubtable'] = $idSubtable; + } + + if($row->getIdSubDataTable() !== null) + { + $depth++; + try{ + $tableStructure[$i]['_subtable']['html'] = $this->renderTable( Piwik_DataTable_Manager::getInstance()->getTable($row->getIdSubDataTable())); + } catch(Exception $e) { + $tableStructure[$i]['_subtable']['html'] = "-- Sub DataTable not loaded"; + } + $tableStructure[$i]['_subtable']['depth'] = $depth; + $depth--; + } + $i++; + } + + $allColumns['_metadata'] = $someMetadata; + $allColumns['_idSubtable'] = $someIdSubTable; + $html = "\n"; + $html .= "<table border=1 width=70%>"; + $html .= "\n<tr>"; + foreach($allColumns as $name => $toDisplay) + { + if($toDisplay !== false) + { + if($name === 0) + { + $name = 'value'; + } + $html .= "\n\t<td><b>$name</b></td>"; + } + } + $colspan = count($allColumns); + + foreach($tableStructure as $row) + { + $html .= "\n\n<tr>"; + foreach($allColumns as $name => $toDisplay) + { + if($toDisplay !== false) + { + $value = "-"; + if(isset($row[$name])) + { + $value = $row[$name]; + } + + $html .= "\n\t<td>$value</td>"; + } + } + $html .= "</tr>"; + + if(isset($row['_subtable'])) + { + $html .= "<tr> + <td class=l{$row['_subtable']['depth']} colspan=$colspan>{$row['_subtable']['html']}</td></tr>"; + } + } + $html .= "\n\n</table>"; + + // display styles if there is a subtable displayed + if($someIdSubTable) + { + $styles="\n\n<style>\n"; + for($i=0;$i<11;$i++) + { + $padding=$i*2; + $styles.= "\t TD.l$i { padding-left:{$padding}em; } \n"; + } + $styles.="</style>\n\n"; + if($depth == 0) + { + $html = $styles . $html; + } + } + return $html; + } +} + + + diff --git a/core/DataTable/Renderer/Json.php b/core/DataTable/Renderer/Json.php new file mode 100644 index 0000000000..729f2af98d --- /dev/null +++ b/core/DataTable/Renderer/Json.php @@ -0,0 +1,52 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Json.php 516 2008-06-08 20:06:43Z matt $ + * + * @package Piwik_DataTable + */ + +require_once "DataTable/Renderer/Php.php"; +/** + * JSON export. Using the php 5.2 feature json_encode. + * Works with recursive DataTable (when a row can be associated with a subDataTable). + * + * @package Piwik_DataTable + * @subpackage Piwik_DataTable_Renderer + */ +class Piwik_DataTable_Renderer_Json extends Piwik_DataTable_Renderer +{ + function __construct($table = null, $renderSubTables = null) + { + parent::__construct($table, $renderSubTables); + } + + function render() + { + return $this->renderTable($this->table); + } + + protected function renderTable($table) + { + $renderer = new Piwik_DataTable_Renderer_Php($table, $this->renderSubTables, $serialize = false); + $array = $renderer->flatRender(); + + if(!is_array($array)) + { + $array = array('value' => $array); + } + $str = json_encode($array); + + if(($jsonCallback = Piwik_Common::getRequestVar('jsoncallback', false)) !== false) + { + if(preg_match('/^[0-9a-zA-Z]*$/', $jsonCallback) > 0) + { + $str = $jsonCallback . "(" . $str . ")"; + } + } + return $str; + } +}
\ No newline at end of file diff --git a/core/DataTable/Renderer/Php.php b/core/DataTable/Renderer/Php.php new file mode 100644 index 0000000000..5e472d7519 --- /dev/null +++ b/core/DataTable/Renderer/Php.php @@ -0,0 +1,203 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Php.php 519 2008-06-09 01:59:24Z matt $ + * + * @package Piwik_DataTable + */ + +/** + * Returns the equivalent PHP array for a given DataTable. + * You can specify in the constructor if you want the serialized version. + * Please note that by default it will produce a flat version of the array. + * See the method flatRender() for details. @see flatRender(); + * + * Works with recursive DataTable (when a row can be associated with a subDataTable). + * + * @package Piwik_DataTable + * @subpackage Piwik_DataTable_Renderer + */ +class Piwik_DataTable_Renderer_Php extends Piwik_DataTable_Renderer +{ + protected $serialize; + + public function __construct($table = null, $renderSubTables = null, $serialize = true) + { + parent::__construct($table, $renderSubTables); + $this->setSerialize($serialize); + } + + public function setSerialize( $bool ) + { + $this->serialize = $bool; + } + + public function __toString() + { + $data = $this->render(); + if(!is_string($data)) + { + $data = serialize($data); + } + return $data; + } + + public function render( $dataTable = null ) + { + if(is_null($dataTable)) + { + $dataTable = $this->table; + } + $toReturn = $this->flatRender( $dataTable ); + + if( false !== Piwik_Common::getRequestVar('prettyDisplay', false) ) + { + if(!is_array($toReturn)) + { + $toReturn = unserialize($toReturn); + } + $toReturn = "<pre>" . var_export($toReturn, true ) . "</pre>"; + } + return $toReturn; + } + + /** + * Produces a flat php array from the DataTable, putting "columns" and "metadata" on the same level. + * + * For example, when a originalRender() would be + * array( 'columns' => array( 'col1_name' => value1, 'col2_name' => value2 ), + * 'metadata' => array( 'metadata1_name' => value_metadata) ) + * + * a flatRender() is + * array( 'col1_name' => value1, + * 'col2_name' => value2, + * 'metadata1_name' => value_metadata ) + * + * @return array Php array representing the 'flat' version of the datatable + * + */ + public function flatRender( $dataTable = null ) + { + if(is_null($dataTable)) + { + $dataTable = $this->table; + } + + if($dataTable instanceof Piwik_DataTable_Array) + { + $flatArray = array(); + foreach($dataTable->getArray() as $keyName => $table) + { + $serializeSave = $this->serialize; + $this->serialize = false; + $flatArray[$keyName] = $this->flatRender($table); + $this->serialize = $serializeSave; + } + } + + // A DataTable_Simple is already flattened so no need to do some crazy stuff to convert it + else if($dataTable instanceof Piwik_DataTable_Simple) + { + $flatArray = $this->renderSimpleTable($dataTable); + + // if we return only one numeric value then we print out the result in a simple <result> tag + // keep it simple! + if(count($flatArray) == 1) + { + $flatArray = current($flatArray); + } + + } + // A normal DataTable needs to be handled specifically + else + { + $array = $this->renderTable($dataTable); + $flatArray = $this->flattenArray($array); + } + + if($this->serialize) + { + $flatArray = serialize($flatArray); + } + + return $flatArray; + } + + protected function flattenArray($array) + { + $flatArray = array(); + foreach($array as $row) + { + $newRow = $row['columns'] + $row['metadata']; + if(isset($row['idsubdatatable'])) + { + $newRow += array('idsubdatatable' => $row['idsubdatatable']); + if(isset($row['subtable'])) + { + $newRow += array('subtable' => $this->flattenArray($row['subtable']) ); + } + } + $flatArray[] = $newRow; + } + return $flatArray; + } + + public function originalRender() + { + if($this->table instanceof Piwik_DataTable_Simple) + { + $array = $this->renderSimpleTable($this->table); + } + else + { + $array = $this->renderTable($this->table); + } + + if($this->serialize) + { + $array = serialize($array); + } + return $array; + } + + protected function renderTable($table) + { + $array = array(); + + foreach($table->getRows() as $row) + { + $newRow = array( + 'columns' => $row->getColumns(), + 'metadata' => $row->getMetadata(), + 'idsubdatatable' => $row->getIdSubDataTable(), + ); + + if($this->renderSubTables + && $row->getIdSubDataTable() !== null) + { + try{ + $subTable = $this->renderTable( Piwik_DataTable_Manager::getInstance()->getTable($row->getIdSubDataTable())); + $newRow['subtable'] = $subTable; + } catch (Exception $e) { + // the subtables are not loaded we dont do anything + } + } + + $array[] = $newRow; + } + return $array; + } + + protected function renderSimpleTable($table) + { + $array = array(); + foreach($table->getRows() as $row) + { + $array[$row->getColumn('label')] = $row->getColumn('value'); + } + return $array; + } +}
\ No newline at end of file diff --git a/core/DataTable/Renderer/Rss.php b/core/DataTable/Renderer/Rss.php new file mode 100644 index 0000000000..18574dfcd5 --- /dev/null +++ b/core/DataTable/Renderer/Rss.php @@ -0,0 +1,169 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Html.php 180 2008-01-17 16:32:37Z matt $ + * + * @package Piwik_DataTable + */ + +/** + * RSS Feed. + * The RSS renderer can be used only on Piwik_DataTable_Array that are arrays of Piwik_DataTable. + * A RSS feed contains one dataTable per element in the Piwik_DataTable_Array. + * + * @package Piwik_DataTable + * @subpackage Piwik_DataTable_Renderer + */ +class Piwik_DataTable_Renderer_Rss extends Piwik_DataTable_Renderer +{ + function __construct($table = null) + { + parent::__construct($table); + } + + function render() + { + return $this->renderTable($this->table); + } + + protected function renderTable($table) + { + if(!($table instanceof Piwik_DataTable_Array) + || $table->getKeyName() != 'date') + { + throw new Exception("RSS Feed only used on Piwik_DataTable_Array with keyName = 'date'"); + } + + $idSite = Piwik_Common::getRequestVar('idSite', 1); + $period = Piwik_Common::getRequestVar('period'); + $currentUrl = Piwik_Url::getCurrentUrlWithoutFileName(); + + $piwikUrl = $currentUrl . "?module=CoreHome&action=index&idSite=" . $idSite . "&period=" . $period; + + $out = ""; + $moreRecentFirst = array_reverse($table->getArray(), true); + foreach($moreRecentFirst as $date => $subtable ) + { + $timestamp = $table->metadata[$date]['timestamp']; + $site = $table->metadata[$date]['site']; + + $pudDate = date('r', $timestamp); + $dateUrl = date('Y-m-d', $timestamp); + $thisPiwikUrl = htmlentities($piwikUrl . "&date=$dateUrl"); + $siteName = $site->getName(); + $title = $siteName . " on ". $date; + + $out .= "\t<item> + <pubDate>$pudDate</pubDate> + <guid>$thisPiwikUrl</guid> + <link>$thisPiwikUrl</link> + <title>$title</title> + <author>http://piwik.org</author> + <description>"; + + $out .= htmlspecialchars( $this->renderDataTable($subtable) ); + $out .= "</description>\n\t</item>\n"; + } + + $header = $this->getRssHeader(); + $footer = $this->getRssFooter(); + + return $this->output( $header . $out . $footer); + } + protected function output($str) + { + @header("Content-Type: text/xml;charset=utf-8"); + return $str; + } + protected function getRssFooter() + { + return "\t</channel>\n</rss>"; + } + protected function getRssHeader() + { + $generationDate = date('r'); + $header = "<?xml version=\"1.0\" encoding=\"UTF-8\"?> +<rss version=\"2.0\"> + <channel> + <title>piwik statistics - RSS</title> + <link>http://piwik.org</link> + <description>Piwik RSS feed</description> + <pubDate>$generationDate</pubDate> + <generator>piwik</generator> + <language>en</language> + <lastBuildDate>$generationDate</lastBuildDate>"; + return $header; + } + + protected function renderDataTable($table) + { + + if($table->getRowsCount() == 0) + { + return "<b><i>Empty table</i></b> <br>\n"; + } + if($table instanceof Piwik_DataTable_Simple + && $table->getRowsCount() ==1) + { + $table->deleteColumn('label'); + } + + $i = 1; + $tableStructure = array(); + + /* + * table = array + * ROW1 = col1 | col2 | col3 | metadata | idSubTable + * ROW2 = col1 | col2 (no value but appears) | col3 | metadata | idSubTable + * subtable here + */ + $allColumns = array(); + foreach($table->getRows() as $row) + { + foreach($row->getColumns() as $column => $value) + { + $allColumns[$column] = true; + $tableStructure[$i][$column] = $value; + } + $i++; + } + $html = "\n"; + $html .= "<table border=1 width=70%>"; + $html .= "\n<tr>"; + foreach($allColumns as $name => $toDisplay) + { + if($toDisplay !== false) + { + $html .= "\n\t<td><b>$name</b></td>"; + } + } + $html .= "\n</tr>"; + $colspan = count($allColumns); + + foreach($tableStructure as $row) + { + $html .= "\n\n<tr>"; + foreach($allColumns as $name => $toDisplay) + { + if($toDisplay !== false) + { + $value = "-"; + if(isset($row[$name])) + { + $value = urldecode($row[$name]); + } + + $html .= "\n\t<td>$value</td>"; + } + } + $html .= "</tr>"; + + } + $html .= "\n\n</table>"; + return $html; + } +} + diff --git a/core/DataTable/Renderer/Xml.php b/core/DataTable/Renderer/Xml.php new file mode 100644 index 0000000000..425f506ae3 --- /dev/null +++ b/core/DataTable/Renderer/Xml.php @@ -0,0 +1,300 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Xml.php 558 2008-07-20 23:10:38Z matt $ + * + * @package Piwik_DataTable + */ + +require_once "DataTable/Renderer/Php.php"; +/** + * XML export of a given DataTable. + * See the tests cases for more information about the XML format (/tests/core/DataTable/Renderer.test.php) + * Or have a look at the API calls examples. + * + * Works with recursive DataTable (when a row can be associated with a subDataTable). + * + * @package Piwik_DataTable + * @subpackage Piwik_DataTable_Renderer + */ +class Piwik_DataTable_Renderer_Xml extends Piwik_DataTable_Renderer +{ + function __construct($table = null, $renderSubTables = null) + { + parent::__construct($table, $renderSubTables); + } + + function render() + { + return $this->renderTable($this->table); + } + + protected function getArrayFromDataTable($table) + { + $renderer = new Piwik_DataTable_Renderer_Php($table, $this->renderSubTables, $serialize = false); + return $renderer->flatRender(); + } + + protected function renderTable($table, $returnOnlyDataTableXml = false, $prefixLines = '') + { + $array = $this->getArrayFromDataTable($table); + + if($table instanceof Piwik_DataTable_Array) + { + $out = $this->renderDataTableArray($table, $array, $prefixLines); + + if($returnOnlyDataTableXml) + { + return $out; + } + $out = "<results>\n$out</results>"; + return $this->output($out); + } + + // integer value of ZERO is a value we want to display + if($array != 0 && empty($array)) + { + if($returnOnlyDataTableXml) + { + throw new Exception("Illegal state, what xml shall we return?"); + } + $out = "<result />"; + return $this->output($out); + } + if($table instanceof Piwik_DataTable_Simple) + { + if(is_array($array)) + { + $out = $this->renderDataTableSimple($array); + } + else + { + $out = $array; + } + if($returnOnlyDataTableXml) + { + return $out; + } + + if(is_array($array)) + { + $out = "<result>\n".$out."</result>"; + } + else + { + $out = "<result>".$out."</result>"; + } + return $this->output($out); + } + + if($table instanceof Piwik_DataTable) + { + $out = $this->renderDataTable($array); + if($returnOnlyDataTableXml) + { + return $out; + } + $out = "<result>\n$out</result>"; + return $this->output($out); + } + + + } + + protected function renderDataTableArray($table, $array, $prefixLines = "") + { + // CASE 1 + //array + // 'day1' => string '14' (length=2) + // 'day2' => string '6' (length=1) + $firstTable = current($array); + if(!is_array( $firstTable )) + { + $xml = ''; + $nameDescriptionAttribute = $table->getKeyName(); + foreach($array as $valueAttribute => $value) + { + if(empty($value)) + { + $xml .= $prefixLines . "\t<result $nameDescriptionAttribute=\"$valueAttribute\" />\n"; + } + elseif($value instanceof Piwik_DataTable_Array ) + { + $out = $this->renderTable($value, true); + //TODO somehow this code is not tested, cover this case + $xml .= "\t<result $nameDescriptionAttribute=\"$valueAttribute\">\n$out</result>\n"; + } + else + { + $xml .= $prefixLines . "\t<result $nameDescriptionAttribute=\"$valueAttribute\">$value</result>\n"; + } + } + return $xml; + } + + $subTables = $table->getArray(); + $firstTable = current($subTables); + + // CASE 2 + //array + // 'day1' => + // array + // 'nb_uniq_visitors' => string '18' + // 'nb_visits' => string '101' + // 'day2' => + // array + // 'nb_uniq_visitors' => string '28' + // 'nb_visits' => string '11' + if( $firstTable instanceof Piwik_DataTable_Simple) + { + $xml = ''; + $nameDescriptionAttribute = $table->getKeyName(); + foreach($array as $valueAttribute => $dataTableSimple) + { + if(count($dataTableSimple) == 0) + { + $xml .= $prefixLines . "\t<result $nameDescriptionAttribute=\"$valueAttribute\" />\n"; + } + else + { + if(is_array($dataTableSimple)) + { + $dataTableSimple = "\n" . $this->renderDataTableSimple($dataTableSimple, $prefixLines . "\t") . "\t" ; + } + $xml .= $prefixLines . "\t<result $nameDescriptionAttribute=\"$valueAttribute\">".$dataTableSimple. $prefixLines . "</result>\n"; + } + } + return $xml; + } + + // CASE 3 + //array + // 'day1' => + // array + // 0 => + // array + // 'label' => string 'phpmyvisites' + // 'nb_uniq_visitors' => int 11 + // 'nb_visits' => int 13 + // 1 => + // array + // 'label' => string 'phpmyvisits' + // 'nb_uniq_visitors' => int 2 + // 'nb_visits' => int 2 + // 'day2' => + // array + // 0 => + // array + // 'label' => string 'piwik' + // 'nb_uniq_visitors' => int 121 + // 'nb_visits' => int 130 + // 1 => + // array + // 'label' => string 'piwik bis' + // 'nb_uniq_visitors' => int 20 + // 'nb_visits' => int 120 + if($firstTable instanceof Piwik_DataTable) + { + $xml = ''; + $nameDescriptionAttribute = $table->getKeyName(); + foreach($array as $keyName => $arrayForSingleDate) + { + $dataTableOut = $this->renderDataTable( $arrayForSingleDate, $prefixLines . "\t" ); + if(empty($dataTableOut)) + { + $xml .= $prefixLines . "\t<result $nameDescriptionAttribute=\"$keyName\" />\n"; + } + else + { + $xml .= $prefixLines . "\t<result $nameDescriptionAttribute=\"$keyName\">\n"; + $xml .= $dataTableOut; + $xml .= $prefixLines . "\t</result>\n"; + } + } + return $xml; + } + + if($firstTable instanceof Piwik_DataTable_Array) + { + $xml = ''; + $tables = $table->getArray(); + $nameDescriptionAttribute = $table->getKeyName(); + foreach( $tables as $valueAttribute => $tableInArray) + { + $out = $this->renderTable($tableInArray, true, $prefixLines . "\t"); + $xml .= $prefixLines . "\t<result $nameDescriptionAttribute=\"$valueAttribute\">\n".$out.$prefixLines."\t</result>\n"; + + } + return $xml; + } + } + + protected function renderDataTable( $array, $prefixLine = "" ) + { + $out = ''; + foreach($array as $row) + { + $out .= $prefixLine."\t<row>"; + + if(count($row) === 1 + && key($row) === 0) + { + $value = current($row); + $out .= $prefixLine . $value; + } + else + { + $out .= "\n"; + foreach($row as $name => $value) + { + // handle the recursive dataTable case by XML outputting the recursive table + if(is_array($value)) + { + $value = "\n".$this->renderDataTable($value, $prefixLine."\t\t"); + $value .= $prefixLine."\t\t"; + } + else + { + $value = $this->formatValue($value); + } + $out .= $prefixLine."\t\t<$name>".$value."</$name>\n"; + } + $out .= "\t"; + } + $out .= $prefixLine."</row>\n"; + } + return $out; + } + + protected function renderDataTableSimple( $array, $prefixLine = "") + { + $out = ''; + foreach($array as $keyName => $value) + { + $out .= $prefixLine."\t<$keyName>".$this->formatValue($value)."</$keyName>\n"; + } + return $out; + } + + protected function formatValue($value) + { + if(is_string($value) + && !is_numeric($value)) + { + $value = html_entity_decode($value, ENT_COMPAT, 'UTF-8'); + $value = htmlspecialchars($value); + } + return $value; + } + + protected function output( $xml ) + { + // silent fail because otherwise it throws an exception in the unit tests + @header("Content-Type: text/xml;charset=utf-8"); + $xml = '<?xml version="1.0" encoding="utf-8" ?>' . "\n" . $xml; + return $xml; + } +}
\ No newline at end of file diff --git a/core/DataTable/Row.php b/core/DataTable/Row.php new file mode 100644 index 0000000000..b2e3e599d2 --- /dev/null +++ b/core/DataTable/Row.php @@ -0,0 +1,404 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Row.php 519 2008-06-09 01:59:24Z matt $ + * + * @package Piwik_DataTable + */ + +/** + * A DataTable is composed of rows. + * + * A row is composed of: + * - columns often at least a 'label' column containing the description + * of the row, and some numeric values ('nb_visits', etc.) + * - metadata: other information never to be shown as 'columns' + * - idSubtable: a row can be linked to a SubTable + * + * IMPORTANT: Make sure that the column named 'label' contains at least one non-numeric character. + * Otherwise the method addDataTable() or sumRow() would fail because they would consider + * the 'label' as being a numeric column to sum. + * + * @package Piwik_DataTable + * @subpackage Piwik_DataTable_Row + * + */ +class Piwik_DataTable_Row +{ + /** + * This array contains the row information: + * - array indexed by self::COLUMNS contains the columns, pairs of (column names, value) + * - (optional) array indexed by self::METADATA contains the metadata, pairs of (metadata name, value) + * - (optional) integer indexed by self::DATATABLE_ASSOCIATED contains the ID of the Piwik_DataTable associated to this row. + * This ID can be used to read the DataTable from the DataTable_Manager. + * + * @var array + * @see constructor for more information + */ + public $c = array(); + + const COLUMNS = 0; + const METADATA = 1; + const DATATABLE_ASSOCIATED = 3; + + + /** + * Efficient load of the Row structure from a well structured php array + * + * @param array The row array has the structure + * array( + * Piwik_DataTable_Row::COLUMNS => array( + * 'label' => 'Piwik', + * 'column1' => 42, + * 'visits' => 657, + * 'time_spent' => 155744, + * ), + * Piwik_DataTable_Row::METADATA => array( + * 'logo' => 'test.png' + * ), + * Piwik_DataTable_Row::DATATABLE_ASSOCIATED => #Piwik_DataTable object (but in the row only the ID will be stored) + * ) + */ + public function __construct( $row = array() ) + { + $this->c[self::COLUMNS] = array(); + $this->c[self::METADATA] = array(); + $this->c[self::DATATABLE_ASSOCIATED] = null; + + if(isset($row[self::COLUMNS])) + { + $this->c[self::COLUMNS] = $row[self::COLUMNS]; + } + if(isset($row[self::METADATA])) + { + $this->c[self::METADATA] = $row[self::METADATA]; + } + if(isset($row[self::DATATABLE_ASSOCIATED]) + && $row[self::DATATABLE_ASSOCIATED] instanceof Piwik_DataTable) + { + $this->c[self::DATATABLE_ASSOCIATED] = $row[self::DATATABLE_ASSOCIATED]->getId(); + } + } + + /** + * When destroyed, a row destroys its associated subTable if there is one + */ + public function __destruct() + { + $idSubtable = $this->c[self::DATATABLE_ASSOCIATED]; + if($idSubtable !== null) + { + Piwik_DataTable_Manager::getInstance()->deleteTable($idSubtable); + $idSubtable = null; + } + } + + /** + * Applys a basic rendering to the Row and returns the output + * + * @return string characterizing the row. Example: - 1 ['label' => 'piwik', 'nb_uniq_visitors' => 1685, 'nb_visits' => 1861, 'nb_actions' => 2271, 'max_actions' => 13, 'sum_visit_length' => 920131, 'bounce_count' => 1599] [] [idsubtable = 1375] + */ + public function __toString() + { + $columns = array(); + foreach($this->getColumns() as $column => $value) + { + if(is_string($value)) $value = "'$value'"; + $columns[] = "'$column' => $value"; + } + $columns = implode(", ", $columns); + $metadata = array(); + foreach($this->getMetadata() as $name => $value) + { + if(is_string($value)) + { + $name = "'$value'"; + } + $metadata[] = "'$name' => $value"; + } + $metadata = implode(", ", $metadata); + $output = "# [".$columns."] [".$metadata."] [idsubtable = " . $this->getIdSubDataTable()."]<br>\n"; + return $output; + } + + /** + * Deletes the given column + * + * @param string Column name + * @return bool True on success, false if the column didn't exist + */ + public function deleteColumn( $name ) + { + if(!isset($this->c[self::COLUMNS][$name])) + { + return false; + } + unset($this->c[self::COLUMNS][$name]); + return true; + } + + /** + * Returns the given column + * + * @param string Column name + * @return mixed|false The column value + */ + public function getColumn( $name ) + { + if(!isset($this->c[self::COLUMNS][$name])) + { + return false; + } + return $this->c[self::COLUMNS][$name]; + } + + /** + * Returns the array of all metadata, + * or the specified metadata + * + * @param string Metadata name + * @return mixed|array|false + */ + public function getMetadata( $name = null ) + { + if(is_null($name)) + { + return $this->c[self::METADATA]; + } + if(!isset($this->c[self::METADATA][$name])) + { + return false; + } + return $this->c[self::METADATA][$name]; + } + + /** + * Returns the array containing all the columns + * + * @return array Example: array( + * 'column1' => VALUE, + * 'label' => 'www.php.net' + * 'nb_visits' => 15894, + * ) + */ + public function getColumns() + { + return $this->c[self::COLUMNS]; + } + + /** + * Returns the ID of the subDataTable. + * If there is no such a table, returns null. + * + * @return int|null + */ + public function getIdSubDataTable() + { + return $this->c[self::DATATABLE_ASSOCIATED]; + } + + /** + * Sums a DataTable to this row subDataTable. + * If this row doesn't have a SubDataTable yet, we create a new one. + * Then we add the values of the given DataTable to this row's DataTable. + * + * @param Piwik_DataTable Table to sum to this row's subDatatable + * @see Piwik_DataTable::addDataTable() for the algorithm used for the sum + */ + public function sumSubtable(Piwik_DataTable $subTable) + { + $thisSubtableID = $this->getIdSubDataTable(); + if($thisSubtableID === null) + { + $thisSubTable = new Piwik_DataTable; + $this->addSubtable($thisSubTable); + } + else + { + $thisSubTable = Piwik_DataTable_Manager::getInstance()->getTable( $thisSubtableID ); + } + + $thisSubTable->addDataTable($subTable); + } + + + /** + * Set a DataTable to be associated to this row. + * If the row already has a DataTable associated to it, throws an Exception. + * + * @param Piwik_DataTable DataTable to associate to this row + * @throws Exception + * + */ + public function addSubtable(Piwik_DataTable $subTable) + { + if(!is_null($this->c[self::DATATABLE_ASSOCIATED])) + { + throw new Exception("Adding a subtable to the row, but it already has a subtable associated."); + } + $this->c[self::DATATABLE_ASSOCIATED] = $subTable->getId(); + } + + /** + * Set a DataTable to this row. If there is already + * a DataTable associated, it is simply overwritten. + * + * @param Piwik_DataTable DataTable to associate to this row + */ + public function setSubtable(Piwik_DataTable $subTable) + { + $this->c[self::DATATABLE_ASSOCIATED] = $subTable->getId(); + } + + /** + * Set all the columns at once. Overwrites previously set columns. + * + * @param array array( + * 'label' => 'www.php.net' + * 'nb_visits' => 15894, + * ) + */ + public function setColumns( $columns ) + { + $this->c[self::COLUMNS] = $columns; + } + + /** + * Set the value $value to the column called $name. + * + * @param string $name of the column to set + * @param mixed $value of the column to set + */ + public function setColumn($name, $value) + { + if(isset($this->c[self::COLUMNS][$name]) + || $name != 'label') + { + $this->c[self::COLUMNS][$name] = $value; + } + // we make sure when adding the label it goes first in the table + else + { + $this->c[self::COLUMNS] = array($name => $value) + $this->c[self::COLUMNS]; + } + } + + /** + * Add a new column to the row. If the column already exists, throws an exception + * + * @param string $name of the column to add + * @param mixed $value of the column to set + * @throws Exception + */ + public function addColumn($name, $value) + { + if(isset($this->c[self::COLUMNS][$name])) + { + throw new Exception("Column $name already in the array!"); + } + $this->c[self::COLUMNS][$name] = $value; + } + + + /** + * Add a new metadata to the row. If the column already exists, throws an exception. + * + * @param string $name of the metadata to add + * @param mixed $value of the metadata to set + * @throws Exception + */ + public function addMetadata($name, $value) + { + if(isset($this->c[self::METADATA][$name])) + { + throw new Exception("Metadata $name already in the array!"); + } + $this->c[self::METADATA][$name] = $value; + } + + /** + * Sums the given $row columns values to the existing row' columns values. + * It will sum only the int or float values of $row. + * It will not sum the column 'label' even if it has a numeric value. + * + * If a given column doesn't exist in $this then it is added with the value of $row. + * If the column already exists in $this then we have + * this.columns[idThisCol] += $row.columns[idThisCol] + */ + public function sumRow( Piwik_DataTable_Row $rowToSum ) + { + foreach($rowToSum->getColumns() as $name => $value) + { + if($name != 'label' + && Piwik::isNumeric($value)) + { + $current = $this->getColumn($name); + if($current === false) + { + $current = 0; + } + $this->setColumn( $name, $current + $value); + } + } + } + + + /** + * Helper function to test if two rows are equal. + * + * Two rows are equal + * - if they have exactly the same columns / metadata + * - if they have a subDataTable associated, then we check that both of them are the same. + * + * @param Piwik_DataTable_Row row1 to compare + * @param Piwik_DataTable_Row row2 to compare + * + * @return bool + */ + static public function isEqual( Piwik_DataTable_Row $row1, Piwik_DataTable_Row $row2 ) + { + //same columns + $cols1 = $row1->getColumns(); + $cols2 = $row2->getColumns(); + + uksort($cols1, 'strnatcasecmp'); + uksort($cols2, 'strnatcasecmp'); + + if($cols1 != $cols2) + { + return false; + } + + $dets1 = $row1->getMetadata(); + $dets2 = $row2->getMetadata(); + + ksort($dets1); + ksort($dets2); + + if($dets1 != $dets2) + { + return false; + } + + // either both are null + // or both have a value + if( !(is_null($row1->getIdSubDataTable()) + && is_null($row2->getIdSubDataTable()) + ) + ) + { + $subtable1 = Piwik_DataTable_Manager::getInstance()->getTable($row1->getIdSubDataTable()); + $subtable2 = Piwik_DataTable_Manager::getInstance()->getTable($row2->getIdSubDataTable()); + if(!Piwik_DataTable::isEqual($subtable1, $subtable2)) + { + return false; + } + } + return true; + } +} + +require_once "Row/DataTableSummary.php"; diff --git a/core/DataTable/Row/DataTableSummary.php b/core/DataTable/Row/DataTableSummary.php new file mode 100644 index 0000000000..6440340b14 --- /dev/null +++ b/core/DataTable/Row/DataTableSummary.php @@ -0,0 +1,33 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: DataTableSummary.php 519 2008-06-09 01:59:24Z matt $ + * + * @package Piwik_DataTable + */ + +/** + * This class creates a row from a given DataTable. + * The row contains + * - for each numeric column, the returned "summary" column is the sum of all the subRows + * - for every other column, it is ignored and will not be in the "summary row" + * + * @see Piwik_DataTable_Row::sumRow() for more information on the algorithm + * + * @package Piwik_DataTable + * @subpackage Piwik_DataTable_Row + */ +class Piwik_DataTable_Row_DataTableSummary extends Piwik_DataTable_Row +{ + function __construct($subTable) + { + parent::__construct(); + foreach($subTable->getRows() as $row) + { + $this->sumRow($row); + } + } +} diff --git a/core/DataTable/Simple.php b/core/DataTable/Simple.php new file mode 100644 index 0000000000..b010fe9abf --- /dev/null +++ b/core/DataTable/Simple.php @@ -0,0 +1,61 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Simple.php 519 2008-06-09 01:59:24Z matt $ + * + * @package Piwik_DataTable + */ + +/** + * The DataTable_Simple is used to provide an easy way to create simple DataGrid. + * A DataTable_Simple actually is a DataTable with 2 columns: 'label' and 'value'. + * + * It is usually best to return a DataTable_Simple instead of + * a PHP array (or other custom data structure) in API methods: + * - the generic filters can be applied automatically (offset, limit, pattern search, sort, etc.) + * - the renderer can be applied (XML, PHP, HTML, etc.) + * So you don't have to write specific renderer for your data, it is already available in all the formats supported natively by Piwik. + * + * @package Piwik_DataTable + */ +class Piwik_DataTable_Simple extends Piwik_DataTable +{ + /** + * Loads in the DataTable the array information + * @param array Array containing the rows information + * array( + * 'Label row 1' => Value row 1, + * 'Label row 2' => Value row 2, + * ) + * @return void + */ + function loadFromArray($array) + { + foreach($array as $label => $value) + { + $row = new Piwik_DataTable_Row; + $row->addColumn('label', $label); + $row->addColumn('value', $value); + $this->addRow($row); + } + } + + /** + * Returns the 'value' column of the row that has a label '$label'. + * + * @param string Label of the row we want the value + * @return false|mixed The 'value' column of the row labelled $label + */ + function getColumn( $label ) + { + $row = $this->getRowFromLabel($label); + if($row === false) + { + return false; + } + return $row->getColumn('value'); + } +} diff --git a/core/Date.php b/core/Date.php new file mode 100644 index 0000000000..e2331f0e68 --- /dev/null +++ b/core/Date.php @@ -0,0 +1,341 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Date.php 561 2008-07-21 00:00:35Z matt $ + * + * @package Piwik_Helper + */ + +/** + * Date object widely used in Piwik. + * + * @package Piwik_Helper + */ +class Piwik_Date +{ + /** + * Returns a Piwik_Date objects. + * Accepts strings 'today' 'yesterday' or any YYYY-MM-DD or timestamp + * + * @param string $strDate + * @return Piwik_Date + */ + static public function factory($strDate) + { + if(is_int($strDate)) + { + return new Date($strDate); + } + if(is_string($strDate)) + { + if($strDate == 'today') + { + return self::today(); + } + elseif($strDate == 'yesterday') + { + return self::yesterday(); + } + else + { + if (($timestamp = strtotime($strDate)) === false) + { + throw new Exception("The date '$strDate' is not correct. The date format is YYYY-MM-DD or you can also use magic keywords such as 'today' or 'yesterday' or any keyword supported by the strtotime function (see http://php.net/strtotime for more information)"); + } + return new Piwik_Date($timestamp); + } + } + } + + /** + * Builds a Piwik_Date object + * + * @param int timestamp + */ + public function __construct( $date ) + { + if(!is_int( $date )) + { + throw new Exception("Piwik_Date is expecting a unix timestamp"); + } + $this->timestamp = $date ; + } + + /** + * Sets the time part of the date + * Doesn't modify $this + * + * @param string $time HH:MM:SS + * @return Piwik_Date The new date with the time part set + */ + //TODO test this method + public function setTime($time) + { + return new Piwik_Date( strtotime( $this->get("j F Y") . " $time")); + } + + /** + * Returns the unix timestamp of the date + * + * @return int + */ + public function getTimestamp() + { + return $this->timestamp; + } + + /** + * Returns true if the current date is older than the given $date + * + * @param Piwik_Date $date + * @return bool + */ + public function isLater( Piwik_Date $date) + { + return $this->getTimestamp() > $date->getTimestamp(); + } + + /** + * Returns true if the current date is earlier than the given $date + * + * @param Piwik_Date $date + * @return bool + */ + public function isEarlier(Piwik_Date $date) + { + return $this->getTimestamp() < $date->getTimestamp(); + } + + /** + * Returns the Y-m-d representation of the string. + * You can specify the output, see the list on php.net/date + * + * @param string $part + * @return string + */ + public function toString($part = 'Y-m-d') + { + return date($part, $this->getTimestamp()); + } + + /** + * @see toString() + * + * @return string + */ + public function __toString() + { + return $this->toString(); + } + + /** + * Sets a new day + * Returned is the new date object + * Doesn't modify $this + * + * @param int Day eg. 31 + * @return Piwik_Date new date + */ + public function setDay( $day ) + { + $ts = $this->getTimestamp(); + $result = mktime( + date('H', $ts), + date('i', $ts), + date('s', $ts), + date('n', $ts), + 1, + date('Y', $ts) + ); + return new Piwik_Date( $result ); + } + + /** + * Sets a new year + * Returned is the new date object + * Doesn't modify $this + * + * @param int 2010 + * @return Piwik_Date new date + */ + public function setYear( $year ) + { + $ts = $this->getTimestamp(); + $result = mktime( + date('H', $ts), + date('i', $ts), + date('s', $ts), + date('n', $ts), + date('j', $ts), + $year + ); + return new Piwik_Date( $result ); + } + + + + /** + * Subtracts days from the existing date object and returns a new Piwik_Date object + * Doesn't modify $this + * + * Returned is the new date object + * @return Piwik_Date new date + */ + public function subDay( $n ) + { + if($n === 0) + { + return clone $this; + } + $ts = strtotime("-$n day", $this->getTimestamp()); + return new Piwik_Date( $ts ); + } + + /** + * Subtracts a month from the existing date object. + * Returned is the new date object + * Doesn't modify $this + * + * @return Piwik_Date new date + */ + public function subMonth( $n ) + { + if($n === 0) + { + return clone $this; + } + $ts = $this->getTimestamp(); + $result = mktime( + date('H', $ts), + date('i', $ts), + date('s', $ts), + date('n', $ts) - $n, + 1, // we set the day to 1 + date('Y', $ts) + ); + return new Piwik_Date( $result ); + } + + /** + * Returns a representation of a date or datepart + * + * @param string OPTIONAL Part of the date to return, if null the timestamp is returned + * @return integer|string date or datepart + */ + public function get($part = null) + { + if(is_null($part)) + { + return $this->getTimestamp(); + } + return date($part, $this->getTimestamp()); + } + + /** + * Returns a localized representation of a date or datepart + * + * @param string OPTIONAL Part of the date to return (in strftime format), if null timestamp is returned + * @return integer|string date or datepart + */ + public function getLocalized($part = null) + { + if(is_null($part)) + { + return $this->getTimestamp(); + } + return strftime($part, $this->getTimestamp()); + } + + /** + * Adds days to the existing date object. + * Returned is the new date object + * Doesn't modify $this + * + * @param int Number of days to add + * @return Piwik_Date new date + */ + public function addDay( $n ) + { + $ts = strtotime("+$n day", $this->getTimestamp()); + return new Piwik_Date( $ts ); + } + + /** + * Compares the week of the current date against the given $date + * Returns 0 if equal, -1 if current week is earlier or 1 if current week is later + * Example: 09.Jan.2007 13:07:25 -> compareWeek(2); -> 0 + * + * @param Piwik_Date $date + * @return integer 0 = equal, 1 = later, -1 = earlier + */ + public function compareWeek(Piwik_Date $date) + { + $currentWeek = date('W', $this->getTimestamp()); + $toCompareWeek = date('W', $date->getTimestamp()); + if( $currentWeek == $toCompareWeek) + { + return 0; + } + if( $currentWeek < $toCompareWeek) + { + return -1; + } + return 1; + } + /** + * Compares the month of the current date against the given $date month + * Returns 0 if equal, -1 if current month is earlier or 1 if current month is later + * For example: 10.03.2000 -> 15.03.1950 -> 0 + * + * @param Piwik_Date $month Month to compare + * @return integer 0 = equal, 1 = later, -1 = earlier + */ + function compareMonth( Piwik_Date $date ) + { + $currentMonth = date('n', $this->getTimestamp()); + $toCompareMonth = date('n', $date->getTimestamp()); + if( $currentMonth == $toCompareMonth) + { + return 0; + } + if( $currentMonth < $toCompareMonth) + { + return -1; + } + return 1; + } + + /** + * Returns true if current date is today + * + * @return bool + */ + public function isToday() + { + return $this->get('Y-m-d') === date('Y-m-d', time()); + } + + /** + * Returns a date object set to today midnight + * + * @return Piwik_Date + */ + static public function today() + { + return new Piwik_Date(strtotime(date("Y-m-d 00:00:00"))); + } + + /** + * Returns a date object set to yesterday midnight + * @return Piwik_Date + */ + static public function yesterday() + { + return new Piwik_Date(strtotime("yesterday")); + } + +} + diff --git a/core/ErrorHandler.php b/core/ErrorHandler.php new file mode 100644 index 0000000000..a985e14e6e --- /dev/null +++ b/core/ErrorHandler.php @@ -0,0 +1,68 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: ErrorHandler.php 476 2008-05-11 18:35:43Z matt $ + * + * @package Piwik_Helper + */ + +require_once "Zend/Registry.php"; + +if(!defined('E_STRICT')) define('E_STRICT', 2048); +if(!defined('E_RECOVERABLE_ERROR')) define('E_RECOVERABLE_ERROR', 4096); +if(!defined('E_EXCEPTION')) define('E_EXCEPTION', 8192); + +/** + * Error handler used to display nicely errors in Piwik + * + * @package Piwik_Helper + */ +function Piwik_ErrorHandler($errno, $errstr, $errfile, $errline) +{ + // if the error has been suppressed by the @ we don't handle the error + if( error_reporting() == 0 ) + { + return; + } + + ob_start(); + debug_print_backtrace(); + $backtrace = ob_get_contents(); + ob_end_clean(); + + + try { + Zend_Registry::get('logger_error')->log($errno, $errstr, $errfile, $errline, $backtrace); + }catch(Exception $e){ + // in case the error occurs before the logger creation, we simply display it + print("<pre>$errstr \nin '$errfile' at the line $errline\n\n$backtrace\n</pre>"); + exit; + } + switch($errno) + { + case E_ERROR: + case E_PARSE: + case E_CORE_ERROR: + case E_CORE_WARNING: + case E_COMPILE_ERROR: + case E_COMPILE_WARNING: + case E_USER_ERROR: + case E_EXCEPTION: + exit; + break; + + case E_WARNING: + case E_NOTICE: + case E_USER_WARNING: + case E_USER_NOTICE: + case E_STRICT: + case E_RECOVERABLE_ERROR: + default: + // do not exit + break; + } +} + diff --git a/core/ExceptionHandler.php b/core/ExceptionHandler.php new file mode 100644 index 0000000000..27ad00a9c7 --- /dev/null +++ b/core/ExceptionHandler.php @@ -0,0 +1,43 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: ExceptionHandler.php 444 2008-04-11 13:38:22Z johmathe $ + * + * @package Piwik_Helper + */ + +require_once "core/Piwik.php"; + +/** + * Exception handler used to display nicely exceptions in Piwik + * + * @package Piwik_Helper + */ +function Piwik_ExceptionHandler(Exception $exception) +{ + try { + Zend_Registry::get('logger_exception')->log($exception); + } catch(Exception $e) { + // case when the exception is raised before the logger being ready + // we handle the exception a la mano, but using the Logger formatting properties + require_once "Log/Exception.php"; + + $event = array(); + $event['errno'] = $exception->getCode(); + $event['message'] = $exception->getMessage(); + $event['errfile'] = $exception->getFile(); + $event['errline'] = $exception->getLine(); + $event['backtrace'] = $exception->getTraceAsString(); + + $formatter = new Piwik_Log_Formatter_Exception_ScreenFormatter; + + $message = $formatter->format($event); + $message .= "<br><br>And this exception raised another exception \"". $e->getMessage()."\""; + + Piwik::exitWithErrorMessage( $message ); + } +} + diff --git a/core/Form.php b/core/Form.php new file mode 100644 index 0000000000..4b931c9ec6 --- /dev/null +++ b/core/Form.php @@ -0,0 +1,101 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Form.php 444 2008-04-11 13:38:22Z johmathe $ + * + * @package Piwik_Helper + */ + + +require_once "HTML/QuickForm.php"; +require_once "HTML/QuickForm/Renderer/ArraySmarty.php"; + +/** + * Parent class for forms to be included in Smarty + * + * For an example, @see Piwik_Login_Form + * + * @package Piwik_Helper + */ +abstract class Piwik_Form extends HTML_QuickForm +{ + protected $a_formElements = array(); + + function __construct( $action = '' ) + { + if(empty($action)) + { + $action = Piwik_Url::getCurrentUrl(); + } + parent::HTML_QuickForm('form', 'POST', $action); + + $this->registerRule( 'checkEmail', 'function', 'Piwik_Form_isValidEmailString'); + $this->registerRule( 'fieldHaveSameValue', 'function', 'Piwik_Form_fieldHaveSameValue'); + + $this->init(); + } + + abstract function init(); + + function getElementList() + { + $listElements=array(); + foreach($this->a_formElements as $title => $a_parameters) + { + foreach($a_parameters as $parameters) + { + if($parameters[1] != 'headertext' + && $parameters[1] != 'submit') + { + // case radio : there are two labels but only record once, unique name + if( !isset($listElements[$title]) + || !in_array($parameters[1], $listElements[$title])) + { + $listElements[$title][] = $parameters[1]; + } + } + } + } + return $listElements; + } + + function addElements( $a_formElements, $sectionTitle = '' ) + { + foreach($a_formElements as $parameters) + { + call_user_func_array(array(&$this , "addElement"), $parameters ); + } + + $this->a_formElements = + array_merge( + $this->a_formElements, + array( + $sectionTitle => $a_formElements + ) + ); + } + + function addRules( $a_formRules) + { + foreach($a_formRules as $parameters) + { + call_user_func_array(array(&$this , "addRule"), $parameters ); + } + + } + +} + +function Piwik_Form_fieldHaveSameValue($element, $value, $arg) +{ + $value2 = Piwik_Common::getRequestVar( $arg, '', 'string'); + return $value === $value2; +} + +function Piwik_Form_isValidEmailString( $element, $value ) +{ + return Piwik::isValidEmailString($value); +}
\ No newline at end of file diff --git a/core/FrontController.php b/core/FrontController.php new file mode 100644 index 0000000000..0e51db1c7e --- /dev/null +++ b/core/FrontController.php @@ -0,0 +1,316 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: FrontController.php 583 2008-07-28 00:37:19Z matt $ + * + * @package Piwik + */ + + +/** + * Zend classes + */ +require_once "Zend/Exception.php"; +require_once "Zend/Loader.php"; +require_once "Zend/Auth.php"; +require_once "Zend/Auth/Adapter/DbTable.php"; + +/** + * Piwik classes + */ +require_once "Timer.php"; +require_once "core/Piwik.php"; +require_once "API/APIable.php"; +require_once "Access.php"; +require_once "Auth.php"; +require_once "API/Proxy.php"; +require_once "Site.php"; +require_once "Translate.php"; +require_once "Mail.php"; +require_once "Url.php"; +require_once "Controller.php"; + +require_once "PluginsFunctions/Menu.php"; +require_once "PluginsFunctions/AdminMenu.php"; +require_once "PluginsFunctions/Widget.php"; +require_once "PluginsFunctions/Sql.php"; + +/** + * Front controller. + * This is the class hit in the first place. + * It dispatches the request to the right controller. + * + * For a detailed explanation, see the documentation on http://dev.piwik.org/trac/wiki/MainSequenceDiagram + * + * @package Piwik + */ +class Piwik_FrontController +{ + /** + * Set to false and the Front Controller will not dispatch the request + * + * @var bool + */ + static public $enableDispatch = true; + + static private $instance = null; + + /** + * returns singleton + * + * @return Piwik_FrontController + */ + static public function getInstance() + { + if (self::$instance == null) + { + $c = __CLASS__; + self::$instance = new $c(); + } + return self::$instance; + } + + /** + * Dispatches the request to the right plugin and executes the requested action on the plugin controller. + * + * @throws Exception in case the plugin doesn't exist, the action doesn't exist, there is not enough permission, etc. + * + * @param string $module + * @param string $action + * @param array $parameters + * @return mixed The returned value of the calls, often nothing as the module print but don't return data + * @see fetchDispatch() + */ + function dispatch( $module = null, $action = null, $parameters = null) + { + if( self::$enableDispatch === false) + { + return; + } + + if(is_null($module)) + { + $defaultModule = 'CoreHome'; + $module = Piwik_Common::getRequestVar('module', $defaultModule, 'string'); + } + + if(is_null($action)) + { + $action = Piwik_Common::getRequestVar('action', false); + } + + if(is_null($parameters)) + { + $parameters = array(); + } + + if(!ctype_alnum($module)) + { + throw new Exception("Invalid module name '$module'"); + } + + if( ! Piwik_PluginsManager::getInstance()->isPluginActivated( $module )) + { + throw new Exception_PluginDeactivated($module); + } + + $controllerClassName = "Piwik_".$module."_Controller"; + if(!class_exists($controllerClassName)) + { + $moduleController = "plugins/" . $module . "/Controller.php"; + if( !Zend_Loader::isReadable($moduleController)) + { + throw new Exception("Module controller $moduleController not found!"); + } + require_once $moduleController; + } + + $controller = new $controllerClassName; + if($action === false) + { + $action = $controller->getDefaultAction(); + } + + if( !is_callable(array($controller, $action))) + { + throw new Exception("Action $action not found in the controller $controllerClassName."); + } + + try { + return call_user_func_array( array($controller, $action ), $parameters); + } catch(Piwik_Access_NoAccessException $e) { + Piwik_PostEvent('FrontController.NoAccessException', $e); + } + } + + /** + * Often plugins controller display stuff using echo/print. + * Using this function instead of dispath() returns the output string form the actions calls. + * + * @param string $controllerName + * @param string $actionName + * @param array $parameters + * @return string + */ + function fetchDispatch( $controllerName = null, $actionName = null, $parameters = null) + { + ob_start(); + $output = $this->dispatch( $controllerName, $actionName, $parameters); + // if nothing returned we try to load something that was printed on the screen + if(empty($output)) + { + $output = ob_get_contents(); + } + ob_end_clean(); + return $output; + } + + /** + * Called at the end of the page generation + * + */ + function __destruct() + { + try { + Piwik::printSqlProfilingReportZend(); + Piwik::printQueryCount(); + } catch(Exception $e) {} + + if(Piwik::getModule() !== 'API') + { +// Piwik::printMemoryUsage(); +// Piwik::printTimer(); + } + } + + /** + * Checks that the directories Piwik needs write access are actually writable + * Displays a nice error page if permissions are missing on some directories + * + * @return void + */ + static public function checkDirectoriesWritableOrDie( $directoriesToCheck = null ) + { + $resultCheck = Piwik::checkDirectoriesWritable( $directoriesToCheck ); + if( array_search(false, $resultCheck) !== false ) + { + $directoryList = ''; + foreach($resultCheck as $dir => $bool) + { + $realpath = Piwik::realpath($dir); + if(!empty($realpath) && $bool === false) + { + $directoryList .= "<code>chmod 777 $realpath</code><br>"; + } + } + $directoryList .= ''; + $directoryMessage = "<p><b>Piwik couldn't write to some directories</b>.</p> <p>Try to Execute the following commands on your Linux server:</P>"; + $directoryMessage .= $directoryList; + $directoryMessage .= "<p>If this doesn't work, you can try to create the directories with your FTP software, and set the CHMOD to 777 (with your FTP software, right click on the directories, permissions)."; + $directoryMessage .= "<p>After applying the modifications, you can <a href='index.php'>refresh the page</a>."; + $directoryMessage .= "<p>If you need more help, try <a href='misc/redirectToUrl.php?url=http://piwik.org'>Piwik.org</a>."; + + Piwik_ExitWithMessage($directoryMessage); + } + } + + /** + * Must be called before dispatch() + * - checks that directories are writable, + * - loads the configuration file, + * - loads the plugin, + * - inits the DB connection, + * - etc. + * + * @return void + */ + function init() + { + try { + Zend_Registry::set('timer', new Piwik_Timer); + + $directoriesToCheck = array( + '/tmp', + '/tmp/templates_c', + '/tmp/cache', + ); + + self::checkDirectoriesWritableOrDie($directoriesToCheck); + self::assignCliParametersToRequest(); + + $exceptionToThrow = false; + + try { + Piwik::createConfigObject(); + } catch(Exception $e) { + Piwik_PostEvent('FrontController.NoConfigurationFile', $e); + $exceptionToThrow = $e; + } + + Piwik::loadPlugins(); + if($exceptionToThrow) + { + throw $exceptionToThrow; + } + Piwik::createDatabaseObject(); + Piwik::createLogObject(); + Piwik::installLoadedPlugins(); + Piwik::install(); + + Piwik_PostEvent('FrontController.initAuthenticationObject'); + try { + $authAdapter = Zend_Registry::get('auth'); + } catch(Exception $e){ + throw new Exception("Object 'auth' cannot be found in the Registry. Maybe the Login plugin is not activated? + <br>You can activate the plugin by adding:<br> + <code>Plugins[] = Login</code><br> + under the <code>[Plugins]</code> section in your config/config.inc.php"); + } + + $access = new Piwik_Access($authAdapter); + Zend_Registry::set('access', $access); + Zend_Registry::get('access')->loadAccess(); + + Piwik::raiseMemoryLimitIfNecessary(); + } catch(Exception $e) { + Piwik_ExitWithMessage($e->getMessage()); + } + } + + /** + * Assign CLI parameters as if they were REQUEST or GET parameters. + * You can trigger Piwik from the command line by + * # /usr/bin/php5 /path/to/piwik/index.php -- "module=API&method=Actions.getActions&idSite=1&period=day&date=previous8&format=php" + * + * @return void + */ + static protected function assignCliParametersToRequest() + { + if(isset($_SERVER['argc']) + && $_SERVER['argc'] > 0) + { + for ($i=1; $i < $_SERVER['argc']; $i++) + { + parse_str($_SERVER['argv'][$i],$tmp); + $_REQUEST = array_merge($_REQUEST, $tmp); + $_GET = array_merge($_GET, $tmp); + } + } + } +} + +/** + * Exception thrown when the requested plugin is not activated in the config file + * + * @package Piwik + */ +class Exception_PluginDeactivated extends Exception +{ + function __construct($module) + { + parent::__construct("The plugin '$module' is not activated. You can activate the plugin on the <a href='?module=CorePluginsAdmin'>Plugins admin page</a>."); + } +} diff --git a/core/Log.php b/core/Log.php new file mode 100644 index 0000000000..3291065e26 --- /dev/null +++ b/core/Log.php @@ -0,0 +1,162 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Log.php 558 2008-07-20 23:10:38Z matt $ + * + * @package Piwik_Log + */ + +require_once "Zend/Log.php"; +require_once "Zend/Log/Formatter/Interface.php"; +require_once "Zend/Log/Writer/Stream.php"; +require_once "Zend/Log/Writer/Db.php"; + +require_once "Common.php"; + +/** + * + * @package Piwik_Log + */ +abstract class Piwik_Log extends Zend_Log +{ + protected $logToDatabaseTableName = null; + protected $logToDatabaseColumnMapping = null; + protected $logToFileFilename = null; + protected $fileFormatter = null; + protected $screenFormatter = null; + + function __construct( $logToFileFilename, + $fileFormatter, + $screenFormatter, + $logToDatabaseTableName, + $logToDatabaseColumnMapping ) + { + parent::__construct(); + + + $this->logToFileFilename = Zend_Registry::get('config')->path->log . $logToFileFilename; + $this->fileFormatter = $fileFormatter; + $this->screenFormatter = $screenFormatter; + $this->logToDatabaseTableName = Piwik::prefixTable($logToDatabaseTableName); + $this->logToDatabaseColumnMapping = $logToDatabaseColumnMapping; + } + + static public function dump($var) + { + Zend_Registry::get('logger_message')->log(var_export($var, true), Piwik_Log::DEBUG); + } + + function addWriteToFile() + { + $writerFile = new Zend_Log_Writer_Stream($this->logToFileFilename); + Piwik::mkdir(Zend_Registry::get('config')->path->log); + $writerFile->setFormatter( $this->fileFormatter ); + $this->addWriter($writerFile); + } + + function addWriteToNull() + { + Zend_Loader::loadClass('Zend_Log_Writer_Null'); + $this->addWriter( new Zend_Log_Writer_Null ); + } + + function addWriteToDatabase() + { + $writerDb = new Zend_Log_Writer_Db( + Zend_Registry::get('db'), + $this->logToDatabaseTableName, + $this->logToDatabaseColumnMapping); + + $this->addWriter($writerDb); + } + + function addWriteToScreen() + { + $writerScreen = new Zend_Log_Writer_Stream('php://output'); + $writerScreen->setFormatter( $this->screenFormatter ); + $this->addWriter($writerScreen); + } + + public function getWritersCount() + { + return count($this->_writers); + } + + /** + * Log an event + * Overload Zend_log::log + */ + public function log($event) + { + // sanity checks + if (empty($this->_writers)) { + throw new Zend_Log_Exception('No writers were added'); + } + + $event['timestamp'] = date('c'); + + // pack into event required by filters and writers + $event = array_merge( $event, $this->_extras); + + // abort if rejected by the global filters + foreach ($this->_filters as $filter) { + if (! $filter->accept($event)) { + return; + } + } + + // send to each writer + foreach ($this->_writers as $writer) { + $writer->write($event); + } + } + +} + +/** + * + * + * @package Piwik_Log + */ +class Piwik_Log_Formatter_FileFormatter implements Zend_Log_Formatter_Interface +{ + /** + * Formats data into a single line to be written by the writer. + * + * @param array $event event data + * @return string formatted line to write to the log + */ + public function format($event) + { + foreach($event as &$value) + { + $value = str_replace("\n", '\n', $value); + $value = '"'.$value.'"'; + } + $str = implode(" ", $event) . "\n"; + return $str; + } +} + +class Piwik_Log_Formatter_ScreenFormatter implements Zend_Log_Formatter_Interface +{ + function format($string) + { + $string = self::getFormattedString($string); + return $string; + } + + static public function getFormattedString($string) + { + if(Piwik::isPhpCliMode()) + { + $string = str_replace(array('<br>','<br />','<br/>'), "\n", $string); + $string = strip_tags($string); + } + return $string; + } +} + diff --git a/core/Log/APICall.php b/core/Log/APICall.php new file mode 100644 index 0000000000..33cbea423b --- /dev/null +++ b/core/Log/APICall.php @@ -0,0 +1,118 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: APICall.php 485 2008-05-19 22:29:55Z matt $ + * + * @package Piwik_Log + * @subpackage Piwik_Log_APICall + */ + +/** + * Class used to log all the API Calls information (class / method / parameters / returned value / time spent) + * + * @package Piwik_Log + * @subpackage Piwik_Log_APICall + */ +class Piwik_Log_APICall extends Piwik_Log +{ + const ID = 'logger_api_call'; + + function __construct() + { + $logToFileFilename = self::ID; + $logToDatabaseTableName = self::ID; + $logToDatabaseColumnMapping = null; + $screenFormatter = new Piwik_Log_Formatter_APICall_ScreenFormatter; + $fileFormatter = new Piwik_Log_Formatter_FileFormatter; + + parent::__construct($logToFileFilename, + $fileFormatter, + $screenFormatter, + $logToDatabaseTableName, + $logToDatabaseColumnMapping ); + + $this->setEventItem('caller_ip', ip2long( Piwik_Common::getIp() ) ); + } + + function log( $className, $methodName, $parameterNames, $parameterValues, $executionTime, $returnedValue) + { + $event = array(); + $event['class_name'] = $className; + $event['method_name'] = $methodName; + $event['parameter_names_default_values'] = serialize($parameterNames); + $event['parameter_values'] = serialize($parameterValues); + $event['execution_time'] = $executionTime; + $event['returned_value'] = is_array($returnedValue) ? serialize($returnedValue) : $returnedValue; + + parent::log($event); + } +} + +/** + * Class used to format the API Call log on the screen. + * + * @package Piwik_Log + * @subpackage Piwik_Log_APICall + */ +class Piwik_Log_Formatter_APICall_ScreenFormatter extends Piwik_Log_Formatter_ScreenFormatter +{ + /** + * Formats data into a single line to be written by the writer. + * + * @param array $event event data + * @return string formatted line to write to the log + */ + public function format($event) + { + $str = "\n<br> "; + $str .= "Called: {$event['class_name']}.{$event['method_name']} (took {$event['execution_time']}ms) \n<br>"; + $str .= "Parameters: "; + $parameterNamesAndDefault = unserialize($event['parameter_names_default_values']); + $parameterValues = unserialize($event['parameter_values']); + + $i = 0; + foreach($parameterNamesAndDefault as $pName => $pDefault) + { + if(isset($parameterValues[$i])) + { + $currentValue = $parameterValues[$i]; + } + else + { + $currentValue = $pDefault; + } + + $currentValue = $this->formatValue($currentValue); + $str .= "$pName = $currentValue, "; + + $i++; + } + $str .= "\n<br> "; + +// $str .= "Returned: ".$this->formatValue($event['returned_value']); + $str .= "\n<br> "; + return parent::format($str); + } + + private function formatValue( $value ) + { + if(is_string($value)) + { + $value = "'$value'"; + } + if(is_null($value)) + { + $value= 'null'; + } + if(is_array($value)) + { + $value = "array( ".implode(", ", $value). ")"; + } + return $value; + + } +} + diff --git a/core/Log/Error.php b/core/Log/Error.php new file mode 100644 index 0000000000..c2ae08f9fd --- /dev/null +++ b/core/Log/Error.php @@ -0,0 +1,118 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Error.php 485 2008-05-19 22:29:55Z matt $ + * + * @package Piwik_Log + * @subpackage Piwik_Log_Error + */ + +/** + * Class used to log an error event. + * + * @package Piwik_Log + * @subpackage Piwik_Log_Error + */ +class Piwik_Log_Error extends Piwik_Log +{ + const ID = 'logger_error'; + function __construct() + { + $logToFileFilename = self::ID; + $logToDatabaseTableName = self::ID; + $logToDatabaseColumnMapping = null; + $screenFormatter = new Piwik_Log_Formatter_Error_ScreenFormatter; + $fileFormatter = new Piwik_Log_Formatter_FileFormatter; + + parent::__construct($logToFileFilename, + $fileFormatter, + $screenFormatter, + $logToDatabaseTableName, + $logToDatabaseColumnMapping ); + } + + function addWriteToScreen() + { + parent::addWriteToScreen(); + $writerScreen = new Zend_Log_Writer_Stream('php://stderr'); + $writerScreen->setFormatter( $this->screenFormatter ); + $this->addWriter($writerScreen); + } + + public function log($errno, $errstr, $errfile, $errline, $backtrace) + { + $event = array(); + $event['errno'] = $errno; + $event['message'] = $errstr; + $event['errfile'] = $errfile; + $event['errline'] = $errline; + $event['backtrace'] = $backtrace; + + parent::log($event); + } +} + + + +/** + * Format an error event to be displayed on the screen. + * + * @package Piwik_Log + * @subpackage Piwik_Log_Error + */ +class Piwik_Log_Formatter_Error_ScreenFormatter extends Piwik_Log_Formatter_ScreenFormatter +{ + /** + * Formats data into a single line to be written by the writer. + * + * @param array $event event data + * @return string formatted line to write to the log + */ + public function format($event) + { + $errno = $event['errno'] ; + $errstr = $event['message'] ; + $errfile = $event['errfile'] ; + $errline = $event['errline'] ; + $backtrace = $event['backtrace'] ; + + $strReturned = ''; + $errno = $errno & error_reporting(); + + // problem when using error_reporting with the @ silent fail operator + // it gives an errno 0, and in this case the objective is to NOT display anything on the screen! + // is there any other case where the errno is zero at this point? + if($errno == 0) return ''; + $strReturned .= "\n<div style='word-wrap: break-word; border: 3px solid red; padding:4px; width:70%; background-color:#FFFF96;'><b>"; + switch($errno) + { + case E_ERROR: $strReturned .= "Error"; break; + case E_WARNING: $strReturned .= "Warning"; break; + case E_PARSE: $strReturned .= "Parse Error"; break; + case E_NOTICE: $strReturned .= "Notice"; break; + case E_CORE_ERROR: $strReturned .= "Core Error"; break; + case E_CORE_WARNING: $strReturned .= "Core Warning"; break; + case E_COMPILE_ERROR: $strReturned .= "Compile Error"; break; + case E_COMPILE_WARNING: $strReturned .= "Compile Warning"; break; + case E_USER_ERROR: $strReturned .= "User Error"; break; + case E_USER_WARNING: $strReturned .= "User Warning"; break; + case E_USER_NOTICE: $strReturned .= "User Notice"; break; + case E_STRICT: $strReturned .= "Strict Notice"; break; + case E_RECOVERABLE_ERROR: $strReturned .= "Recoverable Error"; break; + case E_EXCEPTION: $strReturned .= "Exception"; break; + default: $strReturned .= "Unknown error ($errno)"; break; + } + $strReturned .= ":</b> <i>$errstr</i> in <b>$errfile</b> on line <b>$errline</b>\n"; + $strReturned .= "<br><br>Backtrace --><DIV style='font-family:Courier;font-size:10pt'>"; + $strReturned .= str_replace("\n", "<br>\n", $backtrace); + $strReturned .= "</div><br><br>"; + $strReturned .= "\n</pre></div><br>"; + + return parent::format($strReturned); + } +} + + diff --git a/core/Log/Exception.php b/core/Log/Exception.php new file mode 100644 index 0000000000..d41147dcbf --- /dev/null +++ b/core/Log/Exception.php @@ -0,0 +1,109 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Exception.php 583 2008-07-28 00:37:19Z matt $ + * + * @package Piwik_Log + * @subpackage Piwik_Log_Exception + */ +require_once "Log.php"; + +/** + * Class used to log an exception event. + * Displays the exception with a user friendly error message, suggests to get support from piwik.org + * + * @package Piwik_Log + * @subpackage Piwik_Log_Exception + */ +class Piwik_Log_Exception extends Piwik_Log +{ + const ID = 'logger_exception'; + function __construct() + { + $logToFileFilename = self::ID; + $logToDatabaseTableName = self::ID; + $logToDatabaseColumnMapping = null; + $screenFormatter = new Piwik_Log_Formatter_Exception_ScreenFormatter; + $fileFormatter = new Piwik_Log_Formatter_FileFormatter; + + parent::__construct($logToFileFilename, + $fileFormatter, + $screenFormatter, + $logToDatabaseTableName, + $logToDatabaseColumnMapping ); + } + + function addWriteToScreen() + { + parent::addWriteToScreen(); + $writerScreen = new Zend_Log_Writer_Stream('php://stderr'); + $writerScreen->setFormatter( $this->screenFormatter ); + $this->addWriter($writerScreen); + } + + public function log($exception) + { + + $event = array(); + $event['errno'] = $exception->getCode(); + $event['message'] = $exception->getMessage(); + $event['errfile'] = $exception->getFile(); + $event['errline'] = $exception->getLine(); + $event['backtrace'] = $exception->getTraceAsString(); + + parent::log($event); + } +} + + +/** + * Format an exception event to be displayed on the screen. + * + * @package Piwik_Log + * @subpackage Piwik_Log_Exception + */ +class Piwik_Log_Formatter_Exception_ScreenFormatter extends Piwik_Log_Formatter_ScreenFormatter +{ + /** + * Formats data into a single line to be written by the writer. + * + * @param array $event event data + * @return string formatted line to write to the log + */ + public function format($event) + { + $errno = $event['errno'] ; + $errstr = $event['message'] ; + $errfile = $event['errfile'] ; + $errline = $event['errline'] ; + $backtrace = $event['backtrace'] ; + + $divId = 'div'.$errline.$errno.rand(1,2000); + + $message = "<b>Uncaught exception</b>: '". $errstr."'"; + $message .= "<br><a onclick=\"if(document.getElementById('$divId').style.display=='none') { document.getElementById('$divId').style.display='inline' } else { document.getElementById('$divId').style.display = 'none' }\" href='#'>". + "\nMore information</a>". + "<div style='display:inline' id='$divId'>". + "<br>In <b>$errfile</b> on line <b>$errline</b>". + "<br><small>Backtrace:<br><pre>"; + $message .= str_replace("\n", "<br>", $backtrace); + $message .= "</pre>"; + $message .= "</small></div>"; + + // without javascript it displays the full error message + // but with javascript we hide the DIV and onclick we show it + $message .= "<script>document.getElementById('$divId').style.display='none';</script>"; + + $message .= "<br>You can get help from <a href='misc/redirectToUrl.php?url=http://piwik.org'>Piwik.org</a> (give us the full error message + your PHP and Mysql version)"; + + return parent::format($message); + } +} + + + + + diff --git a/core/Log/Message.php b/core/Log/Message.php new file mode 100644 index 0000000000..37e44a1a24 --- /dev/null +++ b/core/Log/Message.php @@ -0,0 +1,76 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Message.php 492 2008-05-23 01:08:12Z matt $ + * + * @package Piwik_Log + * @subpackage Piwik_Log_Message + */ + +/** + * Class used to log a standard message event. + * + * @package Piwik_Log + * @subpackage Piwik_Log_Message + */ +class Piwik_Log_Message extends Piwik_Log +{ + const ID = 'logger_message'; + function __construct() + { + $logToFileFilename = self::ID; + $logToDatabaseTableName = self::ID; + $logToDatabaseColumnMapping = null; + $screenFormatter = new Piwik_Log_Formatter_Message_ScreenFormatter; + $fileFormatter = new Piwik_Log_Formatter_FileFormatter; + + parent::__construct($logToFileFilename, + $fileFormatter, + $screenFormatter, + $logToDatabaseTableName, + $logToDatabaseColumnMapping ); + } + + public function log( $message ) + { + $event = array(); + $event['message'] = $message; + + parent::log($event); + } +} + + +/** + * Format a standard message event to be displayed on the screen. + * The message can be a PHP array or a string. + * + * @package Piwik_Log + * @subpackage Piwik_Log_Message + */ +class Piwik_Log_Formatter_Message_ScreenFormatter extends Piwik_Log_Formatter_ScreenFormatter +{ + /** + * Formats data into a single line to be written by the writer. + * + * @param array $event event data + * @return string formatted line to write to the log + */ + public function format($event) + { + if(is_array($event['message'])) + { + $message = "<pre>".var_export($event['message'], true)."</pre>"; + } + else + { + $message = $event['message']; + } + + return parent::format($message); + } +} + diff --git a/core/LogStats.php b/core/LogStats.php new file mode 100644 index 0000000000..e9519da763 --- /dev/null +++ b/core/LogStats.php @@ -0,0 +1,303 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: LogStats.php 575 2008-07-26 23:08:32Z matt $ + * + * @package Piwik_LogStats + */ + +/** + * Class used by the logging script piwik.php called by the javascript tag. + * Handles the visitor & his/her actions on the website, saves the data in the DB, saves information in the cookie, etc. + * + * To maximise the performance of the logging module, we use different techniques. + * + * On the PHP-only side: + * - minimize the number of external files included. + * Ideally only one (the configuration file) in all the normal cases. + * We load the Loggers only when an error occurs ; this error is logged in the DB/File/etc + * depending on the loggers settings in the configuration file. + * - we may have to include external classes but we try to include only very + * simple code without any dependency, so that we could simply write a script + * that would merge all this simple code into a big piwik.php file. + * + * On the Database-related side: + * - write all the SQL queries without using any DB abstraction layer. + * Of course we carefully filter all input values. + * - minimize the number of SQL queries necessary to complete the algorithm. + * - carefully index the tables used + * - try to have fixed length rows + * + * [ - use a partitionning by date for the tables ] + * + * - handle the timezone settings?? + * + * We could also imagine a batch system that would read a log file every 5min, + * and which prepares the file containg the rows to insert, then we load DATA INFILE + * + * + * Configuration options for the statsLogEngine module: + * - use_cookie ; defines if we try to get/set a cookie to help recognize a unique visitor + * + * @package Piwik_LogStats + */ +class Piwik_LogStats +{ + protected $stateValid; + + protected $urlToRedirect; + + /** + * + * @var Piwik_LogStats_Db + */ + static protected $db = null; + + const STATE_NOTHING_TO_NOTICE = 1; + const STATE_TO_REDIRECT_URL = 2; + const STATE_LOGGING_DISABLE = 10; + const STATE_NO_GET_VARIABLE = 11; + + const COOKIE_INDEX_IDVISITOR = 1; + const COOKIE_INDEX_TIMESTAMP_LAST_ACTION = 2; + const COOKIE_INDEX_TIMESTAMP_FIRST_ACTION = 3; + const COOKIE_INDEX_ID_VISIT = 4; + const COOKIE_INDEX_ID_LAST_ACTION = 5; + + public function __construct() + { + $this->stateValid = self::STATE_NOTHING_TO_NOTICE; + } + + static function connectDatabase() + { + if( !is_null(self::$db)) + { + return; + } + + $configDb = Piwik_LogStats_Config::getInstance()->database; + + // we decode the password. Password is html encoded because it's enclosed between " double quotes + $configDb['password'] = htmlspecialchars_decode($configDb['password']); + if(!isset($configDb['port'])) + { + // before 0.2.4 there is no port specified in config file + $configDb['port'] = '3306'; + } + self::$db = new Piwik_LogStats_Db( $configDb['host'], + $configDb['username'], + $configDb['password'], + $configDb['dbname'], + $configDb['port'] ); + self::$db->connect(); + } + + public static function getDb() + { + return self::$db; + } + + static function disconnectDb() + { + if(isset(self::$db)) + { + self::$db->disconnect(); + } + } + + private function initProcess() + { + try{ + $pluginsLogStats = Piwik_LogStats_Config::getInstance()->Plugins_LogStats; + if(is_array($pluginsLogStats) + && count($pluginsLogStats) != 0) + { + Piwik_PluginsManager::getInstance()->doNotLoadAlwaysActivatedPlugins(); + Piwik_PluginsManager::getInstance()->setPluginsToLoad( $pluginsLogStats['Plugins_LogStats'] ); + } + } catch(Exception $e) { + } + + $saveStats = Piwik_LogStats_Config::getInstance()->LogStats['record_statistics']; + + if($saveStats == 0) + { + $this->setState(self::STATE_LOGGING_DISABLE); + } + + if( count($_GET) == 0) + { + $this->setState(self::STATE_NO_GET_VARIABLE); + } + + $downloadVariableName = Piwik_LogStats_Config::getInstance()->LogStats['download_url_var_name']; + $urlDownload = Piwik_Common::getRequestVar( $downloadVariableName, '', 'string'); + + if( !empty($urlDownload) ) + { + if( Piwik_Common::getRequestVar( 'redirect', 1, 'int') == 1) + { + $this->setState( self::STATE_TO_REDIRECT_URL ); + } + $this->setUrlToRedirect ( $urlDownload); + } + + $outlinkVariableName = Piwik_LogStats_Config::getInstance()->LogStats['outlink_url_var_name']; + $urlOutlink = Piwik_Common::getRequestVar( $outlinkVariableName, '', 'string'); + + if( !empty($urlOutlink) ) + { + if( Piwik_Common::getRequestVar( 'redirect', 1, 'int') == 1) + { + $this->setState( self::STATE_TO_REDIRECT_URL ); + } + $this->setUrlToRedirect ( $urlOutlink); + } + } + + private function processVisit() + { + return $this->stateValid !== self::STATE_LOGGING_DISABLE + && $this->stateValid !== self::STATE_NO_GET_VARIABLE; + } + + private function getState() + { + return $this->stateValid; + } + + private function setUrlToRedirect( $url ) + { + $this->urlToRedirect = $url; + } + + private function getUrlToRedirect() + { + return $this->urlToRedirect; + } + + private function setState( $value ) + { + $this->stateValid = $value; + } + + /** + * Returns the LogStats_Visit object. + * This method can be overwritten so that we use a different LogStats_Visit object + * + * @return Piwik_LogStats_Visit + */ + protected function getNewVisitObject() + { + $visit = null; + Piwik_PostEvent('LogStats.getNewVisitObject', $visit); + + if(is_null($visit)) + { + $visit = new Piwik_LogStats_Visit(); + } + elseif(!($visit instanceof Piwik_LogStats_Visit_Interface )) + { + throw new Exception("The Visit object set in the plugin must implement Piwik_LogStats_Visit_Interface"); + } + + $visit->setDb(self::$db); + return $visit; + } + + // main algorithm + // => input : variables filtered + // => action : read cookie, read database, database logging, cookie writing + function main() + { + $this->initProcess(); + + if( $this->processVisit() ) + { + try { + self::connectDatabase(); + $visit = $this->getNewVisitObject(); + $visit->handle(); + } catch (PDOException $e) { + $this->setState(self::STATE_LOGGING_DISABLE); + } + } + $this->endProcess(); + } + + // display the logo or pixel 1*1 GIF + // or a marketing page if no parameters in the url + // or redirect to a url + // or load a URL (rss feed) (transmit the cookie as well) + protected function endProcess() + { + switch($this->getState()) + { + case self::STATE_LOGGING_DISABLE: + printDebug("Logging disabled, display transparent logo"); + $this->outputTransparentGif(); + break; + + case self::STATE_NO_GET_VARIABLE: + printDebug("No get variables => piwik page"); + echo "<a href='index.php'>Piwik</a> is a free open source <a href='http://piwik.org'>web analytics</a> alternative to Google analytics."; + break; + + + case self::STATE_TO_REDIRECT_URL: + $this->sendHeader('Location: ' . $this->getUrlToRedirect()); + break; + + + case self::STATE_NOTHING_TO_NOTICE: + default: + printDebug("Nothing to notice => default behaviour"); + $this->outputTransparentGif(); + break; + } + printDebug("End of the page."); + + if($GLOBALS['DEBUGPIWIK'] === true) + { + Piwik::printSqlProfilingReportLogStats(self::$db); + } + + self::disconnectDb(); + } + + protected function outputTransparentGif() + { + if( !isset($GLOBALS['DEBUGPIWIK']) || !$GLOBALS['DEBUGPIWIK'] ) + { + $trans_gif_64 = "R0lGODlhAQABAJEAAAAAAP///////wAAACH5BAEAAAIALAAAAAABAAEAAAICVAEAOw=="; + header("Content-type: image/gif"); + print(base64_decode($trans_gif_64)); + } + } + + protected function sendHeader($header) + { + header($header); + } +} + +function printDebug( $info = '' ) +{ + if(isset($GLOBALS['DEBUGPIWIK']) && $GLOBALS['DEBUGPIWIK']) + { + if(is_array($info)) + { + print("<PRE>"); + print(var_export($info,true)); + print("</PRE>"); + } + else + { + print($info . "<br>\n"); + } + } +} diff --git a/core/LogStats/Action.php b/core/LogStats/Action.php new file mode 100644 index 0000000000..942f1899c9 --- /dev/null +++ b/core/LogStats/Action.php @@ -0,0 +1,242 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Action.php 558 2008-07-20 23:10:38Z matt $ + * + * @package Piwik_LogStats + */ + +/** + * Interface of the Action object. + * New Action classes can be defined in plugins and used instead of the default one. + * + * @package Piwik_LogStats + */ +interface Piwik_LogStats_Action_Interface { + public function getActionId(); + public function record( $idVisit, $idRefererAction, $timeSpentRefererAction ); +} + +/** + * Handles an action by the visitor. + * A request to the piwik.php script is associated with one Action. + * This class is used to build the Action Name (which can be built from the URL, + * or can be directly specified in the JS code, etc.). + * It also saves the Action when necessary in the DB. + * + * About the Action concept: + * - An action is defined by a name. + * - The name can be specified in the JS Code in the variable 'action_name' + * For example you can decide to use the javascript value document.title as an action_name + * - If the name is not specified, we use the URL(path+query) to build a default name. + * For example for "http://piwik.org/test/my_page/test.html" + * the name would be "test/my_page/test.html" + * - If the name is empty we set it to default_action_name found in global.ini.php + * - Handling UTF8 in the action name + * PLUGIN_IDEA - An action is associated to URLs and link to the URL from the reports (currently actions do not link to the url of the pages) + * PLUGIN_IDEA - An action hit by a visitor is associated to the HTML title of the page that triggered the action and this HTML title is displayed in the interface + * + * + * @package Piwik_LogStats + */ +class Piwik_LogStats_Action implements Piwik_LogStats_Action_Interface +{ + private $actionName; + private $url; + private $defaultActionName; + private $nameDownloadOutlink; + + /** + * 3 types of action, Standard action / Download / Outlink click + */ + const TYPE_ACTION = 1; + const TYPE_DOWNLOAD = 3; + const TYPE_OUTLINK = 2; + + /** + * @param Piwik_LogStats_Db Database object to be used + */ + function __construct( $db ) + { + $this->actionName = Piwik_Common::getRequestVar( 'action_name', '', 'string'); + + $downloadVariableName = Piwik_LogStats_Config::getInstance()->LogStats['download_url_var_name']; + $this->downloadUrl = Piwik_Common::getRequestVar( $downloadVariableName, '', 'string'); + + $outlinkVariableName = Piwik_LogStats_Config::getInstance()->LogStats['outlink_url_var_name']; + $this->outlinkUrl = Piwik_Common::getRequestVar( $outlinkVariableName, '', 'string'); + + $nameVariableName = Piwik_LogStats_Config::getInstance()->LogStats['download_outlink_name_var']; + $this->nameDownloadOutlink = Piwik_Common::getRequestVar( $nameVariableName, '', 'string'); + + $this->url = Piwik_Common::getRequestVar( 'url', '', 'string'); + $this->db = $db; + $this->defaultActionName = Piwik_LogStats_Config::getInstance()->LogStats['default_action_name']; + } + + + /** + * Returns the idaction of the current action name. + * This idaction is used in the visitor logging table to link the visit information + * (entry action, exit action) to the actions. + * This idaction is also used in the table that links the visits and their actions. + * + * The methods takes care of creating a new record in the action table if the existing + * action name doesn't exist yet. + * + * @return int Id action that is associated to this action name in the Actions table lookup + */ + function getActionId() + { + $this->loadActionId(); + return $this->idAction; + } + + + /** + * Records in the DB the association between the visit and this action. + * + * @param int idVisit is the ID of the current visit in the DB table log_visit + * @param int idRefererAction is the ID of the last action done by the current visit. + * @param int timeSpentRefererAction is the number of seconds since the last action was done. + * It is directly related to idRefererAction. + */ + public function record( $idVisit, $idRefererAction, $timeSpentRefererAction) + { + $this->db->query("INSERT INTO ".$this->db->prefixTable('log_link_visit_action') + ." (idvisit, idaction, idaction_ref, time_spent_ref_action) VALUES (?,?,?,?)", + array($idVisit, $this->idAction, $idRefererAction, $timeSpentRefererAction) + ); + } + + /** + * Generates the name of the action from the URL or the specified name. + * Sets the name as $this->finalActionName + * + * @return void + */ + private function generateInfo() + { + $actionName = ''; + if(!empty($this->downloadUrl)) + { + $this->actionType = self::TYPE_DOWNLOAD; + $url = $this->downloadUrl; + //$actionName = $this->nameDownloadOutlink; + $actionName = $url; + } + elseif(!empty($this->outlinkUrl)) + { + $this->actionType = self::TYPE_OUTLINK; + $url = $this->outlinkUrl; + //remove the last '/' character if it's present + if(substr($url,-1) == '/') + { + $url = substr($url,0,-1); + } + $actionName = $this->nameDownloadOutlink; + if( empty($actionName) ) + { + $actionName = $url; + } + } + else + { + $this->actionType = self::TYPE_ACTION; + $url = $this->url; + $actionName = $this->actionName; + } + + // the ActionName wasn't specified + if( empty($actionName) ) + { + $actionName = trim(Piwik_Common::getPathAndQueryFromUrl($url)); + + // in case the $actionName is ending with a slash, + // which means that it is the index page of a category + // we append the defaultActionName + // toto/tata/ becomes toto/tata/index + if(strlen($actionName) > 0 + && $actionName[strlen($actionName)-1] == '/' + ) + { + $actionName.=$this->defaultActionName; + } + } + + /* + * Clean the action name + */ + + // get the delimiter, by default '/' + $actionCategoryDelimiter = Piwik_LogStats_Config::getInstance()->General['action_category_delimiter']; + + // case the name is an URL we dont clean the name the same way + if(Piwik_Common::isLookLikeUrl($actionName)) + { + $actionName = trim($actionName); + } + else + { + // create an array of the categories delimited by the delimiter + $split = explode($actionCategoryDelimiter, $actionName); + + // trim every category + $split = array_map('trim', $split); + + // remove empty categories + $split = array_filter($split); + + // rebuild the name from the array of cleaned categories + $actionName = implode($actionCategoryDelimiter, $split); + } + + // remove the extra bad characters if any (shouldn't be any at this point...) + $actionName = str_replace(array("\n", "\r"), '', $actionName); + + if(empty($actionName)) + { + $actionName = $this->defaultActionName; + } + + $this->finalActionName = $actionName; + } + + /** + * Sets the attribute $idAction based on $finalActionName and $actionType. + * + * @see getActionId() + */ + private function loadActionId() + { + $this->generateInfo(); + + $name = $this->finalActionName; + $type = $this->actionType; + + $idAction = $this->db->fetch(" SELECT idaction + FROM ".$this->db->prefixTable('log_action') + ." WHERE name = ? AND type = ?", + array($name, $type) + ); + + // the action name has not been found, create it + if($idAction === false) + { + $this->db->query("INSERT INTO ". $this->db->prefixTable('log_action'). "( name, type ) + VALUES (?,?)",array($name,$type) ); + $idAction = $this->db->lastInsertId(); + } + else + { + $idAction = $idAction['idaction']; + } + + $this->idAction = $idAction; + } + +} + diff --git a/core/LogStats/Config.php b/core/LogStats/Config.php new file mode 100644 index 0000000000..65c38d9144 --- /dev/null +++ b/core/LogStats/Config.php @@ -0,0 +1,80 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Config.php 450 2008-04-20 22:33:27Z matt $ + * + * @package Piwik_LogStats + */ + +/** + * Simple class to access the configuration file + * + * This is essentially a simple version of Zend_Config that we wrote + * because of performance reasons. + * The LogStats module can't afford a dependency with the Zend_Framework. + * + * It's using the php.net/parse_ini_file function to parse the configuration files. + * It can be used to access both user config.ini.php and piwik global.ini.php config file. + * + * @package Piwik_LogStats + */ +class Piwik_LogStats_Config +{ + static private $instance = null; + + /** + * Returns singleton + * + * @return Piwik_LogStats_Config + */ + static public function getInstance() + { + if (self::$instance == null) + { + $c = __CLASS__; + self::$instance = new $c(); + } + return self::$instance; + } + + /** + * Contains configuration files values + * + * @var array + */ + public $config = array(); + + private function __construct() + { + $pathIniFileUser = 'config/config.ini.php'; + $pathIniFileGlobal = 'config/global.ini.php'; + $this->configUser = parse_ini_file($pathIniFileUser, true); + $this->configGlobal = parse_ini_file($pathIniFileGlobal, true); + } + + /** + * Magic get methods catching calls to $config->var_name + * Returns the value if found in the + * + * @param string $name + * @return mixed The value requested, usually a string + * @throws exception if the value requested not found in both files + */ + public function __get( $name ) + { + if(isset($this->configUser[$name])) + { + return $this->configUser[$name]; + } + if(isset($this->configGlobal[$name])) + { + return $this->configGlobal[$name]; + } + throw new Exception("The config element $name is not available in the configuration (check the configuration file)."); + } +} + + diff --git a/core/LogStats/Db.php b/core/LogStats/Db.php new file mode 100644 index 0000000000..f190c0c61c --- /dev/null +++ b/core/LogStats/Db.php @@ -0,0 +1,261 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Db.php 522 2008-06-11 00:31:03Z matt $ + * + * @package Piwik_LogStats + */ + +/** + * Simple database PDO wrapper. + * We can't afford to have a dependency with the Zend_Db module in LogStats. + * We wrote this simple class + * + * @package Piwik_LogStats + */ + +class Piwik_LogStats_Db +{ + private $connection = null; + private $username; + private $password; + + static private $profiling = false; + + protected $queriesProfiling = array(); + + /** + * Builds the DB object + */ + public function __construct( $host, $username, $password, $dbname, $port, $driverName = 'mysql') + { + $this->dsn = $driverName.":dbname=$dbname;host=$host;port=$port"; + $this->username = $username; + $this->password = $password; + } + + + /** + * Returns true if the SQL profiler is enabled + * Only used by the unit test that tests that the profiler is off on a production server + * + * @return bool + */ + static public function isProfilingEnabled() + { + return self::$profiling; + } + + /** + * Enables the SQL profiling. + * For each query, saves in the DB the time spent on this query. + * Very useful to see the slow query under heavy load. + * You can then use Piwik::printSqlProfilingReportLogStats(); + * to display the SQLProfiling report and see which queries take time, etc. + */ + static public function enableProfiling() + { + self::$profiling = true; + } + + /** + * Disables the SQL profiling logging. + */ + static public function disableProfiling() + { + self::$profiling = false; + } + + /** + * Connects to the DB + * + * @throws Exception if there was an error connecting the DB + */ + public function connect() + { + if(self::$profiling) + { + $timer = $this->initProfiler(); + } + + $this->connection = new PDO($this->dsn, $this->username, $this->password); + $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + // we may want to setAttribute(PDO::ATTR_TIMEOUT ) to a few seconds (default is 60) in case the DB is locked + // the piwik.php would stay waiting for the database... bad! + // we delete the password from this object "just in case" it could be printed + $this->password = ''; + + if(self::$profiling) + { + $this->recordQueryProfile('connect', $timer); + } + } + + /** + * Disconnects from the Mysql server + * + * @return void + */ + public function disconnect() + { + if(self::$profiling) + { + $this->recordProfiling(); + } + $this->connection = null; + } + + /** + * Returns the table name prefixed by the table prefix. + * + * @param string The table name to prefix, ie "log_visit" + * @return string The table name prefixed, ie "piwik-production_log_visit" + */ + public function prefixTable( $suffix ) + { + static $prefix; + if (!isset($prefix)) { + $prefix = Piwik_LogStats_Config::getInstance()->database['tables_prefix']; + } + return $prefix . $suffix; + } + + /** + * Returns an array containing all the rows of a query result, using optional bound parameters. + * + * @param string Query + * @param array Parameters to bind + * @see also query() + * @throws Exception if an exception occured + */ + public function fetchAll( $query, $parameters = array() ) + { + try { + $sth = $this->query( $query, $parameters ); + if($sth === false) + { + return false; + } + return $sth->fetchAll(PDO::FETCH_ASSOC); + } catch (PDOException $e) { + throw new Exception("Error query: ".$e->getMessage()); + } + } + + /** + * Returns the first row of a query result, using optional bound parameters. + * + * @param string Query + * @param array Parameters to bind + * @see also query() + * + * @throws Exception if an exception occured + */ + public function fetch( $query, $parameters = array() ) + { + try { + $sth = $this->query( $query, $parameters ); + if($sth === false) + { + return false; + } + return $sth->fetch(PDO::FETCH_ASSOC); + } catch (PDOException $e) { + throw new Exception("Error query: ".$e->getMessage()); + } + } + + /** + * Executes a query, using optional bound parameters. + * + * @param string Query + * @param array Parameters to bind + * + * @return PDOStatement or false if failed + * @throw Exception if an exception occured + */ + public function query($query, $parameters = array()) + { + if(is_null($this->connection)) + { + return false; + } + try { + if(self::$profiling) + { + $timer = $this->initProfiler(); + } + + $sth = $this->connection->prepare($query); + $sth->execute( $parameters ); + + if(self::$profiling) + { + $this->recordQueryProfile($query, $timer); + } + return $sth; + } catch (PDOException $e) { + throw new Exception("Error query: ".$e->getMessage()); + } + } + + protected function initProfiler() + { + require_once "Timer.php"; + return new Piwik_Timer; + } + + protected function recordQueryProfile( $query, $timer ) + { + if(!isset($this->queriesProfiling[$query])) $this->queriesProfiling[$query] = array('sum_time_ms' => 0, 'count' => 0); + $time = $timer->getTimeMs(2); + $time += $this->queriesProfiling[$query]['sum_time_ms']; + $count = $this->queriesProfiling[$query]['count'] + 1; + $this->queriesProfiling[$query] = array('sum_time_ms' => $time, 'count' => $count); + } + + /** + * Returns the last inserted ID in the DB + * Wrapper of PDO::lastInsertId() + * + * @return int + */ + public function lastInsertId() + { + return $this->connection->lastInsertId(); + } + + /** + * When destroyed, if SQL profiled enabled, logs the SQL profiling information + */ + public function recordProfiling() + { + if(is_null($this->connection)) + { + return; + } + + // turn off the profiler so we don't profile the following queries + self::$profiling = false; + + foreach($this->queriesProfiling as $query => $info) + { + $time = $info['sum_time_ms']; + $count = $info['count']; + + $queryProfiling = "INSERT INTO ".$this->prefixTable('log_profiling')." + (query,count,sum_time_ms) VALUES (?,$count,$time) + ON DUPLICATE KEY + UPDATE count=count+$count,sum_time_ms=sum_time_ms+$time"; + $this->query($queryProfiling,array($query)); + } + + // turn back on profiling + self::$profiling = true; + } +} + + diff --git a/core/LogStats/Generator.php b/core/LogStats/Generator.php new file mode 100644 index 0000000000..dfc6b03052 --- /dev/null +++ b/core/LogStats/Generator.php @@ -0,0 +1,666 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Generator.php 492 2008-05-23 01:08:12Z matt $ + * + * @package Piwik_LogStats + */ + +/** + * Class used to generate fake visits. + * Useful to test performances, general functional testing, etc. + * + * Objective: + * Generate thousands of visits / actions per visitor using + * a single request to misc/generateVisits.php + * + * Requirements of the visits generator script. Fields that can be edited: + * - url => campaigns + * - newsletter + * - partner + * - campaign CPC + * - referer + * - search engine + * - misc site + * - same website + * - url => multiple directories, page names + * - multiple idsite + * - multiple settings configurations + * - action_name + * - HTML title + * + * + * @package Piwik_LogStats + * @subpackage Piwik_LogStats_Generator + * + * "Le Generator, il est trop Fort!" + * - Random fan + */ + +class Piwik_LogStats_Generator +{ + /** + * GET parameters array of values to be used for the current visit + * + * @var array ('res' => '1024x768', 'urlref' => 'http://google.com/search?q=piwik', ...) + */ + protected $currentget = array(); + + /** + * Array of all the potential values for the visit parameters + * Values of 'resolution', 'urlref', etc. will be randomly read from this array + * + * @var array ( + * 'res' => array('1024x768','800x600'), + * 'urlref' => array('google.com','intel.com','amazon.com'), + * ....) + */ + protected $allget = array(); + + /** + * See @see setMaximumUrlDepth + * + * @var int + */ + protected $maximumUrlDepth = 1; + + /** + * Unix timestamp to use for the generated visitor + * + * @var int Unix timestamp + */ + protected $timestampToUse; + + /** + * See @see disableProfiler() + * The profiler is enabled by default + * + * @var bool + */ + protected $profiling = true; + + /** + * If set to true, this will TRUNCATE the profiling tables at every new generated visit + * @see initProfiler() + * + * @var bool + */ + public $reinitProfilingAtEveryRequest = true; + + /** + * Hostname used to prefix all the generated URLs + * we could make this variable dynamic so that a visitor can make hit on several hosts and + * only the good ones should be kept (feature not yet implemented in piwik) + * + * @var string + */ + public $host = 'http://localhost'; + + /** + * IdSite to generate visits for (@see setIdSite()) + * + * @var int + */ + public $idSite = 1; + + /** + * Overwrite the global GET/POST/COOKIE variables and set the fake ones @see setFakeRequest() + * Reads the configuration file but disables write to this file + * Creates the database object & enable profiling by default (@see disableProfiler()) + * + */ + public function __construct() + { + $_COOKIE = $_GET = $_REQUEST = $_POST = array(); + + // init GET and REQUEST to the empty array + $this->setFakeRequest(); + + require_once "core/Piwik.php"; + Piwik::createConfigObject('../config/config.ini.php'); + Zend_Registry::get('config')->doWriteFileWhenUpdated = false; + + // setup database + Piwik::createDatabaseObject(); + + Piwik_LogStats_Db::enableProfiling(); + + $this->timestampToUse = time(); + } + + /** + * Sets the depth level of the generated URLs + * value = 1 => path OR path/page1 + * value = 2 => path OR path/pageRand OR path/dir1/pageRand + * + * @param int Depth + */ + public function setMaximumUrlDepth($value) + { + $this->maximumUrlDepth = (int)$value; + } + + /** + * Set the timestamp to use as the starting time for the visitors times + * You have to call this method for every day you want to generate data + * + * @param int Unix timestamp + */ + public function setTimestampToUse($timestamp) + { + $this->timestampToUse = $timestamp; + } + + /** + * Returns the timestamp to be used as the visitor timestamp + * + * @return int Unix timestamp + */ + public function getTimestampToUse() + { + return $this->timestampToUse; + } + + /** + * Set the idsite to generate the visits for + * To be called before init() + * + * @param int idSite + */ + public function setIdSite($idSite) + { + $this->idSite = $idSite; + } + + /** + * Add a value to the GET global array. + * The generator script will then randomly read a value from this array. + * + * For example, $name = 'res' $aValue = '1024x768' + * + * @param string Name of the parameter _GET[$name] + * @param array|mixed Value of the parameter + * @return void + */ + protected function addParam( $name, $aValue) + { + if(is_array($aValue)) + { + $this->allget[$name] = array_merge( $aValue, + (array)@$this->allget[$name]); + } + else + { + $this->allget[$name][] = $aValue; + } + } + + /** + * TRUNCATE all logs related tables to start a fresh logging database. + * Be careful, any data deleted this way is deleted forever + * + * @return void + */ + public function emptyAllLogTables() + { + $db = Zend_Registry::get('db'); + $db->query('TRUNCATE TABLE '.Piwik::prefixTable('log_action')); + $db->query('TRUNCATE TABLE '.Piwik::prefixTable('log_visit')); + $db->query('TRUNCATE TABLE '.Piwik::prefixTable('log_link_visit_action')); + } + + /** + * Call this method to disable the SQL query profiler + */ + public function disableProfiler() + { + $this->profiling = false; + Piwik_LogStats_Db::disableProfiling(); + } + + /** + * This is called at the end of the Generator script. + * Calls the Profiler output if the profiler is enabled. + * + * @return void + */ + public function end() + { + Piwik_LogStats::disconnectDb(); + if($this->profiling) + { + Piwik::printSqlProfilingReportLogStats(); + } + } + + /** + * Init the Generator script: + * - init the SQL profiler + * - init the random generator + * - setup the different possible values for parameters such as 'resolution', + * 'color', 'hour', 'minute', etc. + * - load from DataFiles and setup values for the other parameters such as UserAgent, Referers, AcceptedLanguages, etc. + * @see /misc/generateVisitsData/ + * + * @return void + */ + public function init() + { + Piwik::createLogObject(); + + $this->initProfiler(); + + /* + * Init the random number generator + */ + function make_seed() + { + list($usec, $sec) = explode(' ', microtime()); + return (float) $sec + ((float) $usec * 100000); + } + mt_srand(make_seed()); + + /* + * Sets values for: resolutions, colors, idSite, times + */ + $common = array( + 'res' => array('1289x800','1024x768','800x600','564x644','200x100','50x2000',), + 'col' => array(24,32,16), + 'idsite'=> $this->idSite, + 'h' => range(0,23), + 'm' => range(0,59), + 's' => range(0,59), + ); + + foreach($common as $label => $values) + { + $this->addParam($label,$values); + } + + /* + * Sets values for: outlinks, downloads, campaigns + */ + // we get the name of the Download/outlink variables + $downloadOrOutlink = array( + Piwik_LogStats_Config::getInstance()->LogStats['download_url_var_name'], + Piwik_LogStats_Config::getInstance()->LogStats['outlink_url_var_name'], + ); + // we have a 20% chance to add a download or outlink variable to the URL + $this->addParam('piwik_downloadOrOutlink', $downloadOrOutlink); + $this->addParam('piwik_downloadOrOutlink', array_fill(0,8,'')); + + // we get the variables name for the campaign parameters + $campaigns = array( + Piwik_LogStats_Config::getInstance()->LogStats['campaign_var_name'], + Piwik_LogStats_Config::getInstance()->LogStats['newsletter_var_name'], + Piwik_LogStats_Config::getInstance()->LogStats['partner_var_name'], + ); + // we generate a campaign in the URL in 3/18 % of the generated URls + $this->addParam('piwik_vars_campaign', $campaigns); + $this->addParam('piwik_vars_campaign', array_fill(0,15,'')); + + + /* + * Sets values for: Referers, user agents, accepted languages + */ + // we load some real referers to be used by the generator + $referers = array(); + require_once "misc/generateVisitsData/Referers.php"; + + $this->addParam('urlref',$referers); + + // and we add 2000 empty referers so that some visitors don't come using a referer (direct entry) + $this->addParam('urlref',array_fill(0,2000,'')); + + // load some user agent and accept language + $userAgent = $acceptLanguages = array(); + require_once "misc/generateVisitsData/UserAgent.php"; + require_once "misc/generateVisitsData/AcceptLanguage.php"; + $this->userAgents=$userAgent; + $this->acceptLanguage=$acceptLanguages; + } + + /** + * If the SQL profiler is enabled and if the reinit at every request is set to true, + * then we TRUNCATE the profiling information so that we only profile one visitor at a time + * + * @return void + */ + protected function initProfiler() + { + /* + * Inits the profiler + */ + if($this->profiling) + { + if($this->reinitProfilingAtEveryRequest) + { + $all = Zend_Registry::get('db')->query('TRUNCATE TABLE '.Piwik::prefixTable('log_profiling').'' ); + } + } + } + /** + * Launches the process and generates an exact number of nbVisits + * For each visit, we setup the timestamp to the common timestamp + * Then we generate between 1 and nbActionsMaxPerVisit actions for this visit + * The generated actions will have a growing timestamp so it looks like a real visit + * + * @param int The number of visits to generate + * @param int The maximum number of actions to generate per visit + * + * @return int The number of total actions generated + */ + public function generate( $nbVisits, $nbActionsMaxPerVisit ) + { + $nbActionsTotal = 0; + for($i = 0; $i < $nbVisits; $i++) + { + $nbActions = mt_rand(1, $nbActionsMaxPerVisit); + + Piwik_LogStats_Generator_Visit::setTimestampToUse($this->getTimestampToUse()); + + $this->generateNewVisit(); + for($j = 1; $j <= $nbActions; $j++) + { + $this->generateActionVisit(); + $this->saveVisit(); + } + + $nbActionsTotal += $nbActions; + } + return $nbActionsTotal; + } + + /** + * Generates a new visitor. + * Loads random values for all the necessary parameters (resolution, local time, referers, etc.) from the fake GET array. + * Also generates a random IP. + * + * We change the superglobal values of HTTP_USER_AGENT, HTTP_CLIENT_IP, HTTP_ACCEPT_LANGUAGE to the generated value. + * + * @return void + */ + protected function generateNewVisit() + { + $this->setCurrentRequest( 'urlref' , $this->getRandom('urlref')); + $this->setCurrentRequest( 'idsite', $this->getRandom('idsite')); + $this->setCurrentRequest( 'res' ,$this->getRandom('res')); + $this->setCurrentRequest( 'col' ,$this->getRandom('col')); + $this->setCurrentRequest( 'h' ,$this->getRandom('h')); + $this->setCurrentRequest( 'm' ,$this->getRandom('m')); + $this->setCurrentRequest( 's' ,$this->getRandom('s')); + $this->setCurrentRequest( 'fla' ,$this->getRandom01()); + $this->setCurrentRequest( 'dir' ,$this->getRandom01()); + $this->setCurrentRequest( 'qt' ,$this->getRandom01()); + $this->setCurrentRequest( 'realp' ,$this->getRandom01()); + $this->setCurrentRequest( 'pdf' ,$this->getRandom01()); + $this->setCurrentRequest( 'wma' ,$this->getRandom01()); + $this->setCurrentRequest( 'java' ,$this->getRandom01()); + $this->setCurrentRequest( 'cookie',$this->getRandom01()); + + $_SERVER['HTTP_CLIENT_IP'] = mt_rand(0,255).".".mt_rand(0,255).".".mt_rand(0,255).".".mt_rand(0,255); + $_SERVER['HTTP_USER_AGENT'] = $this->userAgents[mt_rand(0,count($this->userAgents)-1)]; + $_SERVER['HTTP_ACCEPT_LANGUAGE'] = $this->acceptLanguage[mt_rand(0,count($this->acceptLanguage)-1)]; + } + + /** + * Generates a new action for the current visitor. + * We random generate some campaigns, action names, download or outlink clicks, etc. + * We generate a new Referer, that would be read in the case the visit last page is older than 30 minutes. + * + * This function tries to generate actions that use the features of Piwik (campaigns, downloads, outlinks, action_name set in the JS tag, etc.) + * + * @return void + * + */ + protected function generateActionVisit() + { + // we don't keep the previous action values + // reinit them to empty string + $this->setCurrentRequest( Piwik_LogStats_Config::getInstance()->LogStats['download_outlink_name_var'],''); + $this->setCurrentRequest( Piwik_LogStats_Config::getInstance()->LogStats['download_url_var_name'],''); + $this->setCurrentRequest( Piwik_LogStats_Config::getInstance()->LogStats['outlink_url_var_name'],''); + $this->setCurrentRequest( 'action_name', ''); + + // generate new url referer ; case the visitor stays more than 30min + // (when the visit is known this value will simply be ignored) + $this->setCurrentRequest( 'urlref' , $this->getRandom('urlref')); + + // generates the current URL + $url = $this->getRandomUrlFromHost($this->host); + + // we generate a campaign (partner or newsletter or campaign) + $urlVars = $this->getRandom('piwik_vars_campaign'); + + // if we actually generated a campaign + if(!empty($urlVars)) + { + // campaign name + $urlValue = $this->getRandomString(5,3,'lower'); + + // add the parameter to the url + $url .= '?'. $urlVars . '=' . $urlValue; + + // for a campaign of the CPC kind, we sometimes generate a keyword + if($urlVars == Piwik_LogStats_Config::getInstance()->LogStats['campaign_var_name'] + && mt_rand(0,1)==0) + { + $url .= '&'. Piwik_LogStats_Config::getInstance()->LogStats['campaign_keyword_var_name'] + . '=' . $this->getRandomString(6,3,'ALL');; + } + } + else + { + // we generate a download Or Outlink parameter in the GET request so that + // the current action is counted as a download action OR a outlink click action + $GETParamToAdd = $this->getRandom('piwik_downloadOrOutlink'); + if(!empty($GETParamToAdd)) + { + + $possibleDownloadHosts = array('http://piwik.org/',$this->host); + $nameDownload = $this->getRandomUrlFromHost($possibleDownloadHosts[mt_rand(0,1)]); + $extensions = array('.zip','.tar.gz'); + $nameDownload .= $extensions[mt_rand(0,1)]; + $urlValue = $nameDownload; + + // add the parameter to the url + $this->setCurrentRequest( $GETParamToAdd , $urlValue); + + // in 50% we give a special name to the download/outlink + if(mt_rand(0,1)==0) + { + $nameDownload = $this->getRandomString(6,3,'ALL'); + + $this->setCurrentRequest( Piwik_LogStats_Config::getInstance()->LogStats['download_outlink_name_var'] + , $nameDownload); + } + } + + // if we didn't set any campaign NOR any download click + // then we sometimes set a special action name to the current action + elseif(rand(0,2)==1) + { + $this->setCurrentRequest( 'action_name' , $this->getRandomString(1,1)); + } + } + + $this->setCurrentRequest( 'url' ,$url); + + // setup the title of the page + $this->setCurrentRequest( 'title',$this->getRandomString(15,5)); + } + + /** + * Returns a random URL using the $host as the URL host. + * Depth level depends on @see setMaximumUrlDepth() + * + * @param string Hostname of the URL to generate, eg. http://example.com/ + * + * @return string The generated URL + */ + protected function getRandomUrlFromHost( $host ) + { + $url = $host; + + $deep = mt_rand(0,$this->maximumUrlDepth); + for($i=0;$i<$deep;$i++) + { + $name = $this->getRandomString(1,1,'alnum'); + + $url .= '/'.$name; + } + return $url; + } + + /** + * Generates a random string from minLength to maxLength using a specified set of characters + * + * Taken from php.net and then badly hacked by some unknown monkey + * + * @param int (optional) Maximum length of the string to generate + * @param int (optional) Minimum length of the string to generate + * @param string (optional) Characters set to use, 'ALL' or 'lower' or 'upper' or 'numeric' or 'ALPHA' or 'ALNUM' + * + * @return string The generated random string + */ + protected function getRandomString($maxLength = 15, $minLength = 5, $type = 'ALL') + { + $len = mt_rand($minLength, $maxLength); + + // Register the lower case alphabet array + $alpha = array('a', 'd', 'e', 'f', 'g'); + + // Register the upper case alphabet array + $ALPHA = array('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'); + + // Register the numeric array + $num = array('1', '2', '3', '8', '9', '0'); + + // Register the strange array + $strange = array('/', '?', '!','"','£','$','%','^','&','*','(',')',' '); + + // Initialize the keyVals array for use in the for loop + $keyVals = array(); + + // Initialize the key array to register each char + $key = array(); + + // Loop through the choices and register + // The choice to keyVals array + switch ($type) + { + case 'lower' : + $keyVals = $alpha; + break; + case 'upper' : + $keyVals = $ALPHA; + break; + case 'numeric' : + $keyVals = $num; + break; + case 'ALPHA' : + $keyVals = array_merge($alpha, $ALPHA); + break; + case 'alnum' : + $keyVals = array_merge($alpha, $num); + break; + case 'ALNUM' : + $keyVals = array_merge($alpha, $ALPHA, $num); + break; + case 'ALL' : + $keyVals = array_merge($alpha, $ALPHA, $num, $strange); + break; + } + + // Loop as many times as specified + // Register each value to the key array + for($i = 0; $i <= $len-1; $i++) + { + $r = mt_rand(0,count($keyVals)-1); + $key[$i] = $keyVals[$r]; + } + + // Glue the key array into a string and return it + return join("", $key); + } + + /** + * Sets the _GET and _REQUEST superglobal to the current generated array of values. + * @see setCurrentRequest() + * This method is called once the current action parameters array has been generated from + * the global parameters array + * + * @return void + */ + protected function setFakeRequest() + { + $_REQUEST = $_GET = $this->currentget; + } + + /** + * Sets a value in the current action request array. + * + * @param string Name of the parameter to set + * @param string Value of the parameter + */ + protected function setCurrentRequest($name,$value) + { + $this->currentget[$name] = $value; + } + + /** + * Returns a value for the given parameter $name read randomly from the global parameter array. + * @see init() + * + * @param string Name of the parameter value to randomly load and return + * @return mixed Random value for the parameter named $name + * @throws Exception if the parameter asked for has never been set + * + */ + protected function getRandom( $name ) + { + if(!isset($this->allget[$name])) + { + throw new exception("You are asking for $name which doesnt exist"); + } + else + { + $index = mt_rand(0,count($this->allget[$name])-1); + $value =$this->allget[$name][$index]; + return $value; + } + } + + /** + * Returns either 0 or 1 + * + * @return int 0 or 1 + */ + protected function getRandom01() + { + return mt_rand(0,1); + } + + /** + * Saves the visit + * - replaces GET and REQUEST by the fake generated request + * - load the LogStats class and call the method to launch the recording + * + * This will save the visit in the database + * + * @return void + */ + protected function saveVisit() + { + $this->setFakeRequest(); + $process = new Piwik_LogStats_Generator_LogStats; + $process->main(); + } + +} +require_once "Generator/LogStats.php"; +require_once "Generator/Visit.php"; diff --git a/core/LogStats/Generator/LogStats.php b/core/LogStats/Generator/LogStats.php new file mode 100644 index 0000000000..92a8d29c39 --- /dev/null +++ b/core/LogStats/Generator/LogStats.php @@ -0,0 +1,58 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Generator.php 404 2008-03-23 01:09:59Z matt $ + * + * @package Piwik_LogStats + */ + + +/** + * Fake Piwik_LogStats that: + * - overwrite the sendHeader method so that no headers are sent. + * - doesn't print the 1pixel transparent GIF at the end of the visit process + * - overwrite the logstat_visit object to use so we use our own logstats_visit @see Piwik_LogStats_Generator_Visit + * + * @package Piwik_LogStats + * @subpackage Piwik_LogStats_Generator + */ +class Piwik_LogStats_Generator_LogStats extends Piwik_LogStats +{ + /** + * Does nothing instead of sending headers + * + * @return void + */ + protected function sendHeader($header) + { + } + + /** + * Does nothing instead of displaying a 1x1 transparent pixel GIF + * + * @return void + */ + protected function endProcess() + { + } + + /** + * Returns our 'generator home made' Piwik_LogStats_Generator_Visit object. + * + * @return Piwik_LogStats_Generator_Visit + */ + protected function getNewVisitObject() + { + $visit = new Piwik_LogStats_Generator_Visit(); + $visit->setDb(self::$db); + return $visit; + } + + static function disconnectDb() + { + return; + } +}
\ No newline at end of file diff --git a/core/LogStats/Generator/Visit.php b/core/LogStats/Generator/Visit.php new file mode 100644 index 0000000000..01396d126e --- /dev/null +++ b/core/LogStats/Generator/Visit.php @@ -0,0 +1,51 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Generator.php 404 2008-03-23 01:09:59Z matt $ + * + * @package Piwik_LogStats + */ + + +/** + * Fake Piwik_LogStats_Visit class that overwrite all the Time related method to be able + * to setup a given timestamp for the generated visitor and actions. + * + * + * @package Piwik_LogStats + * @subpackage Piwik_LogStats_Generator + */ +class Piwik_LogStats_Generator_Visit extends Piwik_LogStats_Visit +{ + static protected $timestampToUse; + + static public function setTimestampToUse($time) + { + self::$timestampToUse = $time; + } + protected function getCurrentDate( $format = "Y-m-d") + { + return date($format, $this->getCurrentTimestamp() ); + } + + protected function getCurrentTimestamp() + { + self::$timestampToUse = max(@$this->visitorInfo['visit_last_action_time'],self::$timestampToUse); + self::$timestampToUse += mt_rand(4,1840); + return self::$timestampToUse; + } + + protected function getDatetimeFromTimestamp($timestamp) + { + return date("Y-m-d H:i:s",$timestamp); + } + + protected function updateCookie() + { + @parent::updateCookie(); + } + +} diff --git a/core/LogStats/Visit.php b/core/LogStats/Visit.php new file mode 100644 index 0000000000..a148ab7d4c --- /dev/null +++ b/core/LogStats/Visit.php @@ -0,0 +1,837 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Visit.php 575 2008-07-26 23:08:32Z matt $ + * + * @package Piwik_LogStats + */ + + +interface Piwik_LogStats_Visit_Interface { + function handle(); + function setDb($db); +} + +/** + * Class used to handle a Visit. + * A visit is either NEW or KNOWN. + * - If a visit is NEW then we process the visitor information (settings, referers, etc.) and save + * a new line in the log_visit table. + * - If a visit is KNOWN then we update the visit row in the log_visit table, updating the number of pages + * views, time spent, etc. + * + * Whether a visit is NEW or KNOWN we also save the action in the DB. + * One request to the piwik.php script is associated to one action. + * + * @package Piwik_LogStats + */ + +class Piwik_LogStats_Visit implements Piwik_LogStats_Visit_Interface +{ + protected $cookieLog = null; + protected $visitorInfo = array(); + protected $userSettingsInformation = null; + protected $db = null; + + function __construct() + { + $idsite = Piwik_Common::getRequestVar('idsite', 0, 'int'); + if($idsite <= 0) + { + throw new Exception("The 'idsite' in the request is invalid."); + } + + $this->idsite = $idsite; + } + + public function setDb($db) + { + $this->db = $db; + } + + /** + * Returns the current date in the "Y-m-d" PHP format + * @return string + */ + protected function getCurrentDate( $format = "Y-m-d") + { + return date($format, $this->getCurrentTimestamp() ); + } + + /** + * Returns the current Timestamp + * @return int + */ + protected function getCurrentTimestamp() + { + return time(); + } + + /** + * Returns the date in the "Y-m-d H:i:s" PHP format + * @return string + */ + protected function getDatetimeFromTimestamp($timestamp) + { + return date("Y-m-d H:i:s", $timestamp); + } + + /** + * Test if the current visitor is excluded from the statistics. + * + * Plugins can for example exclude visitors based on the + * - IP + * - If a given cookie is found + * + * @return bool True if the visit must not be saved, false otherwise + */ + protected function isExcluded() + { + $excluded = 0; + Piwik_PostEvent('LogStats.Visit.isExcluded', $excluded); + if($excluded) + { + printDebug("Visitor excluded."); + return true; + } + + return false; + } + + /** + * Returns the cookie name used for the Piwik LogStats cookie + * @return string + */ + protected function getCookieName() + { + return Piwik_LogStats_Config::getInstance()->LogStats['cookie_name'] . $this->idsite; + } + + + /** + * This methods tries to see if the visitor has visited the website before. + * + * We have to split the visitor into one of the category + * - Known visitor + * - New visitor + * + * A known visitor is a visitor that has already visited the website in the current month. + * We define a known visitor using the algorithm: + * + * 1) Checking if a cookie contains + * // a unique id for the visitor + * - id_visitor + * + * // the timestamp of the last action in the most recent visit + * - timestamp_last_action + * + * // the timestamp of the first action in the most recent visit + * - timestamp_first_action + * + * // the ID of the most recent visit (which could be in the past or the current visit) + * - id_visit + * + * // the ID of the most recent action + * - id_last_action + * + * 2) If the visitor doesn't have a cookie, we try to look for a similar visitor configuration. + * We search for a visitor with the same plugins/OS/Browser/Resolution for today for this website. + */ + protected function recognizeTheVisitor() + { + $this->visitorKnown = false; + + $this->cookieLog = new Piwik_Cookie( $this->getCookieName() ); + /* + * Case the visitor has the piwik cookie. + * We make sure all the data that should saved in the cookie is available. + */ + + if( false !== ($idVisitor = $this->cookieLog->get( Piwik_LogStats::COOKIE_INDEX_IDVISITOR )) ) + { + $timestampLastAction = $this->cookieLog->get( Piwik_LogStats::COOKIE_INDEX_TIMESTAMP_LAST_ACTION ); + $timestampFirstAction = $this->cookieLog->get( Piwik_LogStats::COOKIE_INDEX_TIMESTAMP_FIRST_ACTION ); + $idVisit = $this->cookieLog->get( Piwik_LogStats::COOKIE_INDEX_ID_VISIT ); + $idLastAction = $this->cookieLog->get( Piwik_LogStats::COOKIE_INDEX_ID_LAST_ACTION ); + + if( $timestampLastAction !== false && is_numeric($timestampLastAction) + && $timestampFirstAction !== false && is_numeric($timestampFirstAction) + && $idVisit !== false && is_numeric($idVisit) + && $idLastAction !== false && is_numeric($idLastAction) + ) + { + $this->visitorInfo['visitor_idcookie'] = $idVisitor; + $this->visitorInfo['visit_last_action_time'] = $timestampLastAction; + $this->visitorInfo['visit_first_action_time'] = $timestampFirstAction; + $this->visitorInfo['idvisit'] = $idVisit; + $this->visitorInfo['visit_exit_idaction'] = $idLastAction; + + $this->visitorKnown = true; + + printDebug("The visitor is known because he has the piwik cookie (idcookie = {$this->visitorInfo['visitor_idcookie']}, idvisit = {$this->visitorInfo['idvisit']}, last action = ".date("r", $this->visitorInfo['visit_last_action_time']).") "); + } + } + + /* + * If the visitor doesn't have the piwik cookie, we look for a visitor that has exactly the same configuration + * and that visited the website today. + */ + if( !$this->visitorKnown ) + { + $userInfo = $this->getUserSettingsInformation(); + $md5Config = $userInfo['config_md5config']; + + $visitRow = $this->db->fetch( + " SELECT visitor_idcookie, + UNIX_TIMESTAMP(visit_last_action_time) as visit_last_action_time, + UNIX_TIMESTAMP(visit_first_action_time) as visit_first_action_time, + idvisit, + visit_exit_idaction + FROM ".$this->db->prefixTable('log_visit'). + " WHERE visit_server_date = ? + AND idsite = ? + AND config_md5config = ? + ORDER BY visit_last_action_time DESC + LIMIT 1", + array( $this->getCurrentDate(), $this->idsite, $md5Config)); + if($visitRow + && count($visitRow) > 0) + { + $this->visitorInfo['visitor_idcookie'] = $visitRow['visitor_idcookie']; + $this->visitorInfo['visit_last_action_time'] = $visitRow['visit_last_action_time']; + $this->visitorInfo['visit_first_action_time'] = $visitRow['visit_first_action_time']; + $this->visitorInfo['idvisit'] = $visitRow['idvisit']; + $this->visitorInfo['visit_exit_idaction'] = $visitRow['visit_exit_idaction']; + + $this->visitorKnown = true; + + printDebug("The visitor is known because of his userSettings+IP (idcookie = {$visitRow['visitor_idcookie']}, idvisit = {$this->visitorInfo['idvisit']}, last action = ".date("r", $this->visitorInfo['visit_last_action_time']).") "); + } + } + } + + /** + * Gets the UserSettings information and returns them in an array of name => value + * + * @return array + */ + protected function getUserSettingsInformation() + { + // we already called this method before, simply returns the result + if(is_array($this->userSettingsInformation)) + { + return $this->userSettingsInformation; + } + + + $plugin_Flash = Piwik_Common::getRequestVar( 'fla', 0, 'int'); + $plugin_Director = Piwik_Common::getRequestVar( 'dir', 0, 'int'); + $plugin_Quicktime = Piwik_Common::getRequestVar( 'qt', 0, 'int'); + $plugin_RealPlayer = Piwik_Common::getRequestVar( 'realp', 0, 'int'); + $plugin_Pdf = Piwik_Common::getRequestVar( 'pdf', 0, 'int'); + $plugin_WindowsMedia = Piwik_Common::getRequestVar( 'wma', 0, 'int'); + $plugin_Java = Piwik_Common::getRequestVar( 'java', 0, 'int'); + $plugin_Cookie = Piwik_Common::getRequestVar( 'cookie', 0, 'int'); + + $userAgent = Piwik_Common::sanitizeInputValues(@$_SERVER['HTTP_USER_AGENT']); + $aBrowserInfo = Piwik_Common::getBrowserInfo($userAgent); + $browserName = $aBrowserInfo['name']; + $browserVersion = $aBrowserInfo['version']; + + $os = Piwik_Common::getOs($userAgent); + + $resolution = Piwik_Common::getRequestVar('res', 'unknown', 'string'); + $colorDepth = Piwik_Common::getRequestVar('col', 32, 'numeric'); + + + $ip = Piwik_Common::getIp(); + $ip = ip2long($ip); + + $browserLang = substr(Piwik_Common::sanitizeInputValues(@$_SERVER['HTTP_ACCEPT_LANGUAGE']), 0, 20); + if(is_null($browserLang)) + { + $browserLang = ''; + } + + + $configurationHash = $this->getConfigHash( + $os, + $browserName, + $browserVersion, + $resolution, + $colorDepth, + $plugin_Flash, + $plugin_Director, + $plugin_RealPlayer, + $plugin_Pdf, + $plugin_WindowsMedia, + $plugin_Java, + $plugin_Cookie, + $ip, + $browserLang); + + $this->userSettingsInformation = array( + 'config_md5config' => $configurationHash, + 'config_os' => $os, + 'config_browser_name' => $browserName, + 'config_browser_version' => $browserVersion, + 'config_resolution' => $resolution, + 'config_color_depth' => $colorDepth, + 'config_pdf' => $plugin_Pdf, + 'config_flash' => $plugin_Flash, + 'config_java' => $plugin_Java, + 'config_director' => $plugin_Director, + 'config_quicktime' => $plugin_Quicktime, + 'config_realplayer' => $plugin_RealPlayer, + 'config_windowsmedia' => $plugin_WindowsMedia, + 'config_cookie' => $plugin_RealPlayer, + 'location_ip' => $ip, + 'location_browser_lang' => $browserLang, + ); + + return $this->userSettingsInformation; + } + + /** + * Returns true if the last action was done during the last 30 minutes + * @return bool + */ + protected function isLastActionInTheSameVisit() + { + return $this->visitorInfo['visit_last_action_time'] + >= ($this->getCurrentTimestamp() - Piwik_LogStats_Config::getInstance()->LogStats['visit_standard_length']); + } + + /** + * Returns true if the recognizeTheVisitor() method did recognize the visitor + */ + protected function isVisitorKnown() + { + return $this->visitorKnown === true; + } + + /** + * Main algorith to handle the visit. + * + * Once we have the visitor information, we have to define if the visit is a new or a known visit. + * + * 1) When the last action was done more than 30min ago, + * or if the visitor is new, then this is a new visit. + * + * 2) If the last action is less than 30min ago, then the same visit is going on. + * Because the visit goes on, we can get the time spent during the last action. + * + * NB: + * - In the case of a new visit, then the time spent + * during the last action of the previous visit is unknown. + * + * - In the case of a new visit but with a known visitor, + * we can set the 'returning visitor' flag. + * + * In all the cases we set a cookie to the visitor with the new information. + */ + public function handle() + { + if($this->isExcluded()) + { + return; + } + + $this->recognizeTheVisitor(); + if( $this->isVisitorKnown() + && $this->isLastActionInTheSameVisit()) + { + $this->handleKnownVisit(); + } + else + { + $this->handleNewVisit(); + } + + // we update the cookie with the new visit information + $this->updateCookie(); + } + + /** + * Update the cookie information. + */ + protected function updateCookie() + { + printDebug("We manage the cookie..."); + + // idcookie has been generated in handleNewVisit or we simply propagate the old value + $this->cookieLog->set( Piwik_LogStats::COOKIE_INDEX_IDVISITOR, + $this->visitorInfo['visitor_idcookie'] ); + + // the last action timestamp is the current timestamp + $this->cookieLog->set( Piwik_LogStats::COOKIE_INDEX_TIMESTAMP_LAST_ACTION, + $this->visitorInfo['visit_last_action_time'] ); + + // the first action timestamp is the timestamp of the first action of the current visit + $this->cookieLog->set( Piwik_LogStats::COOKIE_INDEX_TIMESTAMP_FIRST_ACTION, + $this->visitorInfo['visit_first_action_time'] ); + + // the idvisit has been generated by mysql in handleNewVisit or simply propagated here + $this->cookieLog->set( Piwik_LogStats::COOKIE_INDEX_ID_VISIT, + $this->visitorInfo['idvisit'] ); + + // the last action ID is the current exit idaction + $this->cookieLog->set( Piwik_LogStats::COOKIE_INDEX_ID_LAST_ACTION, + $this->visitorInfo['visit_exit_idaction'] ); + + $this->cookieLog->save(); + } + + + /** + * In the case of a known visit, we have to do the following actions: + * + * 1) Insert the new action + * + * 2) Update the visit information + */ + protected function handleKnownVisit() + { + printDebug("Visit known."); + + /** + * Init the action + */ + $action = $this->getActionObject(); + $actionId = $action->getActionId(); + printDebug("idAction = $actionId"); + + $serverTime = $this->getCurrentTimestamp(); + $datetimeServer = $this->getDatetimeFromTimestamp($serverTime); + + $this->db->query("UPDATE ". $this->db->prefixTable('log_visit')." + SET visit_last_action_time = ?, + visit_exit_idaction = ?, + visit_total_actions = visit_total_actions + 1, + visit_total_time = UNIX_TIMESTAMP(visit_last_action_time) - UNIX_TIMESTAMP(visit_first_action_time) + WHERE idvisit = ? + LIMIT 1", + array( $datetimeServer, + $actionId, + $this->visitorInfo['idvisit'] ) + ); + /** + * Save the action + */ + $timespentLastAction = $serverTime - $this->visitorInfo['visit_last_action_time']; + + $action->record( $this->visitorInfo['idvisit'], + $this->visitorInfo['visit_exit_idaction'], + $timespentLastAction + ); + + + /** + * Cookie fields to be updated + */ + $this->visitorInfo['visit_last_action_time'] = $serverTime; + $this->visitorInfo['visit_exit_idaction'] = $actionId; + + + } + + /** + * In the case of a new visit, we have to do the following actions: + * + * 1) Insert the new action + * + * 2) Insert the visit information + */ + protected function handleNewVisit() + { + printDebug("New Visit."); + + /** + * Get the variables from the REQUEST + */ + $localTime = Piwik_Common::getRequestVar( 'h', $this->getCurrentDate("H"), 'numeric') + .':'. Piwik_Common::getRequestVar( 'm', $this->getCurrentDate("i"), 'numeric') + .':'. Piwik_Common::getRequestVar( 's', $this->getCurrentDate("s"), 'numeric'); + + $serverTime = $this->getCurrentTimestamp(); + $serverDate = $this->getCurrentDate(); + + if($this->isVisitorKnown()) + { + $idcookie = $this->visitorInfo['visitor_idcookie']; + $returningVisitor = 1; + } + else + { + $idcookie = $this->getVisitorUniqueId(); + $returningVisitor = 0; + } + + $defaultTimeOnePageVisit = Piwik_LogStats_Config::getInstance()->LogStats['default_time_one_page_visit']; + + $userInfo = $this->getUserSettingsInformation(); + $country = Piwik_Common::getCountry($userInfo['location_browser_lang']); + $continent = Piwik_Common::getContinent( $country ); + + $refererInfo = $this->getRefererInformation(); + + /** + * Init the action + */ + $action = $this->getActionObject(); + $actionId = $action->getActionId(); + + printDebug("idAction = $actionId"); + + + /** + * Save the visitor + */ + $informationToSave = array( + 'idsite' => $this->idsite, + 'visitor_localtime' => $localTime, + 'visitor_idcookie' => $idcookie, + 'visitor_returning' => $returningVisitor, + 'visit_first_action_time' => $this->getDatetimeFromTimestamp($serverTime), + 'visit_last_action_time' => $this->getDatetimeFromTimestamp($serverTime), + 'visit_server_date' => $serverDate, + 'visit_entry_idaction' => $actionId, + 'visit_exit_idaction' => $actionId, + 'visit_total_actions' => 1, + 'visit_total_time' => $defaultTimeOnePageVisit, + 'referer_type' => $refererInfo['referer_type'], + 'referer_name' => $refererInfo['referer_name'], + 'referer_url' => $refererInfo['referer_url'], + 'referer_keyword' => $refererInfo['referer_keyword'], + 'config_md5config' => $userInfo['config_md5config'], + 'config_os' => $userInfo['config_os'], + 'config_browser_name' => $userInfo['config_browser_name'], + 'config_browser_version' => $userInfo['config_browser_version'], + 'config_resolution' => $userInfo['config_resolution'], + 'config_color_depth' => $userInfo['config_color_depth'], + 'config_pdf' => $userInfo['config_pdf'], + 'config_flash' => $userInfo['config_flash'], + 'config_java' => $userInfo['config_java'], + 'config_director' => $userInfo['config_director'], + 'config_quicktime' => $userInfo['config_quicktime'], + 'config_realplayer' => $userInfo['config_realplayer'], + 'config_windowsmedia' => $userInfo['config_windowsmedia'], + 'config_cookie' => $userInfo['config_cookie'], + 'location_ip' => $userInfo['location_ip'], + 'location_browser_lang' => $userInfo['location_browser_lang'], + 'location_country' => $country, + 'location_continent' => $continent, + ); + + Piwik_PostEvent('LogStats.newVisitorInformation', $informationToSave); + + $fields = implode(", ", array_keys($informationToSave)); + $values = substr(str_repeat( "?,",count($informationToSave)),0,-1); + + $this->db->query( "INSERT INTO ".$this->db->prefixTable('log_visit'). + " ($fields) VALUES ($values)", array_values($informationToSave)); + + $idVisit = $this->db->lastInsertId(); + + // Update the visitor information attribute with this information array + $this->visitorInfo = $informationToSave; + $this->visitorInfo['idvisit'] = $idVisit; + + // we have to save timestamp in the object properties, whereas mysql eats some other datetime format + $this->visitorInfo['visit_first_action_time'] = $serverTime; + $this->visitorInfo['visit_last_action_time'] = $serverTime; + + // saves the action + $action->record( $idVisit, 0, 0 ); + + } + + /** + * Returns an object able to handle the current action + * Plugins can return an override Action that for example, does not record the action in the DB + * + * @return Piwik_LogStats_Action child or fake but with same public interface + */ + protected function getActionObject() + { + $action = null; + Piwik_PostEvent('LogStats.newAction', $action); + + if(is_null($action)) + { + $action = new Piwik_LogStats_Action( $this->db ); + } + elseif(!($action instanceof Piwik_LogStats_Action_Interface)) + { + throw new Exception("The Action object set in the plugin must implement the interface Piwik_LogStats_Action_Interface"); + } + + return $action; + } + + /** + * Returns an array containing the following information: + * - referer_type + * - direct -- absence of referer URL OR referer URL has the same host + * - site -- based on the referer URL + * - search_engine -- based on the referer URL + * - campaign -- based on campaign URL parameter + * - newsletter -- based on newsletter URL parameter + * - partner -- based on partner URL parameter + * + * - referer_name + * - () + * - piwik.net -- site host name + * - google.fr -- search engine host name + * - adwords-search -- campaign name + * - beta-release -- newsletter name + * - my-nice-partner -- partner name + * + * - referer_keyword + * - () + * - () + * - my keyword + * - my paid keyword + * - () + * - () + * + * - referer_url : the same for all the referer types + * + */ + protected function getRefererInformation() + { + // default values for the referer_* fields + $this->typeRefererAnalyzed = Piwik_Common::REFERER_TYPE_DIRECT_ENTRY; + $this->nameRefererAnalyzed = ''; + $this->keywordRefererAnalyzed = ''; + $this->refererHost = ''; + + // get the urls and parse them + $refererUrl = Piwik_Common::getRequestVar( 'urlref', '', 'string'); + $currentUrl = Piwik_Common::getRequestVar( 'url', '', 'string'); + + $this->refererUrlParse = @parse_url($refererUrl); + $this->currentUrlParse = @parse_url($currentUrl); + if(isset($this->refererUrlParse['host'])) + { + $this->refererHost = $this->refererUrlParse['host']; + } + + $refererDetected = false; + if( !empty($this->currentUrlParse['host'])) + { + if( $this->detectRefererNewsletter() + || $this->detectRefererPartner() + || $this->detectRefererCampaign() ) + { + $refererDetected = true; + } + } + + if(!$refererDetected + && !empty($this->refererUrlParse['host']) ) + { + if( $this->detectRefererSearchEngine() + || $this->detectRefererDirectEntry() ) + { + $refererDetected = true; + } + } + + if(!empty($this->refererHost) + && !$refererDetected) + { + $this->typeRefererAnalyzed = Piwik_Common::REFERER_TYPE_WEBSITE; + $this->nameRefererAnalyzed = $this->refererHost; + } + + $refererInformation = array( + 'referer_type' => $this->typeRefererAnalyzed, + 'referer_name' => $this->nameRefererAnalyzed, + 'referer_keyword' => $this->keywordRefererAnalyzed, + 'referer_url' => $refererUrl, + ); + + return $refererInformation; + } + + /* + * Search engine detection + */ + protected function detectRefererSearchEngine() + { + /* + * A referer is a search engine if the URL's host is in the SearchEngines array + * and if we found the keyword in the URL. + * + * For example if someone comes from http://www.google.com/partners.html this will not + * be counted as a search engines, but as a website referer from google.com (because the + * keyword couldn't be found in the URL) + */ + require "core/DataFiles/SearchEngines.php"; + + if(array_key_exists($this->refererHost, $GLOBALS['Piwik_SearchEngines'])) + { + $searchEngineName = $GLOBALS['Piwik_SearchEngines'][$this->refererHost][0]; + $variableName = $GLOBALS['Piwik_SearchEngines'][$this->refererHost][1]; + + if(isset($this->refererUrlParse['query'])) + { + $query = $this->refererUrlParse['query']; + + if($searchEngineName == 'Google Images') + { + $query = urldecode(trim(strtolower(Piwik_Common::getParameterFromQueryString($query, 'prev')))); + $query = str_replace('&', '&', strstr($query, '?')); + } + + // search for keywords now &vname=keyword + $key = trim(strtolower(Piwik_Common::getParameterFromQueryString($query, $variableName))); + + if(!empty($key) + && function_exists('iconv') + && isset($GLOBALS['Piwik_SearchEngines'][$this->refererHost][2])) + { + $charset = trim($GLOBALS['Piwik_SearchEngines'][$this->refererHost][2]); + + if(!empty($charset)) + { + $key = htmlspecialchars( + @iconv( $charset, + 'utf-8//TRANSLIT', + htmlspecialchars_decode($key, Piwik_Common::HTML_ENCODING_QUOTE_STYLE)) + , Piwik_Common::HTML_ENCODING_QUOTE_STYLE); + } + } + + if(!empty($key)) + { + $this->typeRefererAnalyzed = Piwik_Common::REFERER_TYPE_SEARCH_ENGINE; + $this->nameRefererAnalyzed = $searchEngineName; + $this->keywordRefererAnalyzed = $key; + + return true; + } + } + } + } + + /* + * Newsletter analysis + */ + protected function detectRefererNewsletter() + { + if(isset($this->currentUrlParse['query'])) + { + $newsletterVariableName = Piwik_LogStats_Config::getInstance()->LogStats['newsletter_var_name']; + $newsletterVar = Piwik_Common::getParameterFromQueryString( $this->currentUrlParse['query'], $newsletterVariableName); + + if(!empty($newsletterVar)) + { + $this->typeRefererAnalyzed = Piwik_Common::REFERER_TYPE_NEWSLETTER; + $this->nameRefererAnalyzed = $newsletterVar; + + return true; + } + } + } + + /* + * Partner analysis + */ + protected function detectRefererPartner() + { + if(isset($this->currentUrlParse['query'])) + { + $partnerVariableName = Piwik_LogStats_Config::getInstance()->LogStats['partner_var_name']; + $partnerVar = Piwik_Common::getParameterFromQueryString($this->currentUrlParse['query'], $partnerVariableName); + + if(!empty($partnerVar)) + { + $this->typeRefererAnalyzed = Piwik_Common::REFERER_TYPE_PARTNER; + $this->nameRefererAnalyzed = $partnerVar; + + return true; + } + } + } + + /* + * Campaign analysis + */ + protected function detectRefererCampaign() + { + if(isset($this->currentUrlParse['query'])) + { + $campaignVariableName = Piwik_LogStats_Config::getInstance()->LogStats['campaign_var_name']; + $campaignName = Piwik_Common::getParameterFromQueryString($this->currentUrlParse['query'], $campaignVariableName); + + if( !empty($campaignName)) + { + $campaignKeywordVariableName = Piwik_LogStats_Config::getInstance()->LogStats['campaign_keyword_var_name']; + $campaignKeyword = Piwik_Common::getParameterFromQueryString($this->currentUrlParse['query'], $campaignKeywordVariableName); + + $this->typeRefererAnalyzed = Piwik_Common::REFERER_TYPE_CAMPAIGN; + $this->nameRefererAnalyzed = $campaignName; + + if(!empty($campaignKeyword)) + { + $this->keywordRefererAnalyzed = $campaignKeyword; + } + + return true; + } + } + } + + + /* + * Direct entry (referer host is similar to current host) + * And we have previously tried to detect the newsletter/partner/campaign variables in the URL + * so it can only be a direct access + */ + + protected function detectRefererDirectEntry() + { + if(isset($this->currentUrlParse['host'])) + { + $currentHost = $this->currentUrlParse['host']; + + if($currentHost == $this->refererHost) + { + $this->typeRefererAnalyzed = Piwik_Common::REFERER_TYPE_DIRECT_ENTRY; + return true; + } + } + + } + + /** + * Returns a MD5 of all the configuration settings + * @return string + */ + protected function getConfigHash( $os, $browserName, $browserVersion, $resolution, $colorDepth, $plugin_Flash, $plugin_Director, $plugin_RealPlayer, $plugin_Pdf, $plugin_WindowsMedia, $plugin_Java, $plugin_Cookie, $ip, $browserLang) + { + return md5( $os . $browserName . $browserVersion . $resolution . $colorDepth . $plugin_Flash . $plugin_Director . $plugin_RealPlayer . $plugin_Pdf . $plugin_WindowsMedia . $plugin_Java . $plugin_Cookie . $ip . $browserLang ); + } + + /** + * Returns either + * - "-1" for a known visitor + * - a unique 32 char identifier @see Piwik_Common::generateUniqId() + */ + protected function getVisitorUniqueId() + { + if($this->isVisitorKnown()) + { + return -1; + } + else + { + return Piwik_Common::generateUniqId(); + } + } +} diff --git a/core/LogStats/javascriptTag.tpl b/core/LogStats/javascriptTag.tpl new file mode 100644 index 0000000000..bb24479777 --- /dev/null +++ b/core/LogStats/javascriptTag.tpl @@ -0,0 +1,18 @@ + +<!-- Piwik --> +<a href="http://piwik.org" title="{$hrefTitle}" onclick="window.open(this.href);return(false);"> +<script type="text/javascript"> +var pkBaseURL = (("https:" == document.location.protocol) ? "https://{$piwikUrl}" : "http://{$piwikUrl}"); +document.write(unescape("%3Cscript src='" + pkBaseURL + "piwik.js' type='text/javascript'%3E%3C/script%3E")); +</script> +<script type="text/javascript"> +<!-- + piwik_action_name = {$actionName}; + piwik_idsite = {$idSite}; + piwik_url = pkBaseURL + "piwik.php"; + piwik_log(piwik_action_name, piwik_idsite, piwik_url); +//--> +</script><object> +<noscript><p>{$hrefTitle} <img src="http://{$piwikUrl}piwik.php" style="border:0" alt="piwik"/></p> +</noscript></object></a> +<!-- /Piwik -->
\ No newline at end of file diff --git a/core/Mail.php b/core/Mail.php new file mode 100644 index 0000000000..db12d4eaf1 --- /dev/null +++ b/core/Mail.php @@ -0,0 +1,31 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id$ + * + * @package Piwik + */ + +require_once "Zend/Mail.php"; + +/** + * Class for sending mails, for more information see: + * http://framework.zend.com/manual/en/zend.mail.html + * + * @package Piwik + */ +class Piwik_Mail extends Zend_Mail +{ + /** + * Public constructor, default charset utf-8 + * + * @param string $charset + */ + public function __construct($charset = 'utf-8') + { + parent::__construct($charset); + } +} diff --git a/core/Period.php b/core/Period.php new file mode 100644 index 0000000000..50201bf583 --- /dev/null +++ b/core/Period.php @@ -0,0 +1,239 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Period.php 540 2008-06-29 22:44:13Z matt $ + * + * @package Piwik_Helper + */ + +require_once "Period/Day.php"; +require_once "Period/Range.php"; +/** + * Creating a new Piwik_Period subclass: + * + * Every overloaded method must start with the code + * if(!$this->subperiodsProcessed) + * { + * $this->generate(); + * } + * that checks whether the subperiods have already been computed. + * This is for performance improvements, computing the subperiods is done a per demand basis. + * + * + * @package Piwik_Helper + */ +abstract class Piwik_Period +{ + protected $subperiods = array(); + protected $subperiodsProcessed = false; + protected $label = null; + protected $date = null; + + protected static $unknowPeriodException = "The period '%s' is not supported. Try 'day' or 'week' or 'month' or 'year'"; + + public function __construct( $date ) + { + $this->checkInputDate( $date ); + $this->date = clone $date; + } + + static public function factory($strPeriod, $date) + { + switch ($strPeriod) { + case 'day': + return new Piwik_Period_Day($date); + break; + + case 'week': + require_once "Period/Week.php"; + return new Piwik_Period_Week($date); + break; + + case 'month': + require_once "Period/Month.php"; + return new Piwik_Period_Month($date); + break; + + case 'year': + require_once "Period/Year.php"; + return new Piwik_Period_Year($date); + break; + + default: + throw new Exception(sprintf(self::$unknowPeriodException, $strPeriod)); + break; + } + } + + /** + * Returns the first day of the period + * + * @return Piwik_Date First day of the period + */ + public function getDateStart() + { + if(!$this->subperiodsProcessed) + { + $this->generate(); + } + if(count($this->subperiods) == 0) + { + return $this->getDate(); + } + $periods = $this->getSubperiods(); + $currentPeriod = $periods[0]; + while( $currentPeriod->getNumberOfSubperiods() > 0 ) + { + $periods = $currentPeriod->getSubperiods(); + $currentPeriod = $periods[0]; + } + return $currentPeriod->getDate(); + } + + /** + * Returns the last day of the period ; can be a date in the future + * + * @return Piwik_Date Last day of the period + */ + public function getDateEnd() + { + if(!$this->subperiodsProcessed) + { + $this->generate(); + } + if(count($this->subperiods) == 0) + { + return $this->getDate(); + } + $periods = $this->getSubperiods(); + $currentPeriod = $periods[count($periods)-1]; + while( $currentPeriod->getNumberOfSubperiods() > 0 ) + { + $periods = $currentPeriod->getSubperiods(); + $currentPeriod = $periods[count($periods)-1]; + } + return $currentPeriod->getDate(); + } + + public function getId() + { + return Piwik::$idPeriods[$this->getLabel()]; + } + + public function getLabel() + { + return $this->label; + } + + /** + * + * @return Piwik_Date + */ + protected function getDate() + { + return $this->date; + } + + protected function checkInputDate($date) + { + if( !($date instanceof Piwik_Date)) + { + throw new Exception("The date must be a Piwik_Date object. " . var_export($date,true)); + } + } + + protected function generate() + { + $this->subperiodsProcessed = true; + } + + public function getNumberOfSubperiods() + { + if(!$this->subperiodsProcessed) + { + $this->generate(); + } + return count($this->subperiods); + } + + /** + * Returns Period_Day for a period made of days (week, month), + * Period_Month for a period made of months (year) + * + * @return array + */ + public function getSubperiods() + { + if(!$this->subperiodsProcessed) + { + $this->generate(); + } + return $this->subperiods; + } + + /** + * Add a date to the period. + * + * Protected because it not yet supported to add periods after the initialization + * + * @param Piwik_Date Valid Piwik_Date object + */ + protected function addSubperiod( $date ) + { + $this->subperiods[] = $date; + } + + /** + * A period is finished if all the subperiods are finished + */ + public function isFinished() + { + if(!$this->subperiodsProcessed) + { + $this->generate(); + } + foreach($this->subperiods as $period) + { + if(!$period->isFinished()) + { + return false; + } + } + return true; + } + + public function toString() + { + if(!$this->subperiodsProcessed) + { + $this->generate(); + } + $dateString = array(); + foreach($this->subperiods as $period) + { + $dateString[] = $period->toString(); + } + return $dateString; + } + + public function __toString() + { + return $this->toString(); + } + + public function get( $part= null ) + { + if(!$this->subperiodsProcessed) + { + $this->generate(); + } + return $this->date->get($part); + } + + abstract public function getPrettyString(); +} + + diff --git a/core/Period/Day.php b/core/Period/Day.php new file mode 100644 index 0000000000..e4597945de --- /dev/null +++ b/core/Period/Day.php @@ -0,0 +1,40 @@ +<?php + +/** + * + * @package Piwik_Period + */ +class Piwik_Period_Day extends Piwik_Period +{ + protected $label = 'day'; + + public function getPrettyString() + { + $out = $this->getDateStart()->toString() ; + return $out; + } + + public function isFinished() + { + $todayMidnight = Piwik_Date::today(); + if($this->date->isEarlier($todayMidnight)) + { + return true; + } + } + + public function getNumberOfSubperiods() + { + return 0; + } + + public function addSubperiod( $date ) + { + throw new Exception("Adding a subperiod is not supported for Piwik_Period_Day"); + } + + public function toString() + { + return $this->date->toString("Y-m-d"); + } +} diff --git a/core/Period/Month.php b/core/Period/Month.php new file mode 100644 index 0000000000..57fd1d25c5 --- /dev/null +++ b/core/Period/Month.php @@ -0,0 +1,48 @@ +<?php +/** + * + * @package Piwik_Period + */ +class Piwik_Period_Month extends Piwik_Period +{ + protected $label = 'month'; + + public function getPrettyString() + { + $out = $this->getDateStart()->toString('Y-m'); + return $out; + } + + protected function generate() + { + if($this->subperiodsProcessed) + { + return; + } + parent::generate(); + + $date = $this->date; + + $startMonth = $date->setDay(1); + $currentDay = clone $startMonth; + while($currentDay->compareMonth($startMonth) == 0) + { + $this->addSubperiod(new Piwik_Period_Day($currentDay)); + $currentDay = $currentDay->addDay(1); + } + } + + public function isFinished() + { + if(!$this->subperiodsProcessed) + { + $this->generate(); + } + // a month is finished + // if current month > month AND current year == year + // OR if current year > year + $year = $this->date->get("Y"); + return ( date("m") > $this->date->get("m") && date("Y") == $year) + || date("Y") > $year; + } +} diff --git a/core/Period/Range.php b/core/Period/Range.php new file mode 100644 index 0000000000..03c97d3e23 --- /dev/null +++ b/core/Period/Range.php @@ -0,0 +1,165 @@ +<?php + +/** + * from a starting date to an ending date + * + */ +class Piwik_Period_Range extends Piwik_Period +{ + public function __construct( $strPeriod, $strDate ) + { + $this->strPeriod = $strPeriod; + $this->strDate = $strDate; + $this->defaultEndDate = null; + } + + public function getPrettyString() + { + $out = "From ".$this->getDateStart()->toString() . " to " . $this->getDateEnd()->toString(); + return $out; + } + + /** + * + * @param Piwik_Date $date + * @param int $n + * @return Piwik_Date + */ + protected function removePeriod( $date, $n ) + { + switch($this->strPeriod) + { + case 'day': + $startDate = $date->subDay( $n ); + break; + + case 'week': + $startDate = $date->subDay( $n * 7 ); + break; + + case 'month': + $startDate = $date->subMonth( $n ); + break; + + case 'year': + $startDate = $date->subMonth( 12 * $n ); + break; + + default: + throw new Exception(sprintf(self::$unknowPeriodException, $this->strPeriod)); + break; + } + return $startDate; + } + + protected function getMaxN($lastN) + { + switch($this->strPeriod) + { + case 'day': + $lastN = min( $lastN, 5*365 ); + break; + + case 'week': + $lastN = min( $lastN, 5*52 ); + break; + + case 'month': + $lastN = min( $lastN, 5*12 ); + break; + + case 'year': + $lastN = min( $lastN, 10 ); + break; + } + return $lastN; + } + + public function setDefaultEndDate( Piwik_Date $oDate) + { + $this->defaultEndDate = $oDate; + } + + protected function generate() + { + if($this->subperiodsProcessed) + { + return; + } + parent::generate(); + + if(ereg('(last|previous)([0-9]*)', $this->strDate, $regs)) + { + $lastN = $regs[2]; + + $lastOrPrevious = $regs[1]; + + if(!is_null($this->defaultEndDate)) + { + $defaultEndDate = $this->defaultEndDate; + } + else + { + $defaultEndDate = Piwik_Date::today(); + } + if($lastOrPrevious == 'last') + { + $endDate = $defaultEndDate; + } + elseif($lastOrPrevious == 'previous') + { + $endDate = $this->removePeriod($defaultEndDate, 1); + } + + // last1 means only one result ; last2 means 2 results so we remove only 1 to the days/weeks/etc + $lastN--; + $lastN = abs($lastN); + + $lastN = $this->getMaxN($lastN); + + $startDate = $this->removePeriod($endDate, $lastN); + } + elseif(ereg('([0-9]{4}-[0-9]{1,2}-[0-9]{1,2}),([0-9]{4}-[0-9]{1,2}-[0-9]{1,2})', $this->strDate, $regs)) + { + $strDateStart = $regs[1]; + $strDateEnd = $regs[2]; + + $startDate = Piwik_Date::factory($strDateStart); + $endDate = Piwik_Date::factory($strDateEnd); + } + else + { + throw new Exception("The date '$this->strDate' is not a date range. Should have the following format: 'lastN' or 'previousN' or 'YYYY-MM-DD,YYYY-MM-DD'."); + } + + $endSubperiod = Piwik_Period::factory($this->strPeriod, $endDate); + + $arrayPeriods= array(); + $arrayPeriods[] = $endSubperiod; + while($endDate->isLater($startDate) ) + { + $endDate = $this->removePeriod($endDate, 1); + $subPeriod = Piwik_Period::factory($this->strPeriod, $endDate); + $arrayPeriods[] = $subPeriod ; + } + $arrayPeriods = array_reverse($arrayPeriods); + foreach($arrayPeriods as $period) + { + $this->addSubperiod($period); + } + } + + function toString() + { + if(!$this->subperiodsProcessed) + { + $this->generate(); + } + $range = array(); + foreach($this->subperiods as $element) + { + $range[] = $element->toString(); + } + return $range; + } +}
\ No newline at end of file diff --git a/core/Period/Week.php b/core/Period/Week.php new file mode 100644 index 0000000000..f24002c878 --- /dev/null +++ b/core/Period/Week.php @@ -0,0 +1,41 @@ +<?php + +/** + * + * @package Piwik_Period + */ +class Piwik_Period_Week extends Piwik_Period +{ + protected $label = 'week'; + + public function getPrettyString() + { + $out = $this->getDateStart()->toString() . " to " . $this->getDateEnd()->toString(); + return $out; + } + + protected function generate() + { + if($this->subperiodsProcessed) + { + return; + } + parent::generate(); + $date = $this->date; + + if( $date->toString('N') > 1) + { + $date = $date->subDay($date->toString('N')-1); + } + + $startWeek = $date; + + $currentDay = clone $startWeek; + while($currentDay->compareWeek($startWeek) == 0) + { + $this->addSubperiod(new Piwik_Period_Day($currentDay) ); + $currentDay = $currentDay->addDay(1); + } + } + +} diff --git a/core/Period/Year.php b/core/Period/Year.php new file mode 100644 index 0000000000..5ff4f083a7 --- /dev/null +++ b/core/Period/Year.php @@ -0,0 +1,49 @@ +<?php + +require_once "Period/Month.php"; +/** + * + * @package Piwik_Period + */ +class Piwik_Period_Year extends Piwik_Period +{ + protected $label = 'year'; + + public function getPrettyString() + { + $out = $this->getDateStart()->toString('Y'); + return $out; + } + + protected function generate() + { + if($this->subperiodsProcessed) + { + return; + } + parent::generate(); + + $year = $this->date->get("Y"); + for($i=1; $i<=12; $i++) + { + $this->addSubperiod( new Piwik_Period_Month( + Piwik_Date::factory("$year-$i-01") + ) + ); + } + } + + function toString() + { + if(!$this->subperiodsProcessed) + { + $this->generate(); + } + $stringMonth = array(); + foreach($this->subperiods as $month) + { + $stringMonth[] = $month->get("Y")."-".$month->get("m")."-01"; + } + return $stringMonth; + } +} diff --git a/core/Piwik.php b/core/Piwik.php new file mode 100644 index 0000000000..c125b7fca6 --- /dev/null +++ b/core/Piwik.php @@ -0,0 +1,1045 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Piwik.php 581 2008-07-27 23:07:52Z matt $ + * + * @package Piwik + */ + +require_once "Config.php"; +require_once "Zend/Db.php"; +require_once "Zend/Db/Table.php"; +require_once "Log.php"; +require_once "PluginsManager.php"; +require_once "Translate.php"; + +/** + * Main piwik helper class. + * Contains static functions you can call from the plugins. + * + * @package Piwik + */ +class Piwik +{ + const CLASSES_PREFIX = "Piwik_"; + + public static $idPeriods = array( + 'day' => 1, + 'week' => 2, + 'month' => 3, + 'year' => 4, + ); + + /** + * ending WITHOUT slashs + */ + static public function getPathToPiwikRoot() + { + return realpath( dirname(__FILE__). "/../" ); + } + + /** + * path without trailing slash + */ + static public function createHtAccess( $path ) + { + @file_put_contents($path . "/.htaccess", "Deny from all"); + } + + static public function mkdir( $path, $mode = 0755, $denyAccess = true ) + { + if(!is_dir($path)) + { + $directoryParent = Piwik::realpath(dirname($path)); + if( is_writable($directoryParent) ) + { + mkdir($path, $mode, true); + } + } + + if($denyAccess) + { + Piwik::createHtAccess($path); + } + } + + /** + * Checks if directories are writable and create them if they do not exist. + * + * @param array $directoriesToCheck array of directories to check - if not given default Piwik directories that needs write permission are checked + * @return array direcory name => true|false (is writable) + */ + static public function checkDirectoriesWritable($directoriesToCheck = null) + { + if( $directoriesToCheck == null ) + { + $directoriesToCheck = array( + '/', + '/config', + '/tmp', + '/tmp/templates_c', + '/tmp/cache', + ); + } + + $resultCheck = array(); + foreach($directoriesToCheck as $directoryToCheck) + { + if( !ereg('^'.preg_quote(PIWIK_INCLUDE_PATH), $directoryToCheck) ) + { + $directoryToCheck = PIWIK_INCLUDE_PATH . $directoryToCheck; + } + + if(!file_exists($directoryToCheck)) + { + Piwik::mkdir($directoryToCheck, 0755, false); + } + + $directory = Piwik::realpath($directoryToCheck); + $resultCheck[$directory] = false; + if(is_writable($directoryToCheck)) + { + $resultCheck[$directory] = true; + } + } + return $resultCheck; + } + + static public function realpath($path) + { + if (file_exists($path)) + { + return realpath($path); + } + return $path; + } + + /** + * Returns the Javascript code to be inserted on every page to track + * + * @param int $idSite + * @param string $piwikUrl http://path/to/piwik/directory/ + * @param string $actionName + * @return string + */ + static public function getJavascriptCode($idSite, $piwikUrl, $actionName = "''") + { + $jsTag = file_get_contents( "core/LogStats/javascriptTag.tpl"); + $jsTag = nl2br(htmlentities($jsTag)); + $piwikUrl = preg_match('/^(http|https):\/\/(.*)$/', $piwikUrl, $matches); + $piwikUrl = $matches[2]; + $jsTag = str_replace('{$actionName}', $actionName, $jsTag); + $jsTag = str_replace('{$idSite}', $idSite, $jsTag); + $jsTag = str_replace('{$piwikUrl}', $piwikUrl, $jsTag); + $jsTag = str_replace('{$hrefTitle}', Piwik::getRandomTitle(), $jsTag); + return $jsTag; + } + + static public function getMemoryLimitValue() + { + if($memory = ini_get('memory_limit')) + { + return substr($memory, 0, strlen($memory) - 1); + } + return false; + } + + static public function setMemoryLimit($minimumMemoryLimit) + { + $currentValue = self::getMemoryLimitValue(); + if( ($currentValue === false + || $currentValue < $minimumMemoryLimit ) + && @ini_set('memory_limit', $minimumMemoryLimit.'M')) + { + return true; + } + return false; + } + + static public function raiseMemoryLimitIfNecessary() + { + $minimumMemoryLimit = Zend_Registry::get('config')->General->minimum_memory_limit; + $memoryLimit = self::getMemoryLimitValue(); + if($memoryLimit === false + || $memoryLimit < $minimumMemoryLimit) + { + return self::setMemoryLimit($minimumMemoryLimit); + } + + return false; + } + + static public function log($message = '') + { + Zend_Registry::get('logger_message')->log($message); + Zend_Registry::get('logger_message')->log( "<br>" . PHP_EOL); + } + + + static public function error($message = '') + { + trigger_error($message, E_USER_ERROR); + } + + /** + * Display the message in a nice red font with a nice icon + * ... and dies + */ + static public function exitWithErrorMessage( $message ) + { + $output = "<style>a{color:red;}</style>\n". + "<div style='color:red;font-family:Georgia;font-size:120%'>". + "<p><img src='themes/default/images/error_medium.png' style='vertical-align:middle; float:left;padding:20 20 20 20'>". + $message. + "</p></div>"; + print(Piwik_Log_Formatter_ScreenFormatter::getFormattedString($output)); + exit; + } + + /** + * Computes the division of i1 by i2. If either i1 or i2 are not number, or if i2 has a value of zero + * we return 0 to avoid the division by zero. + * + * @param numeric $i1 + * @param numeric $i2 + * @return numeric The result of the division or zero + */ + static public function secureDiv( $i1, $i2 ) + { + if ( is_numeric($i1) && is_numeric($i2) && floatval($i2) != 0) + { + return $i1 / $i2; + } + return 0; + } + static public function getQueryCount() + { + $profiler = Zend_Registry::get('db')->getProfiler(); + return $profiler->getTotalNumQueries(); + } + static public function getDbElapsedSecs() + { + $profiler = Zend_Registry::get('db')->getProfiler(); + return $profiler->getTotalElapsedSecs(); + } + static public function printQueryCount() + { + $totalTime = self::getDbElapsedSecs(); + $queryCount = self::getQueryCount(); + Piwik::log("Total queries = $queryCount (total sql time = ".round($totalTime,2)."s)"); + } + + static public function printSqlProfilingReportLogStats( $db = null ) + { + function maxSumMsFirst($a,$b) + { + return $a['sum_time_ms'] < $b['sum_time_ms']; + } + + if(is_null($db)) + { + $db = Zend_Registry::get('db'); + $tableName = Piwik::prefixTable('log_profiling'); + } + else + { + $tableName = $db->prefixTable('log_profiling'); + } + $all = $db->fetchAll(' SELECT *, sum_time_ms / count as avg_time_ms + FROM '.$tableName ); + if($all === false) + { + return; + } + usort($all, 'maxSumMsFirst'); + + $infoIndexedByQuery = array(); + foreach($all as $infoQuery) + { + $query = $infoQuery['query']; + $count = $infoQuery['count']; + $sum_time_ms = $infoQuery['sum_time_ms']; + $infoIndexedByQuery[$query] = array('count' => $count, 'sumTimeMs' => $sum_time_ms); + } + Piwik::getSqlProfilingQueryBreakdownOutput($infoIndexedByQuery); + } + + /** + * Outputs SQL Profiling reports + * It is automatically called when enabling the SQL profiling in the config file enable_sql_profiler + * + */ + static function printSqlProfilingReportZend() + { + $profiler = Zend_Registry::get('db')->getProfiler(); + + if(!$profiler->getEnabled()) + { + throw new Exception("To display the profiler you should enable enable_sql_profiler on your config/config.ini.php file"); + } + + $infoIndexedByQuery = array(); + foreach($profiler->getQueryProfiles() as $query) + { + if(isset($infoIndexedByQuery[$query->getQuery()])) + { + $existing = $infoIndexedByQuery[$query->getQuery()]; + } + else + { + $existing = array( 'count' => 0, 'sumTimeMs' => 0); + } + $new = array( 'count' => $existing['count'] + 1, + 'sumTimeMs' => $existing['count'] + $query->getElapsedSecs() * 1000); + $infoIndexedByQuery[$query->getQuery()] = $new; + } + function sortTimeDesc($a,$b) + { + return $a['sumTimeMs'] < $b['sumTimeMs']; + } + uasort( $infoIndexedByQuery, 'sortTimeDesc'); + + Piwik::log('<hr><b>SQL Profiler</b>'); + Piwik::log('<hr><b>Summary</b>'); + $totalTime = $profiler->getTotalElapsedSecs(); + $queryCount = $profiler->getTotalNumQueries(); + $longestTime = 0; + $longestQuery = null; + foreach ($profiler->getQueryProfiles() as $query) { + if ($query->getElapsedSecs() > $longestTime) { + $longestTime = $query->getElapsedSecs(); + $longestQuery = $query->getQuery(); + } + } + $str = 'Executed ' . $queryCount . ' queries in ' . round($totalTime,3) . ' seconds' . "\n"; + $str .= '(Average query length: ' . round($totalTime / $queryCount,3) . ' seconds)' . "\n"; + $str .= '<br>Queries per second: ' . round($queryCount / $totalTime,1) . "\n"; + $str .= '<br>Longest query length: ' . round($longestTime,3) . " seconds (<code>$longestQuery</code>) \n"; + Piwik::log($str); + Piwik::getSqlProfilingQueryBreakdownOutput($infoIndexedByQuery); + } + + static private function getSqlProfilingQueryBreakdownOutput( $infoIndexedByQuery ) + { + Piwik::log('<hr><b>Breakdown by query</b>'); + $output = ''; + foreach($infoIndexedByQuery as $query => $queryInfo) + { + $timeMs = round($queryInfo['sumTimeMs'],1); + $count = $queryInfo['count']; + $avgTimeString = ''; + if($count > 1) + { + $avgTimeMs = $timeMs / $count; + $avgTimeString = " (average = <b>". round($avgTimeMs,1) . "ms</b>)"; + } + $query = str_replace(array("\t","\n","\r\n","\r"), "_toberemoved_", $query); + $query = str_replace('_toberemoved__toberemoved_','',$query); + $query = str_replace('_toberemoved_', ' ',$query); + $output .= "Executed <b>$count</b> time". ($count==1?'':'s') ." in <b>".$timeMs."ms</b> $avgTimeString <pre>\t$query</pre>"; + } + Piwik::log($output); + } + + static public function printTimer() + { + echo Zend_Registry::get('timer'); + } + + static public function printMemoryUsage( $prefixString = null ) + { + $memory = false; + if(function_exists('xdebug_memory_usage')) + { + $memory = xdebug_memory_usage(); + } + elseif(function_exists('memory_get_usage')) + { + $memory = memory_get_usage(); + } + + if($memory !== false) + { + $usage = round( $memory / 1024 / 1024, 2); + if(!is_null($prefixString)) + { + Piwik::log($prefixString); + } + Piwik::log("Memory usage = $usage Mb"); + } + else + { + Piwik::log("Memory usage function not found."); + } + } + + static public function isPhpCliMode() + { + return in_array(substr(php_sapi_name(), 0, 3), array('cgi', 'cli')); + } + + static public function isNumeric($value) + { + return !is_array($value) && ereg('^([-]{0,1}[0-9]{1,}[.]{0,1}[0-9]*)$', $value); + } + + static public function getRandomTitle() + { + $titles = array( 'Web analytics', + 'Website analytics', + 'Analytics', + 'Web analytics api', + 'Open source analytics', + 'Open source web analytics', + 'Free analytics', + 'Analytics software', + 'Free web analytics', + 'Free web statistics', + 'Web 2.0 analytics', + 'Web analytic', + 'Web statistics', + 'Web stats', + 'Web 2.0 stats', + 'Statistics web 2.0', + ); + $id = abs(intval(md5(substr(Piwik_Url::getCurrentHost(),7)))); + $title = $titles[ $id % count($titles)]; + return $title; + } + + static public function loadPlugins() + { + Piwik_PluginsManager::getInstance()->setLanguageToLoad( Piwik_Translate::getInstance()->getLanguageToLoad() ); + Piwik_PluginsManager::getInstance()->setPluginsToLoad( Zend_Registry::get('config')->Plugins->Plugins->toArray() ); + } + + static public function installLoadedPlugins() + { + Piwik_PluginsManager::getInstance()->installLoadedPlugins(); + } + + static public function getTableCreateSql( $tableName ) + { + $tables = Piwik::getTablesCreateSql(); + + if(!isset($tables[$tableName])) + { + throw new Exception("The table '$tableName' SQL creation code couldn't be found."); + } + + return $tables[$tableName]; + } + + static public function getTablesCreateSql() + { + $config = Zend_Registry::get('config'); + $prefixTables = $config->database->tables_prefix; + $tables = array( + 'user' => "CREATE TABLE {$prefixTables}user ( + login VARCHAR(20) NOT NULL, + password CHAR(32) NOT NULL, + alias VARCHAR(45) NOT NULL, + email VARCHAR(100) NOT NULL, + token_auth CHAR(32) NOT NULL, + date_registered TIMESTAMP NOT NULL, + PRIMARY KEY(login), + UNIQUE INDEX uniq_keytoken(token_auth) + ) + ", + + 'access' => "CREATE TABLE {$prefixTables}access ( + login VARCHAR(20) NOT NULL, + idsite INTEGER UNSIGNED NOT NULL, + access VARCHAR(10) NULL, + PRIMARY KEY(login, idsite) + ) + ", + + 'site' => "CREATE TABLE {$prefixTables}site ( + idsite INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, + name VARCHAR(90) NOT NULL, + main_url VARCHAR(255) NOT NULL, + ts_created TIMESTAMP NOT NULL, + PRIMARY KEY(idsite) + ) + ", + + 'site_url' => "CREATE TABLE {$prefixTables}site_url ( + idsite INTEGER(10) UNSIGNED NOT NULL, + url VARCHAR(255) NOT NULL, + PRIMARY KEY(idsite, url) + ) + ", + + + 'logger_message' => "CREATE TABLE {$prefixTables}logger_message ( + idlogger_message INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, + timestamp TIMESTAMP NULL, + message TEXT NULL, + PRIMARY KEY(idlogger_message) + ) + ", + + 'logger_api_call' => "CREATE TABLE {$prefixTables}logger_api_call ( + idlogger_api_call INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, + class_name VARCHAR(255) NULL, + method_name VARCHAR(255) NULL, + parameter_names_default_values TEXT NULL, + parameter_values TEXT NULL, + execution_time FLOAT NULL, + caller_ip BIGINT NULL, + timestamp TIMESTAMP NULL, + returned_value TEXT NULL, + PRIMARY KEY(idlogger_api_call) + ) + ", + + 'logger_error' => "CREATE TABLE {$prefixTables}logger_error ( + idlogger_error INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, + timestamp TIMESTAMP NULL, + message TEXT NULL, + errno INTEGER UNSIGNED NULL, + errline INTEGER UNSIGNED NULL, + errfile VARCHAR(255) NULL, + backtrace TEXT NULL, + PRIMARY KEY(idlogger_error) + ) + ", + + 'logger_exception' => "CREATE TABLE {$prefixTables}logger_exception ( + idlogger_exception INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, + timestamp TIMESTAMP NULL, + message TEXT NULL, + errno INTEGER UNSIGNED NULL, + errline INTEGER UNSIGNED NULL, + errfile VARCHAR(255) NULL, + backtrace TEXT NULL, + PRIMARY KEY(idlogger_exception) + ) + ", + + + 'log_action' => "CREATE TABLE {$prefixTables}log_action ( + idaction INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, + name VARCHAR(255) NOT NULL, + type TINYINT UNSIGNED NULL, + PRIMARY KEY(idaction) + ) + ", + + 'log_visit' => "CREATE TABLE {$prefixTables}log_visit ( + idvisit INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, + idsite INTEGER(10) UNSIGNED NOT NULL, + visitor_localtime TIME NOT NULL, + visitor_idcookie CHAR(32) NOT NULL, + visitor_returning TINYINT(1) NOT NULL, + visit_first_action_time DATETIME NOT NULL, + visit_last_action_time DATETIME NOT NULL, + visit_server_date DATE NOT NULL, + visit_exit_idaction INTEGER(11) NOT NULL, + visit_entry_idaction INTEGER(11) NOT NULL, + visit_total_actions SMALLINT(5) UNSIGNED NOT NULL, + visit_total_time SMALLINT(5) UNSIGNED NOT NULL, + referer_type INTEGER UNSIGNED NULL, + referer_name VARCHAR(70) NULL, + referer_url TEXT NOT NULL, + referer_keyword VARCHAR(255) NULL, + config_md5config CHAR(32) NOT NULL, + config_os CHAR(3) NOT NULL, + config_browser_name VARCHAR(10) NOT NULL, + config_browser_version VARCHAR(20) NOT NULL, + config_resolution VARCHAR(9) NOT NULL, + config_color_depth TINYINT(2) UNSIGNED NOT NULL, + config_pdf TINYINT(1) NOT NULL, + config_flash TINYINT(1) NOT NULL, + config_java TINYINT(1) NOT NULL, + config_director TINYINT(1) NOT NULL, + config_quicktime TINYINT(1) NOT NULL, + config_realplayer TINYINT(1) NOT NULL, + config_windowsmedia TINYINT(1) NOT NULL, + config_cookie TINYINT(1) NOT NULL, + location_ip BIGINT(11) NOT NULL, + location_browser_lang VARCHAR(20) NOT NULL, + location_country CHAR(3) NOT NULL, + location_continent CHAR(3) NOT NULL, + PRIMARY KEY(idvisit) + ) + ", + + 'log_link_visit_action' => "CREATE TABLE {$prefixTables}log_link_visit_action ( + idlink_va INTEGER(11) NOT NULL AUTO_INCREMENT, + idvisit INTEGER(10) UNSIGNED NOT NULL, + idaction INTEGER(10) UNSIGNED NOT NULL, + idaction_ref INTEGER(11) UNSIGNED NOT NULL, + time_spent_ref_action INTEGER(10) UNSIGNED NOT NULL, + PRIMARY KEY(idlink_va) + ) + ", + + 'log_profiling' => "CREATE TABLE {$prefixTables}log_profiling ( + query TEXT NOT NULL, + count INTEGER UNSIGNED NULL, + sum_time_ms FLOAT NULL, + UNIQUE INDEX query(query(100)) + ) + ", + + 'archive_numeric' => "CREATE TABLE {$prefixTables}archive_numeric ( + idarchive INTEGER UNSIGNED NOT NULL, + name VARCHAR(255) NOT NULL, + idsite INTEGER UNSIGNED NULL, + date1 DATE NULL, + date2 DATE NULL, + period TINYINT UNSIGNED NULL, + ts_archived DATETIME NULL, + value FLOAT NULL, + PRIMARY KEY(idarchive, name) + ) + ", + 'archive_blob' => "CREATE TABLE {$prefixTables}archive_blob ( + idarchive INTEGER UNSIGNED NOT NULL, + name VARCHAR(255) NOT NULL, + idsite INTEGER UNSIGNED NULL, + date1 DATE NULL, + date2 DATE NULL, + period TINYINT UNSIGNED NULL, + ts_archived DATETIME NULL, + value MEDIUMBLOB NULL, + PRIMARY KEY(idarchive, name) + ) + ", + ); + return $tables; + } + + static public function getCurrentUserLogin() + { + return Zend_Registry::get('access')->getLogin(); + } + + static public function getCurrentUserTokenAuth() + { + return Zend_Registry::get('access')->getTokenAuth(); + } + + /** + * Returns the plugin currently being used to display the page + * + * @return Piwik_Plugin + */ + static public function getCurrentPlugin() + { + return Piwik_PluginsManager::getInstance()->getLoadedPlugin(Piwik::getModule()); + } + + static public function isUserIsSuperUserOrTheUser( $theUser ) + { + try{ + self::checkUserIsSuperUserOrTheUser( $theUser ); + return true; + } catch( Exception $e){ + return false; + } + } + + // Accessible either to the user itself + static public function checkUserIsSuperUserOrTheUser( $theUser ) + { + try{ + if( Piwik::getCurrentUserLogin() !== $theUser) + { + // or to the super user + Piwik::checkUserIsSuperUser(); + } + } catch( Piwik_Access_NoAccessException $e){ + throw new Piwik_Access_NoAccessException("The user has to be either the Super User or the user '$theUser' itself."); + } + } + + static public function isUserIsSuperUser() + { + try{ + self::checkUserIsSuperUser(); + return true; + } catch( Exception $e){ + return false; + } + } + + static public function setUserIsSuperUser() + { + Zend_Registry::get('access')->setSuperUser(); + } + + static public function checkUserIsSuperUser() + { + Zend_Registry::get('access')->checkUserIsSuperUser(); + } + + static public function isUserHasAdminAccess( $idSites ) + { + try{ + self::checkUserHasAdminAccess( $idSites ); + return true; + } catch( Exception $e){ + return false; + } + } + + static public function checkUserHasAdminAccess( $idSites ) + { + Zend_Registry::get('access')->checkUserHasAdminAccess( $idSites ); + } + + static public function isUserHasSomeAdminAccess() + { + try{ + self::checkUserHasSomeAdminAccess(); + return true; + } catch( Exception $e){ + return false; + } + } + + static public function checkUserHasSomeAdminAccess() + { + Zend_Registry::get('access')->checkUserHasSomeAdminAccess(); + } + + static public function isUserHasViewAccess( $idSites ) + { + try{ + self::checkUserHasViewAccess( $idSites ); + return true; + } catch( Exception $e){ + return false; + } + } + + static public function checkUserHasViewAccess( $idSites ) + { + Zend_Registry::get('access')->checkUserHasViewAccess( $idSites ); + } + + static public function prefixClass( $class ) + { + if(substr_count($class, Piwik::CLASSES_PREFIX) > 0) + { + return $class; + } + return Piwik::CLASSES_PREFIX.$class; + } + static public function unprefixClass( $class ) + { + $lenPrefix = strlen(Piwik::CLASSES_PREFIX); + if(substr($class, 0, $lenPrefix) == Piwik::CLASSES_PREFIX) + { + return substr($class, $lenPrefix); + } + return $class; + } + + /** + * Returns the current module read from the URL (eg. 'API', 'UserSettings', etc.) + * + * @return string + */ + static public function getModule() + { + return Piwik_Common::getRequestVar('module', '', 'string'); + } + + /** + * Returns the current action read from the URL + * + * @return string + */ + static public function getAction() + { + return Piwik_Common::getRequestVar('action', '', 'string'); + } + + /** + * returns false if the URL to redirect to is already this URL + */ + static public function redirectToModule( $newModule, $newAction = '' ) + { + $currentModule = self::getModule(); + $currentAction = self::getAction(); + + if($currentModule != $newModule + || $currentAction != $newAction ) + { + + $newUrl = Piwik_URL::getCurrentUrlWithoutQueryString() + . Piwik_Url::getCurrentQueryStringWithParametersModified( + array('module' => $newModule, 'action' => $newAction) + ); + + Piwik_Url::redirectToUrl($newUrl); + } + return false; + } + + static public function prefixTable( $table ) + { + $config = Zend_Registry::get('config'); + $prefixTables = $config->database->tables_prefix; + return $prefixTables . $table; + } + + /** + * Names of all the prefixed tables in piwik + * Doesn't use the DB + */ + static public function getTablesNames() + { + $aTables = array_keys(self::getTablesCreateSql()); + $config = Zend_Registry::get('config'); + $prefixTables = $config->database->tables_prefix; + $return = array(); + foreach($aTables as $table) + { + $return[] = $prefixTables.$table; + } + return $return; + } + + static $tablesInstalled = null; + + static public function getTablesInstalled( $forceReload = true ) + { + if(is_null(self::$tablesInstalled) + || $forceReload === true) + { + + $db = Zend_Registry::get('db'); + $config = Zend_Registry::get('config'); + $prefixTables = $config->database->tables_prefix; + + $allTables = $db->fetchCol("SHOW TABLES"); + + // all the tables to be installed + $allMyTables = self::getTablesNames(); + + // we get the intersection between all the tables in the DB and the tables to be installed + $tablesInstalled = array_intersect($allMyTables, $allTables); + + // at this point we have only the piwik tables which is good + // but we still miss the piwik generated tables (using the class Piwik_TablePartitioning) + + $allArchiveNumeric = $db->fetchCol("SHOW TABLES LIKE '".$prefixTables."archive_numeric%'"); + $allArchiveBlob = $db->fetchCol("SHOW TABLES LIKE '".$prefixTables."archive_blob%'"); + + $allTablesReallyInstalled = array_merge($tablesInstalled, $allArchiveNumeric, $allArchiveBlob); + + self::$tablesInstalled = $allTablesReallyInstalled; + } + return self::$tablesInstalled; + } + + static public function createDatabase() + { + $db = Zend_Registry::get('db'); + $dbName = Zend_Registry::get('config')->database->dbname; + $db->query("CREATE DATABASE IF NOT EXISTS ".$dbName); + } + + static public function dropDatabase() + { + $db = Zend_Registry::get('db'); + $dbName = Zend_Registry::get('config')->database->dbname; + $db->query("DROP DATABASE IF EXISTS ".$dbName); + } + + + static public function createDatabaseObject( $dbInfos = null ) + { + $config = Zend_Registry::get('config'); + + if(is_null($dbInfos)) + { + $dbInfos = $config->database->toArray(); + } + if(!isset($dbInfos['password'])) + { + $dbInfos['password'] = ''; + } + + // test with the password ='][{}!3456&&^#gegq"eQ for example + if(substr($dbInfos['password'],0,1) == '"' + && substr($dbInfos['password'],-1,1) == '"' + && strlen($dbInfos['password']) >= 2 ) + { + $dbInfos['password'] = substr($dbInfos['password'], 1, -1); + } + $dbInfos['password'] = htmlspecialchars_decode($dbInfos['password']); + + $dbInfos['profiler'] = $config->Debug->enable_sql_profiler; + + $db = Zend_Db::factory($config->database->adapter, $dbInfos); + $db->getConnection(); + // see http://framework.zend.com/issues/browse/ZF-1398 + $db->getConnection()->setAttribute(PDO::ATTR_EMULATE_PREPARES, true); + $db->getConnection()->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, true); + Zend_Db_Table::setDefaultAdapter($db); + $db->resetConfigArray(); // we don't want this information to appear in the logs + Zend_Registry::set('db', $db); + } + + static public function createLogObject() + { + require_once "Log/APICall.php"; + require_once "Log/Exception.php"; + require_once "Log/Error.php"; + require_once "Log/Message.php"; + + $configAPI = Zend_Registry::get('config')->log; + + $aLoggers = array( + 'logger_api_call' => new Piwik_Log_APICall, + 'logger_exception' => new Piwik_Log_Exception, + 'logger_error' => new Piwik_Log_Error, + 'logger_message' => new Piwik_Log_Message, + ); + + foreach($configAPI as $loggerType => $aRecordTo) + { + if(isset($aLoggers[$loggerType])) + { + $logger = $aLoggers[$loggerType]; + + foreach($aRecordTo as $recordTo) + { + switch($recordTo) + { + case 'screen': + $logger->addWriteToScreen(); + break; + + case 'database': + $logger->addWriteToDatabase(); + break; + + case 'file': + $logger->addWriteToFile(); + break; + + default: + throw new Exception("TODO"); + break; + } + } + } + } + + foreach($aLoggers as $loggerType =>$logger) + { + if($logger->getWritersCount() == 0) + { + $logger->addWriteToNull(); + } + Zend_Registry::set($loggerType, $logger); + } + } + + + static public function createConfigObject( $pathConfigFile = null ) + { + $config = new Piwik_Config($pathConfigFile); + } + + static public function dropTables( $doNotDelete = array() ) + { + $tablesAlreadyInstalled = self::getTablesInstalled(); + $db = Zend_Registry::get('db'); + + $doNotDeletePattern = "(".implode("|",$doNotDelete).")"; + + foreach($tablesAlreadyInstalled as $tableName) + { + + if( count($doNotDelete) == 0 + || (!in_array($tableName,$doNotDelete) + && !ereg($doNotDeletePattern,$tableName) + ) + ) + { + $db->query("DROP TABLE $tableName"); + } + } + } + + /** + * Returns true if the email is a valid email + * + * @param string email + * @return bool + */ + static public function isValidEmailString( $email ) + { + return (preg_match('/^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9_.-]+\.[a-zA-Z]{2,4}$/', $email) > 0); + } + + /** + * Creates an entry in the User table for the "anonymous" user. + * + * @return void + */ + static public function createAnonymousUser() + { + // The anonymous user is the user that is assigned by default + // note that the token_auth value is anonymous, which is assigned by default as well in the Login plugin + $db = Zend_Registry::get('db'); + $db->query("INSERT INTO ". Piwik::prefixTable("user") . " + VALUES ( 'anonymous', '', 'anonymous', 'anonymous@example.org', 'anonymous', CURRENT_TIMESTAMP );" ); + } + + static public function createTablesIndex() + { + $db = Zend_Registry::get('db'); + $prefixTables = Zend_Registry::get('config')->database->tables_prefix; + + $db->query('CREATE INDEX index_idvisit ON '.$prefixTables.'log_link_visit_action (idvisit)'); + $db->query('CREATE INDEX index_idaction ON '.$prefixTables.'log_action (idaction)'); + $db->query('CREATE INDEX index_idsite ON '.$prefixTables.'log_visit (idsite)'); + $db->query('CREATE INDEX index_visit_server_date ON '.$prefixTables.'log_visit (visit_server_date);'); + } + + static public function createTables() + { + $db = Zend_Registry::get('db'); + $config = Zend_Registry::get('config'); + $prefixTables = $config->database->tables_prefix; + + $tablesAlreadyInstalled = self::getTablesInstalled(); + $tablesToCreate = self::getTablesCreateSql(); + unset($tablesToCreate['archive_blob']); + unset($tablesToCreate['archive_numeric']); + + foreach($tablesToCreate as $tableName => $tableSql) + { + $tableName = $prefixTables . $tableName; + if(!in_array($tableName, $tablesAlreadyInstalled)) + { + $db->query( $tableSql ); + } + } + } + + static public function install() + { + Piwik::mkdir(Zend_Registry::get('config')->smarty->compile_dir); + Piwik::mkdir(Zend_Registry::get('config')->smarty->cache_dir); + } + + static public function uninstall() + { + $db = Zend_Registry::get('db'); + $db->query( "DROP TABLE IF EXISTS ". implode(", ", self::getTablesNames()) ); + } +} + diff --git a/core/Plugin.php b/core/Plugin.php new file mode 100644 index 0000000000..a909d2ed2a --- /dev/null +++ b/core/Plugin.php @@ -0,0 +1,102 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Plugin.php 581 2008-07-27 23:07:52Z matt $ + * + * @package Piwik + */ + + +/** + * Abstract class to define a Piwik_Plugin. + * Any plugin has to at least implement the abstract methods of this class. + * + * @package Piwik + */ +abstract class Piwik_Plugin +{ + /** + * Returns the plugin details + * 'name' => string // plugin name + * 'description' => string // 1/2 sentences description of the plugin + * 'author' => string // plugin author + * 'author_homepage' => string // author homepage (or email "mailto:youremail@example.org") + * 'homepage' => string // plugin homepage + * 'version' => string // plugin version number + * 'LogStatsPlugin' => bool // should we load this plugin during the stats logging process? + */ + abstract function getInformation(); + + /** + * Returns the plugin name + * @var string + */ + public function getName() + { + $info = $this->getInformation(); + return $info['name']; + } + + /** + * Returns the UserCountry part when the plugin class is Piwik_UserCountry + * + * @return string + */ + public function getClassName() + { + return substr(get_class($this), strlen("Piwik_")); + } + + /** + * Returns the list of hooks registered with the methods names + * @var array + */ + function getListHooksRegistered() + { + return array(); + } + + /** + * Returns the names of the required plugins + * @var array + */ + public function getListRequiredPlugins() + { + return array(); + } + + /** + * Executed after loading plugin and registering translations + * Useful for code that uses translated strings from the plugin. + * @return void + */ + public function postLoad() + { + return; + } + + /** + * Install the plugin + * - create tables + * - update existing tables + * - etc. + * @return void + */ + public function install() + { + return; + } + + /** + * Remove the created resources during the install + * @return void + */ + public function uninstall() + { + return; + } +} + diff --git a/core/PluginsFunctions/AdminMenu.php b/core/PluginsFunctions/AdminMenu.php new file mode 100644 index 0000000000..b450eb04f4 --- /dev/null +++ b/core/PluginsFunctions/AdminMenu.php @@ -0,0 +1,33 @@ +<?php +static $adminMenu = array(); + +function Piwik_GetAdminMenu() +{ + global $adminMenu; + foreach($adminMenu as $key => &$element) + { + if(is_null($element)) + { + unset($adminMenu[$key]); + } + } + return $adminMenu; +} + +function Piwik_AddAdminMenu( $adminMenuName, $url ) +{ + global $adminMenu; + + if(!isset($adminMenu[$adminMenuName])) + { + $adminMenu[$adminMenuName] = $url; + } +} + +function Piwik_RenameAdminMenuEntry($adminMenuOriginal, $adminMenuRenamed) +{ + global $adminMenu; + $save = $adminMenu[$adminMenuOriginal]; + unset($adminMenu[$adminMenuOriginal]); + $adminMenu[$adminMenuRenamed] = $save; +} diff --git a/core/PluginsFunctions/Menu.php b/core/PluginsFunctions/Menu.php new file mode 100644 index 0000000000..c120b868af --- /dev/null +++ b/core/PluginsFunctions/Menu.php @@ -0,0 +1,106 @@ +<?php +static $mainMenu = array(); +static $menuEditsToApply = array(); +static $menuRenameToApply = array(); + +// we setup the main categories in a specific order +$mainMenu['Dashboard'] = null; +$mainMenu['General'] = null; +$mainMenu['Visitors'] = null; +$mainMenu['Actions'] = null; +$mainMenu['Referers'] = null; +$mainMenu['Live!'] = null; + + +function Piwik_GetMenu() +{ + global $mainMenu; + global $menuEditsToApply; + global $menuRenameToApply; + + // we apply the list of edits we've registered so far + foreach($menuEditsToApply as $edit) + { + $mainMenuToEdit = $edit[0]; + $subMenuToEdit = $edit[1]; + $newUrl = $edit[2]; + if(!isset($mainMenu[$mainMenuToEdit][$subMenuToEdit])) + { + Piwik_AddMenu($mainMenuToEdit, $subMenuToEdit, $newUrl); + } + else + { + $mainMenu[$mainMenuToEdit][$subMenuToEdit] = $newUrl; + } + } + + // we now apply the menu rename + foreach($menuRenameToApply as $rename) + { + $mainMenuOriginal = $rename[0]; + $subMenuOriginal = $rename[1]; + $mainMenuRenamed = $rename[2]; + $subMenuRenamed = $rename[3]; + if(isset($mainMenu[$mainMenuOriginal][$subMenuOriginal])) + { + $save = $mainMenu[$mainMenuOriginal][$subMenuOriginal]; + unset($mainMenu[$mainMenuOriginal][$subMenuOriginal]); + $mainMenu[$mainMenuRenamed][$subMenuRenamed] = $save; + } + } + + // we now do some cleaning on the menu + foreach($mainMenu as $key => &$element) + { + if(is_null($element)) + { + unset($mainMenu[$key]); + } + else + { + // we want to move some submenus in the first position + $priority = array('Overview','Evolution'); + foreach($priority as $name) + { + if(isset($element[$name])) + { + $newElement = array($name => $element[$name]); + unset($element[$name]); + $element = $newElement + $element; + } + } + $element['_url'] = current($element); + } + } + return $mainMenu; +} + + +function Piwik_AddMenu( $mainMenuName, $subMenuName, $url ) +{ + global $mainMenu; + + if(!isset($mainMenu[$mainMenuName])) + { + $mainMenu[$mainMenuName]['_url'] = $url; + } + if(!empty($subMenuName)) + { + $mainMenu[$mainMenuName][$subMenuName] = $url; + } + +} + +function Piwik_RenameMenuEntry($mainMenuOriginal, $subMenuOriginal, + $mainMenuRenamed, $subMenuRenamed) +{ + global $menuRenameToApply; + $menuRenameToApply[] = array($mainMenuOriginal, $subMenuOriginal, + $mainMenuRenamed, $subMenuRenamed); +} + +function Piwik_EditMenuUrl( $mainMenuToEdit, $subMenuToEdit, $newUrl ) +{ + global $menuEditsToApply; + $menuEditsToApply[] = array($mainMenuToEdit, $subMenuToEdit, $newUrl); +} diff --git a/core/PluginsFunctions/Sql.php b/core/PluginsFunctions/Sql.php new file mode 100644 index 0000000000..f169c4cf7e --- /dev/null +++ b/core/PluginsFunctions/Sql.php @@ -0,0 +1,34 @@ +<?php + +/** + * Executes a SQL query on the DB and returns the Zend_Db_Statement object + * If you want to fetch data from the DB you should use the function Piwik_FetchAll() + * + * See also http://framework.zend.com/manual/en/zend.db.statement.html + * + * @param string $sqlQuery + * @param array Parameters to bind in the query, array( param1 => value1, param2 => value2) + * @return Zend_Db_Statement + */ +function Piwik_Query( $sqlQuery, $parameters = array()) +{ + return Zend_Registry::get('db')->query( $sqlQuery, $parameters); +} + +/** + * Executes the SQL Query and fetches all the rows from the database + * + * @param string $sqlQuery + * @param array Parameters to bind in the query, array( param1 => value1, param2 => value2) + * @return array (one row in the array per row fetched in the DB) + */ +function Piwik_FetchAll( $sqlQuery, $parameters = array()) +{ + return Zend_Registry::get('db')->fetchAll( $sqlQuery, $parameters ); +} + +function Piwik_FetchOne( $sqlQuery, $parameters = array()) +{ + return Zend_Registry::get('db')->fetchOne( $sqlQuery, $parameters ); +} + diff --git a/core/PluginsFunctions/Widget.php b/core/PluginsFunctions/Widget.php new file mode 100644 index 0000000000..65e90d84fa --- /dev/null +++ b/core/PluginsFunctions/Widget.php @@ -0,0 +1,18 @@ +<?php + +Piwik_AddAction('Menu', 'Piwik_BuildMenu'); + +static $widgets = array(); + +function Piwik_GetListWidgets() +{ + global $widgets; + return $widgets; +} + +function Piwik_AddWidget( $pluginName, $controllerMethodToCall, $widgetTitle ) +{ + global $widgets; + // get the plugin name from controller + $widgets[$pluginName][] = array( $widgetTitle, $controllerMethodToCall ); +} diff --git a/core/PluginsManager.php b/core/PluginsManager.php new file mode 100644 index 0000000000..88fe7fca87 --- /dev/null +++ b/core/PluginsManager.php @@ -0,0 +1,502 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: PluginsManager.php 583 2008-07-28 00:37:19Z matt $ + * + * @package Piwik + */ + + +require_once "Plugin.php"; +require_once "Event/Dispatcher.php"; + +/** + * @package Piwik + */ +class Piwik_PluginsManager +{ + /** + * @var Event_Dispatcher + */ + public $dispatcher; + + protected $pluginsToLoad = array(); + protected $installPlugins = false; + protected $doLoadPlugins = true; + protected $languageToLoad = null; + protected $loadedPlugins = array(); + + protected $doLoadAlwaysActivatedPlugins = true; + protected $pluginToAlwaysActivate = array( 'CoreHome', + 'CoreAdminHome', + 'CorePluginsAdmin' + ); + + static private $instance = null; + + /** + * Returns the singleton Piwik_PluginsManager + * + * @return Piwik_PluginsManager + */ + static public function getInstance() + { + if (self::$instance == null) + { + $c = __CLASS__; + self::$instance = new $c(); + } + return self::$instance; + } + + private function __construct() + { + $this->dispatcher = Event_Dispatcher::getInstance(); + } + + public function isPluginAlwaysActivated( $name ) + { + return in_array( $name, $this->pluginToAlwaysActivate); + } + + public function isPluginActivated( $name ) + { + return in_array( $name, $this->pluginsToLoad) + || $this->isPluginAlwaysActivated( $name ); + } + + /** + * Reads the directories inside the plugins/ directory and returns their names in an array + * + * @return array + */ + public function readPluginsDirectory() + { + $pluginsName = glob( "plugins/*",GLOB_ONLYDIR); + $pluginsName = array_map('basename', $pluginsName); + return $pluginsName; + } + + public function deactivatePlugin($pluginName) + { + $plugins = $this->pluginsToLoad; + + $key = array_search($pluginName,$plugins); + if($key !== false) + { + unset($plugins[$key]); + Zend_Registry::get('config')->Plugins = $plugins; + } + + try{ + $pluginsLogStats = Zend_Registry::get('config')->Plugins_LogStats->Plugins_LogStats; + if(!is_null($pluginsLogStats)) + { + $pluginsLogStats = $pluginsLogStats->toArray(); + $key = array_search($pluginName,$pluginsLogStats); + if($key !== false) + { + unset($pluginsLogStats[$key]); + Zend_Registry::get('config')->Plugins_LogStats = $pluginsLogStats; + } + } + } catch(Exception $e) {} + } + + /** + * TODO horrible dirty hack because the Config class is not clean enough. Needs to rewrite the Config + * __set and __get in a cleaner way, also see the __destruct which writes the configuration file. + * + * @return array + */ + protected function getInstalledPlugins() + { + if(!class_exists('Zend_Registry')) + { + throw new Exception("Not possible to list installed plugins (case LogStats module)"); + } + if(!is_null(Zend_Registry::get('config')->PluginsInstalled->PluginsInstalled)) + { + return Zend_Registry::get('config')->PluginsInstalled->PluginsInstalled->toArray(); + } + elseif(is_array(Zend_Registry::get('config')->PluginsInstalled)) + { + return Zend_Registry::get('config')->PluginsInstalled; + } + else + { + return Zend_Registry::get('config')->PluginsInstalled->toArray(); + } + } + + public function installLoadedPlugins() + { + foreach($this->getLoadedPlugins() as $plugin) + { + try { + $this->installPluginIfNecessary( $plugin ); + }catch(Exception $e){ + echo $e->getMessage(); + } + } + } + + protected function installPluginIfNecessary( Piwik_Plugin $plugin ) + { + $pluginName = $plugin->getClassName(); + + // is the plugin already installed or is it the first time we activate it? + $pluginsInstalled = $this->getInstalledPlugins(); + if(!in_array($pluginName,$pluginsInstalled)) + { + $this->installPlugin($plugin); + $pluginsInstalled[] = $pluginName; + Zend_Registry::get('config')->PluginsInstalled = $pluginsInstalled; + } + + $information = $plugin->getInformation(); + + // if the plugin is to be loaded during the statistics logging + if(isset($information['LogStatsPlugin']) + && $information['LogStatsPlugin'] === true) + { + $pluginsLogStats = Zend_Registry::get('config')->Plugins_LogStats->Plugins_LogStats; + if(is_null($pluginsLogStats)) + { + $pluginsLogStats = array(); + } + else + { + $pluginsLogStats = $pluginsLogStats->toArray(); + } + if(!in_array($pluginName, $pluginsLogStats)) + { + $pluginsLogStats[] = $pluginName; + Zend_Registry::get('config')->Plugins_LogStats = $pluginsLogStats; + } + } + } + + public function activatePlugin($pluginName) + { + $plugins = Zend_Registry::get('config')->Plugins->Plugins->toArray(); + if(in_array($pluginName,$plugins)) + { + throw new Exception("Plugin '$pluginName' already activated."); + } + + $existingPlugins = $this->readPluginsDirectory(); + if( array_search($pluginName,$existingPlugins) === false) + { + throw new Exception("Unable to find the plugin '$pluginName'."); + } + + $plugin = $this->loadPlugin($pluginName); + + $this->installPluginIfNecessary($plugin); + + // we add the plugin to the list of activated plugins + $plugins[] = $pluginName; + + // the config file will automatically be saved with the new plugin + Zend_Registry::get('config')->Plugins = $plugins; + } + + public function setPluginsToLoad( array $pluginsToLoad ) + { + // case no plugins to load + if(is_null($pluginsToLoad)) + { + $pluginsToLoad = array(); + } + $this->pluginsToLoad = $pluginsToLoad; + + $this->loadPlugins(); + } + + public function doNotLoadPlugins() + { + $this->doLoadPlugins = false; + } + + public function doNotLoadAlwaysActivatedPlugins() + { + $this->doLoadAlwaysActivatedPlugins = false; + } + + /** + * Add a plugin in the loaded plugins array + * + * @param string plugin name without prefix (eg. 'UserCountry') + * @param Piwik_Plugin $newPlugin + */ + protected function addLoadedPlugin( $pluginName, Piwik_Plugin $newPlugin ) + { + $this->loadedPlugins[$pluginName] = $newPlugin; + } + + /** + * Returns an array containing the plugins class names (eg. 'Piwik_UserCountry' and NOT 'UserCountry') + * + * @return array + */ + public function getLoadedPluginsName() + { + $oPlugins = $this->getLoadedPlugins(); + $pluginNames = array_map('get_class',$oPlugins); + return $pluginNames; + } + + /** + * Returns an array of key,value with the following format: array( + * 'UserCountry' => Piwik_Plugin $pluginObject, + * 'UserSettings' => Piwik_Plugin $pluginObject, + * ); + * + * @return array + */ + public function getLoadedPlugins() + { + return $this->loadedPlugins; + } + + /** + * Returns the given Piwik_Plugin object + * + * @param string $name + * @return Piwik_Piwik + */ + public function getLoadedPlugin($name) + { + if(!isset($this->loadedPlugins[$name])) + { + throw new Exception("The plugin '$name' has not been loaded."); + } + return $this->loadedPlugins[$name]; + } + /** + * Load the plugins classes installed. + * Register the observers for every plugin. + * + */ + public function loadPlugins() + { + $this->pluginsToLoad = array_unique($this->pluginsToLoad); + + $pluginsToLoad = $this->pluginsToLoad; + + if($this->doLoadAlwaysActivatedPlugins) + { + $pluginsToLoad = array_merge($this->pluginsToLoad, $this->pluginToAlwaysActivate); + } + + foreach($pluginsToLoad as $pluginName) + { + $newPlugin = $this->loadPlugin($pluginName); + + // if we have to load the plugins + // and if this plugin is activated + if($this->doLoadPlugins + && $this->isPluginActivated($pluginName)) + { + $this->registerTranslation( $newPlugin, $this->languageToLoad ); + $this->addPluginObservers( $newPlugin ); + $this->addLoadedPlugin( $pluginName, $newPlugin); + + $newPlugin->postLoad(); + } + } + } + + /** + * Loads the plugin filename and instanciates the plugin with the given name, eg. UserCountry + * Do NOT give the class name ie. Piwik_UserCountry, but give the plugin name ie. UserCountry + * + * @param Piwik_Plugin $pluginName + */ + public function loadPlugin( $pluginName ) + { + if(isset($this->loadedPlugins[$pluginName])) + { + return $this->loadedPlugins[$pluginName]; + } + $pluginFileName = $pluginName . '/' . $pluginName . ".php"; + $pluginClassName = "Piwik_".$pluginName; + + if( !Piwik_Common::isValidFilename($pluginName)) + { + throw new Exception("The plugin filename '$pluginFileName' is not a valid filename"); + } + + $path = 'plugins/' . $pluginFileName; + + // case LogStats, we don't throw the exception, we don't want to add the Zend overhead + if(class_exists('Zend_Loader') + && !Zend_Loader::isReadable($path)) + { + throw new Exception("<b>The plugin file {$path} couldn't be found. </b><br> + If you are updating from a 0.2.x version, please <a target=_blank href='http://dev.piwik.org/trac/wiki/FAQ#HowdoIupdatefrom0.2.xtothe0.3'>read the FAQ</a>!<br> + Found in your config/config.ini.php file:<br><code>[Plugins]</code><br><code>Plugins[] = $pluginName;</code>"); + } + + require_once $path; + + if(!class_exists($pluginClassName)) + { + throw new Exception("The class $pluginClassName couldn't be found in the file '$path'"); + } + $newPlugin = new $pluginClassName; + + if(!($newPlugin instanceof Piwik_Plugin)) + { + throw new Exception("The plugin $pluginClassName in the file $path must inherit from Piwik_Plugin."); + } + return $newPlugin; + } + + public function installPlugin( Piwik_Plugin $plugin ) + { + try{ + $plugin->install(); + } catch(Exception $e) { + throw new Piwik_Plugin_Exception($plugin->getName(), $e->getMessage()); } + } + + public function installPlugins() + { + foreach($this->getLoadedPlugins() as $plugin) + { + try{ + $plugin->install(); + } catch(Exception $e) { + throw new Piwik_Plugin_Exception($plugin->getName(), $e->getMessage()); + } + } + } + public function setLanguageToLoad( $code ) + { + $this->languageToLoad = $code; + } + + /** + * For the given plugin, add all the observers of this plugin. + */ + private function addPluginObservers( Piwik_Plugin $plugin ) + { + $hooks = $plugin->getListHooksRegistered(); + + foreach($hooks as $hookName => $methodToCall) + { + $this->dispatcher->addObserver( array( $plugin, $methodToCall), $hookName ); + } + } + public function unloadPlugin( $plugin ) + { + if(!($plugin instanceof Piwik_Plugin )) + { + $plugin = $this->loadPlugin( $plugin ); + } + $hooks = $plugin->getListHooksRegistered(); + + foreach($hooks as $hookName => $methodToCall) + { + $success = $this->dispatcher->removeObserver( array( $plugin, $methodToCall), $hookName ); + if($success !== true) + { + throw new Exception("Error unloading plugin for method = $methodToCall // hook = $hookName "); + } + } + unset($this->loadedPlugins[$plugin->getClassName()]); + } + + public function unloadPlugins() + { + $pluginsLoaded = $this->getLoadedPlugins(); + foreach($pluginsLoaded as $plugin) + { + $this->unloadPlugin($plugin); + } + } + + /** + * @param Piwik_Plugin $plugin + * @param string $langCode + */ + protected function registerTranslation( $plugin, $langCode ) + { + // we are certainly in LogStats mode, Zend is not loaded + if(!class_exists('Zend_Loader')) + { + return ; + } + + $infos = $plugin->getInformation(); + if(!isset($infos['translationAvailable'])) + { + $infos['translationAvailable'] = false; + } + $translationAvailable = $infos['translationAvailable']; + + if(!$translationAvailable) + { + return; + } + + $pluginName = $plugin->getClassName(); + + $path = "plugins/" . $pluginName ."/lang/%s.php"; + + $defaultLangPath = sprintf($path, $langCode); + $defaultEnglishLangPath = sprintf($path, 'en'); + + $translations = array(); + + if(Zend_Loader::isReadable($defaultLangPath)) + { + require $defaultLangPath; + } + elseif(Zend_Loader::isReadable($defaultEnglishLangPath)) + { + require $defaultEnglishLangPath; + } + else + { + throw new Exception("Language file not found for the plugin '$pluginName'."); + } + + Piwik_Translate::getInstance()->addTranslationArray($translations); + } + +} + + +class Piwik_Plugin_Exception extends Exception +{ + function __construct($name, $message) + { + parent::__construct("There was a problem installing the plugin ". $name . " = " . $message. + "<br><b>If this plugin has already been installed, and if you want to hide this message</b>, you must add the following line under the + <code>[PluginsInstalled]</code> entry in your config/config.ini.php file:<br> + <code>PluginsInstalled[] = $name</code><br><br>" ); + } +} + + +/** + * Post an event to the dispatcher which will notice the observers + */ +function Piwik_PostEvent( $eventName, &$object = null, $info = array() ) +{ + Piwik_PluginsManager::getInstance()->dispatcher->post( $object, $eventName, $info, true, false ); +} + +/** + * Register an action to execute for a given event + */ +function Piwik_AddAction( $hookName, $function ) +{ + Piwik_PluginsManager::getInstance()->dispatcher->addObserver( $function, $hookName ); +}
\ No newline at end of file diff --git a/core/Site.php b/core/Site.php new file mode 100644 index 0000000000..12370dde65 --- /dev/null +++ b/core/Site.php @@ -0,0 +1,67 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Site.php 561 2008-07-21 00:00:35Z matt $ + * + * @package Piwik_Site + */ + +/** + * + * @package Piwik_Site + */ +class Piwik_Site +{ + protected $id = null; + + protected static $infoSites = array(); + + function __construct($idsite) + { + $this->id = $idsite; + + if(!isset(self::$infoSites[$this->id])) + { + self::$infoSites[$this->id] = Piwik_SitesManager_API::getSiteFromId($idsite); + } + } + function getName() + { + return self::$infoSites[$this->id]['name']; + } + function getMainUrl() + { + return self::$infoSites[$this->id]['main_url']; + } + + function getId() + { + return $this->id; + } + + function getCreationDate() + { + $date = self::$infoSites[$this->id]['ts_created']; + return Piwik_Date::factory($date); + } + + /** + * @param string comma separated idSite list + * @return array of valid integer + */ + static public function getIdSitesFromIdSitesString( $string ) + { + $ids = explode(',', $string); + $validIds = array(); + foreach($ids as $id) + { + $id = trim($id); + $validIds[] = $id; + } + return $validIds; + } +} + diff --git a/core/SmartyPlugins/function.assignTopBar.php b/core/SmartyPlugins/function.assignTopBar.php new file mode 100644 index 0000000000..79dd80025e --- /dev/null +++ b/core/SmartyPlugins/function.assignTopBar.php @@ -0,0 +1,17 @@ +<?php +/** + * Enter description here... + * + * @param array $params + * @param Smarty $smarty + */ +function smarty_function_assignTopBar($params, &$smarty) +{ + $topBarElements = array( + array('CoreHome', 'Your Dashboard', array('module' => 'CoreHome', 'action' => 'index')), + array('Widgetize', 'Widgets', array('module' => 'Widgetize', 'action' => 'index')), + array('API', 'API', array('module' => 'API', 'action' => 'listAllAPI')), + array('Feedback', 'Give us Feedback!', array('module' => 'Feedback', 'action' => 'index', 'keepThis' => 'true', 'TB_iframe' => 'true', 'height' => '400', 'width' => '350'), 'title="Give us Feedback!" class="thickbox"'), + ); + $smarty->assign("topBarElements", $topBarElements); +}
\ No newline at end of file diff --git a/core/SmartyPlugins/function.hiddenurl.php b/core/SmartyPlugins/function.hiddenurl.php new file mode 100644 index 0000000000..c8933aa6c6 --- /dev/null +++ b/core/SmartyPlugins/function.hiddenurl.php @@ -0,0 +1,46 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: function.url.php 168 2008-01-14 05:26:43Z matt $ + * + * @package SmartyPlugins + */ + +require_once "Url.php"; + +/** + * Smarty {hiddenurl} function plugin. + * Writes an input Hidden field for every parameter in the URL. + * Useful when using GET forms because we need to print the current parameters + * in hidden input so they are to the next URL after the form is submitted. + * + * + * Examples: + * <pre> + * {hiddenurl module="API"} with a URL 'index.php?action=test&module=CoreHome' will output + * <input type=hidden name=action value=test> + * <input type=hidden name=module value=API> + * </pre> + * + * Set a value to null if you want this value not to be passed in the submitted form. + * + * @param array + * @param Smarty + * @return string + */ +function smarty_function_hiddenurl($params, &$smarty) +{ + $urlModified = Piwik_Url::getCurrentQueryStringWithParametersModified( $params ); + $queryString = htmlspecialchars($urlModified); + $urlValues = Piwik_Common::getArrayFromQueryString($queryString); + + $out = ''; + foreach($urlValues as $name => $value) + { + $out .= '<input type="hidden" name="'.$name.'" value="'.$value.'" />'; + } + return $out; +} diff --git a/core/SmartyPlugins/function.loadJavascriptTranslations.php b/core/SmartyPlugins/function.loadJavascriptTranslations.php new file mode 100644 index 0000000000..d470dde784 --- /dev/null +++ b/core/SmartyPlugins/function.loadJavascriptTranslations.php @@ -0,0 +1,61 @@ +<?php + +/** + * inserts javascript translation array into the template from given plugins + * must be called with 'plugins' argument which consists of space-separated module names (i.e. plugins) + * + * + * Example (use in template): + * + * {loadJavascriptTranslations plugins='SitesManager CoreHome General'} + * + * loads javascript array translations from main translation file ('General') + * and both 'CoreHome' and 'SitesManager' plugins translations + * + * Note: You can put noHtml=1 option in order to output pure JS code + * + * only translations with '_fs' suffix will be loaded + * + * in order to use translation in your javascript use _pk_translate function + * (it is always loaded with translations): + * + * <script type="text/javascript"> + * alert(_pk_translate('MY_TRANSLATION_STRING')) + * </script> + * + * Note: Use translation string from your translation file WITHOUT '_js' suffix. + * + * _pk_translate DOES NOT support printf() arguments, but you can call: + * + * sprintf(_pk_translate('_NB_OF_EGGS'),'ten') + * (where _NB_OF_EGGS is defined in translation file as i.e. 'There is %s eggs on the table') + * + * sprintf() function is by default included when loading translations + */ + +function smarty_function_loadJavascriptTranslations($params, &$smarty) +{ + if(!isset($params['plugins'])) + { + throw new Exception("The smarty function loadJavascriptTranslations needs a 'plugins' parameter."); + } + $translate = Piwik_Translate::getInstance(); + $jsTranslations = $translate->getJavascriptTranslations(explode(' ',$params['plugins'])); + + $jsCode = ""; + + if( isset($params['noHtml']) ) + { + $jsCode .= "document.write('<scr'+'ipt language=\"javascript\" src=\"libs/javascript/sprintf.js\"><\/scr'+'ipt>');\n"; + $jsCode .= $jsTranslations; + } + else + { + $jsCode .= '<script type="text/javascript" src="libs/javascript/sprintf.js"></script>'; + $jsCode .= '<script type="text/javascript">'; + $jsCode .= $jsTranslations; + $jsCode .= '</script>'; + } + + return $jsCode; +} diff --git a/core/SmartyPlugins/function.postEvent.php b/core/SmartyPlugins/function.postEvent.php new file mode 100644 index 0000000000..bad7819418 --- /dev/null +++ b/core/SmartyPlugins/function.postEvent.php @@ -0,0 +1,42 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: function.url.php 168 2008-01-14 05:26:43Z matt $ + * + * @package SmartyPlugins + */ + +require_once "Url.php"; + +/** + * Posts an event from a smarty template. This event can then be hooked by another plugin. + * The even will be posted along with a string value that plugins can edit. + * This is useful to allow other plugins to add content at a specific entry point in the template. + * This string will be returned by the smarty function. + * + * Examples: + * <pre> + * {postEvent name="template_footerUserCountry"} + * </pre> + * + * Plugins can then hook on this event by using the Piwik_AddAction function: + * Piwik_AddAction('template_footerUserCountry', 'functionToHookOnThisEvent'); + * + * @param string $name The name of the event + * @return string The string eventually modified by the plugins listening to this event + */ +function smarty_function_postEvent($params, &$smarty) +{ + if(!isset($params['name'])) + { + throw new Exception("The smarty function postEvent needs a 'name' parameter."); + } + $eventName = $params['name']; + + $str = ''; + Piwik_PostEvent($eventName, $str); + return $str; +} diff --git a/core/SmartyPlugins/function.url.php b/core/SmartyPlugins/function.url.php new file mode 100644 index 0000000000..adadea3beb --- /dev/null +++ b/core/SmartyPlugins/function.url.php @@ -0,0 +1,31 @@ +<?php
+/**
+ * Piwik - Open source web analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later
+ * @version $Id: function.url.php 525 2008-06-25 23:49:13Z matt $
+ *
+ * @package SmartyPlugins
+ */
+
+require_once "Url.php";
+
+/**
+ * Smarty {url} function plugin.
+ * Generates a piwik URL with the specified parameters modified.
+ *
+ * Examples:
+ * <pre>
+ * {url module="API"} will rewrite the URL modifying the module GET parameter
+ * {url module="API" method="getKeywords"} will rewrite the URL modifying the parameters module=API method=getKeywords
+ * </pre>
+ *
+ * @see Piwik_Url::getCurrentQueryStringWithParametersModified()
+ * @param $name=$value of the parameters to modify in the generated URL
+ * @return string Something like index.php?module=X&action=Y
+ */
+function smarty_function_url($params, &$smarty)
+{
+ return htmlspecialchars(Piwik_Url::getCurrentScriptName() . Piwik_Url::getCurrentQueryStringWithParametersModified( $params ));
+}
diff --git a/core/SmartyPlugins/modifier.sumtime.php b/core/SmartyPlugins/modifier.sumtime.php new file mode 100644 index 0000000000..2c3e6fd94a --- /dev/null +++ b/core/SmartyPlugins/modifier.sumtime.php @@ -0,0 +1,54 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: modifier.sumtime.php 444 2008-04-11 13:38:22Z johmathe $ + * + * @package SmartyPlugins + */ + +/** + * Returns a string that displays the number of days and hours from a number of seconds + * + * How to use: + * {4200|sumtime} will display '1h 10min' + * + * Examples: + * - 10 gives "10s" + * - 4200 gives "1h 10min" + * - 86400 gives "1 day" + * - 90600 gives "1 day 1h" (it is exactly 1day 1h 10min but we truncate) + * + * @return string + * + */ +function smarty_modifier_sumtime($string) +{ + $seconds = (double)$string; + $days = floor($seconds / 86400); + + $minusDays = $seconds - $days * 86400; + $hours = floor($minusDays / 3600); + + $minusDaysAndHours = $minusDays - $hours * 3600; + $minutes = floor($minusDaysAndHours / 60 ); + + $minusDaysAndHoursAndMinutes = $minusDaysAndHours - $minutes * 60; + $secondsMod = $minusDaysAndHoursAndMinutes; // should be same as $seconds % 60 + + if($days > 0) + { + return sprintf("%d days %d hours", $days, $hours); + } + elseif($hours > 0) + { + return sprintf("%d hours %d min", $hours, $minutes); + } + else + { + return sprintf("%d min %d s", $minutes, $seconds); + } +} + diff --git a/core/SmartyPlugins/modifier.translate.php b/core/SmartyPlugins/modifier.translate.php new file mode 100644 index 0000000000..9942995f66 --- /dev/null +++ b/core/SmartyPlugins/modifier.translate.php @@ -0,0 +1,34 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: modifier.sumtime.php 168 2008-01-14 05:26:43Z matt $ + * + * @package SmartyPlugins + */ + +/** + * Read the translation string from the given index (read form the selected language in Piwik). + * The translations strings are located either in /lang/xx.php or within the plugin lang directory. + * + * Example: + * {'General_Unknown'|translate} will be translated as 'Unknown' (see the entry in /lang/en.php) + * + * @return string The translated string + */ +function smarty_modifier_translate($string) +{ + if(func_num_args() <= 1) + { + $aValues = array(); + } + else + { + $aValues = func_get_args(); + array_shift($aValues); + } + return vsprintf(Piwik_Translate($string), $aValues); +} +
\ No newline at end of file diff --git a/core/SmartyPlugins/modifier.urlRewriteBasicView.php b/core/SmartyPlugins/modifier.urlRewriteBasicView.php new file mode 100644 index 0000000000..949f47059b --- /dev/null +++ b/core/SmartyPlugins/modifier.urlRewriteBasicView.php @@ -0,0 +1,40 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: modifier.sumtime.php 168 2008-01-14 05:26:43Z matt $ + * + * @package SmartyPlugins + */ + +/** + * Rewrites the given URL so that it looks like a URL that can be loaded directly. + * Useful for users who don't handle javascript / ajax, they can still use piwik with these rewritten URLs. + * + * @return string + */ +function smarty_modifier_urlRewriteBasicView($parameters) +{ + // replace module=X by moduleToLoad=X + // replace action=Y by actionToLoad=Y + $parameters['moduleToLoad'] = $parameters['module']; + unset($parameters['module']); + + if(isset( $parameters['action'])) + { + $parameters['actionToLoad'] = $parameters['action']; + unset($parameters['action']); + } + else + { + $parameters['actionToLoad'] = null; + } + $url = Piwik_Url::getCurrentQueryStringWithParametersModified($parameters); + + // add module=CoreHome&action=showInContext + $url = $url . '&module=CoreHome&action=showInContext'; + return htmlspecialchars($url); +} + diff --git a/core/SmartyPlugins/modifier.urlRewriteWithParameters.php b/core/SmartyPlugins/modifier.urlRewriteWithParameters.php new file mode 100644 index 0000000000..e810b8200f --- /dev/null +++ b/core/SmartyPlugins/modifier.urlRewriteWithParameters.php @@ -0,0 +1,23 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: modifier.sumtime.php 168 2008-01-14 05:26:43Z matt $ + * + * @package SmartyPlugins + */ + +/** + * Rewrites the given URL and modify the given parameters. + * @see Piwik_Url::getCurrentQueryStringWithParametersModified() + * + * @return string + */ +function smarty_modifier_urlRewriteWithParameters($parameters) +{ + $url = Piwik_Url::getCurrentQueryStringWithParametersModified($parameters); + return htmlspecialchars($url); +} + diff --git a/core/TablePartitioning.php b/core/TablePartitioning.php new file mode 100644 index 0000000000..eda912f995 --- /dev/null +++ b/core/TablePartitioning.php @@ -0,0 +1,132 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: TablePartitioning.php 386 2008-03-18 19:27:54Z julien $ + * + * @package Piwik + */ + +/** + * + * NB: When a new table is partitionned using this class, we have to update the method + * Piwik::getTablesInstalled() to add the new table to the list of tablename_* to fetch + * + * @package Piwik + */ +abstract class Piwik_TablePartitioning +{ + protected $tableName = null; + protected $generatedTableName = null; + protected $timestamp = null; + + static public $tablesAlreadyInstalled = null; + + public function __construct( $tableName ) + { + $this->tableName = $tableName; + } + + abstract protected function generateTableName() ; + + + public function setTimestamp( $timestamp ) + { + $this->timestamp = $timestamp; + $this->generatedTableName = null; + $this->getTableName(); + } + + public function getTableName() + { + // table name already processed + if(!is_null($this->generatedTableName)) + { + return $this->generatedTableName; + } + + if(is_null($this->timestamp)) + { + throw new Exception("You have to specify a timestamp for a Table Partitioning by date."); + } + + // generate table name + $this->generatedTableName = $this->generateTableName(); + + // we make sure the table already exists + $this->checkTableExists(); + } + + protected function checkTableExists() + { + if(is_null(self::$tablesAlreadyInstalled)) + { + self::$tablesAlreadyInstalled = Piwik::getTablesInstalled( $forceReload = false ); + } + + if(!in_array($this->generatedTableName, self::$tablesAlreadyInstalled)) + { + $db = Zend_Registry::get('db'); + $sql = Piwik::getTableCreateSql($this->tableName); + + $config = Zend_Registry::get('config'); + $prefixTables = $config->database->tables_prefix; + $sql = str_replace( $prefixTables . $this->tableName, $this->generatedTableName, $sql); + + $db->query( $sql ); + + self::$tablesAlreadyInstalled[] = $this->generatedTableName; + } + } + + protected function __toString() + { + return $this->getTableName(); + } +} + +/** + * + * @package Piwik + */ +class Piwik_TablePartitioning_Monthly extends Piwik_TablePartitioning +{ + public function __construct( $tableName ) + { + parent::__construct($tableName); + } + protected function generateTableName() + { + $config = Zend_Registry::get('config'); + $prefixTables = $config->database->tables_prefix; + + $date = date("Y_m", $this->timestamp); + + return $prefixTables . $this->tableName . "_" . $date; + } + +} +/** + * + * @package Piwik + */ +class Piwik_TablePartitioning_Daily extends Piwik_TablePartitioning +{ + public function __construct( $tableName ) + { + parent::__construct($tableName); + } + protected function generateTableName() + { + $config = Zend_Registry::get('config'); + $prefixTables = $config->database->tables_prefix; + + $date = date("Y_m_d", $this->timestamp); + + return $prefixTables . $this->tableName . "_" . $date; + } + +} + diff --git a/core/Timer.php b/core/Timer.php new file mode 100644 index 0000000000..8e71b41ba5 --- /dev/null +++ b/core/Timer.php @@ -0,0 +1,50 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Timer.php 444 2008-04-11 13:38:22Z johmathe $ + * + * @package Piwik_Helper + */ + +/** + * + * @package Piwik_Helper + */ +class Piwik_Timer +{ + private $m_Start; + + public function __construct() + { + $this->m_Start = 0.0; + $this->init(); + } + + private function getMicrotime() + { + list($micro_seconds, $seconds) = explode(" ", microtime()); + return ((float)$micro_seconds + (float)$seconds); + } + + public function init() + { + $this->m_Start = $this->getMicrotime(); + } + + public function getTime($decimals = 2) + { + return number_format($this->getMicrotime() - $this->m_Start, $decimals, '.', ''); + } + public function getTimeMs($decimals = 2) + { + return number_format(1000*($this->getMicrotime() - $this->m_Start), $decimals, '.', ''); + } + + public function __toString() + { + return "Time elapsed: ". $this->getTime() ."s"; + } +} diff --git a/core/Translate.php b/core/Translate.php new file mode 100644 index 0000000000..7f4ea56583 --- /dev/null +++ b/core/Translate.php @@ -0,0 +1,155 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Translate.php 526 2008-06-25 23:57:04Z matt $ + * + * @package Piwik + */ + +/** + * @package Piwik + */ +class Piwik_Translate +{ + static private $instance = null; + + /** + * Returns singleton + * + * @return Piwik_Translate + */ + static public function getInstance() + { + if (self::$instance == null) + { + $c = __CLASS__; + self::$instance = new $c(); + } + return self::$instance; + } + + private function __construct() + { + $translations = array(); + + $language = $this->getFallbackLanguageToLoad(); + require_once "lang/" . $language .".php"; + $this->addTranslationArray($translations); + + $language = $this->getLanguageToLoad(); + require_once "lang/" . $language .".php"; + $this->addTranslationArray($translations); + + setlocale(LC_ALL, $GLOBALS['Piwik_translations']['General_Locale']); + } + + public function addTranslationArray($translation) + { + if(!isset($GLOBALS['Piwik_translations'])) + { + $GLOBALS['Piwik_translations'] = array(); + } + // we could check that no string overlap here + $GLOBALS['Piwik_translations'] = array_merge($GLOBALS['Piwik_translations'], $translation); + } + + /** + * @return string the language filename prefix, eg "en" for english + * @throws exception if the language set in the config file is not a valid filename + */ + public function getLanguageToLoad() + { + $language = Zend_Registry::get('config')->Language->current; + + if( Piwik_Common::isValidFilename($language)) + { + return $language; + } + else + { + throw new Exception("The language selected ('$language') is not a valid language file "); + } + } + + protected function getFallbackLanguageToLoad() + { + return Zend_Registry::get('config')->Language->fallback; + } + + /** + * Generate javascript translations array + * + * @return string containing javascript code with translations array (including <script> tag) + * + */ + public function getJavascriptTranslations($moduleList) + { + if( !$moduleList ) + { + return ''; + } + + $js = 'var translations = {'; + + $moduleRegex = '#^('; + foreach($moduleList as $module) + { + $moduleRegex .= $module.'|'; + } + $moduleRegex = substr($moduleRegex, 0, -1); + $moduleRegex .= ')_([^_]+)_js$#i'; + + foreach($GLOBALS['Piwik_translations'] as $key => $value) + { + $matches = array(); + + if( preg_match($moduleRegex,$key,$matches) ) { + $varName = $matches[1].'_'.$matches[2]; + $varValue = $value; + + $js .= "".$varName.": '".str_replace("'","\\'",$varValue)."',"; + } + + $matches = null; + } + $js = substr($js,0,-1); + $js .= '};'; + $js .= 'function _pk_translate(tvar, str) { '. + 'var s = str; if( typeof(translations[tvar]) != \'undefined\' ) s = translations[tvar];'. + 'return s;}'; + + return $js; + } +} + +function Piwik_Translate($index) +{ + if(isset($GLOBALS['Piwik_translations'][$index])) + { + return $GLOBALS['Piwik_translations'][$index]; + } + throw new Exception("Translation string '$index' not available."); +} + + +/** + * Returns translated string or given message if translation is not found. + * This function does not throw any exception. Use it to translate exceptions. + * + * @param string Translation string index + * @return string + */ +function Piwik_TranslateException($message) +{ + try { + return Piwik_Translate($message); + } + catch(Exception $e) { + return $message; + } +} + + diff --git a/core/Url.php b/core/Url.php new file mode 100644 index 0000000000..f6ece7c37d --- /dev/null +++ b/core/Url.php @@ -0,0 +1,159 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Url.php 498 2008-05-29 03:08:13Z matt $ + * + * @package Piwik_Helper + */ + +/** + * @package Piwik_Helper + * + */ +class Piwik_Url +{ + static function getArrayFromCurrentQueryString() + { + $queryString = Piwik_Url::getCurrentQueryString(); + $queryString = htmlspecialchars($queryString); + $urlValues = Piwik_Common::getArrayFromQueryString($queryString); + return $urlValues; + } + + static function getCurrentQueryStringWithParametersModified( $params ) + { + $urlValues = self::getArrayFromCurrentQueryString(); + + foreach($params as $key => $value) + { + $urlValues[$key] = $value; + } + + $query = http_build_query($urlValues, "", "&"); + + if(strlen($query) > 0) + { + return '?'.$query; + } + else + { + return ''; + } + } + + static public function redirectToUrl( $url ) + { + header("Location: $url"); + exit; + } + + static public function getReferer() + { + if(!empty($_SERVER['HTTP_REFERER'])) + { + return $_SERVER['HTTP_REFERER']; + } + return false; + } + + static public function getCurrentUrl() + { + return self::getCurrentHost() + . self::getCurrentScriptName() + . self::getCurrentQueryString(); + } + + static public function getCurrentUrlWithoutQueryString() + { + + return self::getCurrentHost() + . self::getCurrentScriptName() ; + } + + /** + * Ending with / + */ + static public function getCurrentUrlWithoutFileName() + { + + $host = self::getCurrentHost(); + $queryString = self::getCurrentScriptName() ; + + //add a fake letter case /test/test2/ returns /test which is not expected + $urlDir = dirname ($queryString . 'x'); + // if we are in a subpath we add a trailing slash + if(strlen($urlDir) > 1) + { + $urlDir .= '/'; + } + return $host.$urlDir; + } + + static public function getCurrentScriptName() + { + $url = ''; + if( !empty($_SERVER['PATH_INFO']) ) + { + $url = $_SERVER['PATH_INFO']; + } + else if( !empty($_SERVER['REQUEST_URI']) ) + { + if( ($pos = strpos($_SERVER['REQUEST_URI'], "?")) !== false ) + { + $url = substr($_SERVER['REQUEST_URI'], 0, $pos); + } + else + { + $url = $_SERVER['REQUEST_URI']; + } + } + + if(empty($url)) + { + $url = $_SERVER['SCRIPT_NAME']; + } + return $url; + } + + static public function getCurrentHost() + { + if(isset($_SERVER['HTTPS']) + && ($_SERVER['HTTPS'] == 'on' || $_SERVER['HTTPS'] === true) + ) + { + $url = 'https'; + } + else + { + $url = 'http'; + } + + $url .= '://'; + + if(isset($_SERVER['HTTP_HOST'])) + { + $url .= $_SERVER['HTTP_HOST']; + } + else + { + $url .= 'unknown'; + } + return $url; + } + + + static public function getCurrentQueryString() + { + $url = ''; + if(isset($_SERVER['QUERY_STRING']) + && !empty($_SERVER['QUERY_STRING'])) + { + $url .= "?".$_SERVER['QUERY_STRING']; + } + return $url; + } +} + diff --git a/core/View.php b/core/View.php new file mode 100644 index 0000000000..88debb9a64 --- /dev/null +++ b/core/View.php @@ -0,0 +1,143 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: View.php 450 2008-04-20 22:33:27Z matt $ + * + * @package Piwik_Visualization + */ + +require_once 'Smarty/Smarty.class.php'; + +require_once "iView.php"; + +/** + * + * @package Piwik_Visualization + * + */ +class Piwik_View implements Piwik_iView +{ + private $template = ''; + private $smarty = false; + private $variables = array(); + + public function __construct( $templateFile, $smConf = array()) + { + $this->template = $templateFile; + $this->smarty = new Smarty(); + + if(count($smConf) == 0) + { + $smConf = Zend_Registry::get('config')->smarty; + } + foreach($smConf as $key => $value) + { + $this->smarty->$key = $value; + } + + $this->smarty->template_dir = $smConf->template_dir->toArray(); + $this->smarty->plugins_dir = $smConf->plugins_dir->toArray(); + $this->smarty->compile_dir = $smConf->compile_dir; + $this->smarty->cache_dir = $smConf->cache_dir; + + $this->smarty->load_filter('output','trimwhitespace'); + + // global value accessible to all templates: the piwik base URL for the current request + $this->piwikUrl = Piwik_Url::getCurrentUrlWithoutFileName(); + + } + + /** + * Directly assigns a variable to the view script. + * VAR names may not be prefixed with '_'. + * @param string $key The variable name. + * @param mixed $val The variable value. + * @return void + */ + public function __set($key, $val) + { + $this->smarty->assign($key, $val); + } + + /** + * Retrieves an assigned variable. + * VAR names may not be prefixed with '_'. + * @param string $key The variable name. + * @return mixed The variable value. + */ + public function __get($key) + { + return $this->smarty->get_template_vars($key); + } + + public function render() + { + try { + $this->currentModule = Piwik::getModule(); + $this->currentPluginName = Piwik::getCurrentPlugin()->getName(); + $this->userLogin = Piwik::getCurrentUserLogin(); + $this->sites = Piwik_SitesManager_API::getSitesWithAtLeastViewAccess(); + $this->url = Piwik_Url::getCurrentUrl(); + $this->token_auth = Piwik::getCurrentUserTokenAuth(); + } catch(Exception $e) { + // can fail, for example at installation (no plugin loaded yet) + } + + $this->totalTimeGeneration = Zend_Registry::get('timer')->getTime(); + try { + $this->totalNumberOfQueries = Piwik::getQueryCount(); + } + catch(Exception $e){ + $this->totalNumberOfQueries = 0; + } + header('Content-Type: text/html; charset=utf-8'); + return $this->smarty->fetch($this->template); + } + + public function addForm( $form ) + { + // Create the renderer object + $renderer = new HTML_QuickForm_Renderer_ArraySmarty($this->smarty); + + // build the HTML for the form + $form->accept($renderer); + + // assign array with form data + $this->smarty->assign('form_data', $renderer->toArray()); + $this->smarty->assign('element_list', $form->getElementList()); + } + + public function assign($var, $value=null) + { + if (is_string($var)) + { + $this->smarty->assign($var, $value); + } + elseif (is_array($var)) + { + foreach ($var as $key => $value) + { + $this->smarty->assign($key, $value); + } + } + } + +/* public function isCached($template) + { + if ($this->smarty->is_cached($template)) + { + return true; + } + return false; + } + + + public function setCaching($caching) + { + $this->smarty->caching = $caching; + } +*/ +}
\ No newline at end of file diff --git a/core/ViewDataTable.php b/core/ViewDataTable.php new file mode 100644 index 0000000000..525b645dab --- /dev/null +++ b/core/ViewDataTable.php @@ -0,0 +1,799 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: ViewDataTable.php 581 2008-07-27 23:07:52Z matt $ + * + * @package Piwik_ViewDataTable + */ + +require_once "API/Request.php"; + +/** + * This class is used to load (from the API) and customize the output of a given DataTable. + * The main() method will create an object Piwik_iView + * You can customize the dataTable using the disable* methods. + * + * Example: + * In the Controller of the plugin VisitorInterest + * <pre> + * function getNumberOfVisitsPerVisitDuration( $fetch = false) + * { + * $view = Piwik_ViewDataTable::factory( 'cloud' ); + * $view->init( $this->pluginName, __FUNCTION__, 'VisitorInterest.getNumberOfVisitsPerVisitDuration' ); + * $view->setColumnsToDisplay( array('label','nb_visits') ); + * $view->disableSort(); + * $view->disableExcludeLowPopulation(); + * $view->disableOffsetInformation(); + * + * return $this->renderView($view, $fetch); + * } + * </pre> + * + * @see factory() for all the available output (cloud tags, html table, pie chart, vertical bar chart) + * @package Piwik_ViewDataTable + * + */ + +abstract class Piwik_ViewDataTable +{ + /** + * Template file that will be loaded for this view. + * Usually set in the Piwik_ViewDataTable_* + * + * @var string eg. 'CoreHome/templates/cloud.tpl' + */ + protected $dataTableTemplate = null; + + /** + * Flag used to make sure the main() is only executed once + * + * @var bool + */ + protected $mainAlreadyExecuted = false; + + /** + * Defines if we display the search box under the table + * + * @see disableSearchBox() + * @see getSearchBox() + * + * @var bool + */ + protected $JSsearchBox = true; + + /** + * Defines if we display the "X-Y of Z" under the table + * + * @see disableOffsetInformation() + * @see getOffsetInformation() + * + * @var bool + */ + protected $JSoffsetInformation = true; + + /** + * Defines if we display the "Include all population" link under the table + * + * @see disableExcludeLowPopulation() + * @see getExcludeLowPopulation() + * + * @var bool + */ + protected $JSexcludeLowPopulation = true; + + /** + * Defines if we include the footer after the dataTable output. + * The footer contains all the extra features like the search box, the links Next/Previous, the icons to export in several formats, etc. + * Not showing the footer is useful for example when you want to only display a graph without anything else. + * + * @see doNotShowFooter() + * @see getShowFooter() + * + * @var bool + */ + protected $showFooter = true; + + /** + * Contains the values set for the parameters + * @see getJavascriptVariablesToSet() + * + * @var array + */ + protected $variablesDefault = array(); + + /** + * If the current dataTable refers to a subDataTable (eg. keywordsBySearchEngineId for id=X) this variable is set to the Id + * + * @var bool|int + */ + protected $idSubtable = false; + + /** + * Set to true when the DataTable must be loaded along with all its children subtables + * Useful when searching for a pattern in the DataTable Actions (we display the full hierarchy) + * + * @var bool + */ + protected $recursiveDataTableLoad = false; + + /** + * DataTable loaded from the API for this ViewDataTable. + * + * @var Piwik_DataTable + */ + protected $dataTable = null; + + /** + * @see init() + * + * @var string + */ + protected $currentControllerAction; + + /** + * @see init() + * + * @var string + */ + protected $currentControllerName; + + /** + * @see init() + * + * @var string + */ + protected $actionToLoadTheSubTable = null; + + /** + * @see init() + * + * @var string + */ + protected $moduleNameAndMethod; + + /** + * This view should be an implementation of the Interface Piwik_iView + * The $view object should be created in the main() method. + * + * @var Piwik_iView + */ + protected $view = null; + + /** + * Method to be implemented by the ViewDataTable_*. + * This method should create and initialize a $this->view object @see Piwik_iView + * + * @return mixed either prints the result or returns the output string + */ + abstract public function main(); + + /** + * Returns a Piwik_ViewDataTable_* object. + * By default it will return a ViewDataTable_Html + * If there is a viewDataTable parameter in the URL, a ViewDataTable of this 'viewDataTable' type will be returned. + * If defaultType is specified and if there is no 'viewDataTable' in the URL, a ViewDataTable of this $defaultType will be returned. + * If force is set to true, a ViewDataTable of the $defaultType will be returned in all cases. + * + * @param string defaultType Any of these: table, cloud, graphPie, graphVerticalBar, graphEvolution, sparkline, generateDataChart* + * @force bool If set to true, returns a ViewDataTable of the $defaultType + * + * @return Piwik_ViewDataTable + */ + static public function factory( $defaultType = null, $force = false) + { + if(is_null($defaultType)) + { + $defaultType = 'table'; + } + + if($force === true) + { + $type = $defaultType; + } + else + { + $type = Piwik_Common::getRequestVar('viewDataTable', $defaultType, 'string'); + } + + switch($type) + { + case 'cloud': + require_once "ViewDataTable/Cloud.php"; + return new Piwik_ViewDataTable_Cloud(); + break; + + case 'graphPie': + require_once "ViewDataTable/GenerateGraphHTML/ChartPie.php"; + return new Piwik_ViewDataTable_GenerateGraphHTML_ChartPie(); + break; + + case 'graphVerticalBar': + require_once "ViewDataTable/GenerateGraphHTML/ChartVerticalBar.php"; + return new Piwik_ViewDataTable_GenerateGraphHTML_ChartVerticalBar(); + break; + + case 'graphEvolution': + require_once "ViewDataTable/GenerateGraphHTML/ChartEvolution.php"; + return new Piwik_ViewDataTable_GenerateGraphHTML_ChartEvolution(); + break; + + case 'sparkline': + require_once "ViewDataTable/Sparkline.php"; + return new Piwik_ViewDataTable_Sparkline(); + break; + + case 'generateDataChartVerticalBar': + require_once "ViewDataTable/GenerateGraphData/ChartVerticalBar.php"; + return new Piwik_ViewDataTable_GenerateGraphData_ChartVerticalBar(); + break; + + case 'generateDataChartPie': + require_once "ViewDataTable/GenerateGraphData/ChartPie.php"; + return new Piwik_ViewDataTable_GenerateGraphData_ChartPie(); + break; + + case 'generateDataChartEvolution': + require_once "ViewDataTable/GenerateGraphData/ChartEvolution.php"; + return new Piwik_ViewDataTable_GenerateGraphData_ChartEvolution(); + + break; + + case 'table': + default: + require_once "ViewDataTable/Html.php"; + return new Piwik_ViewDataTable_Html(); + break; + } + } + + /** + * Inits the object given the $currentControllerName, $currentControllerAction of + * the calling controller action, eg. 'Referers' 'getLongListOfKeywords'. + * The initialization also requires the $moduleNameAndMethod of the API method + * to call in order to get the DataTable, eg. 'Referers.getKeywords'. + * The optional $actionToLoadTheSubTable defines the method name of the API to call when there is a idSubtable. + * This value would be used by the javascript code building the GET request to the API. + * + * Example: + * For the keywords listing, a click on the row loads the subTable of the Search Engines for this row. + * In this case $actionToLoadTheSubTable = 'getSearchEnginesFromKeywordId'. + * The GET request will hit 'Referers.getSearchEnginesFromKeywordId'. + * + * @param string $currentControllerName eg. 'Referers' + * @param string $currentControllerAction eg. 'getKeywords' + * @param string $moduleNameAndMethod eg. 'Referers.getKeywords' + * @param string $actionToLoadTheSubTable eg. 'getSearchEnginesFromKeywordId' + * + * @return void + */ + public function init( $currentControllerName, + $currentControllerAction, + $moduleNameAndMethod, + $actionToLoadTheSubTable = null) + { + $this->currentControllerName = $currentControllerName; + $this->currentControllerAction = $currentControllerAction; + $this->moduleNameAndMethod = $moduleNameAndMethod; + $this->actionToLoadTheSubTable = $actionToLoadTheSubTable; + + $this->idSubtable = Piwik_Common::getRequestVar('idSubtable', false, 'int'); + + $this->method = $moduleNameAndMethod; + + $this->JSsearchBox = Piwik_Common::getRequestVar('show_search', true); + $this->showFooter = Piwik_Common::getRequestVar('showDataTableFooter', true); + $this->variablesDefault['filter_excludelowpop_default'] = 'false'; + $this->variablesDefault['filter_excludelowpop_value_default'] = 'false'; + } + + /** + * Forces the View to use a given template. + * Usually the template to use is set in the specific ViewDataTable_* + * eg. 'CoreHome/templates/cloud.tpl' + * + * But some users may want to force this template to some other value + * + * @param string $tpl eg .'MyPlugin/templates/templateToUse.tpl' + */ + public function setTemplate( $tpl ) + { + $this->dataTableTemplate = $tpl; + } + + /** + * Returns the iView. + * You can then call render() on this object. + * + * @return Piwik_iView + * @throws exception if the view object was not created + */ + public function getView() + { + if(is_null($this->view)) + { + throw new Exception('The $this->view object has not been created. + It should be created in the main() method of the Piwik_ViewDataTable_* subclass you are using.'); + } + return $this->view; + } + + /** + * Returns the DataTable loaded from the API + * + * @return Piwik_DataTable + * @throws exception if not yet defined + */ + public function getDataTable() + { + if(is_null($this->dataTable)) + { + throw new Exception("The DataTable requested has not been loaded yet."); + } + return $this->dataTable; + } + /** + * Function called by the ViewDataTable objects in order to fetch data from the API. + * The function init() must have been called before, so that the object knows which API module and action to call. + * It builds the API request string and uses Piwik_API_Request to call the API. + * The requested Piwik_DataTable object is stored in $this->dataTable. + * + * @return void + */ + protected function loadDataTableFromAPI() + { + // we prepare the string to give to the API Request + // we setup the method and format variable + // - we request the method to call to get this specific DataTable + // - the format = original specifies that we want to get the original DataTable structure itself, not rendered + $requestString = 'method='.$this->moduleNameAndMethod + .'&format=original' + ; + if( $this->recursiveDataTableLoad ) + { + $requestString .= '&expanded=1'; + } + + $toSetEventually = array( + 'filter_limit', + 'filter_sort_column', + 'filter_sort_order', + 'filter_excludelowpop', + 'filter_excludelowpop_value', + 'filter_column', + 'filter_pattern', + 'disable_generic_filters', + 'disable_queued_filters', + ); + foreach($toSetEventually as $varToSet) + { + $value = $this->getDefaultOrCurrent($varToSet); + if( false !== $value ) + { + $requestString .= '&'.$varToSet.'='.$value; + } + } + + // We finally make the request to the API + $request = new Piwik_API_Request($requestString); + + // and get the DataTable structure + $dataTable = $request->process(); + + $this->dataTable = $dataTable; + } + + + /** + * For convenience, the client code can call methods that are defined in a specific children class + * without testing the children class type, which would trigger an error with a different children class. + * + * Example: + * ViewDataTable/Html.php defines a setColumnsToDisplay(). The client code calls this methods even if + * the ViewDataTable object is a ViewDataTable_Cloud instance (he doesn't know because of the factory()). + * But ViewDataTable_Cloud doesn't define the setColumnsToDisplay() method. + * Because we don't want to force users to test for the object type we simply catch these + * calls when they are not defined in the child and do nothing. + * + * @param string $function + * @param array $args + */ + public function __call($function, $args) + { + } + + /** + * Returns a unique ID for this ViewDataTable. + * This unique ID is used in the Javascript code: + * Any ajax loaded data is loaded within a DIV that has id=$unique_id + * The jquery code then replaces the existing html div id=$unique_id in the code with this data. + * + * @see datatable.js + * @return string + */ + protected function getUniqIdTable() + { + // if we request a subDataTable the $this->currentControllerAction DIV ID is already there in the page + // we make the DIV ID really unique by appending the ID of the subtable requested + if( $this->idSubtable != 0 // parent DIV has a idSubtable = 0 but the html DIV must have the name of the module.action + && $this->idSubtable !== false // case there is no idSubtable + ) + { + // see also datatable.js (the ID has to match with the html ID created to be replaced by the result of the ajax call) + $uniqIdTable = 'subDataTable_' . $this->idSubtable; + } + else + { + // the $uniqIdTable variable is used as the DIV ID in the rendered HTML + // we use the current Controller action name as it is supposed to be unique in the rendered page + $uniqIdTable = $this->currentControllerName . $this->currentControllerAction; + } + return $uniqIdTable; + } + + /** + * This functions reads the customization values for the DataTable and returns an array (name,value) to be printed in Javascript. + * This array defines things such as: + * - name of the module & action to call to request data for this table + * - display the search box under the table + * - display the links Next & Previous under the table + * - optional filters information, eg. filter_limit and filter_offset + * - etc. + * + * The values are loaded: + * - from the generic filters that are applied by default @see Piwik_API_Request::getGenericFiltersInformation() + * - from the values already available in the GET array + * - from the values set using methods from this class (eg. setSearchPattern(), setLimit(), etc.) + * + * @return array eg. array('show_offset_information' => 0, 'show_ + */ + protected function getJavascriptVariablesToSet() + { + // build javascript variables to set + $javascriptVariablesToSet = array(); + + $genericFilters = Piwik_API_Request::getGenericFiltersInformation(); + foreach($genericFilters as $filter) + { + foreach($filter as $filterVariableName => $filterInfo) + { + // if there is a default value for this filter variable we set it + // so that it is propagated to the javascript + if(isset($filterInfo[1])) + { + $javascriptVariablesToSet[$filterVariableName] = $filterInfo[1]; + + // we set the default specified column and Order to sort by + // when this javascript variable is not set already + // for example during an AJAX call this variable will be set in the URL + // so this will not be executed (and the default sorted not be used as the sorted column might have changed in the meanwhile) + if( false !== ($defaultValue = $this->getDefault($filterVariableName))) + { + $javascriptVariablesToSet[$filterVariableName] = $defaultValue; + } + } + } + } + + foreach($_GET as $name => $value) + { + try{ + $requestValue = Piwik_Common::getRequestVar($name); + } + catch(Exception $e) { + $requestValue = ''; + } + $javascriptVariablesToSet[$name] = $requestValue; + } + + // at this point there are some filters values we may have not set, + // case of the filter without default values and parameters set directly in this class + // for example setExcludeLowPopulation + // we go through all the $this->variablesDefault array and set the variables not set yet + foreach($this->variablesDefault as $name => $value) + { + if(!isset($javascriptVariablesToSet[$name] )) + { + $javascriptVariablesToSet[$name] = $value; + } + } + + $javascriptVariablesToSet['module'] = $this->currentControllerName; + $javascriptVariablesToSet['action'] = $this->currentControllerAction; + $javascriptVariablesToSet['pathToPiwik'] = Piwik_Url::getCurrentUrlWithoutFileName(); + + if(!is_null($this->actionToLoadTheSubTable)) + { + $javascriptVariablesToSet['actionToLoadTheSubTable'] = $this->actionToLoadTheSubTable; + } + +// var_dump($this->variablesDefault); +// var_dump($javascriptVariablesToSet); exit; + + if($this->dataTable) + { + $javascriptVariablesToSet['totalRows'] = $this->dataTable->getRowsCountBeforeLimitFilter(); + } + $javascriptVariablesToSet['show_search'] = $this->getSearchBox(); + $javascriptVariablesToSet['show_offset_information'] = $this->getOffsetInformation(); + $javascriptVariablesToSet['show_exclude_low_population'] = $this->getExcludeLowPopulation(); + + // we escape the values that will be displayed in the javascript footer of each datatable + // to make sure there is malicious code injected (the value are already htmlspecialchar'ed as they + // are loaded with Piwik_Common::getRequestVar() + foreach($javascriptVariablesToSet as &$value) + { + $value = addslashes($value); + } + + return $javascriptVariablesToSet; + } + + /** + * Returns, for a given parameter, the value of this parameter in the REQUEST array. + * If not set, returns the default value for this parameter @see getDefault() + * + * @param string $nameVar + * @return string|mixed Value of this parameter + */ + protected function getDefaultOrCurrent( $nameVar ) + { + if(isset($_REQUEST[$nameVar])) + { + return $_REQUEST[$nameVar]; + } + $default = $this->getDefault($nameVar); + return $default; + } + + /** + * Returns the default value for a given parameter. + * For example, these default values can be set using the disable* methods. + * + * @param string $nameVar + * @return mixed + */ + protected function getDefault($nameVar) + { + if(!isset($this->variablesDefault[$nameVar])) + { + return false; + } + return $this->variablesDefault[$nameVar]; + } + + /** + * The generic filters (limit, offset, sort by visit desc) will not be applied to this datatable. + * + * @return void + * + */ + public function disableGenericFilters() + { + $this->variablesDefault['disable_generic_filters'] = true; + } + /** + * The "X-Y of Z" won't be displayed under this table + * + * @return void + * + */ + public function disableOffsetInformation() + { + $this->JSoffsetInformation = 'false'; + } + + /** + * @see disableOffsetInformation() + * + * @return bool|string If this parameter is enabled or not + * + */ + protected function getOffsetInformation() + { + return $this->JSoffsetInformation; + } + + /** + * The search box won't be displayed under this table + * + * @return void + */ + public function disableSearchBox() + { + $this->JSsearchBox = 'false'; + } + + /** + * @see disableSearchBox() + * + * @return bool|string If this parameter is enabled or not + * + */ + protected function getSearchBox() + { + return $this->JSsearchBox; + } + + /** + * When this method is called, the output will not contain the template datatable_footer.tpl + * + * @return void + */ + public function doNotShowFooter() + { + $this->showFooter = false; + } + + /** + * Returns true if the footer should be included in the template + * + * @return bool + * + */ + protected function getShowFooter() + { + return $this->showFooter; + } + + /** + * The "Include low population" link won't be displayed under this table + * + * @return void + */ + public function disableExcludeLowPopulation() + { + $this->JSexcludeLowPopulation = 'false'; + } + + /** + * @see disableExcludeLowPopulation() + * + * @return bool|string If this parameter is enabled or not + * + */ + protected function getExcludeLowPopulation() + { + return $this->JSexcludeLowPopulation; + } + + + /** + * Sets the value to use for the Exclude low population filter. + * + * @param int|float If a row value is less than this value, it will be removed from the dataTable + * @param string The name of the column for which we compare the value to $minValue + * + * @return void + */ + public function setExcludeLowPopulation( $minValue = null, $columnName = null ) + { + if( is_null( $minValue) ) + { + throw new Exception("setExcludeLowPopulation() value shouldn't be null"); + } + + if(is_null($columnName)) + { + $columnName = Piwik_Archive::INDEX_NB_VISITS; + } + + // column to use to enable low population exclusion if != false + $this->variablesDefault['filter_excludelowpop_default'] + = $this->variablesDefault['filter_excludelowpop'] + = $columnName; + + // the minimum value a row must have to be returned + $this->variablesDefault['filter_excludelowpop_value_default'] + = $this->variablesDefault['filter_excludelowpop_value'] + = $minValue; + } + + /** + * Sets the pattern to look for in the table (only rows matching the pattern will be kept) + * + * @param string $pattern to look for + * @param string $column to compare the pattern to + * + * @return void + */ + public function setSearchPattern($pattern, $column) + { + $this->variablesDefault['filter_pattern'] = $pattern; + $this->variablesDefault['filter_column'] = $column; + } + + /** + * Sets the maximum number of rows of the table + * + * @param int $limit + * + * @return void + */ + public function setLimit( $limit ) + { + if($limit != 0) + { + $this->variablesDefault['filter_limit'] = $limit; + } + } + + /** + * Sets the dataTable column to sort by. This sorting will be applied before applying the (offset, limit) filter. + * + * @param int|string $columnId eg. 'nb_visits' for some tables, or Piwik_Archive::INDEX_NB_VISITS for others + * @param string $order desc or asc + * + * @return void + */ + public function setSortedColumn( $columnId, $order = 'desc') + { + $this->variablesDefault['filter_sort_column']= $columnId; + $this->variablesDefault['filter_sort_order']= $order; + } + + + /** + * Given a Piwik_DataTable_Array made of DataTable_Simple rows, returns a php array with the structure: + * array( + * array( label => X, value => Y), + * array( label => A, value => B), + * ... + * ) + * + * This is used for example for the evolution graph (last 30 days visits) or the sparklines. + * + * @param Piwik_DataTable_Array $dataTableArray + * @return array + */ + protected function generateDataFromDataTableArray( Piwik_DataTable_Array $dataTableArray) + { + $data = array(); + foreach($dataTableArray->getArray() as $keyName => $table) + { + if($table instanceof Piwik_DataTable_Array) + { + throw new Exception("Operation not supported (yet)"); + } + $value = false; + + $onlyRow = $table->getFirstRow(); + if($onlyRow !== false) + { + $value = $onlyRow->getColumn('value'); + if($value == false) + { + // TEMP + // quite a hack, useful in the case at this point we do have a normal row with nb_visits, nb_actions, nb_uniq_visitors, etc. + // instead of the dataTable_Simple row (label, value) + // to do it properly we'd need to + // - create a filter that removes columns + // - apply this filter to keep only the column called nb_uniq_visitors + // - rename this column as 'value' + // and at this point the getcolumn('value') would have worked + // this code is executed eg. when displaying a sparkline for the last 30 days displaying the number of unique visitors coming from search engines + + //TODO solution: use a filter rename column etc. + + // another solution would be to add a method to the Referers API giving directly the integer 'visits from search engines' + // and we would build automatically the dataTable_array of datatatble_simple from these integers + // but we'd have to add this integer to be recorded during archiving etc. + $value = $onlyRow->getColumn('nb_uniq_visitors'); + } + } + + if($value === false) + { + $value = 0; + } + $data[] = array( + 'label' => $keyName, + 'value' => $value + ); + } + return $data; + } + +}
\ No newline at end of file diff --git a/core/ViewDataTable/Cloud.php b/core/ViewDataTable/Cloud.php new file mode 100644 index 0000000000..c6e63bb3f4 --- /dev/null +++ b/core/ViewDataTable/Cloud.php @@ -0,0 +1,108 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Cloud.php 581 2008-07-27 23:07:52Z matt $ + * + * @package Piwik_ViewDataTable + */ + +require_once "Visualization/Cloud.php"; + +/** + * Reads the requested DataTable from the API, and prepares the data to give + * to Piwik_Visualization_Cloud that will display the tag cloud (via the template cloud.tpl). + * + * @package Piwik_ViewDataTable + * + */ +class Piwik_ViewDataTable_Cloud extends Piwik_ViewDataTable +{ + //TODO test this + protected $displayLogoInsteadOfLabel = false; + + /** + * @see Piwik_ViewDataTable::init() + */ + function init($currentControllerName, + $currentControllerAction, + $moduleNameAndMethod ) + { + parent::init($currentControllerName, + $currentControllerAction, + $moduleNameAndMethod ); + $this->dataTableTemplate = 'CoreHome/templates/cloud.tpl'; + + $this->disableOffsetInformation(); + $this->disableExcludeLowPopulation(); + } + + /** + * @see Piwik_ViewDataTable::main() + * + */ + public function main() + { + $this->setLimit( 30 ); + if($this->mainAlreadyExecuted) + { + return; + } + $this->mainAlreadyExecuted = true; + + $this->loadDataTableFromAPI(); + + // We apply a filter to the DataTable, decoding the label column (useful for keywords for example) + $filter = new Piwik_DataTable_Filter_ColumnCallbackReplace( + $this->dataTable, + 'label', + 'urldecode' + ); + + + $view = new Piwik_View($this->dataTableTemplate); + + $words = $labelMetadata = array(); + foreach($this->dataTable->getRows() as $row) + { + $label = $row->getColumn('label'); + $value = $row->getColumn('nb_uniq_visitors'); + + // case no unique visitors + if($value === false) + { + $value = $row->getColumn('nb_visits'); + } + $words[$label] = $value; + + $logo = false; + if($this->displayLogoInsteadOfLabel) + { + $logo = $row->getMetadata('logo'); + } + + $labelMetadata[$label] = array( + 'logo' => $logo, + 'url' => $row->getMetadata('url'), + 'hits' => $value + ); + } + $cloud = new Piwik_Visualization_Cloud($words); + $cloudValues = $cloud->render('array'); + + foreach($cloudValues as &$value) + { + $value['logoWidth'] = round(max(16, $value['percent'])); + } + $view->labelMetadata = $labelMetadata; + $view->cloudValues = $cloudValues; + + $view->method = $this->method; + $view->id = $this->getUniqIdTable(); + $view->javascriptVariablesToSet = $this->getJavascriptVariablesToSet(); + $view->showFooter = $this->getShowFooter(); + $this->view = $view; + } +} diff --git a/core/ViewDataTable/GenerateGraphData.php b/core/ViewDataTable/GenerateGraphData.php new file mode 100644 index 0000000000..6174dbba9c --- /dev/null +++ b/core/ViewDataTable/GenerateGraphData.php @@ -0,0 +1,140 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: GenerateGraphData.php 579 2008-07-27 00:32:59Z matt $ + * + * @package Piwik_ViewDataTable + */ + +/** + * Reads data from the API and prepares data to give to the renderer Piwik_Visualization_Chart. + * This class is used to generate the data for the FLASH charts. It is given as a parameter of the SWF file. + * You can set the number of elements to appear in the graph using: setGraphLimit(); + * Example: + * <pre> + * function getWebsites( $fetch = false) + * { + * $view = Piwik_ViewDataTable::factory(); + * $view->init( $this->pluginName, 'getWebsites', 'Referers.getWebsites', 'getUrlsFromWebsiteId' ); + * $view->setColumnsToDisplay( array('label','nb_visits') ); + * $view->setLimit(10); + * $view->setGraphLimit(12); + * return $this->renderView($view, $fetch); + * } + * </pre> + * + * @package Piwik_ViewDataTable + * + */ +abstract class Piwik_ViewDataTable_GenerateGraphData extends Piwik_ViewDataTable +{ + /** + * @see Piwik_ViewDataTable::init() + */ + function init($currentControllerName, + $currentControllerAction, + $moduleNameAndMethod ) + { + parent::init($currentControllerName, + $currentControllerAction, + $moduleNameAndMethod ); + } + + /** + * Number of elements to display in the graph. + * + * @var int + */ + protected $graphLimit = 5; + + /** + * Sets the number max of elements to display (number of pie slice, vertical bars, etc.) + * If the data has more elements than $limit then the last part of the data will be the sum of all the remaining data. + * + * @param int $limit + */ + function setGraphLimit( $limit ) + { + $this->graphLimit = $limit; + } + /** + * Returns numbers of elemnts to display in the graph + * + * @return int + */ + function getGraphLimit() + { + return $this->graphLimit; + } + + public function main() + { + if($this->mainAlreadyExecuted) + { + return; + } + $this->mainAlreadyExecuted = true; + + $this->setLimit(-1); + + // we load the data with the filters applied + $this->loadDataTableFromAPI(); + $offsetStartSummary = $this->getGraphLimit() - 1; + $this->dataTable->queueFilter('Piwik_DataTable_Filter_AddSummaryRow', array($offsetStartSummary, Piwik_Translate('General_Others'))); + $this->dataAvailable = $this->dataTable->getRowsCount() != 0; + + if(!$this->dataAvailable) + { + $this->view->customizeGraph(); + $this->view->title(Piwik_Translate('General_NoDataForGraph'), '{font-size: 25px;}'); + } + else + { + $data = $this->generateDataFromDataTable(); + $this->view->setData($data); + $this->view->customizeGraph(); + } + } + + /** + * Returns a format friendly array from the dataTable + * + * @return array + */ + protected function generateDataFromDataTable() + { + $this->dataTable->applyQueuedFilters(); + + // We apply a filter to the DataTable, decoding the label column (useful for keywords for example) + $filter = new Piwik_DataTable_Filter_ColumnCallbackReplace( + $this->dataTable, + 'label', + 'urldecode' + ); + $data = array(); + foreach($this->dataTable->getRows() as $row) + { + $label = $row->getColumn('label'); + $value = $row->getColumn('nb_uniq_visitors'); + + // case no unique visitors + if($value === false) + { + $value = $row->getColumn('nb_visits'); + } + + $data[] = array( + 'label' => $label, + 'value' => $value, + 'url' => $row->getMetadata('url'), + ); + } + return $data; + } +} + + + diff --git a/core/ViewDataTable/GenerateGraphData/ChartEvolution.php b/core/ViewDataTable/GenerateGraphData/ChartEvolution.php new file mode 100644 index 0000000000..70bf412f57 --- /dev/null +++ b/core/ViewDataTable/GenerateGraphData/ChartEvolution.php @@ -0,0 +1,21 @@ +<?php +require_once "ViewDataTable/GenerateGraphData.php"; +/** + * Piwik_ViewDataTable_GenerateGraphData for the Evolution graph (eg. Last 30 days visits) using Piwik_Visualization_ChartEvolution + * + * @package Piwik_ViewDataTable + * + */ +class Piwik_ViewDataTable_GenerateGraphData_ChartEvolution extends Piwik_ViewDataTable_GenerateGraphData +{ + function __construct() + { + require_once "Visualization/ChartEvolution.php"; + $this->view = new Piwik_Visualization_ChartEvolution; + } + + protected function generateDataFromDataTable() + { + return $this->generateDataFromDataTableArray($this->dataTable); + } +} diff --git a/core/ViewDataTable/GenerateGraphData/ChartPie.php b/core/ViewDataTable/GenerateGraphData/ChartPie.php new file mode 100644 index 0000000000..53739067c8 --- /dev/null +++ b/core/ViewDataTable/GenerateGraphData/ChartPie.php @@ -0,0 +1,16 @@ +<?php +require_once "ViewDataTable/GenerateGraphData.php"; +/** + * Piwik_ViewDataTable_GenerateGraphData for the pie chart, using Piwik_Visualization_ChartPie + * + * @package Piwik_ViewDataTable + * + */ +class Piwik_ViewDataTable_GenerateGraphData_ChartPie extends Piwik_ViewDataTable_GenerateGraphData +{ + function __construct() + { + require_once "Visualization/ChartPie.php"; + $this->view = new Piwik_Visualization_ChartPie; + } +}
\ No newline at end of file diff --git a/core/ViewDataTable/GenerateGraphData/ChartVerticalBar.php b/core/ViewDataTable/GenerateGraphData/ChartVerticalBar.php new file mode 100644 index 0000000000..1bb9ea27b2 --- /dev/null +++ b/core/ViewDataTable/GenerateGraphData/ChartVerticalBar.php @@ -0,0 +1,16 @@ +<?php +require_once "ViewDataTable/GenerateGraphData.php"; +/** + * Piwik_ViewDataTable_GenerateGraphData for the vertical bar graph, using Piwik_Visualization_ChartVerticalBar + * + * @package Piwik_ViewDataTable + * + */ +class Piwik_ViewDataTable_GenerateGraphData_ChartVerticalBar extends Piwik_ViewDataTable_GenerateGraphData +{ + function __construct() + { + require_once "Visualization/ChartVerticalBar.php"; + $this->view = new Piwik_Visualization_ChartVerticalBar; + } +}
\ No newline at end of file diff --git a/core/ViewDataTable/GenerateGraphHTML.php b/core/ViewDataTable/GenerateGraphHTML.php new file mode 100644 index 0000000000..798bc6dd58 --- /dev/null +++ b/core/ViewDataTable/GenerateGraphHTML.php @@ -0,0 +1,141 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: GenerateGraphHTML.php 581 2008-07-27 23:07:52Z matt $ + * + * @package Piwik_ViewDataTable + */ + +/** + * This class generates the HTML code to embed to flash graphs in the page. + * It doesn't call the API but simply prints the html snippet. + * + * @package Piwik_ViewDataTable + * + */ +abstract class Piwik_ViewDataTable_GenerateGraphHTML extends Piwik_ViewDataTable +{ + protected $width = '100%'; + protected $height = 250; + protected $graphType = 'standard'; + + /** + * @see Piwik_ViewDataTable::init() + * + */ + function init($currentControllerName, + $currentControllerAction, + $moduleNameAndMethod ) + { + parent::init($currentControllerName, + $currentControllerAction, + $moduleNameAndMethod ); + $this->dataTableTemplate = 'CoreHome/templates/graph.tpl'; + + $this->disableOffsetInformation(); + $this->disableExcludeLowPopulation(); + $this->disableSearchBox(); + $this->parametersToModify = array( + 'viewDataTable' => $this->valueParameterViewDataTable, + // in the case this controller is being executed by another controller + // eg. when being widgetized in an IFRAME + // we need to put in the URL of the graph data the real module and action + 'module' => $currentControllerName, + 'action' => $currentControllerAction, + ); + } + + /** + * Sets parameters to modify in the future generated URL + * + * @param array $array array('nameParameter' => $newValue, ...) + */ + public function setParametersToModify($array) + { + $this->parametersToModify = array_merge($this->parametersToModify, $array); + } + + /** + * @see Piwik_ViewDataTable::main() + * + */ + public function main() + { + if($this->mainAlreadyExecuted) + { + return; + } + $this->mainAlreadyExecuted = true; + + $view = new Piwik_View($this->dataTableTemplate); + $this->id = $this->getUniqIdTable(); + $view->graphType = $this->graphType; + + $this->parametersToModify['action'] = $this->currentControllerAction; + $url = Piwik_Url::getCurrentQueryStringWithParametersModified($this->parametersToModify); + $view->jsInvocationTag = $this->getFlashInvocationCode($url); + $view->urlGraphData = $url; + + $view->formEmbedId = "formEmbed".$this->id; + $view->graphCodeEmbed = $this->graphCodeEmbed; + + $view->id = $this->id; + $view->method = $this->method; + $view->javascriptVariablesToSet = $this->getJavascriptVariablesToSet(); + $view->showFooter = $this->getShowFooter(); + $this->view = $view; + } + + protected function getFlashInvocationCode( $url = 'libs/open-flash-chart/data-files/nodata.txt', $use_swfobject = true ) + { + $width = $this->width; + $height = $this->height; + + $libPathInPiwik = 'libs/open-flash-chart/'; + $currentPath = Piwik_Url::getCurrentUrlWithoutFileName(); + $pathToLibraryOpenChart = $currentPath . $libPathInPiwik; + + $url = Piwik_Url::getCurrentUrlWithoutQueryString() . $url; + // escape the & and stuff: + $url = urlencode($url); + + $obj_id = $this->id . "Chart"; + $div_name = $this->id . "FlashContent"; + + $return = ''; + if( $use_swfobject ) + { + // Using library for auto-enabling Flash object on IE, disabled-Javascript proof + $return .= ' + <div id="'. $div_name .'"></div> + <script type="text/javascript"> + var so = new SWFObject("'.$pathToLibraryOpenChart.'open-flash-chart.swf", "'.$obj_id.'_swf", "'. $width . '", "' . $height . '", "9", "#FFFFFF"); + so.addVariable("data", "'. $url . '"); + so.addParam("allowScriptAccess", "sameDomain"); + so.addParam("wmode", "transparent"); + so.write("'. $div_name .'"); + </script> + <noscript> + '; + } + $urlGraph = $pathToLibraryOpenChart."open-flash-chart.swf?data=" . $url; + + $this->graphCodeEmbed .= "<div><object classid='clsid:d27cdb6e-ae6d-11cf-96b8-444553540000' codebase='http://fpdownload.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=8,0,0,0' width='" . $width . "' height='" . $height . "' id='". $obj_id ."' >". + "<param name='movie' value='".$urlGraph."' />". + "<param name='wmode' value='transparent' />". + "<param name='allowScriptAccess' value='sameDomain' /> ". + "<embed src='$urlGraph' allowScriptAccess='sameDomain' quality='high' bgcolor='#FFFFFF' width='". $width ."' height='". $height ."' name='open-flash-chart' type='application/x-shockwave-flash' id='". $obj_id ."' />". + "</object></div>"; + $return .= $this->graphCodeEmbed; + + if ( $use_swfobject ) { + $return .= '</noscript>'; + } + + return $return; + } +} + diff --git a/core/ViewDataTable/GenerateGraphHTML/ChartEvolution.php b/core/ViewDataTable/GenerateGraphHTML/ChartEvolution.php new file mode 100644 index 0000000000..d1db503052 --- /dev/null +++ b/core/ViewDataTable/GenerateGraphHTML/ChartEvolution.php @@ -0,0 +1,31 @@ +<?php +require_once "ViewDataTable/GenerateGraphHTML.php"; +/** + * Generates HTML embed for the Evolution graph + * + * @package Piwik_ViewDataTable + * + */ +class Piwik_ViewDataTable_GenerateGraphHTML_ChartEvolution extends Piwik_ViewDataTable_GenerateGraphHTML +{ + function __construct() + { + $this->valueParameterViewDataTable = 'generateDataChartEvolution'; + $this->width='100%'; + $this->height=150; + // used for the CSS class to apply to the DIV containing the graph + $this->graphType = 'evolution'; + } + + function init($currentControllerName, + $currentControllerAction, + $moduleNameAndMethod ) + { + parent::init($currentControllerName, + $currentControllerAction, + $moduleNameAndMethod ); + + $this->setParametersToModify(array('date' => 'last30')); + $this->doNotShowFooter(); + } +}
\ No newline at end of file diff --git a/core/ViewDataTable/GenerateGraphHTML/ChartPie.php b/core/ViewDataTable/GenerateGraphHTML/ChartPie.php new file mode 100644 index 0000000000..35d037c65b --- /dev/null +++ b/core/ViewDataTable/GenerateGraphHTML/ChartPie.php @@ -0,0 +1,15 @@ +<?php +require_once "ViewDataTable/GenerateGraphHTML.php"; +/** + * Generates HTML embed for the Pie chart + * + * @package Piwik_ViewDataTable + * + */ +class Piwik_ViewDataTable_GenerateGraphHTML_ChartPie extends Piwik_ViewDataTable_GenerateGraphHTML +{ + function __construct() + { + $this->valueParameterViewDataTable = 'generateDataChartPie'; + } +}
\ No newline at end of file diff --git a/core/ViewDataTable/GenerateGraphHTML/ChartVerticalBar.php b/core/ViewDataTable/GenerateGraphHTML/ChartVerticalBar.php new file mode 100644 index 0000000000..c1e03fea78 --- /dev/null +++ b/core/ViewDataTable/GenerateGraphHTML/ChartVerticalBar.php @@ -0,0 +1,16 @@ +<?php +require_once "ViewDataTable/GenerateGraphHTML.php"; +/** + * + * Generates HTML embed for the vertical bar chart + * + * @package Piwik_ViewDataTable + * + */ +class Piwik_ViewDataTable_GenerateGraphHTML_ChartVerticalBar extends Piwik_ViewDataTable_GenerateGraphHTML +{ + function __construct() + { + $this->valueParameterViewDataTable = 'generateDataChartVerticalBar'; + } +}
\ No newline at end of file diff --git a/core/ViewDataTable/Html.php b/core/ViewDataTable/Html.php new file mode 100644 index 0000000000..03d23199e3 --- /dev/null +++ b/core/ViewDataTable/Html.php @@ -0,0 +1,274 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Html.php 581 2008-07-27 23:07:52Z matt $ + * + * @package Piwik_ViewDataTable + */ + +/** + * + * Outputs an AJAX Table for a given DataTable. + * + * Reads the requested DataTable from the API. + * + * @package Piwik_ViewDataTable + * + */ +class Piwik_ViewDataTable_Html extends Piwik_ViewDataTable +{ + /** + * Array of columns names to display + * + * @var array + */ + protected $columnsToDisplay = array(); + + /** + * Array of columns names translations + * + * @var array + */ + protected $columnsTranslations = array(); + + /** + * PHP array conversion of the Piwik_DataTable + * + * @var array + */ + public $arrayDataTable; // phpArray + + /** + * @see Piwik_ViewDataTable::init() + */ + function init($currentControllerName, + $currentControllerAction, + $moduleNameAndMethod, + $actionToLoadTheSubTable = null ) + { + parent::init($currentControllerName, + $currentControllerAction, + $moduleNameAndMethod, + $actionToLoadTheSubTable); + $this->dataTableTemplate = 'CoreHome/templates/datatable.tpl'; + + $this->variablesDefault['enable_sort'] = true; + + // load general columns translations + $this->setColumnTranslation('nb_visits', Piwik_Translate('General_ColumnNbVisits')); + $this->setColumnTranslation('label', Piwik_Translate('General_ColumnLabel')); + $this->setColumnTranslation('nb_uniq_visitors', Piwik_Translate('General_ColumnNbUniqVisitors')); + } + + /** + * @see Piwik_ViewDataTable::main() + * + */ + public function main() + { + if($this->mainAlreadyExecuted) + { + return; + } + $this->mainAlreadyExecuted = true; + +// $i=0;while($i<1500000){ $j=$i*$i;$i++;} + + $this->loadDataTableFromAPI(); + + // We apply a filter to the DataTable, decoding the label column (useful for keywords for example) + $filter = new Piwik_DataTable_Filter_ColumnCallbackReplace( + $this->dataTable, + 'label', + 'urldecode' + ); + + + $view = new Piwik_View($this->dataTableTemplate); + + + // We get the PHP array converted from the DataTable + $phpArray = $this->getPHPArrayFromDataTable(); + + $view->arrayDataTable = $phpArray; + $view->method = $this->method; + + $columns = $this->getColumnsToDisplay($phpArray); + $view->dataTableColumns = $columns; + + $nbColumns = count($columns); + // case no data in the array we use the number of columns set to be displayed + if($nbColumns == 0) + { + $nbColumns = count($this->columnsToDisplay); + } + + $view->nbColumns = $nbColumns; + + $view->id = $this->getUniqIdTable(); + $view->javascriptVariablesToSet = $this->getJavascriptVariablesToSet(); + $view->showFooter = $this->getShowFooter(); + $this->view = $view; + } + + /** + * Returns friendly php array from the Piwik_DataTable + * @see Piwik_DataTable_Renderer_Php + * @return array + */ + protected function getPHPArrayFromDataTable() + { + $renderer = Piwik_DataTable_Renderer::factory('php'); + $renderer->setTable($this->dataTable); + $renderer->setSerialize( false ); + // we get the php array from the datatable + // but conserving the original datatable format, which means rows 'columns', 'metadata' and 'idsubdatatable' + $phpArray = $renderer->originalRender(); + return $phpArray; + } + + /** + * Sets the columns that will be displayed in the HTML output + * By default all columns are displayed ($columnsNames = array() will display all columns) + * + * @param array $columnsNames Array of column names eg. array('nb_visits','nb_hits') + */ + public function setColumnsToDisplay( $columnsNames) + { + $this->columnsToDisplay = $columnsNames; + } + + /** + * Sets translation string for given column + * + * @param string $columnName column name + * @param string $columnTranslation column name translation + */ + public function setColumnTranslation( $columnName, $columnTranslation ) + { + $this->columnsTranslations[$columnName] = $columnTranslation; + } + + /** + * Returns column translation if available, in other case given column name + * + * @param string $columnName column name + */ + public function getColumnTranslation( $columnName ) + { + if( isset($this->columnsTranslations[$columnName]) ) + { + return $this->columnsTranslations[$columnName]; + } + else + { + return $columnName; + } + } + + /** + * Sets columns translations array. + * + * @param array $columnsTranslations An associative array indexed by column names, eg. array('nb_visit'=>"Numer of visits") + */ + public function setColumnsTranslations( $columnsTranslations ) + { + $this->columnsTranslations = $columnsTranslations; + } + + /** + * Returns array( + * array('id' => 1, 'name' => 'nb_visits'), + * array('id' => 3, 'name' => 'nb_uniq_visitors'), + * + * @param array PHP array conversion of the data table + * @return array + */ + protected function getColumnsToDisplay($phpArray) + { + $dataTableColumns = array(); + if(count($phpArray) > 0) + { + // build column information + $id = 0; + foreach($phpArray[0]['columns'] as $columnName => $row) + { + if( $this->isColumnToDisplay( $id, $columnName) ) + { + $dataTableColumns[] = array('id' => $id, 'name' => $columnName, 'displayName' => $this->getColumnTranslation($columnName) ); + } + $id++; + } + } + return $dataTableColumns; + } + + /** + * Returns true if the given column (id = $idColumn or name = $nameColumn) is set to be displayed. + * + * @param int $idColumn + * @param string $nameColumn + * @return bool + */ + protected function isColumnToDisplay( $idColumn, $nameColumn ) + { + // we return true + // - we didn't set any column to display (means we display all the columns) + // - the column has been set as to display + if( count($this->columnsToDisplay) == 0 + || in_array($idColumn, $this->columnsToDisplay) + || in_array($nameColumn, $this->columnsToDisplay)) + { + return true; + } + return false; + } + + /** + * Sets the columns in the HTML table as not sortable (they are not clickable) + * + * @return void + */ + public function disableSort() + { + $this->variablesDefault['enable_sort'] = 'false'; + } + + /** + * Sets the search on a table to be recursive (also searches in subtables) + * Works only on Actions/Downloads/Outlinks tables. + * + * @return bool If the pattern for a recursive search was set in the URL + */ + public function setSearchRecursive() + { + $this->variablesDefault['search_recursive'] = true; + return $this->setRecursiveLoadDataTableIfSearchingForPattern(); + } + + /** + * Set the flag to load the datatable recursively so we can search on subtables as well + * + * @return bool if recursive search is enabled + */ + protected function setRecursiveLoadDataTableIfSearchingForPattern() + { + try{ + $requestValue = Piwik_Common::getRequestVar('filter_column_recursive'); + $requestValue = Piwik_Common::getRequestVar('filter_pattern_recursive'); + // if the 2 variables are set we are searching for something. + // we have to load all the children subtables in this case + + $this->recursiveDataTableLoad = true; + return true; + } + catch(Exception $e) { + $this->recursiveDataTableLoad = false; + return false; + } + } +} + diff --git a/core/ViewDataTable/Sparkline.php b/core/ViewDataTable/Sparkline.php new file mode 100644 index 0000000000..b66630669a --- /dev/null +++ b/core/ViewDataTable/Sparkline.php @@ -0,0 +1,67 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Html.php 404 2008-03-23 01:09:59Z matt $ + * + * @package Piwik_ViewDataTable + */ + + +require_once "Visualization/Sparkline.php"; + +/** + * Reads the requested DataTable from the API and prepare data for the Sparkline view. + * + * @package Piwik_ViewDataTable + * + */ +class Piwik_ViewDataTable_Sparkline extends Piwik_ViewDataTable +{ + + /** + * @see Piwik_ViewDataTable::init() + */ + function init($currentControllerName, + $currentControllerAction, + $moduleNameAndMethod ) + { + parent::init($currentControllerName, + $currentControllerAction, + $moduleNameAndMethod ); + } + + /** + * @see Piwik_ViewDataTable::main() + */ + public function main() + { + if($this->mainAlreadyExecuted) + { + return; + } + $this->mainAlreadyExecuted = true; + + // we load the data with the filters applied + $this->loadDataTableFromAPI(); + + $this->dataAvailable = $this->dataTable->getRowsCount() != 0; + + if(!$this->dataAvailable) + { + throw new Exception( "No data for this graph" ); + } + else + { + $data = $this->generateDataFromDataTableArray($this->dataTable); + + $graph = new Piwik_Visualization_Sparkline; + $graph->setData($data); + $graph->main(); +// var_dump($data);exit; + $this->view = $graph; + } + } +} diff --git a/core/Visualization/Chart.php b/core/Visualization/Chart.php new file mode 100644 index 0000000000..6e58ba721f --- /dev/null +++ b/core/Visualization/Chart.php @@ -0,0 +1,88 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Chart.php 579 2008-07-27 00:32:59Z matt $ + * + * @package Piwik_Visualization + */ + +require_once "Visualization/OpenFlashChart.php"; + +/** + * Generates the data in the Open Flash Chart format, from the given data. + * Uses Open flash chart PHP library @see Piwik_Visualization_OpenFlashChart + * + * @package Piwik_Visualization + */ +abstract class Piwik_Visualization_Chart extends Piwik_Visualization_OpenFlashChart +{ + + protected $dataGraph = array(); + + function setData($data) + { + $this->dataGraph = $data; + } + + function getCount() + { + return count($this->dataGraph); + } + + function customizeGraph() + { + $this->set_num_decimals ( 0 ); + $this->set_is_decimal_separator_comma( false ); + $this->set_is_thousand_separator_disabled( true ); + $this->y_axis_colour = '#ffffff'; + $this->x_axis_colour = '#596171'; + $this->x_grid_colour = $this->y_grid_colour = '#E0E1E4'; + + // approx 5 x labels on the graph + $steps = ceil($this->getCount() / 5); + $steps = $steps + $steps % 2; // make sure modulo 2 + + $this->set_x_label_style( 10, $this->x_axis_colour, 0, $steps, $this->x_grid_colour ); + $this->set_x_axis_steps( $steps / 2 ); + + + $stepsY = ceil($this->getCount() / 4); + $this->y_label_steps( $stepsY / 3 ); + $this->y_label_steps( 4 ); + + $this->bg_colour = '#ffffff'; + $this->set_inner_background('#ffffff'); + + $this->set_tool_tip( '#x_label# <br>#val# #key# ' ); + } + + function prepareData() + { + $label = $data = array(); + $max = 0; + foreach($this->dataGraph as $row) + { + $label[] = $row['label']; + $data[] = $row['value']; + + if($row['value'] > $max) + { + $max = $row['value']; + } + } + $this->arrayData = $data; + $this->arrayLabel = $label; + + $this->arrayLabel = str_replace(","," -",$this->arrayLabel); + + $this->maxData = $max; + if($this->maxData > 10) + { + $this->maxData = $max + 10 - $max % 10; + } + } + +}
\ No newline at end of file diff --git a/core/Visualization/ChartEvolution.php b/core/Visualization/ChartEvolution.php new file mode 100644 index 0000000000..066dbc9cff --- /dev/null +++ b/core/Visualization/ChartEvolution.php @@ -0,0 +1,58 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: ChartVerticalBar.php 168 2008-01-14 05:26:43Z matt $ + * + * @package Piwik_Visualization + */ + +require_once "Visualization/Chart.php"; + +/** + * Customize the Evolution chart style for the flash graph + * + * @package Piwik_Visualization + * + */ +class Piwik_Visualization_ChartEvolution extends Piwik_Visualization_Chart +{ + function customizeGraph() + { + parent::customizeGraph(); + $this->prepareData(); + $this->set_y_max( $this->maxData ); + + $line_1 = new line_hollow( 1, 3, '0x3357A0' ); + $line_1->key( 'visits', 10 ); + + $i = 0; + foreach($this->arrayData as $value) + { + // hack until we have proper date handling + $spacePosition = strpos($this->arrayLabel[$i],' '); + if($spacePosition === false) + { + $spacePosition = strlen($this->arrayLabel[$i]); + } + + // generate the link on the dot, to the given day' statistics + $link = Piwik_Url::getCurrentScriptName() + . Piwik_Url::getCurrentQueryStringWithParametersModified( array( + 'date' => substr($this->arrayLabel[$i],0,$spacePosition), + 'module' => 'CoreHome', + 'action' => 'index', + 'viewDataTable' => null// we reset the viewDataTable parameter (useless in the link) + )); + + $line_1->add_link($value, $link ); + $i++; + } + $this->data_sets[] = $line_1; + + $this->set_x_labels( $this->arrayLabel ); + $this->area_hollow( 1, 3, 4,'0x3357A0', ' visits', 10 ); + } +}
\ No newline at end of file diff --git a/core/Visualization/ChartPie.php b/core/Visualization/ChartPie.php new file mode 100644 index 0000000000..08dc54beb0 --- /dev/null +++ b/core/Visualization/ChartPie.php @@ -0,0 +1,41 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: ChartPie.php 459 2008-05-06 22:39:42Z matt $ + * + * @package Piwik_Visualization + */ +require_once "Visualization/Chart.php"; + +/** + * + * Customize the Pie chart style for the flash graph + * + * @package Piwik_Visualization + */ +class Piwik_Visualization_ChartPie extends Piwik_Visualization_Chart +{ + function customizeGraph() + { + parent::customizeGraph(); + + $this->prepareData(); + + for($i = 0, $cnt = count($this->arrayLabel); $i < $cnt; $i++) + { + $label = $this->arrayLabel[$i]; + $this->arrayLabel[$i] = (strlen($label) > 20 ? substr($label, 0, 20).'...' : $label); + } + $this->set_x_label_style( 12, $this->x_axis_colour, 0, 2, $this->bg_colour ); + $this->pie(60,'#505050','{font-size: 12px; color: #142448}', true); + $this->pie_values( $this->arrayData, $this->arrayLabel ); + $this->pie_slice_colours( array('#3C5A69','#679BB5','#695A3C','#B58E67','#969696') ); + + $this->set_tool_tip( '#x_label# <br>#val# ' ); + + } + +}
\ No newline at end of file diff --git a/core/Visualization/ChartVerticalBar.php b/core/Visualization/ChartVerticalBar.php new file mode 100644 index 0000000000..bfb820f586 --- /dev/null +++ b/core/Visualization/ChartVerticalBar.php @@ -0,0 +1,38 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: ChartVerticalBar.php 459 2008-05-06 22:39:42Z matt $ + * + * @package Piwik_Visualization + */ + +require_once "Visualization/Chart.php"; + +/** + * + * Customize the Vertical bar chart style for the flash graph + * + * @package Piwik_Visualization + * + */ +class Piwik_Visualization_ChartVerticalBar extends Piwik_Visualization_Chart +{ + protected $limit = 10; + + function customizeGraph() + { + parent::customizeGraph(); + $this->prepareData(); + $this->set_data( $this->arrayData ); + $this->set_x_labels( $this->arrayLabel ); + $this->set_x_label_style( 12, $this->x_axis_colour, 0, 2, $this->bg_colour ); + $this->set_x_axis_steps( 2 ); + $this->set_y_max( $this->maxData ); + $this->y_label_steps( 2 ); + $this->bar_filled( 50, '#3B5AA9', '#063E7E', 'visits', 10 ); + } + +}
\ No newline at end of file diff --git a/core/Visualization/Cloud.php b/core/Visualization/Cloud.php new file mode 100644 index 0000000000..46ca66208d --- /dev/null +++ b/core/Visualization/Cloud.php @@ -0,0 +1,169 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Cloud.php 444 2008-04-11 13:38:22Z johmathe $ + * + * @package Piwik_Visualization + */ + + +/** + * Generates a tag cloud from a given data array. + * The generated tag cloud can be in PHP format, or in HTML. + * + * Inspired from Derek Harvey (www.derekharvey.co.uk) + * + * @package Piwik_Visualization + */ +class Piwik_Visualization_Cloud +{ + protected $wordsArray = array(); + + public $truncatingLimit = 30; + + /** + * @param array array( word => 10, word2 => 50, word3 => 1) + */ + function __construct($words = false) + { + if ($words !== false && is_array($words)) + { + foreach ($words as $word => $value) + { + $this->addWord($word, $value); + } + } + } + + /* + * Assign word to array + * + * @param string $word + * @return string + */ + function addWord($word, $value = 1) + { + // $word = strtolower($word); + if (isset($this->wordsArray[$word])) + { + $this->wordsArray[$word] += $value; + } + else + { + $this->wordsArray[$word] = $value; + } + } + + /* + * Shuffle associated names in array + */ + function shuffleCloud() + { + $keys = array_keys($this->wordsArray); + + shuffle($keys); + + if (count($keys) && is_array($keys)) + { + $tmpArray = $this->wordsArray; + $this->wordsArray = array(); + foreach ($keys as $key => $value) + $this->wordsArray[$value] = $tmpArray[$value]; + } + } + + /* + * Calculate size of words array + */ + + function getCloudSize() + { + return array_sum($this->wordsArray); + } + + /* + * Get the class range using a percentage + * + * @returns int $class + */ + function getClassFromPercent($percent) + { + $mapping = array( + 95, + 70, + 50, + 30, + 15, + 5, + 0 + ); + foreach($mapping as $key => $value) + { + if($percent >= $value) + { + return $key; + } + } + } + + /* + * Create the HTML code for each word and apply font size. + * + * @returns string $spans + */ + + function render($returnType = "html") + { + $this->shuffleCloud(); + + if($returnType == "html") + { + $return = ''; + } + else + { + $return = array(); + } + + if (count($this->wordsArray) > 0) + { + $this->max = max($this->wordsArray); + + $return = ($returnType == "html" ? "" : ($returnType == "array" ? array() : "")); + foreach ($this->wordsArray as $word => $popularity) + { + + // truncating the word + $wordTruncated = $word; + if(strlen($word) > $this->truncatingLimit) + { + $wordTruncated = substr($word, 0, $this->truncatingLimit - 3).'...'; + } + + // computing the percentage + $percent = ($popularity / $this->max) * 100; + + // and the CSS style value + $sizeRange = $this->getClassFromPercent($percent); + + if ($returnType == "array") + { + $return[$word]['word'] = $word; + $return[$word]['wordTruncated'] = $wordTruncated; + $return[$word]['size'] = $sizeRange; + $return[$word]['percent'] = $percent; + } + else if ($returnType == "html") + { + $return .= "\n<span title='".$word."' class='word size{$sizeRange}'> {$wordTruncated} </span>"; + } + // print( $word ."=".$percent."<br>"); + } + } + return $return; + } +} + diff --git a/core/Visualization/OpenFlashChart.php b/core/Visualization/OpenFlashChart.php new file mode 100644 index 0000000000..1e85492c5c --- /dev/null +++ b/core/Visualization/OpenFlashChart.php @@ -0,0 +1,1647 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: OpenFlashChart.php 566 2008-07-21 00:34:43Z matt $ + * + * @package Piwik_Visualization + * @subpackage OFC + */ + +require_once "iView.php"; + + +/** + * Original class provided by Open Flash Chart + * + * @package Piwik_Visualization + */ +abstract class Piwik_Visualization_OpenFlashChart implements Piwik_iView +{ + function __construct() + { + + $this->data_sets = array(); + + + $this->data = array(); + $this->links = array(); + $this->width = 250; + $this->height = 200; + $this->js_path = 'js/'; + $this->swf_path = ''; + $this->x_labels = array(); + $this->y_min = ''; + $this->y_max = ''; + $this->x_min = ''; + $this->x_max = ''; + $this->y_steps = ''; + $this->title = ''; + $this->title_style = ''; + $this->occurence = 0; + + $this->x_offset = ''; + + $this->x_tick_size = -1; + + $this->y2_max = ''; + $this->y2_min = ''; + + // GRID styles: + $this->x_axis_colour = ''; + $this->x_axis_3d = ''; + $this->x_grid_colour = ''; + $this->x_axis_steps = 1; + $this->y_axis_colour = ''; + $this->y_grid_colour = ''; + $this->y2_axis_colour = ''; + + // AXIS LABEL styles: + $this->x_label_style = ''; + $this->y_label_style = ''; + $this->y_label_style_right = ''; + + + // AXIS LEGEND styles: + $this->x_legend = ''; + $this->x_legend_size = 20; + $this->x_legend_colour = '#000000'; + + $this->y_legend = ''; + $this->y_legend_right = ''; + //$this->y_legend_size = 20; + //$this->y_legend_colour = '#000000'; + + $this->lines = array(); + $this->line_default['type'] = 'line'; + $this->line_default['values'] = '3,#87421F'; + $this->js_line_default = 'so.addVariable("line","3,#87421F");'; + + $this->bg_colour = ''; + $this->bg_image = ''; + + $this->inner_bg_colour = ''; + $this->inner_bg_colour_2 = ''; + $this->inner_bg_angle = ''; + + // PIE chart ------------ + $this->pie = ''; + $this->pie_values = ''; + $this->pie_colours = ''; + $this->pie_labels = ''; + + $this->tool_tip = ''; + + // which data lines are attached to the + // right Y axis? + $this->y2_lines = array(); + + // Number formatting: + $this->y_format=''; + $this->num_decimals=''; + $this->is_fixed_num_decimals_forced=''; + $this->is_decimal_separator_comma=''; + $this->is_thousand_separator_disabled=''; + + $this->output_type = ''; + + // + // set some default value incase the user forgets + // to set them, so at least they see *something* + // even is it is only the axis and some ticks + // + $this->set_y_min( 0 ); + $this->set_y_max( 20 ); + $this->set_x_axis_steps( 1 ); + $this->y_label_steps( 5 ); + } + + /** + * Set the unique_id to use for the flash object id. + */ + function set_unique_id() + { + $this->unique_id = uniqid(rand(), true); + } + + /** + * Get the flash object ID for the last rendered object. + */ + function get_unique_id() + { + return ($this->unique_id); + } + + /** + * Set the base path for the swfobject.js + * + * @param base_path a string argument. + * The path to the swfobject.js file + */ + function set_js_path($path) + { + $this->js_path = $path; + } + + /** + * Set the base path for the open-flash-chart.swf + * + * @param path a string argument. + * The path to the open-flash-chart.swf file + */ + function set_swf_path($path) + { + $this->swf_path = $path; + } + + /** + * Set the type of output data. + * + * @param type a string argument. + * The type of data. Currently only type is js, or nothing. + */ + function set_output_type($type) + { + $this->output_type = $type; + } + + /** + * returns the next line label for multiple lines. + */ + function next_line() + { + $line_num = ''; + if( count( $this->lines ) > 0 ) + $line_num = '_'. (count( $this->lines )+1); + + return $line_num; + } + + // escape commas (,) + static function esc( $text ) + { + // we replace the comma so it is not URL escaped + // if it is, flash just thinks it is a comma + // which is no good if we are splitting the + // string on commas. + $tmp = str_replace( ',', '#comma#', $text ); + //$tmp = utf8_encode( $tmp ); + // now we urlescape all dodgy characters (like & % $ etc..) + return urlencode( $tmp ); + } + + /** + * Format the text to the type of output. + */ + function format_output($function,$values) + { + if($this->output_type == 'js') + { + $tmp = 'so.addVariable("'. $function .'","'. $values . '");'; + } + else + { + $tmp = '&'. $function .'='. $values .'&'; + } + + return $tmp; + } + + /** + * Set the text and style of the title. + * + * @param title a string argument. + * The text of the title. + * @param style a string. + * CSS styling of the title. + */ + function set_title( $title, $style='' ) + { + $this->title = $this->esc( $title ); + if( strlen( $style ) > 0 ) + $this->title_style = $style; + } + + /** + * Set the width of the chart. + * + * @param width an int argument. + * The width of the chart frame. + */ + function set_width( $width ) + { + $this->width = $width; + } + + /** + * Set the height of the chart. + * + * @param height an int argument. + * The height of the chart frame. + */ + function set_height( $height ) + { + $this->height = $height; + } + + /** + * Set the base path of the swfobject. + * + * @param base a string argument. + * The base path of the swfobject. + */ + function set_base( $base='js/' ) + { + $this->base = $base; + } + + // Number formatting: + function set_y_format( $val ) + { + $this->y_format = $val; + } + + function set_num_decimals( $val ) + { + $this->num_decimals = $val; + } + + function set_is_fixed_num_decimals_forced( $val ) + { + $this->is_fixed_num_decimals_forced = $val?'true':'false'; + } + + function set_is_decimal_separator_comma( $val ) + { + $this->is_decimal_separator_comma = $val?'true':'false'; + } + + function set_is_thousand_separator_disabled( $val ) + { + $this->is_thousand_separator_disabled = $val?'true':'false'; + } + + /** + * Set the data for the chart + * @param a an array argument. + * An array of the data to add to the chart. + */ + function set_data( $a ) + { + $this->data[] = implode(',',$a); + } + + // UGH, these evil functions are making me fell ill + function set_links( $links ) + { + // TO DO escape commas: + $this->links[] = implode(',',$links); + } + + // $val is a boolean + function set_x_offset( $val ) + { + $this->x_offset = $val?'true':'false'; + } + + /** + * Set the tooltip to be displayed on each chart item.\n + * \n + * Replaceable tokens that can be used in the string include: \n + * #val# - The actual value of whatever the mouse is over. \n + * #key# - The key string. \n + * \<br> - New line. \n + * #x_label# - The X label string. \n + * #x_legend# - The X axis legend text. \n + * Default string is: "#x_label#<br>#val#" \n + * + * @param tip a string argument. + * A formatted string to show as the tooltip. + */ + function set_tool_tip( $tip ) + { + $this->tool_tip = $this->esc( $tip ); + } + + /** + * Set the x axis labels + * + * @param a an array argument. + * An array of the x axis labels. + */ + function set_x_labels( $a ) + { + $tmp = array(); + foreach( $a as $item ) + $tmp[] = $this->esc( $item ); + $this->x_labels = $tmp; + } + + /** + * Set the look and feel of the x axis labels + * + * @param font_size an int argument. + * The font size. + * @param colour a string argument. + * The hex colour value. + * @param orientation an int argument. + * The orientation of the x-axis text. + * 0 - Horizontal + * 1 - Vertical + * 2 - 45 degrees + * @param step an int argument. + * Show the label on every $step label. + * @param grid_colour a string argument. + */ + function set_x_label_style( $size, $colour='', $orientation=0, $step=-1, $grid_colour='' ) + { + $this->x_label_style = $size; + + if( strlen( $colour ) > 0 ) + $this->x_label_style .= ','. $colour; + + if( $orientation > -1 ) + $this->x_label_style .= ','. $orientation; + + if( $step > 0 ) + $this->x_label_style .= ','. $step; + + if( strlen( $grid_colour ) > 0 ) + $this->x_label_style .= ','. $grid_colour; + } + + /** + * Set the background colour. + * @param colour a string argument. + * The hex colour value. + */ + function set_bg_colour( $colour ) + { + $this->bg_colour = $colour; + } + + /** + * Set a background image. + * @param url a string argument. + * The location of the image. + * @param x a string argument. + * The x location of the image. 'Right', 'Left', 'Center' + * @param y a string argument. + * The y location of the image. 'Top', 'Bottom', 'Middle' + */ + function set_bg_image( $url, $x='center', $y='center' ) + { + $this->bg_image = $url; + $this->bg_image_x = $x; + $this->bg_image_y = $y; + } + + /** + * Attach a set of data (a line, area or bar chart) to the right Y axis. + * @param data_number an int argument. + * The numbered order the data was attached using set_data. + */ + function attach_to_y_right_axis( $data_number ) + { + $this->y2_lines[] = $data_number; + } + + /** + * Set the background colour of the grid portion of the chart. + * @param col a string argument. + * The hex colour value of the background. + * @param col2 a string argument. + * The hex colour value of the second colour if you want a gradient. + * @param angle an int argument. + * The angle in degrees to make the gradient. + */ + function set_inner_background( $col, $col2='', $angle=-1 ) + { + $this->inner_bg_colour = $col; + + if( strlen($col2) > 0 ) + $this->inner_bg_colour_2 = $col2; + + if( $angle != -1 ) + $this->inner_bg_angle = $angle; + } + + /** + * Internal function to build the y label style for y and y2 + */ + function _set_y_label_style( $size, $colour ) + { + $tmp = $size; + + if( strlen( $colour ) > 0 ) + $tmp .= ','. $colour; + return $tmp; + } + + /** + * Set the look and feel of the y axis labels + * + * @param font_size an int argument. + * The font size. + * @param colour a string argument. + * The hex colour value. + */ + function set_y_label_style( $size, $colour='' ) + { + $this->y_label_style = $this->_set_y_label_style( $size, $colour ); + } + + /** + * Set the look and feel of the right y axis labels + * + * @param font_size an int argument. + * The font size. + * @param colour a string argument. + * The hex colour value. + */ + function set_y_right_label_style( $size, $colour='' ) + { + $this->y_label_style_right = $this->_set_y_label_style( $size, $colour ); + } + + function set_x_max( $max ) + { + $this->x_max = floatval( $max ); + } + + function set_x_min( $min ) + { + $this->x_min = floatval( $min ); + } + + /** + * Set the maximum value of the y axis. + * + * @param max an float argument. + * The maximum value. + */ + function set_y_max( $max ) + { + $this->y_max = floatval( $max ); + } + + /** + * Set the minimum value of the y axis. + * + * @param min an float argument. + * The minimum value. + */ + function set_y_min( $min ) + { + $this->y_min = floatval( $min ); + } + + /** + * Set the maximum value of the right y axis. + * + * @param max an float argument. + * The maximum value. + */ + function set_y_right_max( $max ) + { + $this->y2_max = floatval($max); + } + + /** + * Set the minimum value of the right y axis. + * + * @param min an float argument. + * The minimum value. + */ + function set_y_right_min( $min ) + { + $this->y2_min = floatval($min); + } + + /** + * Show the y label on every $step label. + * + * @param val an int argument. + * Show the label on every $step label. + */ + function y_label_steps( $val ) + { + $this->y_steps = intval( $val ); + } + + function title( $title, $style='' ) + { + $this->title = $this->esc( $title ); + if( strlen( $style ) > 0 ) + $this->title_style = $style; + } + + /** + * Set the parameters of the x legend. + * + * @param text a string argument. + * The text of the x legend. + * @param font_size an int argument. + * The font size of the x legend text. + * @param colour a string argument + * The hex value of the font colour. + */ + function set_x_legend( $text, $size=-1, $colour='' ) + { + $this->x_legend = $this->esc( $text ); + if( $size > -1 ) + $this->x_legend_size = $size; + + if( strlen( $colour )>0 ) + $this->x_legend_colour = $colour; + } + + /** + * Set the size of the x label ticks. + * + * @param size an int argument. + * The size of the ticks in pixels. + */ + function set_x_tick_size( $size ) + { + if( $size > 0 ) + $this->x_tick_size = $size; + } + + /** + * Set how often you would like to show a tick on the x axis. + * + * @param steps an int argument. + * Show a tick ever $steps. + */ + function set_x_axis_steps( $steps ) + { + if ( $steps > 0 ) + $this->x_axis_steps = $steps; + } + + /** + * Set the depth in pixels of the 3D X axis slab. + * + * @param size an int argument. + * The depth in pixels of the 3D X axis. + */ + function set_x_axis_3d( $size ) + { + if( $size > 0 ) + $this->x_axis_3d = intval($size); + } + + /** + * The private method of building the y legend output. + */ + function _set_y_legend( $text, $size, $colour ) + { + $tmp = $text; + + if( $size > -1 ) + $tmp .= ','. $size; + + if( strlen( $colour )>0 ) + $tmp .= ','. $colour; + + return $tmp; + } + + /** + * Set the parameters of the y legend. + * + * @param text a string argument. + * The text of the y legend. + * @param font_size an int argument. + * The font size of the y legend text. + * @param colour a string argument + * The hex colour value of the font colour. + */ + function set_y_legend( $text, $size=-1, $colour='' ) + { + $this->y_legend = $this->_set_y_legend( $text, $size, $colour ); + } + + /** + * Set the parameters of the right y legend. + * + * @param text a string argument. + * The text of the right y legend. + * @param font_size an int argument. + * The font size of the right y legend text. + * @param colour a string argument + * The hex value of the font colour. + */ + function set_y_right_legend( $text, $size=-1, $colour='' ) + { + $this->y_legend_right = $this->_set_y_legend( $text, $size, $colour ); + } + + /** + * Set the colour of the x axis line and grid. + * + * @param axis a string argument. + * The hex colour value of the x axis line. + * @param grid a string argument. + * The hex colour value of the x axis grid. + */ + function x_axis_colour( $axis, $grid='' ) + { + $this->x_axis_colour = $axis; + $this->x_grid_colour = $grid; + } + + /** + * Set the colour of the y axis line and grid. + * + * @param axis a string argument. + * The hex colour value of the y axis line. + * @param grid a string argument. + * The hex colour value of the y axis grid. + */ + function y_axis_colour( $axis, $grid='' ) + { + $this->y_axis_colour = $axis; + + if( strlen( $grid ) > 0 ) + $this->y_grid_colour = $grid; + } + + /** + * Set the colour of the right y axis line. + * + * @param colour a string argument. + * The hex colour value of the right y axis line. + */ + function y_right_axis_colour( $colour ) + { + $this->y2_axis_colour = $colour; + } + + /** + * Draw a line without markers on values. + * + * @param width an int argument. + * The width of the line in pixels. + * @param colour a string argument. + * The hex colour value of the line. + * @param text a string argument. + * The label of the line. + * @param font_size an int argument. + * Font size of the label + * @param circles an int argument + * Need to find out. + */ + function line( $width, $colour='', $text='', $size=-1, $circles=-1 ) + { + $type = 'line'. $this->next_line(); + + $description = ''; + if( $width > 0 ) + { + $description .= $width; + $description .= ','. $colour; + } + + if( strlen( $text ) > 0 ) + { + $description.= ','. $text; + $description .= ','. $size; + } + + if( $circles > 0 ) + $description .= ','. $circles; + + $this->lines[$type] = $description; + } + + /** + * Draw a line with solid dot markers on values. + * + * @param width an int argument. + * The width of the line in pixels. + * @param dot_size an int argument. + * Size in pixels of the dot. + * @param colour a string argument. + * The hex colour value of the line. + * @param text a string argument. + * The label of the line. + * @param font_size an int argument. + * Font size of the label. + */ + function line_dot( $width, $dot_size, $colour, $text='', $font_size='' ) + { + $type = 'line_dot'. $this->next_line(); + + $description = "$width,$colour,$text"; + + if( strlen( $font_size ) > 0 ) + $description .= ",$font_size,$dot_size"; + + $this->lines[$type] = $description; + } + + /** + * Draw a line with hollow dot markers on values. + * + * @param width an int argument. + * The width of the line in pixels. + * @param dot_size an int argument. + * Size in pixels of the dot. + * @param colour a string argument. + * The hex colour value of the line. + * @param text a string argument. + * The label of the line. + * @param font_size an int argument. + * Font size of the label. + */ + function line_hollow( $width, $dot_size, $colour, $text='', $font_size='' ) + { + $type = 'line_hollow'. $this->next_line(); + + $description = "$width,$colour,$text"; + + if( strlen( $font_size ) > 0 ) + $description .= ",$font_size,$dot_size"; + + $this->lines[$type] = $description; + } + + /** + * Draw an area chart. + * + * @param width an int argument. + * The width of the line in pixels. + * @param dot_size an int argument. + * Size in pixels of the dot. + * @param colour a string argument. + * The hex colour value of the line. + * @param alpha an int argument. + * The percentage of transparency of the fill colour. + * @param text a string argument. + * The label of the line. + * @param font_size an int argument. + * Font size of the label. + * @param fill_colour a string argument. + * The hex colour value of the fill colour. + */ + function area_hollow( $width, $dot_size, $colour, $alpha, $text='', $font_size='', $fill_colour='' ) + { + $type = 'area_hollow'. $this->next_line(); + + $description = "$width,$dot_size,$colour,$alpha"; + + if( strlen( $text ) > 0 ) + $description .= ",$text,$font_size"; + + if( strlen( $fill_colour ) > 0 ) + $description .= ','. $fill_colour; + + $this->lines[$type] = $description; + } + + /** + * Draw a bar chart. + * + * @param alpha an int argument. + * The percentage of transparency of the bar colour. + * @param colour a string argument. + * The hex colour value of the line. + * @param text a string argument. + * The label of the line. + * @param font_size an int argument. + * Font size of the label. + */ + function bar( $alpha, $colour='', $text='', $size=-1 ) + { + $type = 'bar'. $this->next_line(); + + $description = $alpha .','. $colour .','. $text .','. $size; + + $this->lines[$type] = $description; + } + + /** + * Draw a bar chart with an outline. + * + * @param alpha an int argument. + * The percentage of transparency of the bar colour. + * @param colour a string argument. + * The hex colour value of the line. + * @param colour_outline a strng argument. + * The hex colour value of the outline. + * @param text a string argument. + * The label of the line. + * @param font_size an int argument. + * Font size of the label. + */ + function bar_filled( $alpha, $colour, $colour_outline, $text='', $size=-1 ) + { + $type = 'filled_bar'. $this->next_line(); + + $description = "$alpha,$colour,$colour_outline,$text,$size"; + + $this->lines[$type] = $description; + } + + function bar_sketch( $alpha, $offset, $colour, $colour_outline, $text='', $size=-1 ) + { + $type = 'bar_sketch'. $this->next_line(); + + $description = "$alpha,$offset,$colour,$colour_outline,$text,$size"; + + $this->lines[$type] = $description; + } + + /** + * Draw a 3D bar chart. + * + * @param alpha an int argument. + * The percentage of transparency of the bar colour. + * @param colour a string argument. + * The hex colour value of the line. + * @param text a string argument. + * The label of the line. + * @param font_size an int argument. + * Font size of the label. + */ + function bar_3D( $alpha, $colour='', $text='', $size=-1 ) + { + $type = 'bar_3d'. $this->next_line(); + + $description = $alpha .','. $colour .','. $text .','. $size; + + $this->lines[$type] = $description; + } + + /** + * Draw a 3D bar chart that looks like glass. + * + * @param alpha an int argument. + * The percentage of transparency of the bar colour. + * @param colour a string argument. + * The hex colour value of the line. + * @param outline_colour a string argument. + * The hex colour value of the outline. + * @param text a string argument. + * The label of the line. + * @param font_size an int argument. + * Font size of the label. + */ + function bar_glass( $alpha, $colour, $outline_colour, $text='', $size=-1 ) + { + $type = 'bar_glass'. $this->next_line(); + + $description = $alpha .','. $colour .','. $outline_colour .','. $text .','. $size; + + $this->lines[$type] = $description; + } + + /** + * Draw a faded bar chart. + * + * @param alpha an int argument. + * The percentage of transparency of the bar colour. + * @param colour a string argument. + * The hex colour value of the line. + * @param text a string argument. + * The label of the line. + * @param font_size an int argument. + * Font size of the label. + */ + function bar_fade( $alpha, $colour='', $text='', $size=-1 ) + { + $type = 'bar_fade'. $this->next_line(); + + $description = $alpha .','. $colour .','. $text .','. $size; + + $this->lines[$type] = $description; + } + + function candle( $data, $alpha, $line_width, $colour, $text='', $size=-1 ) + { + $type = 'candle'. $this->next_line(); + + $description = $alpha .','. $line_width .','. $colour .','. $text .','. $size; + + $this->lines[$type] = $description; + + $a = array(); + foreach( $data as $can ) + $a[] = $can->toString(); + + $this->data[] = implode(',',$a); + } + + function hlc( $data, $alpha, $line_width, $colour, $text='', $size=-1 ) + { + $type = 'hlc'. $this->next_line(); + + $description = $alpha .','. $line_width .','. $colour .','. $text .','. $size; + + $this->lines[$type] = $description; + + $a = array(); + foreach( $data as $can ) + $a[] = $can->toString(); + + $this->data[] = implode(',',$a); + } + + function scatter( $data, $line_width, $colour, $text='', $size=-1 ) + { + $type = 'scatter'. $this->next_line(); + + $description = $line_width .','. $colour .','. $text .','. $size; + + $this->lines[$type] = $description; + + $a = array(); + foreach( $data as $can ) + $a[] = $can->toString(); + + $this->data[] = implode(',',$a); + } + + + // + // Patch by, Jeremy Miller (14th Nov, 2007) + // + /** + * Draw a pie chart. + * + * @param alpha an int argument. + * The percentage of transparency of the pie colour. + * @param $style a string argument. + * CSS style string + * @param label_colour a string argument. + * The hex colour value of the label. + * @param gradient a boolean argument. + * Use a gradient true or false. + * @param border_size an int argument. + * Size of the border in pixels. + */ + function pie( $alpha, $line_colour, $style, $gradient = true, $border_size = false ) + { + $this->pie = $alpha.','.$line_colour.','.$style; + if( !$gradient ) + { + $this->pie .= ','.!$gradient; + } + if ($border_size) + { + if ($gradient === false) + { + $this->pie .= ','; + } + $this->pie .= ','.$border_size; + } + } + + /** + * Set the values of the pie chart. + * + * @param values an array argument. + * An array of the values for the pie chart. + * @param labels an array argument. + * An array of the labels for the pie pieces. + * @param links an array argument. + * An array of the links to the pie pieces. + */ + function pie_values( $values, $labels=array(), $links=array() ) + { + $this->pie_values = implode(',',$values); + $this->pie_labels = implode(',',$labels); + $this->pie_links = implode(",",$links); + } + + /** + * Set the pie slice colours. + * + * @param colours an array argument. + * The hex colour values of the pie pieces. + */ + function pie_slice_colours( $colours ) + { + $this->pie_colours = implode(',',$colours); + } + + + /** + * Render the output. + */ + function render() + { + $tmp = array(); + + //echo headers_sent() ?'yes':'no'; + if( !headers_sent() ) + header('content-type: text; charset: utf-8'); + + if($this->output_type == 'js') + { + $this->set_unique_id(); + + $tmp[] = '<div id="' . $this->unique_id . '"></div>'; + $tmp[] = '<script type="text/javascript" src="' . $this->js_path . 'swfobject.js"></script>'; + $tmp[] = '<script type="text/javascript">'; + $tmp[] = 'var so = new SWFObject("' . $this->swf_path . 'open-flash-chart.swf", "ofc", "'. $this->width . '", "' . $this->height . '", "9", "#FFFFFF");'; + $tmp[] = 'so.addVariable("variables","true");'; + } + + if( strlen( $this->title ) > 0 ) + { + $values = $this->title; + $values .= ','. $this->title_style; + $tmp[] = $this->format_output('title',$values); + } + + if( strlen( $this->x_legend ) > 0 ) + { + $values = $this->x_legend; + $values .= ','. $this->x_legend_size; + $values .= ','. $this->x_legend_colour; + $tmp[] = $this->format_output('x_legend',$values); + } + + if( strlen( $this->x_label_style ) > 0 ) + $tmp[] = $this->format_output('x_label_style',$this->x_label_style); + + if( $this->x_tick_size > 0 ) + $tmp[] = $this->format_output('x_ticks',$this->x_tick_size); + + if( $this->x_axis_steps > 0 ) + $tmp[] = $this->format_output('x_axis_steps',$this->x_axis_steps); + + if( strlen( $this->x_axis_3d ) > 0 ) + $tmp[] = $this->format_output('x_axis_3d',$this->x_axis_3d); + + if( strlen( $this->y_legend ) > 0 ) + $tmp[] = $this->format_output('y_legend',$this->y_legend); + + if( strlen( $this->y_legend_right ) > 0 ) + $tmp[] = $this->format_output('y2_legend',$this->y_legend_right); + + if( strlen( $this->y_label_style ) > 0 ) + $tmp[] = $this->format_output('y_label_style',$this->y_label_style); + + $values = '5,10,'. $this->y_steps; + $tmp[] = $this->format_output('y_ticks',$values); + + if( count( $this->lines ) == 0 && count($this->data_sets)==0 ) + { + $tmp[] = $this->format_output($this->line_default['type'],$this->line_default['values']); + } + else + { + foreach( $this->lines as $type=>$description ) + $tmp[] = $this->format_output($type,$description); + } + + $num = 1; + foreach( $this->data as $data ) + { + if( $num==1 ) + { + $tmp[] = $this->format_output( 'values', $data); + } + else + { + $tmp[] = $this->format_output('values_'. $num, $data); + } + + $num++; + } + + $num = 1; + foreach( $this->links as $link ) + { + if( $num==1 ) + { + $tmp[] = $this->format_output( 'links', $link); + } + else + { + $tmp[] = $this->format_output('links_'. $num, $link); + } + + $num++; + } + + if( count( $this->y2_lines ) > 0 ) + { + $tmp[] = $this->format_output('y2_lines',implode( ',', $this->y2_lines )); + // + // Should this be an option? I think so... + // + $tmp[] = $this->format_output('show_y2','true'); + } + + if( count( $this->x_labels ) > 0 ) + $tmp[] = $this->format_output('x_labels',implode(',',$this->x_labels)); + else + { + if( strlen($this->x_min) > 0 ) + $tmp[] = $this->format_output('x_min',$this->x_min); + + if( strlen($this->x_max) > 0 ) + $tmp[] = $this->format_output('x_max',$this->x_max); + } + + $tmp[] = $this->format_output('y_min',$this->y_min); + $tmp[] = $this->format_output('y_max',$this->y_max); + + if( strlen($this->y2_min) > 0 ) + $tmp[] = $this->format_output('y2_min',$this->y2_min); + + if( strlen($this->y2_max) > 0 ) + $tmp[] = $this->format_output('y2_max',$this->y2_max); + + if( strlen( $this->bg_colour ) > 0 ) + $tmp[] = $this->format_output('bg_colour',$this->bg_colour); + + if( strlen( $this->bg_image ) > 0 ) + { + $tmp[] = $this->format_output('bg_image',$this->bg_image); + $tmp[] = $this->format_output('bg_image_x',$this->bg_image_x); + $tmp[] = $this->format_output('bg_image_y',$this->bg_image_y); + } + + if( strlen( $this->x_axis_colour ) > 0 ) + { + $tmp[] = $this->format_output('x_axis_colour',$this->x_axis_colour); + $tmp[] = $this->format_output('x_grid_colour',$this->x_grid_colour); + } + + if( strlen( $this->y_axis_colour ) > 0 ) + $tmp[] = $this->format_output('y_axis_colour',$this->y_axis_colour); + + if( strlen( $this->y_grid_colour ) > 0 ) + $tmp[] = $this->format_output('y_grid_colour',$this->y_grid_colour); + + if( strlen( $this->y2_axis_colour ) > 0 ) + $tmp[] = $this->format_output('y2_axis_colour',$this->y2_axis_colour); + + if( strlen( $this->x_offset ) > 0 ) + $tmp[] = $this->format_output('x_offset',$this->x_offset); + + if( strlen( $this->inner_bg_colour ) > 0 ) + { + $values = $this->inner_bg_colour; + if( strlen( $this->inner_bg_colour_2 ) > 0 ) + { + $values .= ','. $this->inner_bg_colour_2; + $values .= ','. $this->inner_bg_angle; + } + $tmp[] = $this->format_output('inner_background',$values); + } + + if( strlen( $this->pie ) > 0 ) + { + $tmp[] = $this->format_output('pie',$this->pie); + $tmp[] = $this->format_output('values',$this->pie_values); + $tmp[] = $this->format_output('pie_labels',$this->pie_labels); + $tmp[] = $this->format_output('colours',$this->pie_colours); + $tmp[] = $this->format_output('links',$this->pie_links); + } + + if( strlen( $this->tool_tip ) > 0 ) + $tmp[] = $this->format_output('tool_tip',$this->tool_tip); + + + + if( strlen( $this->y_format ) > 0 ) + $tmp[] = $this->format_output('y_format',$this->y_format); + + if( strlen( $this->num_decimals ) > 0 ) + $tmp[] = $this->format_output('num_decimals',$this->num_decimals); + + if( strlen( $this->is_fixed_num_decimals_forced ) > 0 ) + $tmp[] = $this->format_output('is_fixed_num_decimals_forced',$this->is_fixed_num_decimals_forced); + + if( strlen( $this->is_decimal_separator_comma ) > 0 ) + $tmp[] = $this->format_output('is_decimal_separator_comma',$this->is_decimal_separator_comma); + + if( strlen( $this->is_thousand_separator_disabled ) > 0 ) + $tmp[] = $this->format_output('is_thousand_separator_disabled',$this->is_thousand_separator_disabled); + + + $count = 1; + foreach( $this->data_sets as $set ) + { + $tmp[] = $set->toString( $this->output_type, $count>1?'_'.$count:'' ); + $count++; + } + + if($this->output_type == 'js') + { + $tmp[] = 'so.write("' . $this->unique_id . '");'; + $tmp[] = '</script>'; + } + + return implode("\r\n",$tmp); + } +} + +class line +{ + var $line_width; + var $colour; + var $_key; + var $key; + var $key_size; + // hold the data + var $data; + // extra tool tip info: + var $tips; + + function line( $line_width, $colour ) + { + $this->var = 'line'; + + $this->line_width = $line_width; + $this->colour = $colour; + $this->data = array(); + $this->links = array(); + $this->tips = array(); + $this->_key = false; + } + + + function key( $key, $size ) + { + $this->_key = true; + $this->key = graph::esc( $key ); + $this->key_size = $size; + } + + function add( $data ) + { + $this->data[] = $data; + } + + function add_link( $data, $link ) + { + $this->data[] = $data; + $this->links[] = graph::esc( $link ); + } + + function add_data_tip( $data, $tip ) + { + $this->data[] = $data; + $this->tips[] = graph::esc( $tip ); + } + + function add_data_link_tip( $data, $link, $tip ) + { + $this->data[] = $data; + $this->links[] = graph::esc( $link ); + $this->tips[] = graph::esc( $tip ); + } + + // return the variables for this chart + function _get_variable_list() + { + $values = array(); + $values[] = $this->line_width; + $values[] = $this->colour; + + if( $this->_key ) + { + $values[] = $this->key; + $values[] = $this->key_size; + } + + return $values; + } + + function toString( $output_type, $set_num ) + { + $values = implode( ',', $this->_get_variable_list() ); + + $tmp = array(); + + if( $output_type == 'js' ) + { + $tmp[] = 'so.addVariable("'. $this->var.$set_num .'","'. $values . '");'; + + $tmp[] = 'so.addVariable("values'. $set_num .'","'. implode( ',', $this->data ) .'");'; + + if( count( $this->links ) > 0 ) + $tmp[] = 'so.addVariable("links'. $set_num .'","'. implode( ',', $this->links ) .'");'; + + if( count( $this->tips ) > 0 ) + $tmp[] = 'so.addVariable("tool_tips_set'. $set_num .'","'. implode( ',', $this->tips ) .'");'; + + } + else + { + $tmp[] = '&'. $this->var. $set_num .'='. $values .'&'; + $tmp[] = '&values'. $set_num .'='. implode( ',', $this->data ) .'&'; + + if( count( $this->links ) > 0 ) + $tmp[] = '&links'. $set_num .'='. implode( ',', $this->links ) .'&'; + + if( count( $this->tips ) > 0 ) + $tmp[] = '&tool_tips_set'. $set_num .'='. implode( ',', $this->tips ) .'&'; + } + + return implode( "\r\n", $tmp ); + } +} + +class line_hollow extends line +{ + var $dot_size; + + function line_hollow( $line_width, $dot_size, $colour ) + { + parent::line( $line_width, $colour ); + $this->var = 'line_hollow'; + $this->dot_size = $dot_size; + } + + // return the variables for this chart + function _get_variable_list() + { + $values = array(); + $values[] = $this->line_width; + $values[] = $this->colour; + + if( $this->_key ) + { + $values[] = $this->key; + $values[] = $this->key_size; + } + else + { + $values[] = ''; + $values[] = ''; + } + $values[] = $this->dot_size; + + return $values; + } +} + +class line_dot extends line_hollow +{ + function line_dot( $line_width, $dot_size, $colour ) + { + parent::line_hollow( $line_width, $dot_size,$colour ); + $this->var = 'line_dot'; + } +} + +class bar +{ + var $colour; + var $alpha; + var $data; + var $links; + var $_key; + var $key; + var $key_size; + var $var; + // extra tool tip info: + var $tips; + + function bar( $alpha, $colour ) + { + $this->var = 'bar'; + + $this->alpha = $alpha; + $this->colour = $colour; + $this->data = array(); + $this->links = array(); + $this->tips = array(); + $this->_key = false; + } + + function key( $key, $size ) + { + $this->_key = true; + $this->key = graph::esc( $key ); + $this->key_size = $size; + } + + function add( $data ) + { + $this->data[] = $data; + } + + function add_link( $data, $link ) + { + $this->data[] = $data; + $this->links[] = graph::esc( $link ); + } + + function add_data_tip( $data, $tip ) + { + $this->data[] = $data; + $this->tips[] = graph::esc( $tip ); + } + + // return the variables for this + // bar chart + function _get_variable_list() + { + $values = array(); + $values[] = $this->alpha; + $values[] = $this->colour; + + if( $this->_key ) + { + $values[] = $this->key; + $values[] = $this->key_size; + } + + return $values; + } + + function toString( $output_type, $set_num ) + { + $values = implode( ',', $this->_get_variable_list() ); + + $tmp = array(); + + if( $output_type == 'js' ) + { + $tmp[] = 'so.addVariable("'. $this->var.$set_num .'","'. $values . '");'; + + $tmp[] = 'so.addVariable("values'. $set_num .'","'. implode( ',', $this->data ) .'");'; + + if( count( $this->links ) > 0 ) + $tmp[] = 'so.addVariable("links'. $set_num .'","'. implode( ',', $this->links ) .'");'; + + if( count( $this->tips ) > 0 ) + $tmp[] = 'so.addVariable("tool_tips_set'. $set_num .'","'. implode( ',', $this->tips ) .'");'; + + } + else + { + $tmp[] = '&'. $this->var. $set_num .'='. $values .'&'; + $tmp[] = '&values'. $set_num .'='. implode( ',', $this->data ) .'&'; + + if( count( $this->links ) > 0 ) + $tmp[] = '&links'. $set_num .'='. implode( ',', $this->links ) .'&'; + + if( count( $this->tips ) > 0 ) + $tmp[] = '&tool_tips_set'. $set_num .'='. implode( ',', $this->tips ) .'&'; + } + + return implode( "\r\n", $tmp ); + } + +} + +class bar_3d extends bar +{ + function bar_3d( $alpha, $colour ) + { + parent::bar( $alpha, $colour ); + $this->var = 'bar_3d'; + } +} + +class bar_fade extends bar +{ + function bar_fade( $alpha, $colour ) + { + parent::bar( $alpha, $colour ); + $this->var = 'bar_fade'; + } +} + +class bar_outline extends bar +{ + var $outline_colour; + + function bar_outline( $alpha, $colour, $outline_colour ) + { + parent::bar( $alpha, $colour ); + $this->var = 'filled_bar'; + $this->outline_colour = $outline_colour; + } + + // override the base method + function _get_variable_list() + { + $values = array(); + $values[] = $this->alpha; + $values[] = $this->colour; + $values[] = $this->outline_colour; + + if( $this->_key ) + { + $values[] = $this->key; + $values[] = $this->key_size; + } + + return $values; + } +} + +class bar_glass extends bar_outline +{ + function bar_glass( $alpha, $colour, $outline_colour ) + { + parent::bar_outline( $alpha, $colour, $outline_colour ); + $this->var = 'bar_glass'; + } +} + +// +// this has an outline colour and a 'jiggle' parameter +// called offset +// +class bar_sketch extends bar_outline +{ + var $offset; + + function bar_sketch( $alpha, $offset, $colour, $outline_colour ) + { + parent::bar_outline( $alpha, $colour, $outline_colour ); + $this->var = 'bar_sketch'; + $this->offset = $offset; + } + + // override the base method + function _get_variable_list() + { + $values = array(); + $values[] = $this->alpha; + $values[] = $this->offset; + $values[] = $this->colour; + $values[] = $this->outline_colour; + + if( $this->_key ) + { + $values[] = $this->key; + $values[] = $this->key_size; + } + + return $values; + } +} + +class candle +{ + var $out; + + function candle( $high, $open, $close, $low ) + { + $this->out = array(); + $this->out[] = $high; + $this->out[] = $open; + $this->out[] = $close; + $this->out[] = $low; + } + + function toString() + { + return '['. implode( ',', $this->out ) .']'; + } +} + +class hlc +{ + var $out; + + function hlc( $high, $low, $close ) + { + $this->out = array(); + $this->out[] = $high; + $this->out[] = $low; + $this->out[] = $close; + } + + function toString() + { + return '['. implode( ',', $this->out ) .']'; + } +} + +class point +{ + var $out; + + function point( $x, $y, $size_px ) + { + $this->out = array(); + $this->out[] = $x; + $this->out[] = $y; + $this->out[] = $size_px; + } + + function toString() + { + return '['. implode( ',', $this->out ) .']'; + } +} + +// PIWIK SPECIAL ALIAS HACK - when updating Open Flash Chart, leave this line unchanged +class graph extends Piwik_Visualization_OpenFlashChart {}
\ No newline at end of file diff --git a/core/Visualization/Sparkline.php b/core/Visualization/Sparkline.php new file mode 100644 index 0000000000..65f529f338 --- /dev/null +++ b/core/Visualization/Sparkline.php @@ -0,0 +1,89 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: OpenFlashChart.php 386 2008-03-18 19:27:54Z julien $ + * + * @package Piwik_Visualization + */ + +require_once 'sparkline/lib/Sparkline_Line.php'; + + +/** + * Renders a sparkline image given a PHP data array. + * Using the Sparkline PHP Graphing Library sparkline.org + * + * @package Piwik_Visualization + */ +class Piwik_Visualization_Sparkline implements Piwik_iView +{ + /** + * Sets data. Must have format: array( array('value' => X),array('value' =>Y ), ...) + * + * @param array $data + */ + function setData($data) + { + $this->data = $data; + } + + + function main() + { + $data = $this->data; + $sparkline = new Sparkline_Line(); + + $sparkline->SetColor('lineColor', 22,44,74); // dark blue + $sparkline->SetColorHtml('red', '#FF7F7F'); + $sparkline->SetColorHtml('blue', '#55AAFF'); + $sparkline->SetColorHtml('green', '#75BF7C'); +// $sparkline->SetDebugLevel(DEBUG_NONE); +// $sparkline->SetDebugLevel(DEBUG_ERROR | DEBUG_WARNING | DEBUG_STATS | DEBUG_CALLS | DEBUG_DRAW, 'log.txt'); + + $data = array_reverse($data); + $min = $max= $last = null; + $i = 0; + + foreach($this->data as $row) + { + $value = $row['value']; + + $sparkline->SetData($i, $value); + if( null == $min || $value <= $min[1]) + { + $min = array($i, $value); + } + + if(null == $max || $value >= $max[1]) + { + $max = array($i, $value); + } + + $last = array($i, $value); + + $i++; + } + $sparkline->SetYMin(0); + $sparkline->SetPadding(2); // setpadding is additive + $sparkline->SetPadding(0,//13,//font height + 3, //4 * (strlen("$last[1]")), + 0, //imagefontheight(FONT_2), + 0); + $font = FONT_2; + $sparkline->SetFeaturePoint($min[0]-1,$min[1],'red', 5); + $sparkline->SetFeaturePoint($max[0]-1,$max[1], 'green', 5); + $sparkline->SetFeaturePoint($last[0]-1, $last[1], 'blue',5); + $sparkline->SetLineSize(3); // for renderresampled, linesize is on virtual image + $sparkline->RenderResampled(100, 20, 'lineColor'); + + $this->sparkline = $sparkline; + } + + function render() + { + $this->sparkline->Output(); + } +}
\ No newline at end of file diff --git a/core/iView.php b/core/iView.php new file mode 100644 index 0000000000..c35edafa50 --- /dev/null +++ b/core/iView.php @@ -0,0 +1,27 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: APIable.php 162 2008-01-14 04:27:21Z matt $ + * + * @package Piwik_Visualization + */ + +/** + * Piwik_ViewDataTable must create a $view attribute which implements this interface. + * + * @package Piwik_Visualization + */ +interface Piwik_iView +{ + /** + * Outputs the data. + * Either outputs html, xml, an image, nothing, etc. + * + * @return mixed + * + */ + function render(); +}
\ No newline at end of file diff --git a/core/testMinimumPhpVersion.php b/core/testMinimumPhpVersion.php new file mode 100644 index 0000000000..52c7c6d608 --- /dev/null +++ b/core/testMinimumPhpVersion.php @@ -0,0 +1,98 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id: Common.php 168 2008-01-14 05:26:43Z matt $ + * + * @package Piwik + */ + +/** + * This file is executed before anything else. It checks the minimum Php version required to run Piwik. + * This is done here because on PHP4 piwik would output an error directly. + * Let's try to be user friendly :) + * + * @package Piwik + */ + +// we prefix the global variables +$piwik_minimumPhpVersion = '5.1.3'; +$piwik_currentVersion = phpversion(); + +if( version_compare($piwik_minimumPhpVersion , $piwik_currentVersion ) >= 0 ) +{ + $piwik_errorMessage = "<p><b>To run Piwik you need at least PHP version $piwik_minimumPhpVersion </b></p> + <p>Unfortunately it seems your webserver is using PHP version $piwik_currentVersion. </p> + <p>Please try to update your PHP version, Piwik is really worth it! Nowadays most web hosts + support PHP $piwik_minimumPhpVersion. </p>"; +} + +$piwik_zend_compatibility_mode = ini_get("zend.ze1_compatibility_mode"); + +if($piwik_zend_compatibility_mode == 1) +{ + $piwik_errorMessage = "<p><b>Piwik is not compatible with the directive <code>zend.ze1_compatibility_mode = On</code></b></p> + <p>It seems your php.ini file has <pre>zend.ze1_compatibility_mode = On</pre>It makes PHP5 behave like PHP4. + If you want to use Piwik you need to set <pre>zend.ze1_compatibility_mode = Off</pre> in your php.ini configuration file. You may have to ask your system administrator.</p>"; +} + +function Piwik_ExitWithMessage($message) +{ + $html = '<html> + <head> + <title>Piwik › Error</title> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> + <style> + html { background: #eee; } + body { + background: #fff; + color: #000; + font-family: Georgia, "Times New Roman", Times, serif; + margin-left: 20%; + margin-top: 25px; + margin-right: 20%; + padding: .2em 2em; + } + #h1 { + color: #006; + font-size: 45px; + font-weight: lighter; + } + #subh1 { + color: #879DBD; + font-size: 25px; + font-weight: lighter; + } + p, li, dt { + line-height: 140%; + padding-bottom: 2px; + } + a { color: #006; } + ul, ol { padding: 5px 5px 5px 20px; } + #logo { margin-bottom: 2em; } + code { margin-left: 40px; } + </style> + </head> + <body> + <span id="h1">Piwik </span><span id="subh1"> # open source web analytics</span> + <p>'.$message.'</p> + <ul> + <li><a target="_blank" href="misc/redirectToUrl.php?url=http://piwik.org">Piwik homepage</a></li> + <li><a target="_blank" href="misc/redirectToUrl.php?url=http://piwik.org/demo">Piwik demo</a></li> + </ul> + </body> + </html>'; + echo $html; + exit; +} + +if(isset($piwik_errorMessage)) +{ + Piwik_ExitWithMessage($piwik_errorMessage); +} + +// we now include the upgradephp package to define some functions used in piwik +// that may not be defined in the current php version +require_once "libs/upgradephp/upgrade.php";
\ No newline at end of file |