From 1510afcf0467d7736bbbd197a15722c8a7b65b27 Mon Sep 17 00:00:00 2001
From: Benaka Moorthi <benaka.moorthi@gmail.com>
Date: Mon, 9 Sep 2013 15:01:27 -0400
Subject: [PATCH] Fixes #4063, allow csv renderer to handle arbitrarily nested
 data structures at least a little bit gracefully.

---
 core/DataTable/Renderer/Csv.php             | 81 +++++++++++++--------
 tests/PHPUnit/Integration/CsvExportTest.php |  8 ++
 tests/PHPUnit/IntegrationTestCase.php       |  9 ++-
 3 files changed, 66 insertions(+), 32 deletions(-)

diff --git a/core/DataTable/Renderer/Csv.php b/core/DataTable/Renderer/Csv.php
index 48b107877f..a9c569680f 100644
--- a/core/DataTable/Renderer/Csv.php
+++ b/core/DataTable/Renderer/Csv.php
@@ -202,36 +202,11 @@ class Csv extends Renderer
                 }
             }
         }
+
         $csv = array();
+        $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) {
-                                if ($this->translateColumnNames) {
-                                    $subName = $name != 'goals' ? $name . ' ' . $key
-                                        : Piwik_Translate('Goals_GoalX', $key);
-                                    $columnName = $this->translateColumnName($subKey)
-                                        . ' (' . $subName . ')';
-                                } else {
-                                    // goals_idgoal=1
-                                    $columnName = $name . "_" . $key . "_" . $subKey;
-                                }
-                                $allColumns[$columnName] = true;
-                                $csvRow[$columnName] = $subValue;
-                            }
-                        }
-                    }
-                } else {
-                    $allColumns[$name] = true;
-                    $csvRow[$name] = $value;
-                }
-            }
+            $csvRow = $this->flattenColumnArray($row->getColumns());
 
             if ($this->exportMetadata) {
                 $metadata = $row->getMetadata();
@@ -246,7 +221,6 @@ class Csv extends Renderer
                         $name = 'metadata_' . $name;
                     }
 
-                    $allColumns[$name] = true;
                     $csvRow[$name] = $value;
                 }
             }
@@ -260,6 +234,10 @@ class Csv extends Renderer
                 }
             }
 
+            foreach ($csvRow as $name => $value) {
+                $allColumns[$name] = true;
+            }
+
             $csv[] = $csvRow;
         }
 
@@ -378,4 +356,47 @@ class Csv extends Renderer
         @header('Content-Disposition: attachment; filename="' . $fileName . '"');
         Piwik::overrideCacheControlHeaders();
     }
-}
+
+    /**
+     * Flattens an array of column values so they can be outputted as CSV (which does not support
+     * nested structures).
+     */
+    private function flattenColumnArray($columns, &$csvRow = array(), $csvColumnNameTemplate = '%s')
+    {
+        foreach ($columns as $name => $value) {
+            $csvName = sprintf($csvColumnNameTemplate, $this->getCsvColumnName($name));
+
+            if (is_array($value)) {
+                // if we're translating column names and this is an array of arrays, the column name
+                // format becomes a bit more complicated. also in this case, we assume $value is not
+                // nested beyond 2 levels (ie, array(0 => array(0 => 1, 1 => 2)), but not array(
+                // 0 => array(0 => array(), 1 => array())) )
+                if ($this->translateColumnNames
+                    && is_array(reset($value))
+                ) {
+                    foreach ($value as $level1Key => $level1Value) {
+                        $inner = $name == 'goals' ? Piwik_Translate('Goals_GoalX', $level1Key) : $name . ' ' . $level1Key;
+                        $columnNameTemplate = '%s (' . $inner . ')';
+
+                        $this->flattenColumnArray($level1Value, $csvRow, $columnNameTemplate);
+                    }
+                } else {
+                    $this->flattenColumnArray($value, $csvRow, $csvName . '_%s');
+                }
+            } else {
+                $csvRow[$csvName] = $value;
+            }
+        }
+
+        return $csvRow;
+    }
+
+    private function getCsvColumnName($name)
+    {
+        if ($this->translateColumnNames) {
+            return $this->translateColumnName($name);
+        } else {
+            return $name;
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/PHPUnit/Integration/CsvExportTest.php b/tests/PHPUnit/Integration/CsvExportTest.php
index 7e84ababdb..a5a9dfd475 100755
--- a/tests/PHPUnit/Integration/CsvExportTest.php
+++ b/tests/PHPUnit/Integration/CsvExportTest.php
@@ -44,6 +44,14 @@ class Test_Piwik_Integration_CsvExport extends IntegrationTestCase
                                     'otherRequestParameters' => $deExtraParam,
                                     'language'               => 'de',
                                     'testSuffix'             => '_xp1_inner1_trans-de')),
+
+            // TODO: we cannot currently test the csv output of Live.getLastVisitsDetails. The API method includes
+            //       random output. Normally we remove this when it's in XML output, but it's harder to do that w/
+            //       CSV output. For now, we use compareOutput => false to just check that no errors result.
+            array('Live.getLastVisitsDetails', array('idSite'        => $idSite,
+                                                     'date'          => $dateTime,
+                                                     'format'        => 'csv',
+                                                     'compareOutput' => false))
         );
     }
 
diff --git a/tests/PHPUnit/IntegrationTestCase.php b/tests/PHPUnit/IntegrationTestCase.php
index 19b4a92ca6..1925561934 100755
--- a/tests/PHPUnit/IntegrationTestCase.php
+++ b/tests/PHPUnit/IntegrationTestCase.php
@@ -710,7 +710,7 @@ abstract class IntegrationTestCase extends PHPUnit_Framework_TestCase
         }
     }
 
-    protected function _testApiUrl($testName, $apiId, $requestUrl)
+    protected function _testApiUrl($testName, $apiId, $requestUrl, $compareOutput = true)
     {
         $isTestLogImportReverseChronological = strpos($testName, 'ImportedInRandomOrderTest') === false;
         $isLiveMustDeleteDates = (strpos($requestUrl, 'Live.getLastVisits') !== false
@@ -743,6 +743,10 @@ abstract class IntegrationTestCase extends PHPUnit_Framework_TestCase
 
         file_put_contents($processedFilePath, $response);
 
+        if (!$compareOutput) {
+            return;
+        }
+
         $expected = $this->loadExpectedFile($expectedFilePath);
         if (empty($expected)) {
             print("The expected file is not found at '$expectedFilePath'. The Processed response was:");
@@ -1014,7 +1018,8 @@ abstract class IntegrationTestCase extends PHPUnit_Framework_TestCase
             isset($params['fileExtension']) ? $params['fileExtension'] : false);
 
         foreach ($requestUrls as $apiId => $requestUrl) {
-            $this->_testApiUrl($testName . $testSuffix, $apiId, $requestUrl);
+            $this->_testApiUrl($testName . $testSuffix, $apiId, $requestUrl,
+                isset($params['compareOutput']) ? $params['compareOutput'] : true);
         }
 
         // change the language back to en
-- 
GitLab