| <?php |
|
|
| |
| |
| |
| |
| |
| |
|
|
| namespace Piwik\Plugins\ImageGraph; |
|
|
| use Exception; |
| use Piwik\API\Request; |
| use Piwik\Archive\DataTableFactory; |
| use Piwik\Common; |
| use Piwik\Container\StaticContainer; |
| use Piwik\DataTable; |
| use Piwik\DataTable\Map; |
| use Piwik\Exception\InvalidDimensionException; |
| use Piwik\Filesystem; |
| use Piwik\Period; |
| use Piwik\Piwik; |
| use Piwik\Request as PiwikRequest; |
| use Piwik\SettingsServer; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| 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( |
| StaticGraph::GRAPH_TYPE_BASIC_LINE => array( |
| self::FILENAME_KEY => 'BasicLine', |
| self::TRUNCATE_KEY => 6, |
| self::WIDTH_KEY => 1044, |
| self::HEIGHT_KEY => 290, |
| ), |
| StaticGraph::GRAPH_TYPE_VERTICAL_BAR => array( |
| self::FILENAME_KEY => 'BasicBar', |
| self::TRUNCATE_KEY => 6, |
| self::WIDTH_KEY => 1044, |
| self::HEIGHT_KEY => 290, |
| ), |
| StaticGraph::GRAPH_TYPE_HORIZONTAL_BAR => array( |
| self::FILENAME_KEY => 'HorizontalBar', |
| self::TRUNCATE_KEY => null, |
| self::WIDTH_KEY => 800, |
| self::HEIGHT_KEY => 290, |
| ), |
| StaticGraph::GRAPH_TYPE_3D_PIE => array( |
| self::FILENAME_KEY => '3DPie', |
| self::TRUNCATE_KEY => 5, |
| self::WIDTH_KEY => 1044, |
| self::HEIGHT_KEY => 290, |
| ), |
| 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 |
| => 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'; |
|
|
| |
| public const DEFAULT_NB_ROW_EVOLUTIONS = 5; |
| public const MAX_NB_ROW_LABELS = 10; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| public function get( |
| int $idSite, |
| string $period, |
| string $date, |
| string $apiModule, |
| string $apiAction, |
| $graphType = false, |
| int $outputType = API::GRAPH_OUTPUT_INLINE, |
| $columns = false, |
| $labels = false, |
| bool $showLegend = true, |
| $width = false, |
| $height = false, |
| int $fontSize = API::DEFAULT_FONT_SIZE, |
| $legendFontSize = false, |
| bool $aliasedGraph = true, |
| $idGoal = false, |
| $colors = false, |
| string $textColor = API::DEFAULT_TEXT_COLOR, |
| string $backgroundColor = API::DEFAULT_BACKGROUND_COLOR, |
| string $gridColor = API::DEFAULT_GRID_COLOR, |
| $idSubtable = false, |
| bool $legendAppendMetric = true, |
| $segment = false, |
| $idDimension = false |
| ) { |
| Piwik::checkUserHasViewAccess($idSite); |
|
|
| |
| 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 = [ |
| '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; |
| } |
|
|
| |
| $savedGET = $_GET; |
|
|
| try { |
| $apiParameters = []; |
| if (!empty($idGoal)) { |
| $apiParameters = ['idGoal' => $idGoal]; |
| } |
| if (!empty($idDimension)) { |
| $apiParameters = ['idDimension' => $idDimension]; |
| } |
| |
| $parameters = [ |
| '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 = $fontSize + self::DEFAULT_LEGEND_FONT_SIZE_OFFSET; |
| } |
|
|
| if (empty($graphType)) { |
| if ($isMultiplePeriod) { |
| $graphType = StaticGraph::GRAPH_TYPE_BASIC_LINE; |
| } else { |
| if ($constantRowsCount) { |
| $graphType = StaticGraph::GRAPH_TYPE_VERTICAL_BAR; |
| } else { |
| $graphType = 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 = 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]; |
| } |
|
|
| |
| $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]; |
| } |
|
|
| |
| $defaultFilterTruncate = self::$DEFAULT_PARAMETERS[$graphType][self::TRUNCATE_KEY]; |
| switch ($graphType) { |
| case StaticGraph::GRAPH_TYPE_3D_PIE: |
| case StaticGraph::GRAPH_TYPE_BASIC_PIE: |
| if (count($ordinateColumns) > 1) { |
| |
| throw new Exception("Pie charts do not currently support multiple series"); |
| } |
|
|
| $_GET['filter_sort_column'] = reset($ordinateColumns); |
| $this->setFilterTruncate($defaultFilterTruncate); |
| break; |
|
|
| case StaticGraph::GRAPH_TYPE_VERTICAL_BAR: |
| case StaticGraph::GRAPH_TYPE_BASIC_LINE: |
| if (!$isMultiplePeriod && !$constantRowsCount) { |
| $this->setFilterTruncate($defaultFilterTruncate); |
| } |
| break; |
| } |
|
|
| $ordinateLogos = []; |
|
|
| |
| if ($isMultiplePeriod && $reportHasDimension) { |
| $plottedMetric = reset($ordinateColumns); |
|
|
| |
| |
| if (!$labels) { |
| $savedFilterSortColumnValue = PiwikRequest::fromRequest()->getStringParameter('filter_sort_column', ''); |
| $_GET['filter_sort_column'] = $plottedMetric; |
|
|
| $savedFilterLimitValue = PiwikRequest::fromRequest()->getIntegerParameter('filter_limit', -1); |
| if ($savedFilterLimitValue == -1 || $savedFilterLimitValue > self::MAX_NB_ROW_LABELS) { |
| $_GET['filter_limit'] = self::DEFAULT_NB_ROW_EVOLUTIONS; |
| } |
| } |
|
|
| $parameters = [ |
| '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); |
|
|
| |
| if (!$processedReport) { |
| throw new Exception(Piwik::translate('General_NoDataForGraph')); |
| } |
|
|
| |
| if (!$labels) { |
| $_GET['filter_sort_column'] = $savedFilterSortColumnValue; |
| if ($savedFilterLimitValue != -1) { |
| $_GET['filter_limit'] = $savedFilterLimitValue; |
| } |
| } |
|
|
| |
| $metrics = $processedReport['metadata']['metrics']; |
| $ordinateLabels = []; |
|
|
| |
| if (!array_key_exists($plottedMetric, $metrics)) { |
| $ordinateColumns = []; |
| $i = 0; |
| foreach ($metrics as $metric => $info) { |
| $ordinateColumn = $plottedMetric . '_' . $i++; |
| $ordinateColumns[] = $metric; |
| $ordinateLabels[$ordinateColumn] = $info['name']; |
|
|
| if (isset($info['logo'])) { |
| $ordinateLogo = $info['logo']; |
|
|
| |
| 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 = [ |
| '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, |
| 'format_metrics' => 0, |
| ]; |
| |
| $processedReport = Request::processRequest('API.getProcessedReport', $parameters); |
| } |
|
|
| |
| $abscissaSeries = []; |
| $abscissaLogos = []; |
| $ordinateSeries = []; |
| |
| $reportData = $processedReport['reportData']; |
| $hasData = false; |
| $hasNonZeroValue = false; |
|
|
| if (!$isMultiplePeriod || !($reportData instanceof Map)) { |
| |
| $reportMetadata = $processedReport['reportMetadata']->getRows(); |
|
|
| $i = 0; |
| foreach ($reportData->getRows() as $row) { |
| |
| $rowData = $row->getColumns(); |
| $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 { |
| |
| $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(); |
|
|
| 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; |
| } |
| } |
|
|
| |
| $periodObj = $periodsData[$i]->getMetadata(DataTableFactory::TABLE_METADATA_PERIOD_INDEX); |
| $rowId = $periodObj->getLocalizedShortString(); |
| $abscissaSeries[] = Common::unsanitizeInputValue($rowId); |
| } |
| } |
|
|
| if (!$hasData || !$hasNonZeroValue) { |
| throw new Exception(Piwik::translate('General_NoDataForGraph')); |
| } |
|
|
| |
| $graph = 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); |
|
|
| |
| |
| 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(); |
| } |
|
|
| |
| $_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(int $default): void |
| { |
| $_GET['filter_truncate'] = PiwikRequest::fromRequest()->getIntegerParameter('filter_truncate', $default); |
| } |
|
|
| private static function parseOrdinateValue($ordinateValue): float |
| { |
| if (is_numeric($ordinateValue)) { |
| return (float) $ordinateValue; |
| } |
|
|
| $ordinateValue = str_replace(',', '.', (string) $ordinateValue); |
|
|
| |
| if (preg_match('/([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2}(\.[0-9]{2})?)/', $ordinateValue, $matches)) { |
| $hour = (int)$matches[1]; |
| $min = (int)$matches[2]; |
| $sec = (float)$matches[3]; |
|
|
| return ($hour * 3600) + ($min * 60) + $sec; |
| } |
|
|
| |
| $ordinateValue = preg_replace('/[^0-9.]/', '', $ordinateValue); |
|
|
| if (empty($ordinateValue) || $ordinateValue === '.') { |
| return 0.0; |
| } |
|
|
| return (float) $ordinateValue; |
| } |
|
|
| private static function getFontPath(string $font): string |
| { |
| return PIWIK_INCLUDE_PATH . self::FONT_DIR . $font; |
| } |
|
|
| protected static function getAbsoluteLogoPath(string $relativeLogoPath): string |
| { |
| return PIWIK_INCLUDE_PATH . '/' . $relativeLogoPath; |
| } |
| } |
|
|