diff --git a/oss/piwik/trunk/core/Archive/Array.php b/oss/piwik/trunk/core/Archive/Array.php
new file mode 100644
index 00000000..bfebe92a
--- /dev/null
+++ b/oss/piwik/trunk/core/Archive/Array.php
@@ -0,0 +1,188 @@
+archives as $archive)
+ {
+ destroy($archive);
+ }
+ $this->archives = array();
+ }
+
+ 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->addRowsFromArray( 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->addRowsFromArray( 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;
+ }
+
+ /**
+ * Takes a list of fields defining numeric values and returns a quoted string
+ * of the field names fit to be used in the where clause of a SQL Query
+ *
+ * @param array|string $fields array( fieldName1, fieldName2, ...) Names of the mysql table fields to load
+ * @return String
+ */
+ public static function getSqlStringFieldsArray( $fields )
+ {
+ if(!is_array($fields))
+ {
+ return "'$fields'";
+ }
+ return "'" . implode("', '",$fields) . "'";
+ }
+}
diff --git a/oss/piwik/trunk/core/Archive/Array/IndexedByDate.php b/oss/piwik/trunk/core/Archive/Array/IndexedByDate.php
new file mode 100644
index 00000000..0aa27cb8
--- /dev/null
+++ b/oss/piwik/trunk/core/Archive/Array/IndexedByDate.php
@@ -0,0 +1,144 @@
+getTimezone());
+ 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';
+ }
+
+ /**
+ * Adds metadata information to the Piwik_DataTable_Array
+ * using the information given by the Archive
+ *
+ * @param Piwik_DataTable_Array $table
+ * @param Piwik_Archive $archive
+ */
+ protected function loadMetadata(Piwik_DataTable_Array $table, $archive)
+ {
+ $table->metadata[$archive->getPrettyDate()] = array(
+ 'timestamp' => $archive->getTimestampStartDate(),
+ 'site' => $archive->getSite(),
+ 'period' => $archive->getPeriod(),
+ );
+ }
+
+ 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 )
+ {
+ $inNames = $this->getSqlStringFieldsArray($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(', ', array_filter($aIds));
+ if(empty($inIds))
+ {
+ // Probable timezone configuration error, i.e., mismatch between PHP and MySQL server.
+ continue;
+ }
+
+ $sql = "SELECT value, name, date1 as startDate
+ FROM $table
+ WHERE idarchive IN ( $inIds )
+ AND name IN ( $inNames )";
+ $values = $db->fetchAll($sql);
+ foreach($values as $value)
+ {
+ $timestamp = Piwik_Date::factory($value['startDate'])->getTimestamp();
+ $arrayValues[$timestamp][$value['name']] = (float)$value['value'];
+ }
+ }
+
+ $contentArray = array();
+ // we add empty tables so that every requested date has an entry, even if there is nothing
+ // example:
+ 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']->addRowsFromArray($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;
+ }
+}
diff --git a/oss/piwik/trunk/core/Archive/Array/IndexedBySite.php b/oss/piwik/trunk/core/Archive/Array/IndexedBySite.php
new file mode 100644
index 00000000..4b3af2ec
--- /dev/null
+++ b/oss/piwik/trunk/core/Archive/Array/IndexedBySite.php
@@ -0,0 +1,122 @@
+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
+ * ordered by idsite
+ *
+ * @param array|string $fields array( fieldName1, fieldName2, ...) Names of the mysql table fields to load
+ * @return Piwik_DataTable_Array
+ */
+ public function getDataTableFromNumeric( $fields )
+ {
+ $tableArray = $this->getNewDataTableArray();
+ if ($this->getFirstArchive() instanceof Piwik_Archive_Single)
+ {
+ $values = $this->getValues($fields);
+ foreach($this->archives as $idSite => $archive)
+ {
+ $table = new Piwik_DataTable_Simple();
+ if (array_key_exists($idSite, $values))
+ {
+ $table->addRowsFromArray($values[$idSite]);
+ }
+ $tableArray->addTable($table, $idSite);
+ }
+ }
+ elseif ($this->getFirstArchive() instanceof Piwik_Archive_Array)
+ {
+ foreach($this->archives as $idSite => $archive)
+ {
+ $tableArray->addTable($archive->getDataTableFromNumeric($fields), $idSite);
+ }
+ }
+
+ return $tableArray;
+ }
+
+ private function getValues($fields)
+ {
+ $arrayValues = array();
+ foreach($this->loadValuesFromDB($fields) as $value)
+ {
+ $arrayValues[$value['idsite']][$value['name']] = $value['value'];
+ }
+ return $arrayValues;
+ }
+
+ private function loadValuesFromDB($fields)
+ {
+ $sql = "SELECT value, name, idarchive, idsite
+ FROM {$this->getNumericTableName()}
+ WHERE idarchive IN ( {$this->getArchiveIds()} )
+ AND name IN ( {$this->getSqlStringFieldsArray($fields)} )";
+ return Piwik_FetchAll($sql);
+ }
+
+ private function getFirstArchive()
+ {
+ reset($this->archives);
+ return current($this->archives);
+ }
+
+ private function getArchiveIds()
+ {
+ foreach($this->archives as $archive)
+ {
+ if( $this->getNumericTableName() != $archive->archiveProcessing->getTableArchiveNumericName())
+ {
+ throw new Exception("Piwik_Archive_Array_IndexedBySite::getDataTableFromNumeric() algorithm won't work if data is stored in different tables");
+ }
+ $archiveIds[] = $archive->getIdArchive();
+ }
+ return implode(', ', array_filter($archiveIds));
+ }
+
+ private function getNumericTableName()
+ {
+ return $this->getFirstArchive()->archiveProcessing->getTableArchiveNumericName();
+ }
+}
diff --git a/oss/piwik/trunk/core/Archive/Single.php b/oss/piwik/trunk/core/Archive/Single.php
new file mode 100644
index 00000000..68bed364
--- /dev/null
+++ b/oss/piwik/trunk/core/Archive/Single.php
@@ -0,0 +1,485 @@
+blobCached as $name => $blob)
+ {
+ $this->freeBlob($name);
+ }
+ $this->blobCached = array();
+ }
+
+ /**
+ * 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;
+ }
+
+ public function getPeriod()
+ {
+ return $this->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))
+ {
+ $timestamp = $this->archiveProcessing->getTimestampStartDate();
+ if(!empty($timestamp))
+ {
+ return $timestamp;
+ }
+ }
+ 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.
+ */
+ public function prepareArchive()
+ {
+ $archiveJustProcessed = false;
+ if(!$this->alreadyChecked)
+ {
+ $this->isThereSomeVisits = false;
+ $this->alreadyChecked = true;
+ $logMessage = "Preparing archive: ";
+ // 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() ) )
+ {
+ Piwik::log("$logMessage skipped, archive is before the website was created.");
+ return;
+ }
+
+ // if the starting date is in the future we know there is no visit
+ if( $this->period->getDateStart()->subDay(2)->isLater( Piwik_Date::today() ) )
+ {
+ Piwik::log("$logMessage skipped, archive is after 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();
+ if(empty($idArchive))
+ {
+ Piwik::log("$logMessage not archived yet, starting processing...");
+ $archiveJustProcessed = true;
+ $archiveProcessing->launchArchiving();
+ $idArchive = $archiveProcessing->getIdArchive();
+ }
+ else
+ {
+ Piwik::log("$logMessage archive already processed [id = $idArchive]...");
+ }
+ $this->isThereSomeVisits = $archiveProcessing->isThereSomeVisits;
+ $this->idArchive = $idArchive;
+ $this->archiveProcessing = $archiveProcessing;
+ }
+ return $archiveJustProcessed;
+ }
+
+ /**
+ * 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];
+ }
+
+ if($name == 'idarchive')
+ {
+ return $this->idArchive;
+ }
+
+ 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;
+ }
+
+ $db = Zend_Registry::get('db');
+ $value = $db->fetchOne("/* SHARDING_ID_SITE = ".$this->site->getId()." */ SELECT value
+ FROM $table
+ WHERE idarchive = ?
+ AND name = ?",
+ array( $this->idArchive , $name)
+ );
+
+ if($value === false)
+ {
+ if($typeValue == 'numeric'
+ && $this->cacheEnabledForNumeric)
+ {
+ $this->numericCached[$name] = false;
+ }
+ return $value;
+ }
+
+ // uncompress when selecting from the BLOB table
+ if($typeValue == 'blob' && $db->hasBlobDataType())
+ {
+ $value = gzuncompress($value);
+ }
+
+ if($typeValue == 'numeric'
+ && $this->cacheEnabledForNumeric)
+ {
+ $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
+ */
+ 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, $addMetadataSubtableId);
+
+ // we edit the subtable ID so that it matches the newly table created in memory
+ // NB: we dont overwrite the datatableid in the case we are displaying the table expanded.
+ if($addMetadataSubtableId)
+ {
+ // this will be written back to the column 'idsubdatatable' just before rendering, see Renderer/Php.php
+ $row->addMetadata('idsubdatatable_in_db', $row->getIdSubDataTable());
+ }
+ $row->setSubtable( $subDataTableLoaded );
+ }
+ }
+ }
+
+
+ /**
+ * Free the blob cache memory array
+ */
+ public function freeBlob( $name )
+ {
+ $this->blobCached[$name] = null;
+ unset($this->blobCached[$name]);
+ }
+
+ /**
+ * Fetches all blob fields name_* at once for the current archive for performance reasons.
+ *
+ * @return false if no visits
+ */
+ public function preFetchBlob( $name )
+ {
+ if(!$this->isThereSomeVisits)
+ {
+ return false;
+ }
+
+ $tableBlob = $this->archiveProcessing->getTableArchiveBlobName();
+
+ $db = Zend_Registry::get('db');
+ $hasBlobs = $db->hasBlobDataType();
+ $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'];
+
+ if($hasBlobs)
+ {
+ $this->blobCached[$name] = gzuncompress($value);
+ }
+ else
+ {
+ $this->blobCached[$name] = $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',
+ * 'nb_visits_converted'
+ * );
+ *
+ * @param string|array $fields Name or array of names of Archive fields
+ *
+ * @return Piwik_DataTable_Simple
+ */
+ public function getDataTableFromNumeric( $fields )
+ {
+ if(!is_array($fields))
+ {
+ $fields = array($fields);
+ }
+
+ $values = array();
+ foreach($fields as $field)
+ {
+ $values[$field] = $this->getNumeric($field);
+ }
+
+ $table = new Piwik_DataTable_Simple();
+ $table->addRowsFromArray($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->addRowsFromSerializedArray($data);
+ }
+
+ if($data === false
+ && $idSubTable !== null)
+ {
+ throw new Exception(Piwik_TranslateException('General_ExceptionSubtableNotFoundInArchive'));
+ }
+
+ 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);
+ $this->freeBlob($name);
+ return $dataTableToLoad;
+ }
+}
diff --git a/oss/piwik/trunk/core/ArchiveProcessing/Day.php b/oss/piwik/trunk/core/ArchiveProcessing/Day.php
new file mode 100644
index 00000000..5464eb70
--- /dev/null
+++ b/oss/piwik/trunk/core/ArchiveProcessing/Day.php
@@ -0,0 +1,438 @@
+db = Zend_Registry::get('db');
+ $this->debugAlwaysArchive = Zend_Registry::get('config')->Debug->always_archive_data_day;
+ }
+
+ /**
+ * 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'
+ */
+ 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,
+ sum(case visit_goal_converted when 1 then 1 else 0 end) as nb_visits_converted
+ FROM ".$this->logTable."
+ WHERE visit_last_action_time >= ?
+ AND visit_last_action_time <= ?
+ AND idsite = ?
+ ORDER BY NULL";
+ $row = $this->db->fetchRow($query, array($this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->idsite ) );
+ if($row === false || $row === null || $row['nb_visits'] == 0)
+ {
+ return;
+ }
+
+ $this->isThereSomeVisits = true;
+
+ foreach($row as $name => $value)
+ {
+ $this->insertNumericRecord($name, $value);
+ }
+
+ $this->setNumberOfVisits($row['nb_visits']);
+ $this->setNumberOfVisitsConverted($row['nb_visits_converted']);
+ Piwik_PostEvent('ArchiveProcessing_Day.compute', $this);
+ }
+
+ /**
+ * 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_last_action_time >= ?
+ AND visit_last_action_time <= ?
+ AND idsite = ?";
+ $data = $this->db->fetchRow($query, array( $this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->idsite ));
+
+ foreach($data as $label => &$count)
+ {
+ $count = array($labelCount => $count);
+ }
+ $table = new Piwik_DataTable();
+ $table->addRowsFromArrayWithIndexLabel($data);
+ return $table;
+ }
+
+ public function getDataTableFromArray( $array )
+ {
+ $table = new Piwik_DataTable();
+ $table->addRowsFromArrayWithIndexLabel($array);
+ return $table;
+ }
+
+ /**
+ * Output:
+ * array(
+ * LABEL => array(
+ * Piwik_Archive::INDEX_NB_UNIQ_VISITORS => 0,
+ * Piwik_Archive::INDEX_NB_VISITS => 0
+ * ),
+ * LABEL2 => array(
+ * [...]
+ * )
+ * )
+ *
+ * Helper function that returns an array with 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 array will have a row per distinct operating systems,
+ * and a column per stat (nb of visits, max actions, etc)
+ *
+ * 'label' Piwik_Archive::INDEX_NB_UNIQ_VISITORS Piwik_Archive::INDEX_NB_VISITS etc.
+ * Linux 27 66 ...
+ * Windows XP 12 ...
+ * Mac OS 15 36 ...
+ *
+ * @param string $label Table log_visit field name to be use to compute common stats
+ * @return array
+ */
+ public function getArrayInterestForLabel($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,
+ sum(case visit_goal_converted when 1 then 1 else 0 end) as nb_visits_converted
+ FROM ".$this->logTable."
+ WHERE visit_last_action_time >= ?
+ AND visit_last_action_time <= ?
+ AND idsite = ?
+ GROUP BY label
+ ORDER BY NULL";
+ $query = $this->db->query($query, array( $this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->idsite ) );
+
+ $interest = array();
+ while($row = $query->fetch())
+ {
+ if(!isset($interest[$row['label']])) $interest[$row['label']]= $this->getNewInterestRow();
+ $this->updateInterestStats( $row, $interest[$row['label']]);
+ }
+ return $interest;
+ }
+
+ /**
+ * 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->setColumns( array('label' => $label) + $row->getColumns());
+ $row->addSubtable($subTable);
+ }
+ // 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::addRowsFromArrayWithIndexLabel()
+ * 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->addRowsFromArrayWithIndexLabel($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 (
+ * // Yahoo.com => array( kwd1 => stats, kwd2 => stats )
+ * LABEL => array(col1 => X, col2 => Y),
+ * LABEL2 => array(col1 => X, col2 => Y),
+ * )
+ *
+ * The $subArrayLevel1ByKey must have the format
+ * Example: array(
+ * // Yahoo.com => array( stats )
+ * 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 getDataTableWithSubtablesFromArraysIndexedByLabel( $arrayLevel0, $subArrayLevel1ByKey )
+ {
+ $tablesByLabel = array();
+ foreach($arrayLevel0 as $label => $aAllRowsForThisLabel)
+ {
+ $table = new Piwik_DataTable();
+ $table->addRowsFromArrayWithIndexLabel($aAllRowsForThisLabel);
+ $tablesByLabel[$label] = $table;
+ }
+ $parentTableLevel0 = new Piwik_DataTable();
+ $parentTableLevel0->addRowsFromArrayWithIndexLabel($subArrayLevel1ByKey, $tablesByLabel);
+
+ return $parentTableLevel0;
+ }
+
+ /**
+ * 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,
+ Piwik_Archive::INDEX_NB_VISITS_CONVERTED=> 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['nb_uniq_visitors'];
+ $oldRowToUpdate[Piwik_Archive::INDEX_NB_VISITS] += $newRowToAdd['nb_visits'];
+ $oldRowToUpdate[Piwik_Archive::INDEX_NB_ACTIONS] += $newRowToAdd['nb_actions'];
+ $oldRowToUpdate[Piwik_Archive::INDEX_MAX_ACTIONS] = (float)max($newRowToAdd['max_actions'], $oldRowToUpdate[Piwik_Archive::INDEX_MAX_ACTIONS]);
+ $oldRowToUpdate[Piwik_Archive::INDEX_SUM_VISIT_LENGTH] += $newRowToAdd['sum_visit_length'];
+ $oldRowToUpdate[Piwik_Archive::INDEX_BOUNCE_COUNT] += $newRowToAdd['bounce_count'];
+ $oldRowToUpdate[Piwik_Archive::INDEX_NB_VISITS_CONVERTED] += $newRowToAdd['nb_visits_converted'];
+ }
+
+ public function queryConversionsBySegment($segments = '')
+ {
+ if(!empty($segments))
+ {
+ $segments = ", ". $segments;
+ }
+ $query = "SELECT idgoal,
+ count(*) as nb_conversions,
+ sum(revenue) as revenue
+ $segments
+ FROM ".$this->logConversionTable."
+ WHERE server_time >= ?
+ AND server_time <= ?
+ AND idsite = ?
+ GROUP BY idgoal $segments
+ ORDER BY NULL";
+ $query = $this->db->query($query, array( $this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->idsite ));
+ return $query;
+ }
+
+ public function queryConversionsBySingleSegment($segment)
+ {
+ $query = "SELECT idgoal,
+ count(*) as nb_conversions,
+ sum(revenue) as revenue,
+ $segment as label
+ FROM ".$this->logConversionTable."
+ WHERE server_time >= ?
+ AND server_time <= ?
+ AND idsite = ?
+ GROUP BY idgoal, label
+ ORDER BY NULL";
+ $query = $this->db->query($query, array( $this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->idsite ));
+ return $query;
+ }
+
+ /**
+ * Given an array of stats, it will process the sum of goal conversions
+ * and sum of revenue and add it in the stats array in two new fields.
+ *
+ * @param $interestByLabel Passed by reference, it will be modified as follows:
+ * Input:
+ * array(
+ * LABEL => array( Piwik_Archive::INDEX_NB_VISITS => X,
+ * Piwik_Archive::INDEX_GOALS => array(
+ * idgoal1 => array( [...] ),
+ * idgoal2 => array( [...] ),
+ * ),
+ * [...] ),
+ * LABEL2 => array( Piwik_Archive::INDEX_NB_VISITS => Y, [...] )
+ * );
+ *
+ *
+ * Output:
+ * array(
+ * LABEL => array( Piwik_Archive::INDEX_NB_VISITS => X,
+ * Piwik_Archive::INDEX_NB_CONVERSIONS => Y, // sum of all conversions
+ * Piwik_Archive::INDEX_REVENUE => Z, // sum of all revenue
+ * Piwik_Archive::INDEX_GOALS => array(
+ * idgoal1 => array( [...] ),
+ * idgoal2 => array( [...] ),
+ * ),
+ * [...] ),
+ * LABEL2 => array( Piwik_Archive::INDEX_NB_VISITS => Y, [...] )
+ * );
+ * )
+ *
+ * @param array $interestByLabel Passed by reference, will be modified
+ */
+ function enrichConversionsByLabelArray(&$interestByLabel)
+ {
+ foreach($interestByLabel as $label => &$values)
+ {
+ if(isset($values[Piwik_Archive::INDEX_GOALS]))
+ {
+ $revenue = $conversions = 0;
+ foreach($values[Piwik_Archive::INDEX_GOALS] as $idgoal => $goalValues)
+ {
+ $revenue += $goalValues[Piwik_Archive::INDEX_GOAL_REVENUE];
+ $conversions += $goalValues[Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS];
+ }
+ $values[Piwik_Archive::INDEX_NB_CONVERSIONS] = $conversions;
+ $values[Piwik_Archive::INDEX_REVENUE] = $revenue;
+ }
+ }
+ }
+
+ /**
+ * @param array $interestByLabelAndSubLabel Passed by reference, will be modified
+ */
+ function enrichConversionsByLabelArrayHasTwoLevels(&$interestByLabelAndSubLabel)
+ {
+ foreach($interestByLabelAndSubLabel as $mainLabel => &$interestBySubLabel)
+ {
+ $this->enrichConversionsByLabelArray($interestBySubLabel);
+ }
+ }
+
+ function updateGoalStats($newRowToAdd, &$oldRowToUpdate)
+ {
+ $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS] += $newRowToAdd['nb_conversions'];
+ $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_REVENUE] += $newRowToAdd['revenue'];
+ }
+
+ function getNewGoalRow()
+ {
+ return array( Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS => 0,
+ Piwik_Archive::INDEX_GOAL_REVENUE => 0,
+ );
+ }
+
+ function getGoalRowFromQueryRow($queryRow)
+ {
+ return array( Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS => $queryRow['nb_conversions'],
+ Piwik_Archive::INDEX_GOAL_REVENUE => $queryRow['revenue'],
+ );
+ }
+}
diff --git a/oss/piwik/trunk/core/ArchiveProcessing/Period.php b/oss/piwik/trunk/core/ArchiveProcessing/Period.php
new file mode 100644
index 00000000..cde4b577
--- /dev/null
+++ b/oss/piwik/trunk/core/ArchiveProcessing/Period.php
@@ -0,0 +1,358 @@
+ column name renamed) of the columns for which sum operation is invalid.
+ * The summed value is not accurate and these columns will be renamed accordingly.
+ */
+ static public $invalidSummedColumnNameToRenamedName = array(
+ Piwik_Archive::INDEX_NB_UNIQ_VISITORS => Piwik_Archive::INDEX_SUM_DAILY_NB_UNIQ_VISITORS
+ );
+
+ public function __construct()
+ {
+ parent::__construct();
+ $this->debugAlwaysArchive = Zend_Registry::get('config')->Debug->always_archive_data_period;
+ }
+
+ /**
+ * 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)
+ {
+ if($name == 'nb_uniq_visitors' && ($this->periodId == Piwik::$idPeriods['week'] || $this->periodId == Piwik::$idPeriods['month']))
+ {
+ $value = (float) $this->computeNbUniqVisitors();
+ }
+ $records[$name] = new Piwik_ArchiveProcessing_Record_Numeric(
+ $name,
+ $value
+ );
+ $this->insertRecord($records[$name]);
+ }
+
+ // 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 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_RecordArray 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
+ * @param array (current_column_name => new_column_name) for columns that must change names when summed (eg. unique visitors go from nb_uniq_visitors to sum_daily_nb_uniq_visitors)
+ * @param int Max row count of parent datatable to archive
+ * @param int Max row count of children datatable(s) to archive
+ * @param string Column name to sort by, before truncating rows (ie. if there are more rows than the specified max row count)
+ *
+ * @return array array (
+ * nameTable1 => number of rows,
+ * nameTable2 => number of rows,
+ * )
+ */
+ public function archiveDataTable( $aRecordName,
+ $invalidSummedColumnNameToRenamedName = null,
+ $maximumRowsInDataTableLevelZero = null,
+ $maximumRowsInSubDataTable = null,
+ $columnToSortByBeforeTruncation = null )
+ {
+ if(!is_array($aRecordName))
+ {
+ $aRecordName = array($aRecordName);
+ }
+
+ $nameToCount = array();
+ foreach($aRecordName as $recordName)
+ {
+ $table = $this->getRecordDataTableSum($recordName, $invalidSummedColumnNameToRenamedName);
+
+ $nameToCount[$recordName]['level0'] = $table->getRowsCount();
+ $nameToCount[$recordName]['recursive'] = $table->getRowsCountRecursive();
+
+ $blob = $table->getSerialized( $maximumRowsInDataTableLevelZero, $maximumRowsInSubDataTable, $columnToSortByBeforeTruncation );
+ destroy($table);
+ $this->insertBlobRecord($recordName, $blob);
+ }
+ Piwik_DataTable_Manager::getInstance()->deleteAll();
+
+ 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
+ * @param array columns in the array (old name, new name) to be renamed as the sum operation is not valid on them (eg. nb_uniq_visitors->sum_daily_nb_uniq_visitors)
+ * @return Piwik_DataTable
+ */
+ protected function getRecordDataTableSum( $name, $invalidSummedColumnNameToRenamedName )
+ {
+ $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);
+ }
+
+ if(is_null($invalidSummedColumnNameToRenamedName))
+ {
+ $invalidSummedColumnNameToRenamedName = self::$invalidSummedColumnNameToRenamedName;
+ }
+ foreach($invalidSummedColumnNameToRenamedName as $oldName => $newName)
+ {
+ $table->renameColumn($oldName, $newName);
+ }
+ 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.
+ */
+ protected function compute()
+ {
+ $this->archiveNumericValuesMax( 'max_actions' );
+ $toSum = array(
+ 'nb_uniq_visitors',
+ 'nb_visits',
+ 'nb_actions',
+ 'sum_visit_length',
+ 'bounce_count',
+ 'nb_visits_converted',
+ );
+ $record = $this->archiveNumericValuesSum($toSum);
+
+ $nbVisits = $record['nb_visits']->value;
+ $nbVisitsConverted = $record['nb_visits_converted']->value;
+ $this->isThereSomeVisits = ( $nbVisits > 0);
+ if($this->isThereSomeVisits === false)
+ {
+ return;
+ }
+ $this->setNumberOfVisits($nbVisits);
+ $this->setNumberOfVisitsConverted($nbVisitsConverted);
+ Piwik_PostEvent('ArchiveProcessing_Period.compute', $this);
+ }
+
+ protected function computeNbUniqVisitors()
+ {
+ $query = "
+ SELECT count(distinct visitor_idcookie) as nb_uniq_visitors
+ FROM ".$this->logTable."
+ WHERE visit_last_action_time >= ?
+ AND visit_last_action_time <= ?
+ AND idsite = ?";
+
+ return Zend_Registry::get('db')->fetchOne($query, array( $this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->idsite ));
+ }
+
+ /**
+ * Called at the end of the archiving process.
+ * Does some cleaning job in the database.
+ */
+ protected function postCompute()
+ {
+ parent::postCompute();
+
+ foreach($this->archives as $archive)
+ {
+ destroy($archive);
+ }
+ $this->archives = array();
+
+ $blobTable = $this->tableArchiveBlob->getTableName();
+ $numericTable = $this->tableArchiveNumeric->getTableName();
+
+ $key = 'lastPurge_' . $blobTable;
+ $timestamp = Piwik_GetOption($key);
+ if(!$timestamp
+ || $timestamp < time() - 86400)
+ {
+ Piwik_SetOption($key, time());
+
+ // we delete out of date daily archives from table, maximum once per day
+ // we only delete archives processed that are older than 1 day, to not delete archives we just processed
+ $yesterday = Piwik_Date::factory('yesterday')->getDateTime();
+ $result = Piwik_FetchAll("
+ SELECT idarchive
+ FROM $numericTable
+ WHERE name='done'
+ AND value = ". Piwik_ArchiveProcessing::DONE_OK_TEMPORARY ."
+ AND ts_archived < ?", array($yesterday));
+
+ $idArchivesToDelete = array();
+ if(!empty($result))
+ {
+ foreach($result as $row) {
+ $idArchivesToDelete[] = $row['idarchive'];
+ }
+ $query = "/* SHARDING_ID_SITE = ".$this->idsite." */
+ DELETE
+ FROM %s
+ WHERE idarchive IN (".implode(',',$idArchivesToDelete).")
+ ";
+
+ Piwik_Query(sprintf($query, $blobTable));
+ Piwik_Query(sprintf($query, $numericTable));
+ }
+ Piwik::log("Purging temporary archives: done [ purged archives older than $yesterday from $blobTable and $numericTable ] [Deleted IDs: ". implode(',',$idArchivesToDelete)."]");
+ }
+ else
+ {
+ Piwik::log("Purging temporary archives: skipped.");
+ }
+
+ }
+}
diff --git a/oss/piwik/trunk/core/ArchiveProcessing/Record.php b/oss/piwik/trunk/core/ArchiveProcessing/Record.php
new file mode 100644
index 00000000..7c073f09
--- /dev/null
+++ b/oss/piwik/trunk/core/ArchiveProcessing/Record.php
@@ -0,0 +1,42 @@
+name = $name;
+ $this->value = $value;
+ }
+
+ function __destruct()
+ {
+ destroy($this->value);
+ }
+}
diff --git a/oss/piwik/trunk/core/ArchiveProcessing/Record/Blob.php b/oss/piwik/trunk/core/ArchiveProcessing/Record/Blob.php
new file mode 100644
index 00000000..4eac2bef
--- /dev/null
+++ b/oss/piwik/trunk/core/ArchiveProcessing/Record/Blob.php
@@ -0,0 +1,41 @@
+value);
+ }
+
+ public function __toString()
+ {
+ return $this->name ." = BLOB";//". gzuncompress($this->value);
+ }
+}
diff --git a/oss/piwik/trunk/core/ArchiveProcessing/Record/Numeric.php b/oss/piwik/trunk/core/ArchiveProcessing/Record/Numeric.php
new file mode 100644
index 00000000..8b156b69
--- /dev/null
+++ b/oss/piwik/trunk/core/ArchiveProcessing/Record/Numeric.php
@@ -0,0 +1,31 @@
+name ." = ". $this->value;
+ }
+}
diff --git a/oss/piwik/trunk/core/ArchiveProcessing/RecordArray.php b/oss/piwik/trunk/core/ArchiveProcessing/RecordArray.php
new file mode 100644
index 00000000..3d041e4d
--- /dev/null
+++ b/oss/piwik/trunk/core/ArchiveProcessing/RecordArray.php
@@ -0,0 +1,70 @@
+ 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
+ * @subpackage Piwik_ArchiveProcessing
+ */
+class Piwik_ArchiveProcessing_RecordArray extends Piwik_ArchiveProcessing_Record
+{
+ protected $records = array();
+
+ 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;
+ }
+ $this->records[] = new Piwik_ArchiveProcessing_Record_Blob( $newName, $value );
+ }
+ }
+
+ public function __destruct()
+ {
+ foreach($this->records as $record)
+ {
+ destroy($record);
+ }
+ $this->records = array();
+ }
+
+ public function get()
+ {
+ return $this->records;
+ }
+}
diff --git a/oss/piwik/trunk/core/DataFiles/Countries.php b/oss/piwik/trunk/core/DataFiles/Countries.php
new file mode 100644
index 00000000..615273ff
--- /dev/null
+++ b/oss/piwik/trunk/core/DataFiles/Countries.php
@@ -0,0 +1,290 @@
+ 'unk',
+ 'ad' => 'eur',
+ 'ae' => 'asi',
+ 'af' => 'asi',
+ 'ag' => 'ams',
+ 'ai' => 'ams',
+ 'al' => 'eur',
+ 'am' => 'asi',
+ 'an' => 'ams',
+ 'ao' => 'afr',
+ 'aq' => 'oce',
+ 'ar' => 'ams',
+ 'as' => 'oce',
+ 'at' => 'eur',
+ 'au' => 'oce',
+ 'aw' => 'ams',
+ 'ax' => 'eur',
+ 'az' => 'asi',
+ 'ba' => 'eur',
+ 'bb' => 'ams',
+ 'bd' => 'asi',
+ 'be' => 'eur',
+ 'bf' => 'afr',
+ 'bg' => 'eur',
+ 'bh' => 'asi',
+ 'bi' => 'afr',
+ 'bj' => 'afr',
+ 'bl' => 'ams',
+ 'bm' => 'ams',
+ 'bn' => 'asi',
+ 'bo' => 'ams',
+ 'br' => 'ams',
+ 'bs' => 'ams',
+ 'bt' => 'asi',
+ 'bv' => 'oce',
+ 'bw' => 'afr',
+ 'by' => 'eur',
+ 'bz' => 'ams',
+ 'ca' => 'amn',
+ 'cc' => 'oce',
+ 'cd' => 'afr',
+ 'cf' => 'afr',
+ 'cg' => 'afr',
+ 'ch' => 'eur',
+ 'ci' => 'afr',
+ 'ck' => 'asi',
+ 'cl' => 'ams',
+ 'cm' => 'afr',
+ 'cn' => 'asi',
+ 'co' => 'ams',
+ 'cr' => 'ams',
+ 'cu' => 'ams',
+ 'cv' => 'afr',
+ 'cx' => 'oce',
+ 'cy' => 'eur',
+ 'cz' => 'eur',
+ 'de' => 'eur',
+ 'dj' => 'afr',
+ 'dk' => 'eur',
+ 'dm' => 'ams',
+ 'do' => 'ams',
+ 'dz' => 'afr',
+ 'ec' => 'ams',
+ 'ee' => 'eur',
+ 'eg' => 'afr',
+ 'eh' => 'afr',
+ 'er' => 'afr',
+ 'es' => 'eur',
+ 'et' => 'afr',
+ 'fi' => 'eur',
+ 'fj' => 'oce',
+ 'fk' => 'ams',
+ 'fm' => 'oce',
+ 'fo' => 'eur',
+ 'fr' => 'eur',
+ 'ga' => 'afr',
+ 'gb' => 'eur',
+ 'gd' => 'ams',
+ 'ge' => 'asi',
+ 'gf' => 'ams',
+ 'gg' => 'eur',
+ 'gh' => 'afr',
+ 'gi' => 'afr',
+ 'gl' => 'amn',
+ 'gm' => 'afr',
+ 'gn' => 'afr',
+ 'gp' => 'ams',
+ 'gq' => 'afr',
+ 'gr' => 'eur',
+ 'gs' => 'eur',
+ 'gt' => 'ams',
+ 'gu' => 'asi',
+ 'gw' => 'afr',
+ 'gy' => 'ams',
+ 'hk' => 'asi',
+ 'hm' => 'oce',
+ 'hn' => 'ams',
+ 'hr' => 'eur',
+ 'ht' => 'ams',
+ 'hu' => 'eur',
+ 'id' => 'asi',
+ 'ie' => 'eur',
+ 'il' => 'asi',
+ 'im' => 'eur',
+ 'in' => 'asi',
+ 'io' => 'asi',
+ 'iq' => 'asi',
+ 'ir' => 'asi',
+ 'is' => 'eur',
+ 'it' => 'eur',
+ 'je' => 'eur',
+ 'jm' => 'ams',
+ 'jo' => 'asi',
+ 'jp' => 'asi',
+ 'ke' => 'afr',
+ 'kg' => 'asi',
+ 'kh' => 'asi',
+ 'ki' => 'oce',
+ 'km' => 'afr',
+ 'kn' => 'ams',
+ 'kp' => 'asi',
+ 'kr' => 'asi',
+ 'kw' => 'asi',
+ 'ky' => 'ams',
+ 'kz' => 'asi',
+ 'la' => 'asi',
+ 'lb' => 'asi',
+ 'lc' => 'ams',
+ 'li' => 'eur',
+ 'lk' => 'asi',
+ 'lr' => 'afr',
+ 'ls' => 'afr',
+ 'lt' => 'eur',
+ 'lu' => 'eur',
+ 'lv' => 'eur',
+ 'ly' => 'afr',
+ 'ma' => 'afr',
+ 'mc' => 'eur',
+ 'md' => 'eur',
+ 'me' => 'eur',
+ 'mf' => 'ams',
+ 'mg' => 'afr',
+ 'mh' => 'oce',
+ 'mk' => 'eur',
+ 'ml' => 'afr',
+ 'mm' => 'asi',
+ 'mn' => 'asi',
+ 'mo' => 'asi',
+ 'mp' => 'asi',
+ 'mq' => 'ams',
+ 'mr' => 'afr',
+ 'ms' => 'ams',
+ 'mt' => 'eur',
+ 'mu' => 'afr',
+ 'mv' => 'asi',
+ 'mw' => 'afr',
+ 'mx' => 'ams',
+ 'my' => 'asi',
+ 'mz' => 'afr',
+ 'na' => 'afr',
+ 'nc' => 'oce',
+ 'ne' => 'afr',
+ 'nf' => 'oce',
+ 'ng' => 'afr',
+ 'ni' => 'ams',
+ 'nl' => 'eur',
+ 'no' => 'eur',
+ 'np' => 'asi',
+ 'nr' => 'oce',
+ 'nu' => 'oce',
+ 'nz' => 'oce',
+ 'om' => 'asi',
+ 'pa' => 'ams',
+ 'pe' => 'ams',
+ 'pf' => 'oce',
+ 'pg' => 'oce',
+ 'ph' => 'asi',
+ 'pk' => 'asi',
+ 'pl' => 'eur',
+ 'pm' => 'amn',
+ 'pn' => 'oce',
+ 'pr' => 'ams',
+ 'ps' => 'asi',
+ 'pt' => 'eur',
+ 'pw' => 'oce',
+ 'py' => 'ams',
+ 'qa' => 'asi',
+ 're' => 'afr',
+ 'ro' => 'eur',
+ 'rs' => 'eur',
+ 'ru' => 'eur',
+ 'rw' => 'afr',
+ 'sa' => 'asi',
+ 'sb' => 'oce',
+ 'sc' => 'afr',
+ 'sd' => 'afr',
+ 'se' => 'eur',
+ 'sg' => 'asi',
+ 'sh' => 'afr',
+ 'si' => 'eur',
+ 'sj' => 'eur',
+ 'sk' => 'eur',
+ 'sl' => 'afr',
+ 'sm' => 'eur',
+ 'sn' => 'afr',
+ 'so' => 'afr',
+ 'sr' => 'ams',
+ 'st' => 'afr',
+ 'sv' => 'ams',
+ 'sy' => 'asi',
+ 'sz' => 'afr',
+ 'tc' => 'ams',
+ 'td' => 'afr',
+ 'tf' => 'oce',
+ 'tg' => 'afr',
+ 'th' => 'asi',
+ 'tj' => 'asi',
+ 'tk' => 'oce',
+ 'tl' => 'asi',
+ 'tm' => 'asi',
+ 'tn' => 'afr',
+ 'to' => 'oce',
+ 'tr' => 'eur',
+ 'tt' => 'ams',
+ 'tv' => 'asi',
+ 'tw' => 'asi',
+ 'tz' => 'afr',
+ 'ua' => 'eur',
+ 'ug' => 'afr',
+ 'um' => 'oce',
+ 'us' => 'amn',
+ 'uy' => 'ams',
+ 'uz' => 'asi',
+ 'va' => 'eur',
+ 'vc' => 'ams',
+ 've' => 'ams',
+ 'vg' => 'ams',
+ 'vi' => 'ams',
+ 'vn' => 'asi',
+ 'vu' => 'oce',
+ 'wf' => 'oce',
+ 'ws' => 'asi',
+ 'ye' => 'asi',
+ 'yt' => 'afr',
+ 'za' => 'afr',
+ 'zm' => 'afr',
+ 'zw' => 'afr',
+
+ // exceptionally reserved
+ 'ac' => 'afr', // .ac TLD
+ 'cp' => 'ams',
+ 'dg' => 'asi',
+ 'ea' => 'afr',
+ 'eu' => 'eur', // .eu TLD
+ 'fx' => 'eur',
+ 'ic' => 'afr',
+ 'su' => 'eur', // .su TLD
+ 'ta' => 'afr',
+ 'uk' => 'eur', // .uk TLD
+
+ // transitionally reserved
+ 'bu' => 'asi',
+ 'cs' => 'eur', // former Serbia and Montenegro
+ 'nt' => 'asi',
+ 'sf' => 'eur',
+ 'tp' => 'oce', // .tp TLD
+ 'yu' => 'eur', // .yu TLD
+ 'zr' => 'afr',
+ );
+}
diff --git a/oss/piwik/trunk/core/DataFiles/LanguageToCountry.php b/oss/piwik/trunk/core/DataFiles/LanguageToCountry.php
new file mode 100644
index 00000000..8c284b18
--- /dev/null
+++ b/oss/piwik/trunk/core/DataFiles/LanguageToCountry.php
@@ -0,0 +1,66 @@
+ 'bg', // Bulgarian => Bulgaria
+ 'ca' => 'es', // Catalan => Spain
+ 'cs' => 'cz', // Czech => Czech Republic
+ 'da' => 'dk', // Danish => Denmark
+ 'de' => 'de', // German => Germany
+ 'el' => 'gr', // Greek => Greece
+ 'es' => 'es', // Spanish => Spain
+ 'et' => 'ee', // Estonian => Estonia
+ 'fa' => 'ir', // Farsi => Iran
+ 'fi' => 'fi', // Finnish => Finland
+ 'fr' => 'fr', // French => France
+ 'he' => 'il', // Hebrew => Israel
+ 'hr' => 'hr', // Croatian => Croatia
+ 'hu' => 'hu', // Hungarian => Hungary
+ 'id' => 'id', // Indonesian => Indonesia
+ 'is' => 'is', // Icelandic => Iceland
+ 'it' => 'it', // Italian => Italy
+ 'ja' => 'jp', // Japanese => Japan
+ 'ko' => 'kr', // Korean => South Korea
+ 'lt' => 'lt', // Lithuanian => Lithuania
+ 'lv' => 'lv', // Latvian => Latvia
+ 'mk' => 'mk', // Macedonian => Macedonia
+ 'ms' => 'my', // Malay => Malaysia
+ 'nb' => 'no', // Bokmål => Norway
+ 'nl' => 'nl', // Dutch => Netherlands
+ 'nn' => 'no', // Nynorsk => Norway
+ 'no' => 'no', // Norwegian => Norway
+ 'pl' => 'pl', // Polish => Poland
+ 'pt' => 'pt', // Portugese => Portugal
+ 'ro' => 'ro', // Romanian => Romania
+ 'ru' => 'ru', // Russian => Russia
+ 'sk' => 'sk', // Slovak => Slovakia
+ 'sl' => 'si', // Slovene => Slovenia
+ 'sq' => 'al', // Albanian => Albania
+ 'sr' => 'rs', // Serbian => Serbia
+ 'sv' => 'se', // Swedish => Sweden
+ 'th' => 'th', // Thai => Thailand
+ 'tr' => 'tr', // Turkish => Turkey
+ 'uk' => 'ua', // Ukrainian => Ukraine
+ );
+}
diff --git a/oss/piwik/trunk/core/DataFiles/SearchEngines.php b/oss/piwik/trunk/core/DataFiles/SearchEngines.php
new file mode 100644
index 00000000..2aea94ae
--- /dev/null
+++ b/oss/piwik/trunk/core/DataFiles/SearchEngines.php
@@ -0,0 +1,1343 @@
+ array( SearchEngineName, KeywordParameter, [path containing the keyword], [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
+ * To help Piwik link directly the search engine result page for the keyword, specify the third entry in the array
+ * using the macro {k} that will automatically be replaced by the keyword.
+ *
+ * A simple example is:
+ * 'www.google.com' => array('Google', 'q', 'search?q={k}'),
+ *
+ * A more complicated example, with an array of possible variable names, and a custom charset:
+ * 'www.baidu.com' => array('Baidu', array('wd', 'word', 'kw'), 's?wd={k}', 'gb2312'),
+ */
+if(!isset($GLOBALS['Piwik_SearchEngines'] ))
+{
+ $GLOBALS['Piwik_SearchEngines'] = array(
+ // 1
+ '1.cz' => array('1.cz', 'q', 'index.php?q={k}', 'iso-8859-2'),
+ 'www.1.cz' => array('1.cz', 'q', false, 'iso-8859-2'),
+
+ // 123people
+ 'www.123people.com' => array('123people', 'search_term'),
+ 'www.123people.de' => array('123people', 'search_term'),
+ 'www.123people.es' => array('123people', 'search_term'),
+ 'www.123people.fr' => array('123people', 'search_term'),
+
+ // 1und1
+ 'portal.1und1.de' => array('1und1', 'search'),
+ 'search.1und1.de' => array('1und1', 'su', 'search/web/?mc=suche%40web%40home.suche%40web&allparams=&smode=&su={k}&search=Suche&webRb='),
+
+ // A9
+ 'www.a9.com' => array('A9', ''),
+ 'a9.com' => array('A9', ''),
+
+ // Abacho
+ 'search.abacho.com' => array('Abacho', 'q'),
+
+ // ABCsøk
+ 'abcsok.no' => array('ABCsøk', 'q'),
+ 'www.abcsok.no' => array('ABCsøk', 'q'),
+
+ // about
+ 'search.about.com' => array('About', 'terms'),
+
+ // Acoon
+ 'www.acoon.de' => array('Acoon', 'begriff'),
+
+ // Alexa
+ 'www.alexa.com' => array('Alexa', 'q', 'search?q={k}'),
+ 'alexa.com' => array('Alexa', 'q'),
+
+ // Alice Adsl
+ 'rechercher.aliceadsl.fr' => array('Alice Adsl', 'qs'),
+ 'search.alice.it' => array('Alice (powered by Virgilio)', 'qt'),
+ 'ricerca.alice.it' => array('Alice (powered by Virgilio)', 'qs'),
+
+ // 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'),
+
+ // Apollo Latvia
+ 'apollo.lv/portal/search/' => array('Apollo lv', 'q'),
+
+ // APOLLO7
+ 'www.apollo7.de' => array('Apollo7', 'query'),
+ 'apollo7.de' => array('Apollo7', 'query'),
+
+ // AOL
+ 'search.aol.com' => array('AOL', array('query', 'q'), 'aol/search?query={k}'),
+ 'aolsearch.aol.com' => array('AOL', array('query', 'q')),
+ 'www.aolrecherche.aol.fr' => array('AOL', array('query', 'q')),
+ 'www.aolrecherches.aol.fr' => array('AOL', array('query', 'q')),
+ 'www.aolimages.aol.fr' => array('AOL', array('query', 'q')),
+ 'www.recherche.aol.fr' => array('AOL', array('query', 'q')),
+ 'find.web.aol.com' => array('AOL', array('query', 'q')),
+ 'recherche.aol.ca' => array('AOL', array('query', 'q')),
+ 'aolsearch.aol.co.uk' => array('AOL', array('query', 'q')),
+ 'search.aol.co.uk' => array('AOL', array('query', 'q')),
+ 'aolrecherche.aol.fr' => array('AOL', array('query', 'q')),
+ 'sucheaol.aol.de' => array('AOL', array('query', 'q')),
+ 'suche.aol.de' => array('AOL', array('query', 'q')),
+ 'suche.aolsvc.de' => array('AOL', array('query', 'q')),
+ 'aolbusqueda.aol.com.mx' => array('AOL', array('query', 'q')),
+ 'alicesuchet.aol.de' => array('AOL', array('query', 'q')),
+ 'suche.aolsvc.de' => array('AOL', array('query', 'q')),
+ 'suche.aol.de' => array('AOL', array('query', 'q')),
+ 'alicesuche.aol.de' => array('AOL', array('query', 'q')),
+ 'suchet2.aol.de' => array('AOL', array('query', 'q')),
+ 'search.hp.my.aol.com.au' => array('AOL', array('query', 'q')),
+ 'search.hp.my.aol.de' => array('AOL', array('query', 'q')),
+
+ // 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', array('ask', 'q'), 'web?q={k}'),
+ 'web.ask.com' => array('Ask', array('ask', 'q')),
+ 'images.ask.com' => array('Ask', 'q'),
+ 'ask.reference.com' => array('Ask', 'q'),
+ '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', '?q={k}', 'windows-1250'),
+
+ // Austronaut
+ 'www2.austronaut.at' => array('Austronaut', 'begriff'),
+
+ // Babylon
+ 'search.babylon.com' => array('Babylon (Powered by Google)', 'q'),
+
+ // Baidu
+ 'www.baidu.com' => array('Baidu', array('wd', 'word', 'kw'), 's?wd={k}', 'gb2312'),
+ 'www1.baidu.com' => array('Baidu', array('wd', 'word', 'kw'), false, 'gb2312'),
+ 'zhidao.baidu.com' => array('Baidu', array('wd', 'word', 'kw'), false, 'gb2312'),
+ 'tieba.baidu.com' => array('Baidu', array('wd', 'word', 'kw'), false, 'gb2312'),
+ 'news.baidu.com' => array('Baidu', array('wd', 'word', 'kw'), false, 'gb2312'),
+ 'web.gougou.com' => array('Baidu', 'search'),
+
+ // 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'),
+
+ // Bing
+ 'www.bing.com' => array('Bing', 'q', 'search?q={k}'),
+
+ // Bing Images
+ 'www.bing.com/images/search'=> array('Bing Images', 'q', 'search?q={k}'),
+
+ // 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
+ 'search.centrum.cz' => array('Centrum', 'q', 'index.php?q={k}', 'windows-1250'),
+ 'fulltext.centrum.cz' => array('Centrum', 'q', false, 'windows-1250'),
+ 'morfeo.centrum.cz' => array('Centrum', 'q', false, 'windows-1250'),
+
+ // Chello
+ 'www.chello.fr' => array('Chello', 'q1'),
+
+ // Club Internet
+ 'recherche.club-internet.fr'=> array('Club Internet', 'q'),
+
+ // Clusty
+ 'clusty.com' => array('Clusty', 'query', 'search?query={k}'),
+
+ // Conduit
+ 'search.conduit.com' => array('Conduit.com', 'q', 'Results.aspx?q={k}'),
+
+ // Comcast
+ 'www.comcast.net' => array('Comcast', 'query'),
+ 'search.comcast.net' => array('Comcast', 'q'),
+ 'search3.comcast.com' => array('Comcast', 'url'),
+
+ // 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'),
+
+ // Cuil
+ 'www.cuil.com' => array('Cuil', 'q', 'search?q={k}'),
+
+ // Daemon search
+ 'www.daemon-search.com' => array('Daemon search', 'q', 'explore/web?q={k}'),
+
+ // DasOertliche
+ 'www.dasoertliche.de' => array('DasOertliche', 'kw'),
+
+ // DasTelefonbuch
+ 'www.4call.dastelefonbuch.de'=> array('DasTelefonbuch', 'kw'),
+
+ // Defind.de
+ 'suche.defind.de' => array('Defind.de', 'search'),
+
+ // Delfi Latvia
+ 'smart.delfi.lv' => array('Delfi lv', 'q'),
+
+ // Delfi
+ 'otsing.delfi.ee' => array('Delfi EE', 'q'),
+
+ // Deskfeeds
+ 'www.deskfeeds.com' => array('Deskfeeds', 'sx'),
+
+ // Digg
+ 'digg.com' => array('Digg', 's', 'search?s={k}'),
+
+ // 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'),
+
+ // DuckDuckGo
+ 'duckduckgo.com' => array('DuckDuckGo', 'q', '?q={k}'),
+
+ // earthlink
+ 'search.earthlink.net' => array('Earthlink', 'q'),
+
+ // Ecosia (powered by Bing)
+ 'ecosia.org' => array('Ecosia', 'q', 'search.php?q={k}'),
+ 'www.ecosia.org' => array('Ecosia', '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'),
+
+ // Everyclick
+ 'www.everyclick.com' => array('Everyclick', 'keyword'),
+
+ // 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', 'search/results?q={k}'),
+ 'www.exalead.com' => array('Exalead', 'q'),
+
+ // eo
+ 'eo.st' => array('eo', 'q'),
+
+ // Facebook
+ 'www.facebook.com' => array('Facebook', 'q', 'search/?q={k}'),
+
+ // 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'),
+ 'www.fireball.de' => array('Fireball', 'q'),
+
+ // Firstfind
+ 'www.firstsfind.com' => array('Firstsfind', 'qry'),
+
+ // Fixsuche
+ 'www.fixsuche.de' => array('Fixsuche', 'q'),
+
+ // Flix
+ 'www.flix.de' => array('Flix.de', 'keyword'),
+
+ // Forestle
+ 'de.forestle.org' => array('Forestle', 'q', 'search.php?q={k}'),
+ 'at.forestle.org' => array('Forestle', 'q', 'search.php?q={k}'),
+ 'ch.forestle.org' => array('Forestle', 'q', 'search.php?q={k}'),
+ 'us.forestle.org' => array('Forestle', 'q', 'search.php?q={k}'),
+ 'fr.forestle.org' => array('Forestle', 'q', 'search.php?q={k}'),
+
+ // Free
+ 'search.free.fr' => array('Free', 'q'),
+ 'search1-2.free.fr' => array('Free', 'q'),
+ 'search1-1.free.fr' => array('Free', 'q'),
+
+ // Freecause
+ 'search.freecause.com' => array('FreeCause', 'q'),
+
+ // Freenet
+ 'suche.freenet.de' => array('Freenet', 'query'),
+
+ // FriendFeed
+ 'friendfeed.com' => array('FriendFeed', 'q'),
+
+ // 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'),
+
+ // GMX
+ 'suche.gmx.net' => array('GMX', 'su'),
+ 'www.gmx.net' => array('GMX', 'su'),
+
+ // Gnadenmeer
+ 'www.gnadenmeer.de' => array('Gnadenmeer', 'keyword'),
+
+ // goo
+ 'search.goo.ne.jp' => array('goo', 'mt'),
+ 'ocnsearch.goo.ne.jp' => array('goo', 'mt'),
+
+ // Google
+ 'www.google.com' => array('Google', 'q', 'search?q={k}'),
+ 'www2.google.com' => array('Google', 'q'),
+ 'ipv6.google.com' => array('Google', 'q'),
+ 'w.google.com' => array('Google', 'q'),
+ 'ww.google.com' => array('Google', 'q'),
+ 'wwwgoogle.com' => array('Google', 'q'),
+ 'www.goggle.com' => array('Google', 'q'),
+ 'www.gogole.com' => array('Google', 'q'),
+ 'www.gppgle.com' => array('Google', 'q'),
+ 'go.google.com' => array('Google', 'q'),
+ 'www.google.ad' => array('Google', 'q'),
+ 'www.google.ae' => array('Google', 'q'),
+ 'www.google.am' => array('Google', 'q'),
+ 'www.google.it.ao' => 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.ba' => array('Google', 'q'),
+ 'www.google.be' => array('Google', 'q'),
+ 'www.google.bf' => array('Google', 'q'),
+ 'www.google.bg' => array('Google', 'q'),
+ 'google.bg' => array('Google', 'q'),
+ 'www.google.bi' => array('Google', 'q'),
+ 'www.google.bj' => array('Google', 'q'),
+ 'www.google.bs' => array('Google', 'q'),
+ 'www.google.ca' => array('Google', 'q'),
+ 'ww.google.ca' => array('Google', 'q'),
+ 'w.google.ca' => array('Google', 'q'),
+ 'www.google.cat' => array('Google', 'q'),
+ 'www.google.cc' => array('Google', 'q'),
+ 'www.google.cd' => array('Google', 'q'),
+ 'google.cf' => 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'),
+ 'google.co.ck' => array('Google', 'q'),
+ 'www.google.cl' => array('Google', 'q'),
+ 'www.google.cn' => array('Google', 'q'),
+ 'google.cm' => 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'),
+ 'google.dm' => array('Google', 'q'),
+ 'google.dz' => array('Google', 'q'),
+ 'www.google.ee' => 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.googel.fi' => array('Google', 'q'),
+ 'www.google.fm' => 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'),
+ 'www.google.ga' => array('Google', 'q'),
+ 'google.ge' => array('Google', 'q'),
+ 'w.google.ge' => array('Google', 'q'),
+ 'ww.google.ge' => array('Google', 'q'),
+ 'www.google.ge' => array('Google', 'q'),
+ 'www.google.gg' => array('Google', 'q'),
+ 'google.gr' => array('Google', 'q'),
+ 'www.google.gl' => array('Google', 'q'),
+ 'www.google.gm' => array('Google', 'q'),
+ 'www.google.gp' => array('Google', 'q'),
+ 'www.google.gr' => array('Google', 'q'),
+ 'www.google.gy' => array('Google', 'q'),
+ 'www.google.hn' => array('Google', 'q'),
+ 'www.google.hr' => array('Google', 'q'),
+ 'www.google.ht' => array('Google', 'q'),
+ 'www.google.hu' => array('Google', 'q'),
+ 'www.google.ie' => array('Google', 'q'),
+ 'www.google.im' => array('Google', 'q'),
+ 'www.google.is' => array('Google', 'q'),
+ 'www.google.it' => array('Google', 'q'),
+ 'www.google.je' => array('Google', 'q'),
+ 'www.google.jo' => array('Google', 'q'),
+ 'www.google.ki' => array('Google', 'q'),
+ 'www.google.kg' => array('Google', 'q'),
+ 'www.google.kz' => array('Google', 'q'),
+ 'www.google.la' => array('Google', 'q'),
+ 'www.google.li' => array('Google', 'q'),
+ 'www.google.lk' => array('Google', 'q'),
+ 'www.google.lt' => array('Google', 'q'),
+ 'www.google.lu' => array('Google', 'q'),
+ 'www.google.lv' => array('Google', 'q'),
+ 'www.google.md' => array('Google', 'q'),
+ 'www.google.me' => array('Google', 'q'),
+ 'www.google.mg' => array('Google', 'q'),
+ 'www.google.mk' => array('Google', 'q'),
+ 'www.google.ml' => array('Google', 'q'),
+ 'www.google.mn' => array('Google', 'q'),
+ 'www.google.ms' => array('Google', 'q'),
+ 'www.google.mu' => array('Google', 'q'),
+ 'www.google.mv' => array('Google', 'q'),
+ 'www.google.mw' => array('Google', 'q'),
+ 'www.google.ne' => array('Google', 'q'),
+ 'www.google.nl' => array('Google', 'q'),
+ 'www.google.no' => array('Google', 'q'),
+ 'www.google.nr' => array('Google', 'q'),
+ 'www.google.nu' => array('Google', 'q'),
+ 'www.google.ps' => array('Google', 'q'),
+ 'www.google.pl' => array('Google', 'q'),
+ 'www.google.pn' => array('Google', 'q'),
+ 'www.google.pt' => array('Google', 'q'),
+ 'www.google.ro' => array('Google', 'q'),
+ 'www.google.rs' => array('Google', 'q'),
+ 'www.google.ru' => array('Google', 'q'),
+ 'www.google.rw' => array('Google', 'q'),
+ 'www.google.sc' => array('Google', 'q'),
+ 'www.google.se' => array('Google', 'q'),
+ 'www.google.sh' => array('Google', 'q'),
+ 'www.google.si' => array('Google', 'q'),
+ 'www.google.sk' => array('Google', 'q'),
+ 'www.google.sm' => array('Google', 'q'),
+ 'www.google.sn' => array('Google', 'q'),
+ 'www.google.st' => array('Google', 'q'),
+ 'www.google.td' => array('Google', 'q'),
+ 'www.google.tg' => array('Google', 'q'),
+ 'www.google.tk' => array('Google', 'q'),
+ 'www.google.tl' => array('Google', 'q'),
+ 'www.google.tm' => array('Google', 'q'),
+ 'www.google.to' => array('Google', 'q'),
+ 'www.google.tt' => array('Google', 'q'),
+ 'www.google.uz' => array('Google', 'q'),
+ 'www.google.vu' => array('Google', 'q'),
+ 'www.google.vg' => array('Google', 'q'),
+ 'www.google.ws' => array('Google', 'q'),
+ 'www.google.co.bw' => 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.ma' => array('Google', 'q'),
+ 'www.google.co.mz' => array('Google', 'q'),
+ 'www.google.co.nz' => array('Google', 'q'),
+ 'www.google.co.th' => array('Google', 'q'),
+ 'www.google.co.tz' => array('Google', 'q'),
+ 'www.google.co.ug' => array('Google', 'q'),
+ 'www.google.co.uk' => array('Google', 'q'),
+ 'www.google.co.uz' => array('Google', 'q'),
+ 'www.google.co.vi' => array('Google', 'q'),
+ 'www.google.co.ve' => array('Google', 'q'),
+ 'www.google.co.za' => array('Google', 'q'),
+ 'www.google.co.zm' => array('Google', 'q'),
+ 'www.google.co.zw' => array('Google', 'q'),
+ 'www.google.com.af' => array('Google', 'q'),
+ 'www.google.com.ag' => array('Google', 'q'),
+ 'www.google.com.ai' => array('Google', 'q'),
+ 'www.google.com.ar' => array('Google', 'q'),
+ 'www.google.com.au' => array('Google', 'q'),
+ 'www.google.com.bd' => array('Google', 'q'),
+ 'www.google.com.bh' => array('Google', 'q'),
+ 'www.google.com.bn' => array('Google', 'q'),
+ 'www.google.com.bo' => array('Google', 'q'),
+ 'www.google.com.br' => array('Google', 'q'),
+ 'www.google.com.by' => array('Google', 'q'),
+ 'www.google.com.bz' => array('Google', 'q'),
+ 'www.google.com.co' => array('Google', 'q'),
+ 'www.google.com.cu' => array('Google', 'q'),
+ 'www.google.com.do' => array('Google', 'q'),
+ 'www.google.com.ec' => array('Google', 'q'),
+ 'www.google.com.eg' => array('Google', 'q'),
+ 'www.google.com.et' => array('Google', 'q'),
+ 'www.google.com.fj' => array('Google', 'q'),
+ 'www.google.com.gh' => array('Google', 'q'),
+ 'www.google.com.gi' => 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.jm' => array('Google', 'q'),
+ 'www.google.com.kh' => array('Google', 'q'),
+ 'www.google.com.kw' => array('Google', 'q'),
+ 'www.google.com.lb' => 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.na' => array('Google', 'q'),
+ 'www.google.com.nf' => array('Google', 'q'),
+ 'www.google.com.ng' => array('Google', 'q'),
+ 'www.google.com.ni' => array('Google', 'q'),
+ 'www.google.com.np' => array('Google', 'q'),
+ 'www.google.com.om' => 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.ru' => array('Google', 'q'),
+ 'www.google.com.sa' => array('Google', 'q'),
+ 'www.google.com.sb' => array('Google', 'q'),
+ 'www.google.com.sg' => array('Google', 'q'),
+ 'www.google.com.sl' => array('Google', 'q'),
+ 'www.google.com.sv' => array('Google', 'q'),
+ 'www.google.com.tj' => 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'),
+
+ // 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.adelphia.net' => array('Google', 'q'),
+ 'so.qq.com' => array('Google', 'word'),
+ 'misc.skynet.be' => array('Google', 'keywords'),
+ 'verden.abcsok.no' => array('Google', 'q'),
+ 'search3.incredimail.com' => array('Google', 'q'),
+ 'search.incredimail.com' => array('Google', 'q'),
+ 'search.sweetim.com' => array('Google', 'q'),
+
+ // Google Earth
+ 'www.googleearth.de' => array('Google', 'q'),
+ 'www.googleearth.fr' => array('Google', 'q'),
+
+ // Google Blogsearch
+ 'blogsearch.google.com' => array('Google Blogsearch', 'q', 'blogsearch?q={k}'),
+ 'blogsearch.google.net' => array('Google Blogsearch', 'q'),
+ 'blogsearch.google.at' => array('Google Blogsearch', 'q'),
+ 'blogsearch.google.be' => array('Google Blogsearch', 'q'),
+ 'blogsearch.google.ch' => array('Google Blogsearch', 'q'),
+ 'blogsearch.google.de' => array('Google Blogsearch', 'q'),
+ 'blogsearch.google.es' => array('Google Blogsearch', 'q'),
+ 'blogsearch.google.fr' => array('Google Blogsearch', 'q'),
+ 'blogsearch.google.it' => array('Google Blogsearch', 'q'),
+ 'blogsearch.google.nl' => array('Google Blogsearch', 'q'),
+ 'blogsearch.google.pl' => array('Google Blogsearch', 'q'),
+ 'blogsearch.google.ru' => array('Google Blogsearch', 'q'),
+ 'blogsearch.google.co.in' => array('Google Blogsearch', 'q'),
+ 'blogsearch.google.co.uk' => array('Google Blogsearch', 'q'),
+
+ // Google Custom Search
+ 'www.google.com/cse' => array('Google Custom Search', 'q'),
+
+ // Google translation
+ 'translate.google.com' => array('Google Translations', 'q'),
+
+ // Google Images
+ 'images.google.com' => array('Google Images', 'q', 'images?q={k}'),
+ 'images.google.at' => array('Google Images', 'q'),
+ 'images.google.be' => array('Google Images', 'q'),
+ 'images.google.bg' => array('Google Images', 'q'),
+ 'images.google.ca' => array('Google Images', 'q'),
+ 'images.google.ch' => array('Google Images', 'q'),
+ 'images.google.ci' => array('Google Images', 'q'),
+ 'images.google.cz' => array('Google Images', 'q'),
+ 'images.google.de' => array('Google Images', 'q'),
+ 'images.google.dk' => array('Google Images', 'q'),
+ 'images.google.ee' => array('Google Images', 'q'),
+ 'images.google.es' => array('Google Images', 'q'),
+ 'images.google.fi' => array('Google Images', 'q'),
+ 'images.google.fr' => array('Google Images', 'q'),
+ 'images.google.gg' => array('Google Images', 'q'),
+ 'images.google.gr' => array('Google Images', 'q'),
+ 'images.google.hr' => array('Google Images', 'q'),
+ 'images.google.hu' => array('Google Images', 'q'),
+ 'images.google.it' => array('Google Images', 'q'),
+ 'images.google.lt' => 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.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.uk' => array('Google Images', 'q'),
+ 'images.google.co.ve' => array('Google Images', 'q'),
+ 'images.google.co.za' => array('Google Images', 'q'),
+ 'images.google.com.ar' => array('Google Images', 'q'),
+ 'images.google.com.au' => 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.kw' => 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.sa' => 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'),
+
+ // Google News
+ 'news.google.com' => array('Google News', 'q'),
+ 'news.google.at' => array('Google News', 'q'),
+ 'news.google.ca' => array('Google News', 'q'),
+ 'news.google.ch' => array('Google News', 'q'),
+ 'news.google.cl' => array('Google News', 'q'),
+ 'news.google.de' => array('Google News', 'q'),
+ 'news.google.es' => array('Google News', 'q'),
+ 'news.google.fr' => array('Google News', 'q'),
+ 'news.google.ie' => array('Google News', 'q'),
+ 'news.google.it' => array('Google News', 'q'),
+ 'news.google.lt' => array('Google News', 'q'),
+ 'news.google.lu' => array('Google News', 'q'),
+ 'news.google.se' => array('Google News', 'q'),
+ 'news.google.sm' => array('Google News', 'q'),
+ 'news.google.co.in' => array('Google News', 'q'),
+ 'news.google.co.jp' => array('Google News', 'q'),
+ 'news.google.co.uk' => array('Google News', 'q'),
+ 'news.google.co.ve' => array('Google News', 'q'),
+ 'news.google.com.ar' => array('Google News', 'q'),
+ 'news.google.com.au' => array('Google News', 'q'),
+ 'news.google.com.co' => array('Google News', 'q'),
+ 'news.google.com.hk' => array('Google News', 'q'),
+ 'news.google.com.ly' => array('Google News', 'q'),
+ 'news.google.com.mx' => array('Google News', 'q'),
+ 'news.google.com.pe' => array('Google News', 'q'),
+ 'news.google.com.tw' => array('Google News', 'q'),
+
+ // Google syndicated search
+ 'googlesyndicatedsearch.com'=> array('Google syndicated search', 'q'),
+
+ // Goyellow.de
+ 'www.goyellow.de' => array('GoYellow.de', 'MDN'),
+
+ // Gule Sider
+ 'www.gulesider.no' => array('Gule Sider', 'q'),
+
+ // 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'),
+
+ // Holmes.ge
+ 'holmes.ge' => array('Holmes', 'q'),
+
+ // Hooseek.com
+ 'www.hooseek.com' => array('Hooseek', 'recherche'),
+
+ // 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/results.php?q={k}'),
+ 'search.icq.com' => array('ICQ', 'q'),
+
+ // Ilse
+ 'www.ilse.nl' => array('Ilse NL', 'search_for', '?search_for={k}'),
+
+ // 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'),
+
+ // kostenlos
+ 'www.kostenlos.de' => array('kostenlos.de', 'q'),
+
+ // Kvasir
+ 'kvasir.no' => array('Kvasir', 'q'),
+ 'www.kvasir.no' => array('Kvasir', 'q'),
+
+ // Latne
+ 'www.latne.lv' => array('Latne', 'q'),
+
+ // 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
+ 'search.live.com' => array('Live', 'q', 'results.aspx?q={k}'),
+ 'beta.search.live.com' => array('Live', 'q'),
+ 'www.live.com' => array('Live', 'q'),
+ 'search.msn.com' => array('Live', 'q'),
+ 'beta.search.msn.fr' => array('Live', 'q'),
+ 'search.msn.fr' => array('Live', 'q'),
+ 'search.msn.es' => array('Live', 'q'),
+ 'search.msn.se' => array('Live', 'q'),
+ 'search.latam.msn.com' => array('Live', 'q'),
+ 'search.msn.nl' => array('Live', 'q'),
+ 'leguide.fr.msn.com' => array('Live', 's'),
+ 'leguide.msn.fr' => array('Live', 's'),
+ 'search.msn.co.jp' => array('Live', 'q'),
+ 'search.msn.no' => array('Live', 'q'),
+ 'search.msn.at' => array('Live', 'q'),
+ 'search.msn.com.hk' => array('Live', 'q'),
+ 'search.t1msn.com.mx' => array('Live', 'q'),
+ 'fr.ca.search.msn.com' => array('Live', 'q'),
+ 'search.msn.be' => array('Live', 'q'),
+ 'search.fr.msn.be' => array('Live', 'q'),
+ 'search.msn.it' => array('Live', 'q'),
+ 'sea.search.msn.it' => array('Live', 'q'),
+ 'sea.search.msn.fr' => array('Live', 'q'),
+ 'sea.search.msn.de' => array('Live', 'q'),
+ 'sea.search.msn.com' => array('Live', 'q'),
+ 'sea.search.fr.msn.be' => array('Live', 'q'),
+ 'search.msn.com.tw' => array('Live', 'q'),
+ 'search.msn.de' => array('Live', 'q'),
+ 'search.msn.co.uk' => array('Live', 'q'),
+ 'search.msn.co.za' => array('Live', 'q'),
+ 'search.msn.ch' => array('Live', 'q'),
+ 'search.msn.es' => array('Live', 'q'),
+ 'search.msn.com.br' => array('Live', 'q'),
+ 'search.ninemsn.com.au' => array('Live', 'q'),
+ 'search.msn.dk' => array('Live', 'q'),
+ 'search.arabia.msn.com' => array('Live', 'q'),
+ 'search.prodigy.msn.com' => array('Live', 'q'),
+
+ // Looksmart
+ 'www.looksmart.com' => array('Looksmart', 'key'),
+
+ // Lo.st
+ 'lo.st' => array('Lo.st (Powered by Google)', 'x_query'),
+
+ // 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'),
+
+ // maailm.com
+ 'www.maailm.com' => array('maailm.com', 'tekst'),
+
+ // Mail.ru
+ 'go.mail.ru' => array('Mailru', 'q', 'search?q={k}', 'windows-1251'),
+
+ // 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'),
+
+ // Meta
+ 'meta.ua' => array('Meta.ua', 'q'),
+
+ // Metacrawler
+ 'search.metacrawler.com' => array('Metacrawler', 'general'),
+
+ // Metager
+ 'meta.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'),
+ 'home.meinestadt.de' => array('Meinestadt.de', 'words'),
+
+ // Mister Wong
+ 'www.mister-wong.com' => array('Mister Wong', 'keywords', 'search/?keywords={k}'),
+ 'www.mister-wong.de' => array('Mister Wong', 'keywords'),
+
+ // 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'),
+
+ // El Mundo
+ 'ariadna.elmundo.es' => array('El Mundo', 'q'),
+
+ // MySpace
+ 'searchservice.myspace.com' => array('MySpace', 'qry', 'index.cfm?fuseaction=sitesearch.results&type=Web&qry={k}'),
+
+ // MySearch / MyWay / MyWebSearch (default: powered by Ask.com)
+ 'www.mysearch.com' => array('MyWebSearch', 'searchfor', 'search/Ajmain.jhtml?searchfor={k}'),
+ 'ms114.mysearch.com' => array('MyWebSearch', 'searchfor'),
+ 'ms146.mysearch.com' => array('MyWebSearch', 'searchfor'),
+ 'kf.mysearch.myway.com' => array('MyWebSearch', 'searchfor'),
+ 'ki.mysearch.myway.com' => array('MyWebSearch', 'searchfor'),
+ 'search.myway.com' => array('MyWebSearch', 'searchfor'),
+ 'search.mywebsearch.com' => array('MyWebSearch', 'searchfor', 'mywebsearch/Ajmain.jhtml?searchfor={k}'),
+
+
+ // Najdi
+ 'www.najdi.si' => array('Najdi.si', 'q'),
+
+ // Naver
+ 'search.naver.com' => array('Naver', 'query', 'search.naver?query={k}', 'x-windows-949'),
+
+ // Needtofind
+ 'ko.search.need2find.com' => array('Needtofind', 'searchfor'),
+
+ // Neti
+ 'www.neti.ee' => array('Neti', 'query', 'cgi-bin/otsing?query={k}', 'iso-8859-1'),
+
+ // 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'),
+
+ // Nifty
+ 'search.nifty.com' => array('Nifty', 'q'),
+
+ // Nigma
+ 'www.nigma.ru' => array('Nigma', 's', 'index.php?s={k}'),
+ 'nigma.ru' => array('Nigma', 's'),
+
+ // 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'),
+
+ // Online.no
+ 'www.online.no' => array('Online.no', 'q'),
+ 'online.no' => array('Online.no', 'q'),
+
+ // 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'),
+
+ // Opplysningen 1881
+ 'www.1881.no' => array('Opplysningen 1881', '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'),
+
+ // Poisk.Ru
+ 'poisk.ru' => array('Poisk.Ru', 'text', 'cgi-bin/poisk?text={k}', 'windows-1251'),
+
+ // Postami
+ 'www.postami.com' => array('Postami', 'query'),
+
+ // qip
+ 'start.qip.ru' => array('qip.ru', 'query', 'search?query={k}'),
+ 'search.qip.ru' => array('qip.ru', 'query'),
+
+ // Quick searches
+ 'data.quicksearches.net' => array('QuickSearches', 'q'),
+
+ // Qualigo
+ 'www.qualigo.at' => array('Qualigo', 'q'),
+ 'www.qualigo.ch' => array('Qualigo', 'q'),
+ 'www.qualigo.de' => array('Qualigo', 'q'),
+ 'www.qualigo.nl' => array('Qualigo', 'q'),
+
+ // Rambler
+ 'nova.rambler.ru' => array('Rambler', array('query', 'words'), 'search?query={k}'),
+ 'search.rambler.ru' => array('Rambler', 'words'),
+ 'www.rambler.ru' => array('Rambler', 'words'),
+
+ // Reacteur.com
+ 'www.reacteur.com' => array('Reacteur', 'kw'),
+
+ // RPMFind
+ 'www.rpmfind.net' => array('rpmfind', 'query', 'linux/rpm2html/search.php?query={k}'),
+ 'rpmfind.net' => array('rpmfind', 'query'),
+ 'fr2.rpmfind.net' => array('rpmfind', 'query'),
+
+ // 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'),
+
+ // Searchalot
+ 'www.searchalot.com' => array('Searchalot', 'q', '?q={k}'),
+ 'searchalot.com' => array('Searchalot', 'q'),
+
+ // Seek
+ 'www.seek.fr' => array('Searchalot', 'qry_str'),
+
+ // Searchscout
+ 'www.searchscout.com' => array('Search Scout', 'gt_keywords'),
+
+ // Searchy
+ 'www.searchy.co.uk' => array('Searchy', 'search_term'),
+
+ // Sesam
+ 'sesam.no' => array('Sesam', 'q'),
+
+ // Setooz
+ 'bg.setooz.com' => array('Setooz', 'query'),
+ 'el.setooz.com' => array('Setooz', 'query'),
+ 'et.setooz.com' => array('Setooz', 'query'),
+ 'fi.setooz.com' => array('Setooz', 'query'),
+ 'hu.setooz.com' => array('Setooz', 'query'),
+ 'lt.setooz.com' => array('Setooz', 'query'),
+ 'lv.setooz.com' => array('Setooz', 'query'),
+ 'no.setooz.com' => array('Setooz', 'query'),
+ 'pl.setooz.com' => array('Setooz', 'query'),
+ 'sk.setooz.com' => array('Setooz', 'query'),
+ 'sv.setooz.com' => array('Setooz', 'query'),
+ 'tr.setooz.com' => array('Setooz', 'query'),
+ 'uk.setooz.com' => array('Setooz', 'query'),
+
+ // Seznam
+ 'search.seznam.cz' => array('Seznam', 'q'),
+ 'search1.seznam.cz' => array('Seznam', 'q'),
+ 'search2.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'),
+
+ // Sogou
+ 'www.sogou.com' => array('Sogou', 'query', 'web?query={k}'),
+
+ // soso.com
+ 'www.soso.com' => array('Soso', 'w', 'q?w={k}', 'gb2312'),
+
+ // Sphere
+ 'www.sphere.com' => array('Sphere', 'q'),
+
+ // Start.no
+ 'www.start.no' => array('Google', 'q'),
+
+ // Startpagina
+ 'startgoogle.startpagina.nl'=> array('Startpagina (Google)', 'q'),
+
+ // Suchmaschine.com
+ 'www.suchmaschine.com' => array('Suchmaschine.com', 'suchstr'),
+
+ // 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'),
+
+ // 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', false, 'windows-1250'),
+
+ // Tixuma
+ 'www.tixuma.de' => array('Tixuma', 'sc', 'index.php?mp=search&stp=&sc={k}&tg=0'),
+
+ // T-Online
+ 'suche.t-online.de' => array('T-Online', 'q'),
+ 'navigationshilfe.t-online.de'=> array('T-Online', 'q', 'dtag/dns/results?mode=search_top&q={k}'),
+
+ // Trouvez.com
+ 'www.trouvez.com' => array('Trouvez.com', 'query'),
+
+ // Trusted-Search
+ 'www.trusted--search.com' => array('Trusted Search', 'w'),
+
+ // Twingly
+ 'www.twingly.com' => array('Twingly', 'q'),
+
+ // Vinden
+ 'zoek.vinden.nl' => array('Vinden', 'query'),
+
+ // Vindex
+ 'www.vindex.nl' => array('Vindex', 'search_for'),
+
+ // Virgilio
+ 'search.virgilio.it' => array('Virgilio', 'qs'),
+ 'ricerca.virgilio.it' => array('Virgilio', 'qs'),
+
+ // vivisimo
+ 'vivisimo.com' => array('Vivisimo', 'query', 'search/?query={k}'),
+ 'search.vivisimo.com' => array('Vivisimo', 'query'),
+
+ 'de.vivisimo.com' => array('Vivisimo', 'query', 'search/?query={k}&dlang=de&v%3Aproject=de-vivisimo-com'),
+
+ // 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
+ 'volny.zlatestranky.cz' => array('Volny', 'search', 'fulltext/?search={k}', 'windows-1250'),
+ 'web.volny.cz' => array('Volny', 'search', false, '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'),
+
+ // 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'),
+
+ // Webtip
+ 'www.webtip.de' => array('Webtip', 'keyword'),
+
+ // Wedoo
+ 'fr.wedoo.com' => array('Wedoo', 'keyword'),
+
+ // Witch
+ 'www.witch.de' => array('Witch', 'search'),
+
+ // WXS
+ 'wxsl.nl' => array('Planet Internet', 'q'),
+
+ // WWW
+ 'search.www.ee' => array('www värav', 'query'),
+
+ // X-recherche
+ 'www.x-recherche.com' => array('X-Recherche', 'mots'),
+
+ // Yahoo
+ 'search.yahoo.com' => array('Yahoo!', 'p', 'search?p={k}'),
+ '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'),
+ 'fi.yahoo.com' => array('Yahoo!', 'p'),
+ 'ar.search.yahoo.com' => array('Yahoo!', 'p'),
+ 'au.search.yahoo.com' => array('Yahoo!', 'p'),
+ 'br.search.yahoo.com' => array('Yahoo!', 'p'),
+ 'ch.search.yahoo.com' => array('Yahoo!', 'p'),
+ 'ca.search.yahoo.com' => array('Yahoo!', 'p'),
+ 'cade.search.yahoo.com' => array('Yahoo!', 'p'),
+ 'cf.search.yahoo.com' => array('Yahoo!', 'p'),
+ 'de.search.yahoo.com' => array('Yahoo!', 'p'),
+ 'es.search.yahoo.com' => array('Yahoo!', 'p'),
+ 'espanol.search.yahoo.com' => array('Yahoo!', 'p'),
+ 'fr.search.yahoo.com' => array('Yahoo!', 'p'),
+ 'hk.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'),
+ 'qc.search.yahoo.com' => array('Yahoo!', 'p'),
+ 'ru.search.yahoo.com' => array('Yahoo!', 'p'),
+ 'se.search.yahoo.com' => array('Yahoo!', 'p'),
+ 'tw.search.yahoo.com' => array('Yahoo!', 'p'),
+ 'uk.search.yahoo.com' => array('Yahoo!', 'p'),
+ 'us.search.yahoo.com' => array('Yahoo!', 'p'),
+ 'search.cn.yahoo.com' => array('Yahoo!', 'p'),
+ 'one.cn.yahoo.com' => array('Yahoo!', 'p'),
+ 'cns.3721.com' => array('Yahoo!', 'p'), // acquired by Yahoo!
+
+ 'au.yhs.search.yahoo.com' => array('Yahoo!', 'p', 'avg/search?p={k}'),
+ 'de.yhs.search.yahoo.com' => array('Yahoo!', 'p', 'avg/search?p={k}'),
+ 'us.yhs.search.yahoo.com' => array('Yahoo!', 'p', 'avg/search?p={k}'),
+ 'de.dir.yahoo.com' => array('Yahoo! Webverzeichnis', ''),
+ 'cf.dir.yahoo.com' => array('Yahoo! Directory', ''),
+ 'fr.dir.yahoo.com' => array('Yahoo! Directory', ''),
+
+ // Yahoo! Images
+ 'images.search.yahoo.com' => array('Yahoo! Images', 'p', 'search/images?p={k}'),
+
+ // Yandex
+ 'yandex.ru' => array('Yandex', 'text', 'yandsearch?text={k}'),
+ 'yandex.ua' => array('Yandex', 'text'),
+ 'www.yandex.ru' => array('Yandex', 'text'),
+ 'search.yaca.yandex.ru' => array('Yandex', 'text'),
+ 'ya.ru' => array('Yandex', 'text'),
+ 'www.ya.ru' => array('Yandex', 'text'),
+
+ // Yandex Images
+ 'images.yandex.ru' => array('Yandex Images', 'text', 'yandsearch?text={k}'),
+
+ // Yasni
+ 'www.yasni.de' => array('Yasni', 'name'),
+ 'www.yasni.com' => array('Yasni', 'name'),
+ 'www.yasni.co.uk' => array('Yasni', 'name'),
+ 'www.yasni.ch' => array('Yasni', 'name'),
+ 'www.yasni.at' => array('Yasni', 'name'),
+
+ // Yellowmap
+ 'www.yellowmap.de' => array('Yellowmap', ' '),
+ 'yellowmap.de' => array('Yellowmap', ' '),
+
+ // 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', '?q={k}', 'windows-1250'),
+ 'www.zoohoo.cz' => array('Zoohoo', 'q', false, '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/oss/piwik/trunk/core/DataTable/Array.php b/oss/piwik/trunk/core/DataTable/Array.php
new file mode 100644
index 00000000..c8030c08
--- /dev/null
+++ b/oss/piwik/trunk/core/DataTable/Array.php
@@ -0,0 +1,170 @@
+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 )
+ */
+ 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.
+ */
+ public function applyQueuedFilters()
+ {
+ foreach($this->array as $table)
+ {
+ $table->applyQueuedFilters();
+ }
+ }
+
+ /**
+ * Apply a filter to all tables in the array
+ * @param $className
+ * @param $parameters
+ */
+ public function filter($className, $parameters = array())
+ {
+ foreach($this->array as $id => $table)
+ {
+ $table->filter($className, $parameters);
+ }
+ }
+
+ /**
+ * 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();
+ $renderer->setTable($this);
+ return (string)$renderer;
+ }
+
+ /**
+ * @see Piwik_DataTable::enableRecursiveSort()
+ */
+ public function enableRecursiveSort()
+ {
+ foreach($this->array as $table)
+ {
+ $table->enableRecursiveSort();
+ }
+ }
+
+ public function deleteColumns($columns)
+ {
+ foreach($this->array as $table)
+ {
+ $table->deleteColumns($columns);
+ }
+ }
+}
diff --git a/oss/piwik/trunk/core/DataTable/Filter.php b/oss/piwik/trunk/core/DataTable/Filter.php
new file mode 100644
index 00000000..e3fb6b6a
--- /dev/null
+++ b/oss/piwik/trunk/core/DataTable/Filter.php
@@ -0,0 +1,44 @@
+table = $table;
+ }
+
+ abstract protected function filter();
+}
diff --git a/oss/piwik/trunk/core/DataTable/Filter/AddColumnsWhenShowAllColumns.php b/oss/piwik/trunk/core/DataTable/Filter/AddColumnsWhenShowAllColumns.php
new file mode 100644
index 00000000..97052c57
--- /dev/null
+++ b/oss/piwik/trunk/core/DataTable/Filter/AddColumnsWhenShowAllColumns.php
@@ -0,0 +1,58 @@
+filter();
+ }
+
+ protected function filter()
+ {
+ $rowsIdToDelete = array();
+ foreach($this->table->getRows() as $key => $row)
+ {
+ $nbVisits = $row->getColumn(Piwik_Archive::INDEX_NB_VISITS);
+ if($nbVisits == 0)
+ {
+ // case of keyword/website/campaign with a conversion for this day,
+ // but no visit, we don't show it
+ $rowsIdToDelete[] = $key;
+ continue;
+ }
+
+ // nb_actions / nb_visits => Actions/visit
+ // sum_visit_length / nb_visits => Avg. Time on Site
+ // bounce_count=> Bounce Rate
+ $actionsPerVisit = round($row->getColumn(Piwik_Archive::INDEX_NB_ACTIONS) / $nbVisits, $this->roundPrecision);
+ $averageTimeOnSite = round($row->getColumn(Piwik_Archive::INDEX_SUM_VISIT_LENGTH) / $nbVisits, $this->roundPrecision);
+ $bounceRate = round(100 * $row->getColumn(Piwik_Archive::INDEX_BOUNCE_COUNT) / $nbVisits, $this->roundPrecision);
+ $row->addColumn('nb_actions_per_visit', $actionsPerVisit);
+ $row->addColumn('avg_time_on_site', $averageTimeOnSite);
+ $row->addColumn('bounce_rate', $bounceRate);
+ }
+ $this->table->deleteRows($rowsIdToDelete);
+ }
+}
diff --git a/oss/piwik/trunk/core/DataTable/Filter/AddConstantMetadata.php b/oss/piwik/trunk/core/DataTable/Filter/AddConstantMetadata.php
new file mode 100644
index 00000000..011674e0
--- /dev/null
+++ b/oss/piwik/trunk/core/DataTable/Filter/AddConstantMetadata.php
@@ -0,0 +1,43 @@
+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/oss/piwik/trunk/core/DataTable/Filter/AddSummaryRow.php b/oss/piwik/trunk/core/DataTable/Filter/AddSummaryRow.php
new file mode 100644
index 00000000..d42e28f0
--- /dev/null
+++ b/oss/piwik/trunk/core/DataTable/Filter/AddSummaryRow.php
@@ -0,0 +1,74 @@
+startRowToSummarize = $startRowToSummarize;
+ $this->labelSummaryRow = $labelSummaryRow;
+ $this->columnToSortByBeforeTruncating = $columnToSortByBeforeTruncating;
+
+ if($table->getRowsCount() > $startRowToSummarize + 1)
+ {
+ $this->filter();
+ }
+ }
+
+ protected function filter()
+ {
+ $this->table->filter('Sort',
+ array( $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->setColumns(array('label' => $this->labelSummaryRow) + $newRow->getColumns());
+ $this->table->filter('Limit', array(0, $this->startRowToSummarize));
+ $this->table->addSummaryRow($newRow);
+ unset($rows);
+ }
+}
diff --git a/oss/piwik/trunk/core/DataTable/Filter/ColumnCallbackAddColumnPercentage.php b/oss/piwik/trunk/core/DataTable/Filter/ColumnCallbackAddColumnPercentage.php
new file mode 100644
index 00000000..b87e51fb
--- /dev/null
+++ b/oss/piwik/trunk/core/DataTable/Filter/ColumnCallbackAddColumnPercentage.php
@@ -0,0 +1,34 @@
+getVisits($idSite, $period, $date);
+ * $dataTable->queueFilter('ColumnCallbackAddColumnPercentage', array('nb_visits', 'nb_visits_percentage', $nbVisits, 1));
+ *
+ * @package Piwik
+ * @subpackage Piwik_DataTable
+ */
+class Piwik_DataTable_Filter_ColumnCallbackAddColumnPercentage extends Piwik_DataTable_Filter_ColumnCallbackAddColumnQuotient
+{
+ protected function formatValue($value, $divisor)
+ {
+ return Piwik::getPercentageSafe($value, $divisor, $this->quotientPrecision) . '%';
+ }
+}
diff --git a/oss/piwik/trunk/core/DataTable/Filter/ColumnCallbackAddColumnQuotient.php b/oss/piwik/trunk/core/DataTable/Filter/ColumnCallbackAddColumnQuotient.php
new file mode 100644
index 00000000..7f31a840
--- /dev/null
+++ b/oss/piwik/trunk/core/DataTable/Filter/ColumnCallbackAddColumnQuotient.php
@@ -0,0 +1,80 @@
+columnValueToRead = $columnValueToRead;
+ $this->columnNameToAdd = $columnNameToAdd;
+ if(is_numeric($divisorValueOrDivisorColumnName))
+ {
+ $this->totalValueUsedAsDivisor = $divisorValueOrDivisorColumnName;
+ }
+ else
+ {
+ $this->columnNameUsedAsDivisor = $divisorValueOrDivisorColumnName;
+ }
+ $this->filter();
+ }
+
+ protected function filter()
+ {
+ foreach($this->table->getRows() as $key => $row)
+ {
+ $value = $row->getColumn($this->columnValueToRead);
+ if(!is_null($this->totalValueUsedAsDivisor))
+ {
+ $divisor = $this->totalValueUsedAsDivisor;
+ }
+ else
+ {
+ $divisor = $row->getColumn($this->columnNameUsedAsDivisor);
+ }
+ $formattedValue = $this->formatValue($value, $divisor);
+ $row->addColumn($this->columnNameToAdd, $formattedValue);
+ }
+ }
+
+ protected function formatValue($value, $divisor)
+ {
+ $quotient = 0;
+ if($divisor > 0 && $value > 0)
+ {
+ $quotient = round($value / $divisor, $this->quotientPrecision);
+ }
+ return $quotient;
+ }
+}
diff --git a/oss/piwik/trunk/core/DataTable/Filter/ColumnCallbackAddMetadata.php b/oss/piwik/trunk/core/DataTable/Filter/ColumnCallbackAddMetadata.php
new file mode 100644
index 00000000..5dad9a1f
--- /dev/null
+++ b/oss/piwik/trunk/core/DataTable/Filter/ColumnCallbackAddMetadata.php
@@ -0,0 +1,61 @@
+ UserSettings/icons/linux.png)
+ *
+ * @package Piwik
+ * @subpackage Piwik_DataTable
+ */
+class Piwik_DataTable_Filter_ColumnCallbackAddMetadata extends Piwik_DataTable_Filter
+{
+ private $columnToRead;
+ private $functionToApply;
+ private $functionParameters;
+ private $metadataToAdd;
+
+ public function __construct( $table, $columnToRead, $metadataToAdd, $functionToApply = null, $functionParameters = null )
+ {
+ parent::__construct($table);
+ $this->functionToApply = $functionToApply;
+ $this->functionParameters = $functionParameters;
+ $this->columnToRead = $columnToRead;
+ $this->metadataToAdd = $metadataToAdd;
+ $this->filter();
+ }
+
+ protected function filter()
+ {
+ foreach($this->table->getRows() as $key => $row)
+ {
+ $oldValue = $row->getColumn($this->columnToRead);
+ $parameters = array($oldValue);
+ if(!is_null($this->functionParameters))
+ {
+ $parameters = array_merge($parameters, $this->functionParameters);
+ }
+ if(!is_null($this->functionToApply))
+ {
+ $newValue = call_user_func_array( $this->functionToApply, $parameters);
+ }
+ else
+ {
+ $newValue = $oldValue;
+ }
+ $row->addMetadata($this->metadataToAdd, $newValue);
+ }
+ }
+}
diff --git a/oss/piwik/trunk/core/DataTable/Filter/ColumnCallbackDeleteRow.php b/oss/piwik/trunk/core/DataTable/Filter/ColumnCallbackDeleteRow.php
new file mode 100644
index 00000000..2a9cbde3
--- /dev/null
+++ b/oss/piwik/trunk/core/DataTable/Filter/ColumnCallbackDeleteRow.php
@@ -0,0 +1,43 @@
+function = $function;
+ $this->columnToFilter = $columnToFilter;
+ $this->filter();
+ }
+
+ protected function filter()
+ {
+ foreach($this->table->getRows() as $key => $row)
+ {
+ $columnValue = $row->getColumn($this->columnToFilter);
+ if( !call_user_func( $this->function, $columnValue))
+ {
+ $this->table->deleteRow($key);
+ }
+ }
+ }
+}
diff --git a/oss/piwik/trunk/core/DataTable/Filter/ColumnCallbackReplace.php b/oss/piwik/trunk/core/DataTable/Filter/ColumnCallbackReplace.php
new file mode 100644
index 00000000..a7cd6735
--- /dev/null
+++ b/oss/piwik/trunk/core/DataTable/Filter/ColumnCallbackReplace.php
@@ -0,0 +1,62 @@
+functionToApply = $functionToApply;
+ $this->functionParameters = $functionParameters;
+ $this->columnToFilter = $columnToFilter;
+ $this->filter();
+ }
+
+ protected function filter()
+ {
+ foreach($this->table->getRows() as $key => $row)
+ {
+ // when a value is not defined, we set it to zero by default (rather than displaying '-')
+ $value = $this->getElementToReplace($row, $this->columnToFilter);
+ if($value === false)
+ {
+ $value = 0;
+ }
+ $parameters = array($value);
+ if(!is_null($this->functionParameters))
+ {
+ $parameters = array_merge($parameters, $this->functionParameters);
+ }
+ $newValue = call_user_func_array( $this->functionToApply, $parameters);
+ $this->setElementToReplace($row, $this->columnToFilter, $newValue);
+ }
+ }
+
+ protected function setElementToReplace($row, $columnToFilter, $newValue)
+ {
+ $row->setColumn($columnToFilter, $newValue);
+ }
+ protected function getElementToReplace($row, $columnToFilter)
+ {
+ return $row->getColumn($columnToFilter);
+ }
+}
diff --git a/oss/piwik/trunk/core/DataTable/Filter/ColumnDelete.php b/oss/piwik/trunk/core/DataTable/Filter/ColumnDelete.php
new file mode 100644
index 00000000..41aa6d99
--- /dev/null
+++ b/oss/piwik/trunk/core/DataTable/Filter/ColumnDelete.php
@@ -0,0 +1,36 @@
+columnToDelete = $columnToDelete;
+ $this->filter();
+ }
+
+ protected function filter()
+ {
+ $this->table->deleteColumn($this->columnToDelete);
+ }
+
+}
diff --git a/oss/piwik/trunk/core/DataTable/Filter/ExcludeLowPopulation.php b/oss/piwik/trunk/core/DataTable/Filter/ExcludeLowPopulation.php
new file mode 100644
index 00000000..43642c1e
--- /dev/null
+++ b/oss/piwik/trunk/core/DataTable/Filter/ExcludeLowPopulation.php
@@ -0,0 +1,59 @@
+columnToFilter = $columnToFilter;
+
+ if($minimumValue == 0)
+ {
+ if($minimumPercentageThreshold === false)
+ {
+ $minimumPercentageThreshold = self::MINIMUM_SIGNIFICANT_PERCENTAGE_THRESHOLD;
+ }
+ $allValues = $this->table->getColumn($this->columnToFilter);
+ $sumValues = array_sum($allValues);
+ $minimumValue = $sumValues * $minimumPercentageThreshold;
+ }
+ self::$minimumValue = $minimumValue;
+ $this->filter();
+ }
+
+ function filter()
+ {
+ $this->table->filter('ColumnCallbackDeleteRow',
+ array($this->columnToFilter,
+ array("Piwik_DataTable_Filter_ExcludeLowPopulation", "excludeLowPopulation")
+ )
+ );
+ }
+
+ static public function excludeLowPopulation($value)
+ {
+ return $value >= self::$minimumValue;
+ }
+}
diff --git a/oss/piwik/trunk/core/DataTable/Filter/Limit.php b/oss/piwik/trunk/core/DataTable/Filter/Limit.php
new file mode 100644
index 00000000..4b9d746a
--- /dev/null
+++ b/oss/piwik/trunk/core/DataTable/Filter/Limit.php
@@ -0,0 +1,57 @@
+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
+ if($this->offset > 0)
+ {
+ $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/oss/piwik/trunk/core/DataTable/Filter/MetadataCallbackAddMetadata.php b/oss/piwik/trunk/core/DataTable/Filter/MetadataCallbackAddMetadata.php
new file mode 100644
index 00000000..c48f8f99
--- /dev/null
+++ b/oss/piwik/trunk/core/DataTable/Filter/MetadataCallbackAddMetadata.php
@@ -0,0 +1,48 @@
+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/oss/piwik/trunk/core/DataTable/Filter/MetadataCallbackReplace.php b/oss/piwik/trunk/core/DataTable/Filter/MetadataCallbackReplace.php
new file mode 100644
index 00000000..dd08941a
--- /dev/null
+++ b/oss/piwik/trunk/core/DataTable/Filter/MetadataCallbackReplace.php
@@ -0,0 +1,36 @@
+setMetadata($metadataToFilter, $newValue);
+ }
+
+ protected function getElementToReplace($row, $metadataToFilter)
+ {
+ return $row->getMetadata($metadataToFilter);
+ }
+}
diff --git a/oss/piwik/trunk/core/DataTable/Filter/Null.php b/oss/piwik/trunk/core/DataTable/Filter/Null.php
new file mode 100644
index 00000000..b419743c
--- /dev/null
+++ b/oss/piwik/trunk/core/DataTable/Filter/Null.php
@@ -0,0 +1,35 @@
+filter();
+ }
+
+ protected function filter()
+ {
+ foreach($this->table->getRows() as $key => $row)
+ {
+ }
+ }
+}
diff --git a/oss/piwik/trunk/core/DataTable/Filter/Pattern.php b/oss/piwik/trunk/core/DataTable/Filter/Pattern.php
new file mode 100644
index 00000000..70a0b5cd
--- /dev/null
+++ b/oss/piwik/trunk/core/DataTable/Filter/Pattern.php
@@ -0,0 +1,63 @@
+patternToSearch = $patternToSearch;
+ $this->patternToSearchQuoted = self::getPatternQuoted($patternToSearch);
+ $this->columnToFilter = $columnToFilter;
+ $this->filter();
+ }
+
+ static public function getPatternQuoted( $pattern )
+ {
+ return '/'. str_replace('/', '\/', $pattern) .'/';
+ }
+
+ /*
+ * Performs case insensitive match
+ */
+ static public function match($pattern, $patternQuoted, $string)
+ {
+ return @preg_match($patternQuoted . "i", $string) == 1;
+ }
+
+ protected function filter()
+ {
+ foreach($this->table->getRows() as $key => $row)
+ {
+ //instead search must handle
+ // - negative search with -piwik
+ // - exact match with ""
+ // see (?!pattern) A subexpression that performs a negative lookahead search, which matches the search string at any point where a string not matching pattern begins.
+ if( !self::match($this->patternToSearch, $this->patternToSearchQuoted, $row->getColumn($this->columnToFilter)))
+ {
+ $this->table->deleteRow($key);
+ }
+ }
+ }
+}
diff --git a/oss/piwik/trunk/core/DataTable/Filter/PatternRecursive.php b/oss/piwik/trunk/core/DataTable/Filter/PatternRecursive.php
new file mode 100644
index 00000000..fdcd7c89
--- /dev/null
+++ b/oss/piwik/trunk/core/DataTable/Filter/PatternRecursive.php
@@ -0,0 +1,80 @@
+patternToSearch = $patternToSearch;
+ $this->patternToSearchQuoted = Piwik_DataTable_Filter_Pattern::getPatternQuoted($patternToSearch);
+ $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
+ && !Piwik_DataTable_Filter_Pattern::match($this->patternToSearch, $this->patternToSearchQuoted, $row->getColumn($this->columnToFilter))
+ )
+ {
+ $table->deleteRow($key);
+ }
+ }
+
+ return $table->getRowsCount();
+ }
+}
diff --git a/oss/piwik/trunk/core/DataTable/Filter/ReplaceColumnNames.php b/oss/piwik/trunk/core/DataTable/Filter/ReplaceColumnNames.php
new file mode 100644
index 00000000..29a32a7a
--- /dev/null
+++ b/oss/piwik/trunk/core/DataTable/Filter/ReplaceColumnNames.php
@@ -0,0 +1,102 @@
+ NEW_COLUMN NAME,
+ * OLD_COLUMN_NAME2 => NEW_COLUMN NAME2,
+ * )
+ */
+ public function __construct( $table, $recursive = false, $mappingToApply = null )
+ {
+ parent::__construct($table);
+ $this->mappingToApply = Piwik_Archive::$mappingFromIdToName;
+ $this->applyFilterRecursively = $recursive;
+ 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)
+ {
+ $oldColumns = $row->getColumns();
+ $newColumns = $this->getRenamedColumns($oldColumns);
+ $row->setColumns( $newColumns );
+ if($this->applyFilterRecursively)
+ {
+ 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 getRenamedColumns($columns)
+ {
+ $newColumns = array();
+ foreach($columns as $columnName => $columnValue)
+ {
+ if(isset($this->mappingToApply[$columnName]))
+ {
+ $columnName = $this->mappingToApply[$columnName];
+
+ if($columnName == 'goals')
+ {
+ $newSubColumns = array();
+ foreach($columnValue as $idGoal => $goalValues)
+ {
+ foreach($goalValues as $id => $goalValue)
+ {
+ $subColumnName = Piwik_Archive::$mappingFromIdToNameGoal[$id];
+ $newSubColumns['idgoal='.$idGoal][$subColumnName] = $goalValue;
+ }
+ }
+ $columnValue = $newSubColumns;
+ }
+ }
+ $newColumns[$columnName] = $columnValue;
+ }
+ return $newColumns;
+ }
+}
diff --git a/oss/piwik/trunk/core/DataTable/Filter/ReplaceSummaryRowLabel.php b/oss/piwik/trunk/core/DataTable/Filter/ReplaceSummaryRowLabel.php
new file mode 100644
index 00000000..2fe94a2e
--- /dev/null
+++ b/oss/piwik/trunk/core/DataTable/Filter/ReplaceSummaryRowLabel.php
@@ -0,0 +1,43 @@
+newLabel = $newLabel;
+ $this->filter();
+ }
+
+ protected function filter()
+ {
+ $rows = $this->table->getRows();
+ foreach($rows as $row)
+ {
+ if($row->getColumn('label') == Piwik_DataTable::LABEL_SUMMARY_ROW)
+ {
+ $row->setColumn('label', $this->newLabel);
+ break;
+ }
+ }
+ }
+}
diff --git a/oss/piwik/trunk/core/DataTable/Filter/SafeDecodeLabel.php b/oss/piwik/trunk/core/DataTable/Filter/SafeDecodeLabel.php
new file mode 100644
index 00000000..73c680fd
--- /dev/null
+++ b/oss/piwik/trunk/core/DataTable/Filter/SafeDecodeLabel.php
@@ -0,0 +1,45 @@
+columnToDecode = 'label';
+ $this->filter();
+ }
+
+ protected function filter()
+ {
+ foreach($this->table->getRows() as $row)
+ {
+ $value = $row->getColumn($this->columnToDecode);
+ if($value !== false)
+ {
+ $row->setColumn(
+ $this->columnToDecode,
+ htmlspecialchars(
+ htmlspecialchars_decode(
+ urldecode($value),
+ ENT_QUOTES),
+ ENT_QUOTES)
+ );
+ }
+ }
+ }
+}
diff --git a/oss/piwik/trunk/core/DataTable/Filter/Sort.php b/oss/piwik/trunk/core/DataTable/Filter/Sort.php
new file mode 100644
index 00000000..feaf5466
--- /dev/null
+++ b/oss/piwik/trunk/core/DataTable/Filter/Sort.php
@@ -0,0 +1,188 @@
+enableRecursiveSort();
+ }
+ $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 (!isset($a->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort])
+ && !isset($b->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort])
+ )
+ ? 0
+ : (
+ !isset($a->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort])
+ ? 1
+ :(
+ !isset($b->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort])
+ ? -1
+ : $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 !isset($a->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort])
+ && !isset($b->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort] )
+ ? 0
+ : (!isset($a->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort])
+ ? 1
+ : (!isset($b->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort] )
+ ? -1
+ : $this->sign * strnatcasecmp(
+ $a->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort],
+ $b->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort]
+ )
+ )
+ )
+ ;
+ }
+
+ function sortString($a, $b)
+ {
+ return !isset($a->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort])
+ && !isset($b->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort] )
+ ? 0
+ : (!isset($a->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort])
+ ? 1
+ : (!isset($b->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort] )
+ ? -1
+ : $this->sign *
+ strcasecmp($a->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort],
+ $b->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort]
+ )
+ )
+ )
+ ;
+ }
+
+ /**
+ * @param Piwik_DataTable_Row
+ */
+ protected function selectColumnToSort($row)
+ {
+ $value = $row->getColumn($this->columnToSort);
+ if($value !== false)
+ {
+ return $this->columnToSort;
+ }
+
+ // sorting by "nb_visits" but the index is Piwik_Archive::INDEX_NB_VISITS in the table
+ if(isset(Piwik_Archive::$mappingFromNameToId[$this->columnToSort]))
+ {
+ $column = Piwik_Archive::$mappingFromNameToId[$this->columnToSort];
+ $value = $row->getColumn($column);
+
+ if($value !== false)
+ {
+ return $column;
+ }
+ }
+
+ // eg. was previously sorted by revenue_per_visit, but this table
+ // doesn't have this column; defaults with nb_visits
+ $column = Piwik_Archive::INDEX_NB_VISITS;
+ $value = $row->getColumn($column);
+ if($value !== false)
+ {
+ return $column;
+ }
+
+ // even though this column is not set properly in the table,
+ // we select it for the sort, so that the table's internal state is set properly
+ return $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);
+ if($row === false)
+ {
+ return;
+ }
+ $this->columnToSort = $this->selectColumnToSort($row);
+
+ $value = $row->getColumn($this->columnToSort);
+ if( is_numeric($value))
+ {
+ $methodToUse = "sort";
+ }
+ else
+ {
+ if($this->naturalSort)
+ {
+ $methodToUse = "naturalSort";
+ }
+ else
+ {
+ $methodToUse = "sortString";
+ }
+ }
+ $this->table->sort( array($this,$methodToUse), $this->columnToSort );
+ }
+}
diff --git a/oss/piwik/trunk/core/DataTable/Filter/UpdateColumnsWhenShowAllGoals.php b/oss/piwik/trunk/core/DataTable/Filter/UpdateColumnsWhenShowAllGoals.php
new file mode 100644
index 00000000..17808707
--- /dev/null
+++ b/oss/piwik/trunk/core/DataTable/Filter/UpdateColumnsWhenShowAllGoals.php
@@ -0,0 +1,153 @@
+mappingIdToNameGoal = Piwik_Archive::$mappingFromIdToNameGoal;
+ $this->processOnlyIdGoal = $processOnlyIdGoal;
+ $this->filter();
+ }
+
+ protected function filter()
+ {
+ $invalidDivision = 'N/A';
+ $roundingPrecision = 2;
+ $expectedColumns = array();
+ foreach($this->table->getRows() as $key => $row)
+ {
+ $currentColumns = $row->getColumns();
+ $newColumns = array();
+
+ $nbVisits = 0;
+ // visits could be undefined when there is a conversion but no visit
+ if(isset($currentColumns[Piwik_Archive::INDEX_NB_VISITS]))
+ {
+ $nbVisits = $currentColumns[Piwik_Archive::INDEX_NB_VISITS];
+ }
+ $newColumns['nb_visits'] = $nbVisits;
+ $newColumns['label'] = $currentColumns['label'];
+ if(isset($currentColumns[Piwik_Archive::INDEX_GOALS]))
+ {
+ $nbVisitsConverted = $revenue = 0;
+ if(isset($currentColumns[Piwik_Archive::INDEX_NB_VISITS_CONVERTED]))
+ {
+ $nbVisitsConverted = $currentColumns[Piwik_Archive::INDEX_NB_VISITS_CONVERTED];
+ $revenue = $currentColumns[Piwik_Archive::INDEX_REVENUE];
+ }
+
+ if($nbVisitsConverted == 0)
+ {
+ $conversionRate = $invalidDivision;
+ }
+ else
+ {
+ $conversionRate = round(100 * $nbVisitsConverted / $nbVisits, $roundingPrecision);
+ }
+ $newColumns['goals_conversion_rate'] = $conversionRate;
+
+ if($nbVisits == 0)
+ {
+ $revenuePerVisit = $invalidDivision;
+ }
+ else
+ {
+ $revenuePerVisit = round( $revenue / $nbVisits, $roundingPrecision );
+ }
+ $newColumns['revenue_per_visit'] = $revenuePerVisit;
+ foreach($currentColumns[Piwik_Archive::INDEX_GOALS] as $goalId => $columnValue)
+ {
+ if($this->processOnlyIdGoal > self::GOALS_FULL_TABLE
+ && $this->processOnlyIdGoal != $goalId)
+ {
+ continue;
+ }
+
+ // Goal Conversion rate
+ $name = 'goal_' . $goalId . '_conversion_rate';
+ if($nbVisits == 0)
+ {
+ $value = $invalidDivision;
+ }
+ else
+ {
+ $value = round(100 * $columnValue[Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS] / $nbVisits, $roundingPrecision);
+ }
+ $newColumns[$name] = $value;
+ $expectedColumns[$name] = true;
+
+ // When the table is displayed by clicking on the flag icon, we only display the columns
+ // Visits, Conversions, Per goal conversion rate, Revenue
+ if($this->processOnlyIdGoal == self::GOALS_OVERVIEW)
+ {
+ continue;
+ }
+
+ // Goal Conversions
+ $name = 'goal_' . $goalId . '_nb_conversions';
+ $newColumns[$name] = $columnValue[Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS];
+ $expectedColumns[$name] = true;
+
+ // Goal Revenue per visit
+ $name = 'goal_' . $goalId . '_revenue_per_visit';
+ if($nbVisits == 0)
+ {
+ $value = $invalidDivision;
+ }
+ else
+ {
+ $revenuePerVisit = round( $columnValue[Piwik_Archive::INDEX_GOAL_REVENUE] / $nbVisits, $roundingPrecision );
+ }
+ $newColumns[$name] = $revenuePerVisit;
+ $expectedColumns[$name] = true;
+
+ }
+ }
+
+ $row->setColumns($newColumns);
+ }
+ $expectedColumns['revenue_per_visit'] = true;
+ $expectedColumns['goals_conversion_rate'] = true;
+
+ // make sure all goals values are set, 0 by default
+ // if no value then sorting would put at the end
+ $expectedColumns = array_keys($expectedColumns);
+ $rows = $this->table->getRows();
+ foreach($rows as &$row)
+ {
+ foreach($expectedColumns as $name)
+ {
+ if(false === $row->getColumn($name))
+ {
+ $row->addColumn($name, 0);
+ }
+ }
+ }
+ }
+}
diff --git a/oss/piwik/trunk/core/DataTable/Manager.php b/oss/piwik/trunk/core/DataTable/Manager.php
new file mode 100644
index 00000000..5f0fed63
--- /dev/null
+++ b/oss/piwik/trunk/core/DataTable/Manager.php
@@ -0,0 +1,143 @@
+tables[$this->nextTableId] = $table;
+ $this->nextTableId++;
+ return $this->nextTableId - 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
+ */
+ public function deleteAll()
+ {
+ foreach($this->tables as $id => $table)
+ {
+ $this->deleteTable($id);
+ }
+ $this->tables = array();
+ $this->nextTableId = 0;
+ }
+
+ /**
+ * Deletes (unsets) the datatable given its id and removes it from the manager
+ * Subsequent get for this table will fail
+ *
+ * @param int $id
+ */
+ public function deleteTable( $id )
+ {
+ if(isset($this->tables[$id]))
+ {
+ $this->setTableDeleted($id);
+ destroy($this->tables[$id]);
+ }
+ }
+
+ /**
+ * Remove the table from the manager (table has already been unset)
+ *
+ * @param $id
+ */
+ public function setTableDeleted($id)
+ {
+ $this->tables[$id] = null;
+ }
+
+ /**
+ * Debug only. Dumps all tables currently registered in the Manager
+ */
+ public function dumpAllTables()
+ {
+ echo "
Piwik_DataTable_Manager->dumpAllTables()
";
+ foreach($this->tables as $id => $table)
+ {
+ if(!($table instanceof Piwik_DataTable ))
+ {
+ echo "Error table $id is not instance of datatable
";
+ var_dump($table);
+ }
+ else
+ {
+ echo "
";
+ echo "Table (index=$id) TableId = ". $table->getId() . "
";
+ echo $table;
+ echo "
";
+ }
+ }
+ echo "
-- End Piwik_DataTable_Manager->dumpAllTables()
";
+ }
+}
diff --git a/oss/piwik/trunk/core/DataTable/Renderer.php b/oss/piwik/trunk/core/DataTable/Renderer.php
new file mode 100644
index 00000000..719e69df
--- /dev/null
+++ b/oss/piwik/trunk/core/DataTable/Renderer.php
@@ -0,0 +1,124 @@
+setTable($dataTable);
+ * echo $render;
+ *
+ * @package Piwik
+ * @subpackage Piwik_DataTable
+ */
+abstract class Piwik_DataTable_Renderer
+{
+ protected $table;
+ protected $exception;
+ protected $renderSubTables = false;
+
+ public function __construct()
+ {
+ }
+
+ public function setRenderSubTables($enableRenderSubTable)
+ {
+ $this->renderSubTables = (bool)$enableRenderSubTable;
+ }
+
+ protected function isRenderSubtables()
+ {
+ return $this->renderSubTables;
+ }
+
+ /**
+ * Computes the dataTable output and returns the string/binary
+ *
+ * @return string
+ */
+ abstract public function render();
+
+ /**
+ * Computes the exception output and returns the string/binary
+ *
+ * @return string
+ */
+ abstract public function renderException();
+
+ /**
+ * @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;
+ }
+
+ /**
+ * Set the Exception to be rendered
+ * @param Exception $exception to be rendered
+ */
+ public function setException($exception)
+ {
+ if(!($exception instanceof Exception))
+ {
+ throw new Exception("The exception renderer accepts only an Exception object.");
+ }
+ $this->exception = $exception;
+ }
+
+ /**
+ * 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));
+ $className = 'Piwik_DataTable_Renderer_' . $name;
+
+ try {
+ Piwik_Loader::autoload($className);
+ return new $className;
+ } catch(Exception $e) {
+ $availableRenderers = 'xml, json, csv, tsv, html, php, original';
+ throw new Exception(Piwik_TranslateException('General_ExceptionInvalidRendererFormat', array($name, $availableRenderers)));
+ }
+ }
+
+ /**
+ * Returns $rawData after all applicable characters have been converted to HTML entities.
+ *
+ * @param String $rawData to be converted
+ * @return String
+ */
+ static protected function renderHtmlEntities( $rawData )
+ {
+ return htmlentities($rawData, ENT_COMPAT, "UTF-8");
+ }
+}
\ No newline at end of file
diff --git a/oss/piwik/trunk/core/DataTable/Renderer/Console.php b/oss/piwik/trunk/core/DataTable/Renderer/Console.php
new file mode 100644
index 00000000..f12e1826
--- /dev/null
+++ b/oss/piwik/trunk/core/DataTable/Renderer/Console.php
@@ -0,0 +1,135 @@
+renderTable($this->table);
+ }
+
+ function renderException()
+ {
+ $exceptionMessage = self::renderHtmlEntities($this->exception->getMessage());
+ return 'Error: '.$exceptionMessage;
+ }
+
+ function setPrefixRow($str)
+ {
+ $this->prefixRows = $str;
+ }
+
+ protected function renderDataTableArray(Piwik_DataTable_Array $tableArray, $prefix )
+ {
+ $output = "Piwik_DataTable_Array
";
+ $prefix = $prefix . ' ';
+ foreach($tableArray->getArray() as $descTable => $table)
+ {
+ $output .= $prefix . "". $descTable. "
";
+ $output .= $prefix . $this->renderTable($table, $prefix . ' ');
+ $output .= "
";
+ }
+ $output .= "Metadata
";
+ foreach($tableArray->metadata as $id => $metadata)
+ {
+ $output .= "
";
+ $output .= $prefix . " $id
";
+ foreach($metadata as $name => $value)
+ {
+ $output .= $prefix . $prefix . "$name => $value";
+ }
+ }
+ $output .= "
";
+ 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
\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'";
+ elseif(is_array($value)) $value = var_export($value, true);
+
+ $columns[] = "'$column' => $value";
+ }
+ if($dataTableArrayBreak === true)
+ {
+ continue;
+ }
+ $columns = implode(", ", $columns);
+
+ $metadata = array();
+ foreach($row->getMetadata() as $name => $value)
+ {
+ if(is_string($value)) $value = "'$value'";
+ elseif(is_array($value)) $value = var_export($value, true);
+ $metadata[] = "'$name' => $value";
+ }
+ $metadata = implode(", ", $metadata);
+
+ $output.= str_repeat($this->prefixRows, $depth)
+ . "- $i [".$columns."] [".$metadata."] [idsubtable = "
+ . $row->getIdSubDataTable()."]
\n";
+
+ if($row->getIdSubDataTable() !== null)
+ {
+ $depth++;
+ try{
+ $output.= $this->renderTable(
+ Piwik_DataTable_Manager::getInstance()->getTable(
+ $row->getIdSubDataTable()
+ ),
+ $prefix . ' '
+ );
+ } catch(Exception $e) {
+ $output.= "-- Sub DataTable not loaded
\n";
+ }
+ $depth--;
+ }
+ $i++;
+ }
+
+ return $output;
+
+ }
+}
diff --git a/oss/piwik/trunk/core/DataTable/Renderer/Csv.php b/oss/piwik/trunk/core/DataTable/Renderer/Csv.php
new file mode 100644
index 00000000..f0fcf3ac
--- /dev/null
+++ b/oss/piwik/trunk/core/DataTable/Renderer/Csv.php
@@ -0,0 +1,276 @@
+output($this->renderTable($this->table));
+ }
+
+ function renderException()
+ {
+ $exceptionMessage = self::renderHtmlEntities($this->exception->getMessage());
+ return 'Error: '.$exceptionMessage;
+ }
+
+ public function setConvertToUnicode($bool)
+ {
+ $this->convertToUnicode = $bool;
+ }
+
+ public function setSeparator($separator)
+ {
+ $this->separator = $separator;
+ }
+
+ 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
+ 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 $str;
+ }
+
+ protected function renderDataTable( $table )
+ {
+ if($table instanceof Piwik_DataTable_Simple)
+ {
+ $row = $table->getFirstRow();
+ if($row !== false)
+ {
+ $columnNameToValue = $row->getColumns();
+ if(count($columnNameToValue) == 1)
+ {
+ $value = array_values($columnNameToValue);
+ $str = 'value' . $this->lineEnd . $this->formatValue($value[0]);
+ return $str;
+ }
+ }
+ }
+ $csv = $allColumns = array();
+ foreach($table->getRows() as $row)
+ {
+ $csvRow = array();
+
+ $columns = $row->getColumns();
+ foreach($columns as $name => $value)
+ {
+ //goals => array( 'idgoal=1' =>array(..), 'idgoal=2' => array(..))
+ if(is_array($value))
+ {
+ foreach($value as $key => $subValues)
+ {
+ if(is_array($subValues))
+ {
+ foreach($subValues as $subKey => $subValue)
+ {
+ // goals_idgoal=1
+ $columnName = $name . "_" . $key . "_" . $subKey;
+ $allColumns[$columnName] = true;
+ $csvRow[$columnName] = $subValue;
+ }
+ }
+ }
+ }
+ else
+ {
+ $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');
+ }
+ elseif($value === false)
+ {
+ $value = 0;
+ }
+ if(strpos($value, '"') !== false
+ || strpos($value, $this->separator) !== false )
+ {
+ $value = '"'. str_replace('"', '""', $value). '"';
+ }
+ 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;
+ }
+}
diff --git a/oss/piwik/trunk/core/DataTable/Renderer/Html.php b/oss/piwik/trunk/core/DataTable/Renderer/Html.php
new file mode 100644
index 00000000..11ca86b9
--- /dev/null
+++ b/oss/piwik/trunk/core/DataTable/Renderer/Html.php
@@ -0,0 +1,187 @@
+tableId = str_replace('.', '_', $id);
+ }
+
+ function render()
+ {
+ $this->tableStructure = array();
+ $this->allColumns = array();
+ $this->i = 0;
+
+ return $this->renderTable($this->table);
+ }
+
+ function renderException()
+ {
+ $exceptionMessage = self::renderHtmlEntities($this->exception->getMessage());
+ return nl2br($exceptionMessage);
+ }
+
+ protected function renderTable($table)
+ {
+ if($table instanceof Piwik_DataTable_Array)
+ {
+ foreach($table->getArray() as $date => $subtable )
+ {
+ if ($subtable->getRowsCount()) {
+ $this->buildTableStructure($subtable, '_'. $table->getKeyName(), $date);
+ }
+ }
+ }
+ else // Piwik_DataTable_Simple
+ {
+ if($table->getRowsCount())
+ {
+ $this->buildTableStructure($table);
+ }
+ }
+
+ $out = $this->renderDataTable();
+ return $this->output($out);
+ }
+
+ protected function buildTableStructure($table, $columnToAdd = null, $valueToAdd = null)
+ {
+ $i = $this->i;
+ $someMetadata = false;
+ $someIdSubTable = false;
+
+ /*
+ * table = array
+ * ROW1 = col1 | col2 | col3 | metadata | idSubTable
+ * ROW2 = col1 | col2 (no value but appears) | col3 | metadata | idSubTable
+ */
+ foreach($table->getRows() as $row)
+ {
+ if(isset($columnToAdd) && isset($valueToAdd))
+ {
+ $this->allColumns[$columnToAdd] = true;
+ $this->tableStructure[$i][$columnToAdd] = $valueToAdd;
+ }
+
+ foreach($row->getColumns() as $column => $value)
+ {
+ $this->allColumns[$column] = true;
+ $this->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("
", $metadata);
+ $this->tableStructure[$i]['_metadata'] = $metadata;
+ }
+
+ $idSubtable = $row->getIdSubDataTable();
+ if(!is_null($idSubtable))
+ {
+ $someIdSubTable = true;
+ $this->tableStructure[$i]['_idSubtable'] = $idSubtable;
+ }
+
+ $i++;
+ }
+ $this->i = $i;
+
+ $this->allColumns['_metadata'] = $someMetadata;
+ $this->allColumns['_idSubtable'] = $someIdSubTable;
+ }
+
+ protected function renderDataTable()
+ {
+ $html = "tableId ? "id=\"{$this->tableId}\" " : "") ."border=\"1\">\n\n\t\n";
+
+ foreach($this->allColumns as $name => $toDisplay)
+ {
+ if($toDisplay !== false)
+ {
+ if($name === 0)
+ {
+ $name = 'value';
+ }
+ $html .= "\t\t| $name | \n";
+ }
+ }
+
+ $html .= "\t
\n\n\n";
+
+ foreach($this->tableStructure as $row)
+ {
+ $html .= "\t\n";
+ foreach($this->allColumns as $name => $toDisplay)
+ {
+ if($toDisplay !== false)
+ {
+ $value = "-";
+ if(isset($row[$name]))
+ {
+ $value = $this->formatValue($row[$name]);
+ }
+
+ $html .= "\t\t| $value | \n";
+ }
+ }
+ $html .= "\t
\n";
+ }
+
+ $html .= "\n
\n";
+
+ return $html;
+ }
+
+ protected function formatValue($value)
+ {
+ if(is_string($value)
+ && !is_numeric($value))
+ {
+ $value = html_entity_decode($value, ENT_COMPAT, 'UTF-8');
+ $value = htmlspecialchars($value);
+ }
+ elseif($value===false)
+ {
+ $value = 0;
+ }
+ return $value;
+ }
+
+ protected function output( $xml )
+ {
+ // silent fail because otherwise it throws an exception in the unit tests
+ @header("Content-Type: text/html;charset=utf-8");
+ return $xml;
+ }
+}
diff --git a/oss/piwik/trunk/core/DataTable/Renderer/Json.php b/oss/piwik/trunk/core/DataTable/Renderer/Json.php
new file mode 100644
index 00000000..24e58732
--- /dev/null
+++ b/oss/piwik/trunk/core/DataTable/Renderer/Json.php
@@ -0,0 +1,73 @@
+renderTable($this->table);
+ }
+
+ function renderException()
+ {
+ Piwik_DataTable_Renderer_Json::renderHeader();
+
+ $exceptionMessage = self::renderHtmlEntities($this->exception->getMessage());
+ $exceptionMessage = str_replace("\n", "", $exceptionMessage);
+ $exceptionMessage = '{"result":"error", "message":"'.$exceptionMessage.'"}';
+
+ return $this->jsonpWrap($exceptionMessage);
+ }
+
+ protected function renderTable($table)
+ {
+ $renderer = new Piwik_DataTable_Renderer_Php();
+ $renderer->setTable($table);
+ $renderer->setRenderSubTables($this->isRenderSubtables());
+ $renderer->setSerialize(false);
+ $array = $renderer->flatRender();
+
+ if(!is_array($array))
+ {
+ $array = array('value' => $array);
+ }
+ $str = json_encode($array);
+
+ return $this->jsonpWrap($str);
+ }
+
+ protected function jsonpWrap($str)
+ {
+ if(($jsonCallback = Piwik_Common::getRequestVar('jsoncallback', false)) !== false)
+ {
+ if(preg_match('/^[0-9a-zA-Z]*$/', $jsonCallback) > 0)
+ {
+ $str = $jsonCallback . "(" . $str . ")";
+ }
+ }
+
+ return $str;
+ }
+
+ static private function renderHeader ()
+ {
+ @header( "Content-Type: application/json" );
+ }
+}
diff --git a/oss/piwik/trunk/core/DataTable/Renderer/Php.php b/oss/piwik/trunk/core/DataTable/Renderer/Php.php
new file mode 100644
index 00000000..8d986f57
--- /dev/null
+++ b/oss/piwik/trunk/core/DataTable/Renderer/Php.php
@@ -0,0 +1,227 @@
+serialize = (bool)$bool;
+ }
+ public function setPrettyDisplay($bool)
+ {
+ $this->prettyDisplay = (bool)$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( $this->prettyDisplay )
+ {
+ if(!is_array($toReturn))
+ {
+ $toReturn = unserialize($toReturn);
+ }
+ $toReturn = "" . var_export($toReturn, true ) . "
";
+ }
+ return $toReturn;
+ }
+
+ function renderException()
+ {
+ $exceptionMessage = self::renderHtmlEntities($this->exception->getMessage());
+
+ $return = array('result' => 'error', 'message' => $exceptionMessage);
+
+ if($this->serialize)
+ {
+ $return = serialize($return);
+ }
+
+ return $return;
+ }
+
+ /**
+ * 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;
+ }
+ }
+
+ 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 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->isRenderSubtables()
+ && $row->getIdSubDataTable() !== null)
+ {
+ try{
+ $subTable = $this->renderTable( Piwik_DataTable_Manager::getInstance()->getTable($row->getIdSubDataTable()));
+ $newRow['subtable'] = $subTable;
+ if(isset($newRow['metadata']['idsubdatatable_in_db']))
+ {
+ $newRow['columns']['idsubdatatable'] = $newRow['metadata']['idsubdatatable_in_db'];
+ unset($newRow['metadata']['idsubdatatable_in_db']);
+ }
+ } catch (Exception $e) {
+ // the subtables are not loaded we dont do anything
+ }
+ }
+
+ $array[] = $newRow;
+ }
+ return $array;
+ }
+
+ protected function renderSimpleTable($table)
+ {
+ $array = array();
+
+ $row = $table->getFirstRow();
+ if($row === false)
+ {
+ return $array;
+ }
+ foreach($row->getColumns() as $columnName => $columnValue)
+ {
+ $array[$columnName] = $columnValue;
+ }
+ return $array;
+ }
+}
diff --git a/oss/piwik/trunk/core/DataTable/Renderer/Rss.php b/oss/piwik/trunk/core/DataTable/Renderer/Rss.php
new file mode 100644
index 00000000..22f4fbd6
--- /dev/null
+++ b/oss/piwik/trunk/core/DataTable/Renderer/Rss.php
@@ -0,0 +1,171 @@
+renderTable($this->table);
+ }
+
+ function renderException()
+ {
+ $exceptionMessage = self::renderHtmlEntities($this->exception->getMessage());
+ return 'Error: '.$exceptionMessage;
+ }
+
+ 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);
+
+ $dateInSiteTimezone = Piwik_Date::factory($timestamp)->setTimezone($site->getTimezone())->toString('Y-m-d');
+ $thisPiwikUrl = htmlentities($piwikUrl . "&date=$dateInSiteTimezone");
+ $siteName = $site->getName();
+ $title = $siteName . " on ". $date;
+
+ $out .= "\t-
+ $pudDate
+ $thisPiwikUrl
+ $thisPiwikUrl
+ $title
+ http://piwik.org
+ ";
+
+ $out .= htmlspecialchars( $this->renderDataTable($subtable) );
+ $out .= "\n\t
\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\n";
+ }
+ protected function getRssHeader()
+ {
+ $generationDate = date('r');
+ $header = "
+
+
+ piwik statistics - RSS
+ http://piwik.org
+ Piwik RSS feed
+ $generationDate
+ piwik
+ en
+ $generationDate";
+ return $header;
+ }
+
+ protected function renderDataTable($table)
+ {
+ if($table->getRowsCount() == 0)
+ {
+ return "Empty table
\n";
+ }
+
+ $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)
+ {
+ // for example, goals data is array: not supported in export RSS
+ // in the future we shall reuse ViewDataTable for html exports in RSS anyway
+ if(is_array($value))
+ {
+ continue;
+ }
+ $allColumns[$column] = true;
+ $tableStructure[$i][$column] = $value;
+ }
+ $i++;
+ }
+ $html = "\n";
+ $html .= "";
+ $html .= "\n";
+ foreach($allColumns as $name => $toDisplay)
+ {
+ if($toDisplay !== false)
+ {
+ $html .= "\n\t| $name | ";
+ }
+ }
+ $html .= "\n
";
+ $colspan = count($allColumns);
+
+ foreach($tableStructure as $row)
+ {
+ $html .= "\n\n";
+ foreach($allColumns as $columnName => $toDisplay)
+ {
+ if($toDisplay !== false)
+ {
+ $value = "-";
+ if(isset($row[$columnName]))
+ {
+ $value = urldecode($row[$columnName]);
+ }
+
+ $html .= "\n\t| $value | ";
+ }
+ }
+ $html .= "
";
+
+ }
+ $html .= "\n\n
";
+ return $html;
+ }
+}
diff --git a/oss/piwik/trunk/core/DataTable/Renderer/Tsv.php b/oss/piwik/trunk/core/DataTable/Renderer/Tsv.php
new file mode 100644
index 00000000..5980ec04
--- /dev/null
+++ b/oss/piwik/trunk/core/DataTable/Renderer/Tsv.php
@@ -0,0 +1,34 @@
+setSeparator("\t");
+ }
+
+ function render()
+ {
+ return parent::render();
+ }
+}
diff --git a/oss/piwik/trunk/core/DataTable/Renderer/Xml.php b/oss/piwik/trunk/core/DataTable/Renderer/Xml.php
new file mode 100644
index 00000000..c9a5dfa6
--- /dev/null
+++ b/oss/piwik/trunk/core/DataTable/Renderer/Xml.php
@@ -0,0 +1,328 @@
+renderTable($this->table);
+ }
+
+ function renderException()
+ {
+ $exceptionMessage = self::renderHtmlEntities($this->exception->getMessage());
+
+ @header("Content-Type: text/xml;charset=utf-8");
+ $return =
+ "\n" .
+ "\n".
+ "\t\n".
+ "";
+
+ return $return;
+ }
+
+ protected function getArrayFromDataTable($table)
+ {
+ $renderer = new Piwik_DataTable_Renderer_Php();
+ $renderer->setRenderSubTables($this->isRenderSubtables());
+ $renderer->setSerialize(false);
+ $renderer->setTable($table);
+ return $renderer->flatRender();
+ }
+
+ protected function renderTable($table, $returnOnlyDataTableXml = false, $prefixLines = '')
+ {
+ $array = $this->getArrayFromDataTable($table);
+// var_dump($array);exit;
+ if($table instanceof Piwik_DataTable_Array)
+ {
+ $out = $this->renderDataTableArray($table, $array, $prefixLines);
+
+ if($returnOnlyDataTableXml)
+ {
+ return $out;
+ }
+ $out = "\n$out";
+ 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 = "";
+ 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 = "\n".$out."";
+ }
+ else
+ {
+ $out = "".$this->formatValue($out)."";
+ }
+ return $this->output($out);
+ }
+
+ if($table instanceof Piwik_DataTable)
+ {
+ $out = $this->renderDataTable($array);
+ if($returnOnlyDataTableXml)
+ {
+ return $out;
+ }
+ $out = "\n$out";
+ 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\n";
+ }
+ elseif($value instanceof Piwik_DataTable_Array )
+ {
+ $out = $this->renderTable($value, true);
+ //TODO somehow this code is not tested, cover this case
+ $xml .= "\t\n$out\n";
+ }
+ else
+ {
+ $xml .= $prefixLines . "\t".$this->formatValue($value)."\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\n";
+ }
+ else
+ {
+ if(is_array($dataTableSimple))
+ {
+ $dataTableSimple = "\n" . $this->renderDataTableSimple($dataTableSimple, $prefixLines . "\t") . $prefixLines . "\t";
+ }
+ $xml .= $prefixLines . "\t".$dataTableSimple. "\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\n";
+ }
+ else
+ {
+ $xml .= $prefixLines . "\t\n";
+ $xml .= $dataTableOut;
+ $xml .= $prefixLines . "\t\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\n".$out.$prefixLines."\t\n";
+
+ }
+ return $xml;
+ }
+ }
+
+ protected function renderDataTable( $array, $prefixLine = "" )
+ {
+ $out = '';
+ foreach($array as $rowId => $row)
+ {
+ if(!is_array($row))
+ {
+ $value = $this->formatValue($row);
+ $out .= $prefixLine."\t\t<$rowId>".$value."$rowId>\n";
+ continue;
+ }
+ $rowAttribute = '';
+ if(($equalFound = strstr($rowId, '=')) !== false)
+ {
+ $rowAttribute = explode('=', $rowId);
+ $rowAttribute = " " . $rowAttribute[0] . "='" . $rowAttribute[1] . "'";
+ }
+ $out .= $prefixLine."\t";
+
+ 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."
\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);
+ }
+ elseif($value===false)
+ {
+ $value = 0;
+ }
+ 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 = '' . "\n" . $xml;
+ return $xml;
+ }
+}
diff --git a/oss/piwik/trunk/core/DataTable/Row.php b/oss/piwik/trunk/core/DataTable/Row.php
new file mode 100644
index 00000000..55283a71
--- /dev/null
+++ b/oss/piwik/trunk/core/DataTable/Row.php
@@ -0,0 +1,437 @@
+ 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'";
+ elseif(is_array($value)) $value = var_export($value, true);
+ $columns[] = "'$column' => $value";
+ }
+ $columns = implode(", ", $columns);
+ $metadata = array();
+ foreach($this->getMetadata() as $name => $value)
+ {
+ if(is_string($value)) $value = "'$value'";
+ elseif(is_array($value)) $value = var_export($value, true);
+ $metadata[] = "'$name' => $value";
+ }
+ $metadata = implode(", ", $metadata);
+ $output = "# [".$columns."] [".$metadata."] [idsubtable = " . $this->getIdSubDataTable()."]
\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;
+ }
+
+ public function renameColumn($oldName, $newName)
+ {
+ if(isset($this->c[self::COLUMNS][$oldName]))
+ {
+ $this->c[self::COLUMNS][$newName] = $this->c[self::COLUMNS][$oldName];
+ unset($this->c[self::COLUMNS][$oldName]);
+ }
+ }
+
+ /**
+ * 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)
+ {
+ $this->c[self::COLUMNS][$name] = $value;
+ }
+
+ /**
+ * Set the value $value to the metadata called $name.
+ *
+ * @param string $name of the metadata to set
+ * @param mixed $value of the metadata to set
+ */
+ public function setMetadata($name, $value)
+ {
+ $this->c[self::METADATA][$name] = $value;
+ }
+
+ /**
+ * 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 $columnToSumName => $columnToSumValue)
+ {
+ if($columnToSumName != 'label')
+ {
+ $thisColumnValue = $this->getColumn($columnToSumName);
+ $newValue = $this->sumRowArray($thisColumnValue, $columnToSumValue);
+ $this->setColumn( $columnToSumName, $newValue);
+ }
+ }
+ }
+
+ protected function sumRowArray( $thisColumnValue, $columnToSumValue )
+ {
+ if(is_numeric($columnToSumValue))
+ {
+ if($thisColumnValue === false)
+ {
+ $thisColumnValue = 0;
+ }
+ return $thisColumnValue + $columnToSumValue;
+ }
+
+ if(is_array($columnToSumValue))
+ {
+ if($thisColumnValue == false)
+ {
+ return $columnToSumValue;
+ }
+ $newValue = $thisColumnValue;
+ foreach($columnToSumValue as $arrayIndex => $arrayValue)
+ {
+ if(!isset($newValue[$arrayIndex]))
+ {
+ $newValue[$arrayIndex] = false;
+ }
+ $newValue[$arrayIndex] = $this->sumRowArray($newValue[$arrayIndex], $arrayValue);
+ }
+ return $newValue;
+ }
+ return 0;
+ }
+
+ /**
+ * 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();
+ if(array_diff($cols1, $cols2) !== array_diff($cols2, $cols1))
+ {
+ 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;
+ }
+}
diff --git a/oss/piwik/trunk/core/DataTable/Row/DataTableSummary.php b/oss/piwik/trunk/core/DataTable/Row/DataTableSummary.php
new file mode 100644
index 00000000..96ef2329
--- /dev/null
+++ b/oss/piwik/trunk/core/DataTable/Row/DataTableSummary.php
@@ -0,0 +1,34 @@
+getRows() as $row)
+ {
+ $this->sumRow($row);
+ }
+ }
+}
diff --git a/oss/piwik/trunk/core/DataTable/Simple.php b/oss/piwik/trunk/core/DataTable/Simple.php
new file mode 100644
index 00000000..acdbdce4
--- /dev/null
+++ b/oss/piwik/trunk/core/DataTable/Simple.php
@@ -0,0 +1,41 @@
+ Value row 1,
+ * 'Label row 2' => Value row 2,
+ * )
+ */
+ function addRowsFromArray($array)
+ {
+ $this->addRowsFromSimpleArray(array($array));
+ }
+}