<?php

/**
 * Matomo - free/libre analytics platform
 *
 * @link    https://matomo.org
 * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
 */
namespace Piwik\Plugins\ImageGraph;

use Exception;
use Piwik\API\Request;
use Piwik\Archive\DataTableFactory;
use Piwik\Common;
use Piwik\Container\StaticContainer;
use Piwik\DataTable\Map;
use Piwik\DataTable\Simple;
use Piwik\Exception\InvalidDimensionException;
use Piwik\Filesystem;
use Piwik\Period;
use Piwik\Piwik;
use Piwik\SettingsServer;
/**
 * The ImageGraph.get API call lets you generate beautiful static PNG Graphs for any existing Matomo report.
 * Supported graph types are: line plot, 2D/3D pie chart and vertical bar chart.
 *
 * A few notes about some of the parameters available:<br/>
 * - $graphType defines the type of graph plotted, accepted values are: 'evolution', 'verticalBar', 'pie' and '3dPie'<br/>
 * - $colors accepts a comma delimited list of colors that will overwrite the default Matomo colors <br/>
 * - you can also customize the width, height, font size, metric being plotted (in case the data contains multiple columns/metrics).
 *
 * See also <a href='https://developer.matomo.org/api-reference/reporting-api-metadata#static-image-graphs'>How to embed static Image Graphs?</a> for more information.
 *
 * @method static \Piwik\Plugins\ImageGraph\API getInstance()
 */
class API extends \Piwik\Plugin\API
{
    public const FILENAME_KEY = 'filename';
    public const TRUNCATE_KEY = 'truncate';
    public const WIDTH_KEY = 'width';
    public const HEIGHT_KEY = 'height';
    public const MAX_WIDTH = 2048;
    public const MAX_HEIGHT = 2048;
    private static $DEFAULT_PARAMETERS = array(\Piwik\Plugins\ImageGraph\StaticGraph::GRAPH_TYPE_BASIC_LINE => array(self::FILENAME_KEY => 'BasicLine', self::TRUNCATE_KEY => 6, self::WIDTH_KEY => 1044, self::HEIGHT_KEY => 290), \Piwik\Plugins\ImageGraph\StaticGraph::GRAPH_TYPE_VERTICAL_BAR => array(self::FILENAME_KEY => 'BasicBar', self::TRUNCATE_KEY => 6, self::WIDTH_KEY => 1044, self::HEIGHT_KEY => 290), \Piwik\Plugins\ImageGraph\StaticGraph::GRAPH_TYPE_HORIZONTAL_BAR => array(
        self::FILENAME_KEY => 'HorizontalBar',
        self::TRUNCATE_KEY => null,
        // horizontal bar graphs are dynamically truncated
        self::WIDTH_KEY => 800,
        self::HEIGHT_KEY => 290,
    ), \Piwik\Plugins\ImageGraph\StaticGraph::GRAPH_TYPE_3D_PIE => array(self::FILENAME_KEY => '3DPie', self::TRUNCATE_KEY => 5, self::WIDTH_KEY => 1044, self::HEIGHT_KEY => 290), \Piwik\Plugins\ImageGraph\StaticGraph::GRAPH_TYPE_BASIC_PIE => array(self::FILENAME_KEY => 'BasicPie', self::TRUNCATE_KEY => 5, self::WIDTH_KEY => 1044, self::HEIGHT_KEY => 290));
    private static $DEFAULT_GRAPH_TYPE_OVERRIDE = array('Referrers_getReferrerType' => array(\false => \Piwik\Plugins\ImageGraph\StaticGraph::GRAPH_TYPE_HORIZONTAL_BAR));
    public const GRAPH_OUTPUT_INLINE = 0;
    public const GRAPH_OUTPUT_FILE = 1;
    public const GRAPH_OUTPUT_PHP = 2;
    public const DEFAULT_ORDINATE_METRIC = 'nb_visits';
    public const FONT_DIR = '/plugins/ImageGraph/fonts/';
    public const DEFAULT_FONT = 'tahoma.ttf';
    public const UNICODE_FONT = 'unifont.ttf';
    public const DEFAULT_FONT_SIZE = 9;
    public const DEFAULT_LEGEND_FONT_SIZE_OFFSET = 2;
    public const DEFAULT_TEXT_COLOR = '222222';
    public const DEFAULT_BACKGROUND_COLOR = 'FFFFFF';
    public const DEFAULT_GRID_COLOR = 'CCCCCC';
    // number of row evolutions to plot when no labels are specified, can be overridden using &filter_limit
    public const DEFAULT_NB_ROW_EVOLUTIONS = 5;
    public const MAX_NB_ROW_LABELS = 10;
    public function get($idSite, $period, $date, $apiModule, $apiAction, $graphType = \false, $outputType = \Piwik\Plugins\ImageGraph\API::GRAPH_OUTPUT_INLINE, $columns = \false, $labels = \false, $showLegend = \true, $width = \false, $height = \false, $fontSize = \Piwik\Plugins\ImageGraph\API::DEFAULT_FONT_SIZE, $legendFontSize = \false, $aliasedGraph = \true, $idGoal = \false, $colors = \false, $textColor = \Piwik\Plugins\ImageGraph\API::DEFAULT_TEXT_COLOR, $backgroundColor = \Piwik\Plugins\ImageGraph\API::DEFAULT_BACKGROUND_COLOR, $gridColor = \Piwik\Plugins\ImageGraph\API::DEFAULT_GRID_COLOR, $idSubtable = \false, $legendAppendMetric = \true, $segment = \false, $idDimension = \false)
    {
        $idSite = (int) $idSite;
        Piwik::checkUserHasViewAccess($idSite);
        // Health check - should we also test for GD2 only?
        if (!SettingsServer::isGdExtensionEnabled()) {
            throw new Exception('Error: To create graphs in Matomo, please enable GD php extension (with Freetype support) in php.ini,
            and restart your web server.');
        }
        $useUnicodeFont = array('am', 'ar', 'el', 'fa', 'fi', 'he', 'ja', 'ka', 'ko', 'te', 'th', 'zh-cn', 'zh-tw');
        $languageLoaded = StaticContainer::get('Piwik\\Translation\\Translator')->getCurrentLanguage();
        $font = self::getFontPath(self::DEFAULT_FONT);
        if (in_array($languageLoaded, $useUnicodeFont)) {
            $unicodeFontPath = self::getFontPath(self::UNICODE_FONT);
            $font = file_exists($unicodeFontPath) ? $unicodeFontPath : $font;
        }
        // save original GET to reset after processing. Important for API-in-API-call
        $savedGET = $_GET;
        try {
            $apiParameters = array();
            if (!empty($idGoal)) {
                $apiParameters = array('idGoal' => $idGoal);
            }
            if (!empty($idDimension)) {
                $apiParameters = array('idDimension' => $idDimension);
            }
            // Fetch the metadata for given api-action
            $parameters = array('idSite' => $idSite, 'apiModule' => $apiModule, 'apiAction' => $apiAction, 'apiParameters' => $apiParameters, 'language' => $languageLoaded, 'period' => $period, 'date' => $date, 'hideMetricsDoc' => \false, 'showSubtableReports' => \true);
            $metadata = Request::processRequest('API.getMetadata', $parameters);
            if (!$metadata) {
                throw new Exception('Invalid API Module and/or API Action');
            }
            $metadata = $metadata[0];
            $reportHasDimension = !empty($metadata['dimension']);
            $constantRowsCount = !empty($metadata['constantRowsCount']);
            $isMultiplePeriod = Period::isMultiplePeriod($date, $period);
            if (!$reportHasDimension && !$isMultiplePeriod) {
                throw new Exception('The graph cannot be drawn for this combination of \'date\' and \'period\' parameters.');
            }
            if (empty($legendFontSize)) {
                $legendFontSize = (int) $fontSize + self::DEFAULT_LEGEND_FONT_SIZE_OFFSET;
            }
            if (empty($graphType)) {
                if ($isMultiplePeriod) {
                    $graphType = \Piwik\Plugins\ImageGraph\StaticGraph::GRAPH_TYPE_BASIC_LINE;
                } else {
                    if ($constantRowsCount) {
                        $graphType = \Piwik\Plugins\ImageGraph\StaticGraph::GRAPH_TYPE_VERTICAL_BAR;
                    } else {
                        $graphType = \Piwik\Plugins\ImageGraph\StaticGraph::GRAPH_TYPE_HORIZONTAL_BAR;
                    }
                }
                $reportUniqueId = $metadata['uniqueId'];
                if (isset(self::$DEFAULT_GRAPH_TYPE_OVERRIDE[$reportUniqueId][$isMultiplePeriod])) {
                    $graphType = self::$DEFAULT_GRAPH_TYPE_OVERRIDE[$reportUniqueId][$isMultiplePeriod];
                }
            } else {
                $availableGraphTypes = \Piwik\Plugins\ImageGraph\StaticGraph::getAvailableStaticGraphTypes();
                if (!in_array($graphType, $availableGraphTypes)) {
                    throw new Exception(Piwik::translate('General_ExceptionInvalidStaticGraphType', array($graphType, implode(', ', $availableGraphTypes))));
                }
            }
            $width = (int) $width;
            $height = (int) $height;
            if (empty($width)) {
                $width = self::$DEFAULT_PARAMETERS[$graphType][self::WIDTH_KEY];
            }
            if (empty($height)) {
                $height = self::$DEFAULT_PARAMETERS[$graphType][self::HEIGHT_KEY];
            }
            // Cap width and height to a safe amount
            $width = min($width, self::MAX_WIDTH);
            $height = min($height, self::MAX_HEIGHT);
            $reportColumns = array_merge(!empty($metadata['metrics']) ? $metadata['metrics'] : array(), !empty($metadata['processedMetrics']) ? $metadata['processedMetrics'] : array(), !empty($metadata['metricsGoal']) ? $metadata['metricsGoal'] : array(), !empty($metadata['processedMetricsGoal']) ? $metadata['processedMetricsGoal'] : array());
            $ordinateColumns = array();
            if (empty($columns)) {
                if (!empty($reportColumns[self::DEFAULT_ORDINATE_METRIC])) {
                    $ordinateColumns[] = self::DEFAULT_ORDINATE_METRIC;
                } elseif (!empty($metadata['metrics'])) {
                    $ordinateColumns[] = key($metadata['metrics']);
                } else {
                    throw new Exception(Piwik::translate('ImageGraph_ColumnOrdinateMissing', array(self::DEFAULT_ORDINATE_METRIC, implode(',', array_keys($reportColumns)))));
                }
            } else {
                $ordinateColumns = explode(',', $columns);
                foreach ($ordinateColumns as $column) {
                    if (empty($reportColumns[$column])) {
                        throw new Exception(Piwik::translate('ImageGraph_ColumnOrdinateMissing', array($column, implode(',', array_keys($reportColumns)))));
                    }
                }
            }
            $ordinateLabels = array();
            foreach ($ordinateColumns as $column) {
                $ordinateLabels[$column] = $reportColumns[$column];
            }
            // sort and truncate filters
            $defaultFilterTruncate = self::$DEFAULT_PARAMETERS[$graphType][self::TRUNCATE_KEY];
            switch ($graphType) {
                case \Piwik\Plugins\ImageGraph\StaticGraph::GRAPH_TYPE_3D_PIE:
                case \Piwik\Plugins\ImageGraph\StaticGraph::GRAPH_TYPE_BASIC_PIE:
                    if (count($ordinateColumns) > 1) {
                        // CpChart doesn't support multiple series on pie charts
                        throw new Exception("Pie charts do not currently support multiple series");
                    }
                    $_GET['filter_sort_column'] = reset($ordinateColumns);
                    $this->setFilterTruncate($defaultFilterTruncate);
                    break;
                case \Piwik\Plugins\ImageGraph\StaticGraph::GRAPH_TYPE_VERTICAL_BAR:
                case \Piwik\Plugins\ImageGraph\StaticGraph::GRAPH_TYPE_BASIC_LINE:
                    if (!$isMultiplePeriod && !$constantRowsCount) {
                        $this->setFilterTruncate($defaultFilterTruncate);
                    }
                    break;
            }
            $ordinateLogos = array();
            // row evolutions
            if ($isMultiplePeriod && $reportHasDimension) {
                $plottedMetric = reset($ordinateColumns);
                // when no labels are specified, getRowEvolution returns the top N=filter_limit row evolutions
                // rows are sorted using filter_sort_column (see DataTableGenericFilter for more info)
                if (!$labels) {
                    $savedFilterSortColumnValue = Common::getRequestVar('filter_sort_column', '');
                    $_GET['filter_sort_column'] = $plottedMetric;
                    $savedFilterLimitValue = Common::getRequestVar('filter_limit', -1, 'int');
                    if ($savedFilterLimitValue == -1 || $savedFilterLimitValue > self::MAX_NB_ROW_LABELS) {
                        $_GET['filter_limit'] = self::DEFAULT_NB_ROW_EVOLUTIONS;
                    }
                }
                $parameters = array('idSite' => $idSite, 'period' => $period, 'date' => $date, 'apiModule' => $apiModule, 'apiAction' => $apiAction, 'label' => $labels, 'segment' => $segment, 'column' => $plottedMetric, 'language' => $languageLoaded, 'idGoal' => $idGoal, 'idDimension' => $idDimension, 'legendAppendMetric' => $legendAppendMetric, 'labelUseAbsoluteUrl' => \false);
                $processedReport = Request::processRequest('API.getRowEvolution', $parameters);
                //@review this test will need to be updated after evaluating the @review comment in API/API.php
                if (!$processedReport) {
                    throw new Exception(Piwik::translate('General_NoDataForGraph'));
                }
                // restoring generic filter parameters
                if (!$labels) {
                    $_GET['filter_sort_column'] = $savedFilterSortColumnValue;
                    if ($savedFilterLimitValue != -1) {
                        $_GET['filter_limit'] = $savedFilterLimitValue;
                    }
                }
                // retrieve metric names & labels
                $metrics = $processedReport['metadata']['metrics'];
                $ordinateLabels = array();
                // getRowEvolution returned more than one label
                if (!array_key_exists($plottedMetric, $metrics)) {
                    $ordinateColumns = array();
                    $i = 0;
                    foreach ($metrics as $metric => $info) {
                        $ordinateColumn = $plottedMetric . '_' . $i++;
                        $ordinateColumns[] = $metric;
                        $ordinateLabels[$ordinateColumn] = $info['name'];
                        if (isset($info['logo'])) {
                            $ordinateLogo = $info['logo'];
                            // @review pChart does not support gifs in graph legends, would it be possible to convert all plugin pictures (cookie.gif, flash.gif, ..) to png files?
                            if (!strstr($ordinateLogo, '.gif')) {
                                $absoluteLogoPath = self::getAbsoluteLogoPath($ordinateLogo);
                                if (file_exists($absoluteLogoPath)) {
                                    $ordinateLogos[$ordinateColumn] = $absoluteLogoPath;
                                }
                            }
                        }
                    }
                } else {
                    $ordinateLabels[$plottedMetric] = $processedReport['label'] . ' (' . $metrics[$plottedMetric]['name'] . ')';
                }
            } else {
                $parameters = array('idSite' => $idSite, 'period' => $period, 'date' => $date, 'apiModule' => $apiModule, 'apiAction' => $apiAction, 'segment' => $segment, 'apiParameters' => \false, 'idGoal' => $idGoal, 'idDimension' => $idDimension, 'language' => $languageLoaded, 'showTimer' => \true, 'hideMetricsDoc' => \false, 'idSubtable' => $idSubtable, 'showRawMetrics' => \false);
                $processedReport = Request::processRequest('API.getProcessedReport', $parameters);
            }
            // prepare abscissa and ordinate series
            $abscissaSeries = array();
            $abscissaLogos = array();
            $ordinateSeries = array();
            /** @var \Piwik\DataTable\Simple|\Piwik\DataTable\Map $reportData */
            $reportData = $processedReport['reportData'];
            $hasData = \false;
            $hasNonZeroValue = \false;
            if (!$isMultiplePeriod || !$reportData instanceof Map) {
                $reportMetadata = $processedReport['reportMetadata']->getRows();
                $i = 0;
                // $reportData instanceof DataTable
                foreach ($reportData->getRows() as $row) {
                    // Row[]
                    // $row instanceof Row
                    $rowData = $row->getColumns();
                    // Associative Array
                    $abscissaSeries[] = Common::unsanitizeInputValue($rowData['label']);
                    foreach ($ordinateColumns as $column) {
                        $parsedOrdinateValue = $this->parseOrdinateValue($rowData[$column]);
                        $hasData = \true;
                        if ($parsedOrdinateValue != 0) {
                            $hasNonZeroValue = \true;
                        }
                        $ordinateSeries[$column][] = $parsedOrdinateValue;
                    }
                    if (isset($reportMetadata[$i])) {
                        $rowMetadata = $reportMetadata[$i]->getColumns();
                        if (isset($rowMetadata['logo'])) {
                            $absoluteLogoPath = self::getAbsoluteLogoPath($rowMetadata['logo']);
                            if (file_exists($absoluteLogoPath)) {
                                $abscissaLogos[$i] = $absoluteLogoPath;
                            }
                        }
                    }
                    $i++;
                }
            } else {
                // if the report has no dimension we have multiple reports each with only one row within the reportData
                /** @var Simple[] $periodsData */
                $periodsData = array_values($reportData->getDataTables());
                $periodsCount = count($periodsData);
                for ($i = 0; $i < $periodsCount; $i++) {
                    if (empty($periodsData[$i])) {
                        continue;
                    }
                    $rows = $periodsData[$i]->getRows();
                    if (array_key_exists(0, $rows)) {
                        $rowData = $rows[0]->getColumns();
                        // associative Array
                        foreach ($ordinateColumns as $column) {
                            if (!isset($rowData[$column])) {
                                continue;
                            }
                            $ordinateValue = $rowData[$column];
                            $parsedOrdinateValue = $this->parseOrdinateValue($ordinateValue);
                            $hasData = \true;
                            if (!empty($parsedOrdinateValue)) {
                                $hasNonZeroValue = \true;
                            }
                            $ordinateSeries[$column][] = $parsedOrdinateValue;
                        }
                    } else {
                        foreach ($ordinateColumns as $column) {
                            $ordinateSeries[$column][] = 0;
                        }
                    }
                    $rowId = $periodsData[$i]->getMetadata(DataTableFactory::TABLE_METADATA_PERIOD_INDEX)->getLocalizedShortString();
                    $abscissaSeries[] = Common::unsanitizeInputValue($rowId);
                }
            }
            if (!$hasData || !$hasNonZeroValue) {
                throw new Exception(Piwik::translate('General_NoDataForGraph'));
            }
            /** @var StaticGraph $graph */
            $graph = \Piwik\Plugins\ImageGraph\StaticGraph::factory($graphType);
            $graph->setWidth($width);
            $graph->setHeight($height);
            $graph->setFont($font);
            $graph->setFontSize($fontSize);
            $graph->setLegendFontSize($legendFontSize);
            $graph->setOrdinateLabels($ordinateLabels);
            $graph->setShowLegend($showLegend);
            $graph->setAliasedGraph($aliasedGraph);
            $graph->setAbscissaSeries($abscissaSeries);
            $graph->setAbscissaLogos($abscissaLogos);
            $graph->setOrdinateSeries($ordinateSeries);
            $graph->setOrdinateLogos($ordinateLogos);
            $graph->setColors(!empty($colors) ? explode(',', $colors) : array());
            $graph->setTextColor($textColor);
            $graph->setBackgroundColor($backgroundColor);
            $graph->setGridColor($gridColor);
            // when requested period is day, x-axis unit is time and all date labels can not be displayed
            // within requested width, force labels to be skipped every 6 days to delimit weeks
            if ($period == 'day' && $isMultiplePeriod) {
                $graph->setForceSkippedLabels(6);
            }
            $graph->renderGraph();
        } catch (InvalidDimensionException $e) {
            throw $e;
        } catch (\Exception $e) {
            $graph = new \Piwik\Plugins\ImageGraph\StaticGraph\Exception();
            $graph->setWidth($width);
            $graph->setHeight($height);
            $graph->setFont($font);
            $graph->setFontSize($fontSize);
            $graph->setBackgroundColor($backgroundColor);
            $graph->setTextColor($textColor);
            $graph->setException($e);
            $graph->renderGraph();
        }
        // restoring get parameters
        $_GET = $savedGET;
        switch ($outputType) {
            case self::GRAPH_OUTPUT_FILE:
                if ($idGoal != '') {
                    $idGoal = '_' . $idGoal;
                }
                if ($idDimension != '') {
                    $idDimension = '__' . $idDimension;
                }
                $fileName = self::$DEFAULT_PARAMETERS[$graphType][self::FILENAME_KEY] . '_' . $apiModule . '_' . $apiAction . $idGoal . $idDimension . ' ' . str_replace(',', '-', $date) . ' ' . $idSite . '.png';
                $fileName = str_replace(array(' ', '/'), '_', $fileName);
                if (!Filesystem::isValidFilename($fileName)) {
                    throw new Exception('Error: Image graph filename ' . $fileName . ' is not valid.');
                }
                return $graph->sendToDisk($fileName);
            case self::GRAPH_OUTPUT_PHP:
                return $graph->getRenderedImage();
            case self::GRAPH_OUTPUT_INLINE:
            default:
                $graph->sendToBrowser();
                exit;
        }
    }
    private function setFilterTruncate($default)
    {
        $_GET['filter_truncate'] = Common::getRequestVar('filter_truncate', $default, 'int');
    }
    private static function parseOrdinateValue($ordinateValue)
    {
        $ordinateValue = @str_replace(',', '.', $ordinateValue);
        // convert hh:mm:ss formatted time values to number of seconds
        if (preg_match('/([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2}(\\.[0-9]{2})?)/', $ordinateValue, $matches)) {
            $hour = $matches[1];
            $min = $matches[2];
            $sec = $matches[3];
            $ordinateValue = $hour * 3600 + $min * 60 + $sec;
        }
        // OK, only numbers from here please (strip out currency sign)
        $ordinateValue = preg_replace('/[^0-9.]/', '', $ordinateValue);
        return $ordinateValue;
    }
    private static function getFontPath($font)
    {
        return PIWIK_INCLUDE_PATH . self::FONT_DIR . $font;
    }
    protected static function getAbsoluteLogoPath($relativeLogoPath)
    {
        return PIWIK_INCLUDE_PATH . '/' . $relativeLogoPath;
    }
}
