PageRenderTime 52ms CodeModel.GetById 18ms RepoModel.GetById 1ms app.codeStats 0ms

/plugins/ImageGraph/API.php

https://github.com/CodeYellowBV/piwik
PHP | 548 lines | 431 code | 68 blank | 49 comment | 62 complexity | 87c5ac1c961a12aaae7a38ed39aab605 MD5 | raw file
Possible License(s): LGPL-3.0, JSON, MIT, GPL-3.0, LGPL-2.1, GPL-2.0, AGPL-1.0, BSD-2-Clause, BSD-3-Clause
  1. <?php
  2. /**
  3. * Piwik - free/libre analytics platform
  4. *
  5. * @link http://piwik.org
  6. * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  7. *
  8. */
  9. namespace Piwik\Plugins\ImageGraph;
  10. use Exception;
  11. use Piwik\Archive\DataTableFactory;
  12. use Piwik\Common;
  13. use Piwik\Filesystem;
  14. use Piwik\Period;
  15. use Piwik\Piwik;
  16. use Piwik\Plugins\API\API as APIMetadata;
  17. use Piwik\Plugins\ImageGraph\StaticGraph;
  18. use Piwik\SettingsServer;
  19. use Piwik\Translate;
  20. /**
  21. * The ImageGraph.get API call lets you generate beautiful static PNG Graphs for any existing Piwik report.
  22. * Supported graph types are: line plot, 2D/3D pie chart and vertical bar chart.
  23. *
  24. * A few notes about some of the parameters available:<br/>
  25. * - $graphType defines the type of graph plotted, accepted values are: 'evolution', 'verticalBar', 'pie' and '3dPie'<br/>
  26. * - $colors accepts a comma delimited list of colors that will overwrite the default Piwik colors <br/>
  27. * - you can also customize the width, height, font size, metric being plotted (in case the data contains multiple columns/metrics).
  28. *
  29. * See also <a href='http://piwik.org/docs/analytics-api/metadata/#toc-static-image-graphs'>How to embed static Image Graphs?</a> for more information.
  30. *
  31. * @method static \Piwik\Plugins\ImageGraph\API getInstance()
  32. */
  33. class API extends \Piwik\Plugin\API
  34. {
  35. const FILENAME_KEY = 'filename';
  36. const TRUNCATE_KEY = 'truncate';
  37. const WIDTH_KEY = 'width';
  38. const HEIGHT_KEY = 'height';
  39. const MAX_WIDTH = 2048;
  40. const MAX_HEIGHT = 2048;
  41. static private $DEFAULT_PARAMETERS = array(
  42. StaticGraph::GRAPH_TYPE_BASIC_LINE => array(
  43. self::FILENAME_KEY => 'BasicLine',
  44. self::TRUNCATE_KEY => 6,
  45. self::WIDTH_KEY => 1044,
  46. self::HEIGHT_KEY => 290,
  47. ),
  48. StaticGraph::GRAPH_TYPE_VERTICAL_BAR => array(
  49. self::FILENAME_KEY => 'BasicBar',
  50. self::TRUNCATE_KEY => 6,
  51. self::WIDTH_KEY => 1044,
  52. self::HEIGHT_KEY => 290,
  53. ),
  54. StaticGraph::GRAPH_TYPE_HORIZONTAL_BAR => array(
  55. self::FILENAME_KEY => 'HorizontalBar',
  56. self::TRUNCATE_KEY => null, // horizontal bar graphs are dynamically truncated
  57. self::WIDTH_KEY => 800,
  58. self::HEIGHT_KEY => 290,
  59. ),
  60. StaticGraph::GRAPH_TYPE_3D_PIE => array(
  61. self::FILENAME_KEY => '3DPie',
  62. self::TRUNCATE_KEY => 5,
  63. self::WIDTH_KEY => 1044,
  64. self::HEIGHT_KEY => 290,
  65. ),
  66. StaticGraph::GRAPH_TYPE_BASIC_PIE => array(
  67. self::FILENAME_KEY => 'BasicPie',
  68. self::TRUNCATE_KEY => 5,
  69. self::WIDTH_KEY => 1044,
  70. self::HEIGHT_KEY => 290,
  71. ),
  72. );
  73. static private $DEFAULT_GRAPH_TYPE_OVERRIDE = array(
  74. 'UserSettings_getPlugin' => array(
  75. false // override if !$isMultiplePeriod
  76. => StaticGraph::GRAPH_TYPE_HORIZONTAL_BAR,
  77. ),
  78. 'Referrers_getReferrerType' => array(
  79. false // override if !$isMultiplePeriod
  80. => StaticGraph::GRAPH_TYPE_HORIZONTAL_BAR,
  81. ),
  82. );
  83. const GRAPH_OUTPUT_INLINE = 0;
  84. const GRAPH_OUTPUT_FILE = 1;
  85. const GRAPH_OUTPUT_PHP = 2;
  86. const DEFAULT_ORDINATE_METRIC = 'nb_visits';
  87. const FONT_DIR = '/plugins/ImageGraph/fonts/';
  88. const DEFAULT_FONT = 'tahoma.ttf';
  89. const UNICODE_FONT = 'unifont.ttf';
  90. const DEFAULT_FONT_SIZE = 9;
  91. const DEFAULT_LEGEND_FONT_SIZE_OFFSET = 2;
  92. const DEFAULT_TEXT_COLOR = '222222';
  93. const DEFAULT_BACKGROUND_COLOR = 'FFFFFF';
  94. const DEFAULT_GRID_COLOR = 'CCCCCC';
  95. // number of row evolutions to plot when no labels are specified, can be overridden using &filter_limit
  96. const DEFAULT_NB_ROW_EVOLUTIONS = 5;
  97. const MAX_NB_ROW_LABELS = 10;
  98. public function get(
  99. $idSite,
  100. $period,
  101. $date,
  102. $apiModule,
  103. $apiAction,
  104. $graphType = false,
  105. $outputType = API::GRAPH_OUTPUT_INLINE,
  106. $columns = false,
  107. $labels = false,
  108. $showLegend = true,
  109. $width = false,
  110. $height = false,
  111. $fontSize = API::DEFAULT_FONT_SIZE,
  112. $legendFontSize = false,
  113. $aliasedGraph = true,
  114. $idGoal = false,
  115. $colors = false,
  116. $textColor = API::DEFAULT_TEXT_COLOR,
  117. $backgroundColor = API::DEFAULT_BACKGROUND_COLOR,
  118. $gridColor = API::DEFAULT_GRID_COLOR,
  119. $idSubtable = false,
  120. $legendAppendMetric = true,
  121. $segment = false
  122. )
  123. {
  124. Piwik::checkUserHasViewAccess($idSite);
  125. // Health check - should we also test for GD2 only?
  126. if (!SettingsServer::isGdExtensionEnabled()) {
  127. throw new Exception('Error: To create graphs in Piwik, please enable GD php extension (with Freetype support) in php.ini,
  128. and restart your web server.');
  129. }
  130. $useUnicodeFont = array(
  131. 'am', 'ar', 'el', 'fa', 'fi', 'he', 'ja', 'ka', 'ko', 'te', 'th', 'zh-cn', 'zh-tw',
  132. );
  133. $languageLoaded = Translate::getLanguageLoaded();
  134. $font = self::getFontPath(self::DEFAULT_FONT);
  135. if (in_array($languageLoaded, $useUnicodeFont)) {
  136. $unicodeFontPath = self::getFontPath(self::UNICODE_FONT);
  137. $font = file_exists($unicodeFontPath) ? $unicodeFontPath : $font;
  138. }
  139. // save original GET to reset after processing. Important for API-in-API-call
  140. $savedGET = $_GET;
  141. try {
  142. $apiParameters = array();
  143. if (!empty($idGoal)) {
  144. $apiParameters = array('idGoal' => $idGoal);
  145. }
  146. // Fetch the metadata for given api-action
  147. $metadata = APIMetadata::getInstance()->getMetadata(
  148. $idSite, $apiModule, $apiAction, $apiParameters, $languageLoaded, $period, $date,
  149. $hideMetricsDoc = false, $showSubtableReports = true);
  150. if (!$metadata) {
  151. throw new Exception('Invalid API Module and/or API Action');
  152. }
  153. $metadata = $metadata[0];
  154. $reportHasDimension = !empty($metadata['dimension']);
  155. $constantRowsCount = !empty($metadata['constantRowsCount']);
  156. $isMultiplePeriod = Period::isMultiplePeriod($date, $period);
  157. if (!$reportHasDimension && !$isMultiplePeriod) {
  158. throw new Exception('The graph cannot be drawn for this combination of \'date\' and \'period\' parameters.');
  159. }
  160. if (empty($legendFontSize)) {
  161. $legendFontSize = (int)$fontSize + self::DEFAULT_LEGEND_FONT_SIZE_OFFSET;
  162. }
  163. if (empty($graphType)) {
  164. if ($isMultiplePeriod) {
  165. $graphType = StaticGraph::GRAPH_TYPE_BASIC_LINE;
  166. } else {
  167. if ($constantRowsCount) {
  168. $graphType = StaticGraph::GRAPH_TYPE_VERTICAL_BAR;
  169. } else {
  170. $graphType = StaticGraph::GRAPH_TYPE_HORIZONTAL_BAR;
  171. }
  172. }
  173. $reportUniqueId = $metadata['uniqueId'];
  174. if (isset(self::$DEFAULT_GRAPH_TYPE_OVERRIDE[$reportUniqueId][$isMultiplePeriod])) {
  175. $graphType = self::$DEFAULT_GRAPH_TYPE_OVERRIDE[$reportUniqueId][$isMultiplePeriod];
  176. }
  177. } else {
  178. $availableGraphTypes = StaticGraph::getAvailableStaticGraphTypes();
  179. if (!in_array($graphType, $availableGraphTypes)) {
  180. throw new Exception(
  181. Piwik::translate(
  182. 'General_ExceptionInvalidStaticGraphType',
  183. array($graphType, implode(', ', $availableGraphTypes))
  184. )
  185. );
  186. }
  187. }
  188. $width = (int)$width;
  189. $height = (int)$height;
  190. if (empty($width)) {
  191. $width = self::$DEFAULT_PARAMETERS[$graphType][self::WIDTH_KEY];
  192. }
  193. if (empty($height)) {
  194. $height = self::$DEFAULT_PARAMETERS[$graphType][self::HEIGHT_KEY];
  195. }
  196. // Cap width and height to a safe amount
  197. $width = min($width, self::MAX_WIDTH);
  198. $height = min($height, self::MAX_HEIGHT);
  199. $reportColumns = array_merge(
  200. !empty($metadata['metrics']) ? $metadata['metrics'] : array(),
  201. !empty($metadata['processedMetrics']) ? $metadata['processedMetrics'] : array(),
  202. !empty($metadata['metricsGoal']) ? $metadata['metricsGoal'] : array(),
  203. !empty($metadata['processedMetricsGoal']) ? $metadata['processedMetricsGoal'] : array()
  204. );
  205. $ordinateColumns = array();
  206. if (empty($columns)) {
  207. $ordinateColumns[] =
  208. empty($reportColumns[self::DEFAULT_ORDINATE_METRIC]) ? key($metadata['metrics']) : self::DEFAULT_ORDINATE_METRIC;
  209. } else {
  210. $ordinateColumns = explode(',', $columns);
  211. foreach ($ordinateColumns as $column) {
  212. if (empty($reportColumns[$column])) {
  213. throw new Exception(
  214. Piwik::translate(
  215. 'ImageGraph_ColumnOrdinateMissing',
  216. array($column, implode(',', array_keys($reportColumns)))
  217. )
  218. );
  219. }
  220. }
  221. }
  222. $ordinateLabels = array();
  223. foreach ($ordinateColumns as $column) {
  224. $ordinateLabels[$column] = $reportColumns[$column];
  225. }
  226. // sort and truncate filters
  227. $defaultFilterTruncate = self::$DEFAULT_PARAMETERS[$graphType][self::TRUNCATE_KEY];
  228. switch ($graphType) {
  229. case StaticGraph::GRAPH_TYPE_3D_PIE:
  230. case StaticGraph::GRAPH_TYPE_BASIC_PIE:
  231. if (count($ordinateColumns) > 1) {
  232. // pChart doesn't support multiple series on pie charts
  233. throw new Exception("Pie charts do not currently support multiple series");
  234. }
  235. $_GET['filter_sort_column'] = reset($ordinateColumns);
  236. $this->setFilterTruncate($defaultFilterTruncate);
  237. break;
  238. case StaticGraph::GRAPH_TYPE_VERTICAL_BAR:
  239. case StaticGraph::GRAPH_TYPE_BASIC_LINE:
  240. if (!$isMultiplePeriod && !$constantRowsCount) {
  241. $this->setFilterTruncate($defaultFilterTruncate);
  242. }
  243. break;
  244. }
  245. $ordinateLogos = array();
  246. // row evolutions
  247. if ($isMultiplePeriod && $reportHasDimension) {
  248. $plottedMetric = reset($ordinateColumns);
  249. // when no labels are specified, getRowEvolution returns the top N=filter_limit row evolutions
  250. // rows are sorted using filter_sort_column (see DataTableGenericFilter for more info)
  251. if (!$labels) {
  252. $savedFilterSortColumnValue = Common::getRequestVar('filter_sort_column', '');
  253. $_GET['filter_sort_column'] = $plottedMetric;
  254. $savedFilterLimitValue = Common::getRequestVar('filter_limit', -1, 'int');
  255. if ($savedFilterLimitValue == -1 || $savedFilterLimitValue > self::MAX_NB_ROW_LABELS) {
  256. $_GET['filter_limit'] = self::DEFAULT_NB_ROW_EVOLUTIONS;
  257. }
  258. }
  259. $processedReport = APIMetadata::getInstance()->getRowEvolution(
  260. $idSite,
  261. $period,
  262. $date,
  263. $apiModule,
  264. $apiAction,
  265. $labels,
  266. $segment,
  267. $plottedMetric,
  268. $languageLoaded,
  269. $idGoal,
  270. $legendAppendMetric,
  271. $labelUseAbsoluteUrl = false
  272. );
  273. //@review this test will need to be updated after evaluating the @review comment in API/API.php
  274. if (!$processedReport) {
  275. throw new Exception(Piwik::translate('General_NoDataForGraph'));
  276. }
  277. // restoring generic filter parameters
  278. if (!$labels) {
  279. $_GET['filter_sort_column'] = $savedFilterSortColumnValue;
  280. if ($savedFilterLimitValue != -1) {
  281. $_GET['filter_limit'] = $savedFilterLimitValue;
  282. }
  283. }
  284. // retrieve metric names & labels
  285. $metrics = $processedReport['metadata']['metrics'];
  286. $ordinateLabels = array();
  287. // getRowEvolution returned more than one label
  288. if (!array_key_exists($plottedMetric, $metrics)) {
  289. $ordinateColumns = array();
  290. $i = 0;
  291. foreach ($metrics as $metric => $info) {
  292. $ordinateColumn = $plottedMetric . '_' . $i++;
  293. $ordinateColumns[] = $metric;
  294. $ordinateLabels[$ordinateColumn] = $info['name'];
  295. if (isset($info['logo'])) {
  296. $ordinateLogo = $info['logo'];
  297. // @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?
  298. if (!strstr($ordinateLogo, '.gif')) {
  299. $absoluteLogoPath = self::getAbsoluteLogoPath($ordinateLogo);
  300. if (file_exists($absoluteLogoPath)) {
  301. $ordinateLogos[$ordinateColumn] = $absoluteLogoPath;
  302. }
  303. }
  304. }
  305. }
  306. } else {
  307. $ordinateLabels[$plottedMetric] = $processedReport['label'] . ' (' . $metrics[$plottedMetric]['name'] . ')';
  308. }
  309. } else {
  310. $processedReport = APIMetadata::getInstance()->getProcessedReport(
  311. $idSite,
  312. $period,
  313. $date,
  314. $apiModule,
  315. $apiAction,
  316. $segment,
  317. $apiParameters = false,
  318. $idGoal,
  319. $languageLoaded,
  320. $showTimer = true,
  321. $hideMetricsDoc = false,
  322. $idSubtable,
  323. $showRawMetrics = false
  324. );
  325. }
  326. // prepare abscissa and ordinate series
  327. $abscissaSeries = array();
  328. $abscissaLogos = array();
  329. $ordinateSeries = array();
  330. /** @var \Piwik\DataTable\Simple|\Piwik\DataTable\Map $reportData */
  331. $reportData = $processedReport['reportData'];
  332. $hasData = false;
  333. $hasNonZeroValue = false;
  334. if (!$isMultiplePeriod) {
  335. $reportMetadata = $processedReport['reportMetadata']->getRows();
  336. $i = 0;
  337. // $reportData instanceof DataTable
  338. foreach ($reportData->getRows() as $row) // Row[]
  339. {
  340. // $row instanceof Row
  341. $rowData = $row->getColumns(); // Associative Array
  342. $abscissaSeries[] = Common::unsanitizeInputValue($rowData['label']);
  343. foreach ($ordinateColumns as $column) {
  344. $parsedOrdinateValue = $this->parseOrdinateValue($rowData[$column]);
  345. $hasData = true;
  346. if ($parsedOrdinateValue != 0) {
  347. $hasNonZeroValue = true;
  348. }
  349. $ordinateSeries[$column][] = $parsedOrdinateValue;
  350. }
  351. if (isset($reportMetadata[$i])) {
  352. $rowMetadata = $reportMetadata[$i]->getColumns();
  353. if (isset($rowMetadata['logo'])) {
  354. $absoluteLogoPath = self::getAbsoluteLogoPath($rowMetadata['logo']);
  355. if (file_exists($absoluteLogoPath)) {
  356. $abscissaLogos[$i] = $absoluteLogoPath;
  357. }
  358. }
  359. }
  360. $i++;
  361. }
  362. } else // if the report has no dimension we have multiple reports each with only one row within the reportData
  363. {
  364. // $periodsData instanceof Simple[]
  365. $periodsData = array_values($reportData->getDataTables());
  366. $periodsCount = count($periodsData);
  367. for ($i = 0; $i < $periodsCount; $i++) {
  368. // $periodsData[$i] instanceof Simple
  369. // $rows instanceof Row[]
  370. if (empty($periodsData[$i])) {
  371. continue;
  372. }
  373. $rows = $periodsData[$i]->getRows();
  374. if (array_key_exists(0, $rows)) {
  375. $rowData = $rows[0]->getColumns(); // associative Array
  376. foreach ($ordinateColumns as $column) {
  377. $ordinateValue = $rowData[$column];
  378. $parsedOrdinateValue = $this->parseOrdinateValue($ordinateValue);
  379. $hasData = true;
  380. if (!empty($parsedOrdinateValue)) {
  381. $hasNonZeroValue = true;
  382. }
  383. $ordinateSeries[$column][] = $parsedOrdinateValue;
  384. }
  385. } else {
  386. foreach ($ordinateColumns as $column) {
  387. $ordinateSeries[$column][] = 0;
  388. }
  389. }
  390. $rowId = $periodsData[$i]->getMetadata(DataTableFactory::TABLE_METADATA_PERIOD_INDEX)->getLocalizedShortString();
  391. $abscissaSeries[] = Common::unsanitizeInputValue($rowId);
  392. }
  393. }
  394. if (!$hasData || !$hasNonZeroValue) {
  395. throw new Exception(Piwik::translate('General_NoDataForGraph'));
  396. }
  397. //Setup the graph
  398. $graph = StaticGraph::factory($graphType);
  399. $graph->setWidth($width);
  400. $graph->setHeight($height);
  401. $graph->setFont($font);
  402. $graph->setFontSize($fontSize);
  403. $graph->setLegendFontSize($legendFontSize);
  404. $graph->setOrdinateLabels($ordinateLabels);
  405. $graph->setShowLegend($showLegend);
  406. $graph->setAliasedGraph($aliasedGraph);
  407. $graph->setAbscissaSeries($abscissaSeries);
  408. $graph->setAbscissaLogos($abscissaLogos);
  409. $graph->setOrdinateSeries($ordinateSeries);
  410. $graph->setOrdinateLogos($ordinateLogos);
  411. $graph->setColors(!empty($colors) ? explode(',', $colors) : array());
  412. $graph->setTextColor($textColor);
  413. $graph->setBackgroundColor($backgroundColor);
  414. $graph->setGridColor($gridColor);
  415. // when requested period is day, x-axis unit is time and all date labels can not be displayed
  416. // within requested width, force labels to be skipped every 6 days to delimit weeks
  417. if ($period == 'day' && $isMultiplePeriod) {
  418. $graph->setForceSkippedLabels(6);
  419. }
  420. // render graph
  421. $graph->renderGraph();
  422. } catch (\Exception $e) {
  423. $graph = new \Piwik\Plugins\ImageGraph\StaticGraph\Exception();
  424. $graph->setWidth($width);
  425. $graph->setHeight($height);
  426. $graph->setFont($font);
  427. $graph->setFontSize($fontSize);
  428. $graph->setBackgroundColor($backgroundColor);
  429. $graph->setTextColor($textColor);
  430. $graph->setException($e);
  431. $graph->renderGraph();
  432. }
  433. // restoring get parameters
  434. $_GET = $savedGET;
  435. switch ($outputType) {
  436. case self::GRAPH_OUTPUT_FILE:
  437. if ($idGoal != '') {
  438. $idGoal = '_' . $idGoal;
  439. }
  440. $fileName = self::$DEFAULT_PARAMETERS[$graphType][self::FILENAME_KEY] . '_' . $apiModule . '_' . $apiAction . $idGoal . ' ' . str_replace(',', '-', $date) . ' ' . $idSite . '.png';
  441. $fileName = str_replace(array(' ', '/'), '_', $fileName);
  442. if (!Filesystem::isValidFilename($fileName)) {
  443. throw new Exception('Error: Image graph filename ' . $fileName . ' is not valid.');
  444. }
  445. return $graph->sendToDisk($fileName);
  446. case self::GRAPH_OUTPUT_PHP:
  447. return $graph->getRenderedImage();
  448. case self::GRAPH_OUTPUT_INLINE:
  449. default:
  450. $graph->sendToBrowser();
  451. exit;
  452. }
  453. }
  454. private function setFilterTruncate($default)
  455. {
  456. $_GET['filter_truncate'] = Common::getRequestVar('filter_truncate', $default, 'int');
  457. }
  458. private static function parseOrdinateValue($ordinateValue)
  459. {
  460. $ordinateValue = @str_replace(',', '.', $ordinateValue);
  461. // convert hh:mm:ss formatted time values to number of seconds
  462. if (preg_match('/([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2})/', $ordinateValue, $matches)) {
  463. $hour = $matches[1];
  464. $min = $matches[2];
  465. $sec = $matches[3];
  466. $ordinateValue = ($hour * 3600) + ($min * 60) + $sec;
  467. }
  468. // OK, only numbers from here please (strip out currency sign)
  469. $ordinateValue = preg_replace('/[^0-9.]/', '', $ordinateValue);
  470. return $ordinateValue;
  471. }
  472. private static function getFontPath($font)
  473. {
  474. return PIWIK_INCLUDE_PATH . self::FONT_DIR . $font;
  475. }
  476. protected static function getAbsoluteLogoPath($relativeLogoPath)
  477. {
  478. return PIWIK_INCLUDE_PATH . '/' . $relativeLogoPath;
  479. }
  480. }