Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/matomo-org/matomo.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/core
diff options
context:
space:
mode:
authormatt <matt@59fd770c-687e-43c8-a1e3-f5a4ff64c105>2008-08-04 04:11:03 +0400
committermatt <matt@59fd770c-687e-43c8-a1e3-f5a4ff64c105>2008-08-04 04:11:03 +0400
commit995c6623213e211318bb0dd36420ea4c05841f42 (patch)
tree5214705435461179efecb331075a9830a21a5594 /core
parent1dfc7ae2d07f99daccd432762c232b9329ea4285 (diff)
oops i totally screwed up my last commit, deleting /modules instead of renaming it...
Diffstat (limited to 'core')
-rw-r--r--core/API/APIable.php55
-rw-r--r--core/API/Proxy.php485
-rw-r--r--core/API/Request.php588
-rw-r--r--core/Access.php326
-rw-r--r--core/Archive.php224
-rw-r--r--core/Archive/Array.php161
-rw-r--r--core/Archive/Array/IndexedByDate.php122
-rw-r--r--core/Archive/Array/IndexedBySite.php98
-rw-r--r--core/Archive/Single.php463
-rw-r--r--core/ArchiveProcessing.php570
-rw-r--r--core/ArchiveProcessing/Day.php348
-rw-r--r--core/ArchiveProcessing/Period.php270
-rw-r--r--core/ArchiveProcessing/Record.php55
-rw-r--r--core/ArchiveProcessing/Record/Blob.php33
-rw-r--r--core/ArchiveProcessing/Record/BlobArray.php63
-rw-r--r--core/ArchiveProcessing/Record/Manager.php109
-rw-r--r--core/ArchiveProcessing/Record/Numeric.php30
-rw-r--r--core/Auth.php52
-rw-r--r--core/Common.php643
-rw-r--r--core/Config.php223
-rw-r--r--core/Controller.php270
-rw-r--r--core/Cookie.php282
-rw-r--r--core/DataFiles/Browsers.php59
-rw-r--r--core/DataFiles/Countries.php240
-rw-r--r--core/DataFiles/OS.php86
-rw-r--r--core/DataFiles/SearchEngines.php1080
-rw-r--r--core/DataTable.php1014
-rw-r--r--core/DataTable/Array.php152
-rw-r--r--core/DataTable/Filter.php58
-rw-r--r--core/DataTable/Filter/AddConstantMetadata.php43
-rw-r--r--core/DataTable/Filter/AddSummaryRow.php67
-rw-r--r--core/DataTable/Filter/ColumnCallbackAddMetadata.php49
-rw-r--r--core/DataTable/Filter/ColumnCallbackDeleteRow.php44
-rw-r--r--core/DataTable/Filter/ColumnCallbackReplace.php42
-rw-r--r--core/DataTable/Filter/ExcludeLowPopulation.php50
-rw-r--r--core/DataTable/Filter/Limit.php58
-rw-r--r--core/DataTable/Filter/MetadataCallbackAddMetadata.php48
-rw-r--r--core/DataTable/Filter/Null.php35
-rw-r--r--core/DataTable/Filter/Pattern.php44
-rw-r--r--core/DataTable/Filter/PatternRecursive.php77
-rw-r--r--core/DataTable/Filter/ReplaceColumnNames.php93
-rw-r--r--core/DataTable/Filter/ReplaceSummaryRowLabel.php42
-rw-r--r--core/DataTable/Filter/Sort.php121
-rw-r--r--core/DataTable/Manager.php101
-rw-r--r--core/DataTable/Renderer.php104
-rw-r--r--core/DataTable/Renderer/Console.php125
-rw-r--r--core/DataTable/Renderer/Csv.php237
-rw-r--r--core/DataTable/Renderer/Html.php186
-rw-r--r--core/DataTable/Renderer/Json.php52
-rw-r--r--core/DataTable/Renderer/Php.php203
-rw-r--r--core/DataTable/Renderer/Rss.php169
-rw-r--r--core/DataTable/Renderer/Xml.php300
-rw-r--r--core/DataTable/Row.php404
-rw-r--r--core/DataTable/Row/DataTableSummary.php33
-rw-r--r--core/DataTable/Simple.php61
-rw-r--r--core/Date.php341
-rw-r--r--core/ErrorHandler.php68
-rw-r--r--core/ExceptionHandler.php43
-rw-r--r--core/Form.php101
-rw-r--r--core/FrontController.php316
-rw-r--r--core/Log.php162
-rw-r--r--core/Log/APICall.php118
-rw-r--r--core/Log/Error.php118
-rw-r--r--core/Log/Exception.php109
-rw-r--r--core/Log/Message.php76
-rw-r--r--core/LogStats.php303
-rw-r--r--core/LogStats/Action.php242
-rw-r--r--core/LogStats/Config.php80
-rw-r--r--core/LogStats/Db.php261
-rw-r--r--core/LogStats/Generator.php666
-rw-r--r--core/LogStats/Generator/LogStats.php58
-rw-r--r--core/LogStats/Generator/Visit.php51
-rw-r--r--core/LogStats/Visit.php837
-rw-r--r--core/LogStats/javascriptTag.tpl18
-rw-r--r--core/Mail.php31
-rw-r--r--core/Period.php239
-rw-r--r--core/Period/Day.php40
-rw-r--r--core/Period/Month.php48
-rw-r--r--core/Period/Range.php165
-rw-r--r--core/Period/Week.php41
-rw-r--r--core/Period/Year.php49
-rw-r--r--core/Piwik.php1045
-rw-r--r--core/Plugin.php102
-rw-r--r--core/PluginsFunctions/AdminMenu.php33
-rw-r--r--core/PluginsFunctions/Menu.php106
-rw-r--r--core/PluginsFunctions/Sql.php34
-rw-r--r--core/PluginsFunctions/Widget.php18
-rw-r--r--core/PluginsManager.php502
-rw-r--r--core/Site.php67
-rw-r--r--core/SmartyPlugins/function.assignTopBar.php17
-rw-r--r--core/SmartyPlugins/function.hiddenurl.php46
-rw-r--r--core/SmartyPlugins/function.loadJavascriptTranslations.php61
-rw-r--r--core/SmartyPlugins/function.postEvent.php42
-rw-r--r--core/SmartyPlugins/function.url.php31
-rw-r--r--core/SmartyPlugins/modifier.sumtime.php54
-rw-r--r--core/SmartyPlugins/modifier.translate.php34
-rw-r--r--core/SmartyPlugins/modifier.urlRewriteBasicView.php40
-rw-r--r--core/SmartyPlugins/modifier.urlRewriteWithParameters.php23
-rw-r--r--core/TablePartitioning.php132
-rw-r--r--core/Timer.php50
-rw-r--r--core/Translate.php155
-rw-r--r--core/Url.php159
-rw-r--r--core/View.php143
-rw-r--r--core/ViewDataTable.php799
-rw-r--r--core/ViewDataTable/Cloud.php108
-rw-r--r--core/ViewDataTable/GenerateGraphData.php140
-rw-r--r--core/ViewDataTable/GenerateGraphData/ChartEvolution.php21
-rw-r--r--core/ViewDataTable/GenerateGraphData/ChartPie.php16
-rw-r--r--core/ViewDataTable/GenerateGraphData/ChartVerticalBar.php16
-rw-r--r--core/ViewDataTable/GenerateGraphHTML.php141
-rw-r--r--core/ViewDataTable/GenerateGraphHTML/ChartEvolution.php31
-rw-r--r--core/ViewDataTable/GenerateGraphHTML/ChartPie.php15
-rw-r--r--core/ViewDataTable/GenerateGraphHTML/ChartVerticalBar.php16
-rw-r--r--core/ViewDataTable/Html.php274
-rw-r--r--core/ViewDataTable/Sparkline.php67
-rw-r--r--core/Visualization/Chart.php88
-rw-r--r--core/Visualization/ChartEvolution.php58
-rw-r--r--core/Visualization/ChartPie.php41
-rw-r--r--core/Visualization/ChartVerticalBar.php38
-rw-r--r--core/Visualization/Cloud.php169
-rw-r--r--core/Visualization/OpenFlashChart.php1647
-rw-r--r--core/Visualization/Sparkline.php89
-rw-r--r--core/iView.php27
-rw-r--r--core/testMinimumPhpVersion.php98
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&parameter_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 &amp;) eg. module=test&amp;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 = '&amp;';
+
+ $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 '&amp;'
+ * - '"'(double quote) becomes '&quot;'
+ * - '<' (less than) becomes '&lt;'
+ * - '>' (greater than) becomes '&gt;'
+ * - 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 />&nbsp;&nbsp;&nbsp;<b><a href='?module=Login&amp;action=logout'>&rsaquo; ".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 . '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;';
+ foreach($table->getArray() as $descTable => $table)
+ {
+ $output .= $prefix . "<b>". $descTable. "</b><br>";
+ $output .= $prefix . $this->renderTable($table, $prefix . '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;');
+ $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 . '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'
+ );
+ } 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('&', '&amp;', 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 . '&amp;module=CoreHome&amp;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}'> &nbsp; {$wordTruncated} &nbsp; </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 &rsaquo; 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