From bc5b8001cb84dba043aef711a3f03181b642200c Mon Sep 17 00:00:00 2001 From: YuCheng Hu Date: Sat, 10 Jul 2010 01:20:47 +0000 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20Piwik=20=E5=88=B0=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E5=BA=93=E4=B8=AD=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit +YUCHENG HU+ git-svn-id: https://svn.code.sf.net/p/hawebs/svn@427 a2543c7e-f6e9-4f8a-8bff-1ffc34733512 --- oss/piwik/trunk/core/Archive/Array.php | 188 +++ .../core/Archive/Array/IndexedByDate.php | 144 ++ .../core/Archive/Array/IndexedBySite.php | 122 ++ oss/piwik/trunk/core/Archive/Single.php | 485 ++++++ .../trunk/core/ArchiveProcessing/Day.php | 438 ++++++ .../trunk/core/ArchiveProcessing/Period.php | 358 +++++ .../trunk/core/ArchiveProcessing/Record.php | 42 + .../core/ArchiveProcessing/Record/Blob.php | 41 + .../core/ArchiveProcessing/Record/Numeric.php | 31 + .../core/ArchiveProcessing/RecordArray.php | 70 + oss/piwik/trunk/core/DataFiles/Countries.php | 290 ++++ .../core/DataFiles/LanguageToCountry.php | 66 + .../trunk/core/DataFiles/SearchEngines.php | 1343 +++++++++++++++++ oss/piwik/trunk/core/DataTable/Array.php | 170 +++ oss/piwik/trunk/core/DataTable/Filter.php | 44 + .../Filter/AddColumnsWhenShowAllColumns.php | 58 + .../DataTable/Filter/AddConstantMetadata.php | 43 + .../core/DataTable/Filter/AddSummaryRow.php | 74 + .../ColumnCallbackAddColumnPercentage.php | 34 + .../ColumnCallbackAddColumnQuotient.php | 80 + .../Filter/ColumnCallbackAddMetadata.php | 61 + .../Filter/ColumnCallbackDeleteRow.php | 43 + .../Filter/ColumnCallbackReplace.php | 62 + .../core/DataTable/Filter/ColumnDelete.php | 36 + .../DataTable/Filter/ExcludeLowPopulation.php | 59 + .../trunk/core/DataTable/Filter/Limit.php | 57 + .../Filter/MetadataCallbackAddMetadata.php | 48 + .../Filter/MetadataCallbackReplace.php | 36 + .../trunk/core/DataTable/Filter/Null.php | 35 + .../trunk/core/DataTable/Filter/Pattern.php | 63 + .../DataTable/Filter/PatternRecursive.php | 80 + .../DataTable/Filter/ReplaceColumnNames.php | 102 ++ .../Filter/ReplaceSummaryRowLabel.php | 43 + .../core/DataTable/Filter/SafeDecodeLabel.php | 45 + .../trunk/core/DataTable/Filter/Sort.php | 188 +++ .../Filter/UpdateColumnsWhenShowAllGoals.php | 153 ++ oss/piwik/trunk/core/DataTable/Manager.php | 143 ++ oss/piwik/trunk/core/DataTable/Renderer.php | 124 ++ .../trunk/core/DataTable/Renderer/Console.php | 135 ++ .../trunk/core/DataTable/Renderer/Csv.php | 276 ++++ .../trunk/core/DataTable/Renderer/Html.php | 187 +++ .../trunk/core/DataTable/Renderer/Json.php | 73 + .../trunk/core/DataTable/Renderer/Php.php | 227 +++ .../trunk/core/DataTable/Renderer/Rss.php | 171 +++ .../trunk/core/DataTable/Renderer/Tsv.php | 34 + .../trunk/core/DataTable/Renderer/Xml.php | 328 ++++ oss/piwik/trunk/core/DataTable/Row.php | 437 ++++++ .../core/DataTable/Row/DataTableSummary.php | 34 + oss/piwik/trunk/core/DataTable/Simple.php | 41 + 49 files changed, 7442 insertions(+) create mode 100644 oss/piwik/trunk/core/Archive/Array.php create mode 100644 oss/piwik/trunk/core/Archive/Array/IndexedByDate.php create mode 100644 oss/piwik/trunk/core/Archive/Array/IndexedBySite.php create mode 100644 oss/piwik/trunk/core/Archive/Single.php create mode 100644 oss/piwik/trunk/core/ArchiveProcessing/Day.php create mode 100644 oss/piwik/trunk/core/ArchiveProcessing/Period.php create mode 100644 oss/piwik/trunk/core/ArchiveProcessing/Record.php create mode 100644 oss/piwik/trunk/core/ArchiveProcessing/Record/Blob.php create mode 100644 oss/piwik/trunk/core/ArchiveProcessing/Record/Numeric.php create mode 100644 oss/piwik/trunk/core/ArchiveProcessing/RecordArray.php create mode 100644 oss/piwik/trunk/core/DataFiles/Countries.php create mode 100644 oss/piwik/trunk/core/DataFiles/LanguageToCountry.php create mode 100644 oss/piwik/trunk/core/DataFiles/SearchEngines.php create mode 100644 oss/piwik/trunk/core/DataTable/Array.php create mode 100644 oss/piwik/trunk/core/DataTable/Filter.php create mode 100644 oss/piwik/trunk/core/DataTable/Filter/AddColumnsWhenShowAllColumns.php create mode 100644 oss/piwik/trunk/core/DataTable/Filter/AddConstantMetadata.php create mode 100644 oss/piwik/trunk/core/DataTable/Filter/AddSummaryRow.php create mode 100644 oss/piwik/trunk/core/DataTable/Filter/ColumnCallbackAddColumnPercentage.php create mode 100644 oss/piwik/trunk/core/DataTable/Filter/ColumnCallbackAddColumnQuotient.php create mode 100644 oss/piwik/trunk/core/DataTable/Filter/ColumnCallbackAddMetadata.php create mode 100644 oss/piwik/trunk/core/DataTable/Filter/ColumnCallbackDeleteRow.php create mode 100644 oss/piwik/trunk/core/DataTable/Filter/ColumnCallbackReplace.php create mode 100644 oss/piwik/trunk/core/DataTable/Filter/ColumnDelete.php create mode 100644 oss/piwik/trunk/core/DataTable/Filter/ExcludeLowPopulation.php create mode 100644 oss/piwik/trunk/core/DataTable/Filter/Limit.php create mode 100644 oss/piwik/trunk/core/DataTable/Filter/MetadataCallbackAddMetadata.php create mode 100644 oss/piwik/trunk/core/DataTable/Filter/MetadataCallbackReplace.php create mode 100644 oss/piwik/trunk/core/DataTable/Filter/Null.php create mode 100644 oss/piwik/trunk/core/DataTable/Filter/Pattern.php create mode 100644 oss/piwik/trunk/core/DataTable/Filter/PatternRecursive.php create mode 100644 oss/piwik/trunk/core/DataTable/Filter/ReplaceColumnNames.php create mode 100644 oss/piwik/trunk/core/DataTable/Filter/ReplaceSummaryRowLabel.php create mode 100644 oss/piwik/trunk/core/DataTable/Filter/SafeDecodeLabel.php create mode 100644 oss/piwik/trunk/core/DataTable/Filter/Sort.php create mode 100644 oss/piwik/trunk/core/DataTable/Filter/UpdateColumnsWhenShowAllGoals.php create mode 100644 oss/piwik/trunk/core/DataTable/Manager.php create mode 100644 oss/piwik/trunk/core/DataTable/Renderer.php create mode 100644 oss/piwik/trunk/core/DataTable/Renderer/Console.php create mode 100644 oss/piwik/trunk/core/DataTable/Renderer/Csv.php create mode 100644 oss/piwik/trunk/core/DataTable/Renderer/Html.php create mode 100644 oss/piwik/trunk/core/DataTable/Renderer/Json.php create mode 100644 oss/piwik/trunk/core/DataTable/Renderer/Php.php create mode 100644 oss/piwik/trunk/core/DataTable/Renderer/Rss.php create mode 100644 oss/piwik/trunk/core/DataTable/Renderer/Tsv.php create mode 100644 oss/piwik/trunk/core/DataTable/Renderer/Xml.php create mode 100644 oss/piwik/trunk/core/DataTable/Row.php create mode 100644 oss/piwik/trunk/core/DataTable/Row/DataTableSummary.php create mode 100644 oss/piwik/trunk/core/DataTable/Simple.php 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\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\n"; + } + } + $html .= "\t\n"; + } + + $html .= "\n
$name
$value
\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"; + } + } + $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"; + } + } + $html .= ""; + + } + $html .= "\n\n
$name
$value
"; + 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."\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."\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)."\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)); + } +}