1<?php
2/**
3 * Matomo - free/libre analytics platform
4 *
5 * @link https://matomo.org
6 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
7 *
8 */
9namespace Piwik\ReportRenderer;
10
11use Piwik\Common;
12use Piwik\Filesystem;
13use Piwik\NumberFormatter;
14use Piwik\Piwik;
15use Piwik\Plugins\API\API;
16use Piwik\Plugins\CoreAdminHome\CustomLogo;
17use Piwik\ReportRenderer;
18use Piwik\TCPDF;
19
20/**
21 * @see libs/tcpdf
22 */
23require_once PIWIK_INCLUDE_PATH . '/plugins/ScheduledReports/config/tcpdf_config.php';
24
25/**
26 * PDF report renderer
27 */
28class Pdf extends ReportRenderer
29{
30    const IMAGE_GRAPH_WIDTH_LANDSCAPE = 1050;
31    const IMAGE_GRAPH_WIDTH_PORTRAIT = 760;
32    const IMAGE_GRAPH_HEIGHT = 220;
33
34    const LANDSCAPE = 'L';
35    const PORTRAIT = 'P';
36
37    const MAX_ROW_COUNT = 28;
38    const TABLE_HEADER_ROW_COUNT = 6;
39    const NO_DATA_ROW_COUNT = 6;
40    const MAX_GRAPH_REPORTS = 3;
41    const MAX_2COL_TABLE_REPORTS = 2;
42
43    const PDF_CONTENT_TYPE = 'pdf';
44
45    private $reportFontStyle = '';
46    private $reportSimpleFontSize = 9;
47    private $reportHeaderFontSize = 16;
48    private $cellHeight = 6;
49    private $bottomMargin = 17;
50    private $reportWidthPortrait = 195;
51    private $reportWidthLandscape = 270;
52    private $minWidthLabelCell = 100;
53    private $maxColumnCountPortraitOrientation = 6;
54    private $logoWidth = 16;
55    private $logoHeight = 16;
56    private $totalWidth;
57    private $cellWidth;
58    private $labelCellWidth;
59    private $truncateAfter = 55;
60    private $leftSpacesBeforeLogo = 7;
61    private $logoImagePosition = array(10, 40);
62    private $headerTextColor;
63    private $reportTextColor;
64    private $tableHeaderBackgroundColor;
65    private $tableHeaderTextColor;
66    private $tableCellBorderColor;
67    private $tableBackgroundColor;
68    private $rowTopBottomBorder = array(231, 231, 231);
69    private $reportMetadata;
70    private $displayGraph;
71    private $evolutionGraph;
72    private $displayTable;
73    private $segment;
74    private $reportColumns;
75    private $reportRowsMetadata;
76    private $currentPage = 0;
77    private $reportFont = ReportRenderer::DEFAULT_REPORT_FONT_FAMILY;
78    private $TCPDF;
79    private $orientation = self::PORTRAIT;
80
81    public function __construct()
82    {
83        $this->TCPDF = new TCPDF();
84        $this->headerTextColor = preg_split("/,/", ReportRenderer::REPORT_TITLE_TEXT_COLOR);
85        $this->reportTextColor = preg_split("/,/", ReportRenderer::REPORT_TEXT_COLOR);
86        $this->tableHeaderBackgroundColor = preg_split("/,/", ReportRenderer::TABLE_HEADER_BG_COLOR);
87        $this->tableHeaderTextColor = preg_split("/,/", ReportRenderer::TABLE_HEADER_TEXT_COLOR);
88        $this->tableCellBorderColor = preg_split("/,/", ReportRenderer::TABLE_CELL_BORDER_COLOR);
89        $this->tableBackgroundColor = preg_split("/,/", ReportRenderer::TABLE_BG_COLOR);
90    }
91
92    public function setLocale($locale)
93    {
94        // WARNING
95        // To make Piwik release smaller, we're deleting some fonts from the Piwik build package.
96        // If you change this code below, make sure that the fonts are NOT deleted from the Piwik package:
97        // https://github.com/piwik/piwik-package/blob/master/scripts/build-package.sh
98        switch ($locale) {
99            case 'bn':
100            case 'hi':
101                $reportFont = 'freesans';
102                break;
103
104            case 'zh-tw':
105                $reportFont = 'msungstdlight';
106                break;
107
108            case 'ja':
109                $reportFont = 'kozgopromedium';
110                break;
111
112            case 'zh-cn':
113                $reportFont = 'stsongstdlight';
114                break;
115
116            case 'ko':
117                $reportFont = 'hysmyeongjostdmedium';
118                break;
119
120            case 'ar':
121                $reportFont = 'aealarabiya';
122                break;
123
124            case 'am':
125            case 'ta':
126            case 'th':
127                $reportFont = 'freeserif';
128                break;
129
130            case 'te':
131                // not working with bundled fonts
132            case 'en':
133            default:
134                $reportFont = ReportRenderer::DEFAULT_REPORT_FONT_FAMILY;
135                break;
136        }
137        // WARNING: Did you read the warning above?
138
139        $this->reportFont = $reportFont;
140    }
141
142    public function sendToDisk($filename)
143    {
144        $filename = ReportRenderer::makeFilenameWithExtension($filename, self::PDF_CONTENT_TYPE);
145        $outputFilename = ReportRenderer::getOutputPath($filename);
146
147        $this->TCPDF->Output($outputFilename, 'F');
148
149        return $outputFilename;
150    }
151
152    public function sendToBrowserDownload($filename)
153    {
154        $filename = ReportRenderer::makeFilenameWithExtension($filename, self::PDF_CONTENT_TYPE);
155        $this->TCPDF->Output($filename, 'D');
156    }
157
158    public function sendToBrowserInline($filename)
159    {
160        $filename = ReportRenderer::makeFilenameWithExtension($filename, self::PDF_CONTENT_TYPE);
161        $this->TCPDF->Output($filename, 'I');
162    }
163
164    public function getRenderedReport()
165    {
166        return $this->TCPDF->Output(null, 'S');
167    }
168
169    public function renderFrontPage($reportTitle, $prettyDate, $description, $reportMetadata, $segment)
170    {
171        $reportTitle = $this->formatText($reportTitle);
172        $dateRange = $this->formatText(Piwik::translate('General_DateRange') . " " . $prettyDate);
173
174        // footer
175        $this->TCPDF->SetFooterFont(array($this->reportFont, $this->reportFontStyle, $this->reportSimpleFontSize));
176        $this->TCPDF->SetFooterContent($reportTitle . " | " . $dateRange . " | ");
177
178        // add first page
179        $this->TCPDF->setPrintHeader(false);
180        $this->TCPDF->AddPage(self::PORTRAIT);
181        $this->TCPDF->AddFont($this->reportFont, '', '', false);
182        $this->TCPDF->SetFont($this->reportFont, $this->reportFontStyle, $this->reportSimpleFontSize);
183        $this->TCPDF->Bookmark(Piwik::translate('ScheduledReports_FrontPage'));
184
185        // logo
186        $customLogo = new CustomLogo();
187        $this->TCPDF->Image($customLogo->getLogoUrl(true), $this->logoImagePosition[0], $this->logoImagePosition[1], 180 / $factor = 2, 0, $type = '', $link = '', $align = '', $resize = false, $dpi = 300);
188        $this->TCPDF->Ln(8);
189
190        // report title
191        $this->TCPDF->SetFont($this->reportFont, '', $this->reportHeaderFontSize + 5);
192        $this->TCPDF->SetTextColor($this->headerTextColor[0], $this->headerTextColor[1], $this->headerTextColor[2]);
193        $this->TCPDF->Cell(40, 210, $reportTitle);
194        $this->TCPDF->Ln(8 * 4);
195
196        // date and period
197        $this->TCPDF->SetFont($this->reportFont, '', $this->reportHeaderFontSize);
198        $this->TCPDF->SetTextColor($this->reportTextColor[0], $this->reportTextColor[1], $this->reportTextColor[2]);
199        $this->TCPDF->Cell(40, 210, $dateRange);
200        $this->TCPDF->Ln(8 * 20);
201
202        // description
203        $this->TCPDF->Write(1, $this->formatText($description));
204
205        // segment
206        if ($segment != null) {
207            $this->TCPDF->Ln();
208            $this->TCPDF->Ln();
209            $this->TCPDF->SetFont($this->reportFont, '', $this->reportHeaderFontSize - 2);
210            $this->TCPDF->SetTextColor($this->headerTextColor[0], $this->headerTextColor[1], $this->headerTextColor[2]);
211            $this->TCPDF->Write(1, $this->formatText(Piwik::translate('ScheduledReports_CustomVisitorSegment') . ' ' . $segment['name']));
212        }
213
214        $this->TCPDF->Ln(8);
215        $this->TCPDF->SetFont($this->reportFont, '', $this->reportHeaderFontSize);
216        $this->TCPDF->Ln();
217    }
218
219    /**
220     * Generate a header of page.
221     */
222    private function paintReportHeader()
223    {
224        $isAggregateReport = !empty($this->reportMetadata['dimension']);
225
226        // Graph-only report
227        static $graphOnlyReportCount = 0;
228        $graphOnlyReport = $isAggregateReport && $this->displayGraph && !$this->displayTable;
229
230        // Table-only report
231        $tableOnlyReport = $isAggregateReport
232            && !$this->displayGraph
233            && $this->displayTable;
234
235        $columnCount = count($this->reportColumns);
236
237        // Table-only 2-column report
238        static $tableOnly2ColumnReportCount = 0;
239        $tableOnly2ColumnReport = $tableOnlyReport
240            && $columnCount == 2;
241
242        // Table-only report with more than 2 columns
243        static $tableOnlyManyColumnReportRowCount = 0;
244        $tableOnlyManyColumnReport = $tableOnlyReport
245            && $columnCount > 3;
246
247        $reportHasData = $this->reportHasData();
248
249        $rowCount = $reportHasData ? $this->report->getRowsCount() + self::TABLE_HEADER_ROW_COUNT : self::NO_DATA_ROW_COUNT;
250
251        // Only a page break before if the current report has some data
252        if ($reportHasData &&
253            // and
254            (
255                // it is the first report
256                $this->currentPage == 0
257                // or, it is a graph-only report and it is the first of a series of self::MAX_GRAPH_REPORTS
258                || ($graphOnlyReport && $graphOnlyReportCount == 0)
259                // or, it is a table-only 2-column report and it is the first of a series of self::MAX_2COL_TABLE_REPORTS
260                || ($tableOnly2ColumnReport && $tableOnly2ColumnReportCount == 0)
261                // or it is a table-only report with more than 2 columns and it is the first of its series or there isn't enough space left on the page
262                || ($tableOnlyManyColumnReport && ($tableOnlyManyColumnReportRowCount == 0 || $tableOnlyManyColumnReportRowCount + $rowCount >= self::MAX_ROW_COUNT))
263                // or it is a report with both a table and a graph
264                || !$graphOnlyReport && !$tableOnlyReport
265            )
266        ) {
267            $this->currentPage++;
268            $this->TCPDF->AddPage();
269
270            // Table-only reports with more than 2 columns are always landscape
271            if ($tableOnlyManyColumnReport) {
272                $tableOnlyManyColumnReportRowCount = 0;
273                $this->orientation = self::LANDSCAPE;
274            } else {
275                // Graph-only reports are always portrait
276                $this->orientation = $graphOnlyReport ? self::PORTRAIT : ($columnCount > $this->maxColumnCountPortraitOrientation ? self::LANDSCAPE : self::PORTRAIT);
277            }
278
279            $this->TCPDF->setPageOrientation($this->orientation, '', $this->bottomMargin);
280        }
281
282        $graphOnlyReportCount = ($graphOnlyReport && $reportHasData) ? ($graphOnlyReportCount + 1) % self::MAX_GRAPH_REPORTS : 0;
283        $tableOnly2ColumnReportCount = ($tableOnly2ColumnReport && $reportHasData) ? ($tableOnly2ColumnReportCount + 1) % self::MAX_2COL_TABLE_REPORTS : 0;
284        $tableOnlyManyColumnReportRowCount = $tableOnlyManyColumnReport ? ($tableOnlyManyColumnReportRowCount + $rowCount) : 0;
285
286        $title = $this->formatText($this->reportMetadata['name']);
287        $this->TCPDF->SetFont($this->reportFont, $this->reportFontStyle, $this->reportHeaderFontSize);
288        $this->TCPDF->SetTextColor($this->headerTextColor[0], $this->headerTextColor[1], $this->headerTextColor[2]);
289        $this->TCPDF->Bookmark($title);
290        $this->TCPDF->Cell(40, 15, $title);
291        $this->TCPDF->Ln();
292        $this->TCPDF->SetFont($this->reportFont, '', $this->reportSimpleFontSize);
293        $this->TCPDF->SetTextColor($this->reportTextColor[0], $this->reportTextColor[1], $this->reportTextColor[2]);
294    }
295
296    private function reportHasData()
297    {
298        return $this->report->getRowsCount() > 0;
299    }
300
301    private function setBorderColor()
302    {
303        $this->TCPDF->SetDrawColor($this->tableCellBorderColor[0], $this->tableCellBorderColor[1], $this->tableCellBorderColor[2]);
304    }
305
306    public function renderReport($processedReport)
307    {
308        $this->reportMetadata = $processedReport['metadata'];
309        $this->reportRowsMetadata = $processedReport['reportMetadata'];
310        $this->displayGraph = $processedReport['displayGraph'];
311        $this->evolutionGraph = $processedReport['evolutionGraph'];
312        $this->displayTable = $processedReport['displayTable'];
313        $this->segment = $processedReport['segment'];
314        list($this->report, $this->reportColumns) = self::processTableFormat($this->reportMetadata, $processedReport['reportData'], $processedReport['columns']);
315
316        $this->paintReportHeader();
317
318        if (!$this->reportHasData()) {
319            $this->paintMessage(Piwik::translate('CoreHome_ThereIsNoDataForThisReport'));
320            return;
321        }
322
323        if ($this->displayGraph) {
324            $this->paintGraph();
325        }
326
327        if ($this->displayGraph && $this->displayTable) {
328            $this->TCPDF->Ln(5);
329        }
330
331        if ($this->displayTable) {
332            $this->paintReportTableHeader();
333            $this->paintReportTable();
334        }
335    }
336
337    private function formatText($text)
338    {
339        return Common::unsanitizeInputValue($text);
340    }
341
342    private function paintReportTable()
343    {
344        //Color and font restoration
345        $this->TCPDF->SetFillColor($this->tableBackgroundColor[0], $this->tableBackgroundColor[1], $this->tableBackgroundColor[2]);
346        $this->TCPDF->SetTextColor($this->reportTextColor[0], $this->reportTextColor[1], $this->reportTextColor[2]);
347        $this->TCPDF->SetFont('');
348
349        $fill = true;
350        $url = false;
351        $leftSpacesBeforeLogo = str_repeat(' ', $this->leftSpacesBeforeLogo);
352
353        $logoWidth = $this->logoWidth;
354        $logoHeight = $this->logoHeight;
355
356        $rowsMetadata = $this->reportRowsMetadata->getRows();
357
358        // Draw a body of report table
359        foreach ($this->report->getRows() as $rowId => $row) {
360            $rowMetrics = $row->getColumns();
361            $rowMetadata = isset($rowsMetadata[$rowId]) ? $rowsMetadata[$rowId]->getColumns() : array();
362            if (isset($rowMetadata['url'])) {
363                $url = $rowMetadata['url'];
364            }
365            foreach ($this->reportColumns as $columnId => $columnName) {
366                // Label column
367                if ($columnId == 'label') {
368                    $isLogoDisplayable = isset($rowMetadata['logo']);
369                    $text = '';
370                    $posX = $this->TCPDF->GetX();
371                    $posY = $this->TCPDF->GetY();
372                    if (isset($rowMetrics[$columnId])) {
373                        $text = substr($rowMetrics[$columnId], 0, $this->truncateAfter);
374                        if ($isLogoDisplayable) {
375                            $text = $leftSpacesBeforeLogo . $text;
376                        }
377                    }
378                    $text = $this->formatText($text);
379
380                    $this->TCPDF->Cell($this->labelCellWidth, $this->cellHeight, $text, 'LR', 0, 'L', $fill, $url);
381
382                    if ($isLogoDisplayable) {
383                        if (isset($rowMetadata['logoWidth'])) {
384                            $logoWidth = $rowMetadata['logoWidth'];
385                        }
386                        if (isset($rowMetadata['logoHeight'])) {
387                            $logoHeight = $rowMetadata['logoHeight'];
388                        }
389                        $restoreY = $this->TCPDF->getY();
390                        $restoreX = $this->TCPDF->getX();
391                        $this->TCPDF->SetY($posY);
392                        $this->TCPDF->SetX($posX);
393                        $topMargin = 1.3;
394                        // Country flags are not very high, force a bigger top margin
395                        if ($logoHeight < 16) {
396                            $topMargin = 2;
397                        }
398                        $path = Filesystem::getPathToPiwikRoot() . "/" . $rowMetadata['logo'];
399                        if (file_exists($path)) {
400                            $this->TCPDF->Image($path, $posX + ($leftMargin = 2), $posY + $topMargin, $logoWidth / 4);
401                        }
402                        $this->TCPDF->SetXY($restoreX, $restoreY);
403                    }
404                } // metrics column
405                else {
406                    // No value means 0
407                    if (empty($rowMetrics[$columnId])) {
408                        $rowMetrics[$columnId] = 0;
409                    }
410                    $this->TCPDF->Cell($this->cellWidth, $this->cellHeight, NumberFormatter::getInstance()->format($rowMetrics[$columnId]), 'LR', 0, 'L', $fill);
411                }
412            }
413
414            $this->TCPDF->Ln();
415
416            // Top/Bottom grey border for all cells
417            $this->TCPDF->SetDrawColor($this->rowTopBottomBorder[0], $this->rowTopBottomBorder[1], $this->rowTopBottomBorder[2]);
418            $this->TCPDF->Cell($this->totalWidth, 0, '', 'T');
419            $this->setBorderColor();
420            $this->TCPDF->Ln(0.2);
421
422            $fill = !$fill;
423        }
424    }
425
426    private function paintGraph()
427    {
428        $imageGraph = parent::getStaticGraph(
429            $this->reportMetadata,
430            $this->orientation == self::PORTRAIT ? self::IMAGE_GRAPH_WIDTH_PORTRAIT : self::IMAGE_GRAPH_WIDTH_LANDSCAPE,
431            self::IMAGE_GRAPH_HEIGHT,
432            $this->evolutionGraph,
433            $this->segment
434        );
435
436        $this->TCPDF->Image(
437            '@' . $imageGraph,
438            $x = '',
439            $y = '',
440            $w = 0,
441            $h = 0,
442            $type = '',
443            $link = '',
444            $align = 'N',
445            $resize = false,
446            $dpi = 72,
447            $palign = '',
448            $ismask = false,
449            $imgmask = false,
450            $order = 0,
451            $fitbox = false,
452            $hidden = false,
453            $fitonpage = true,
454            $alt = false,
455            $altimgs = array()
456        );
457
458        unset($imageGraph);
459    }
460
461    /**
462     * Draw the table header (first row)
463     */
464    private function paintReportTableHeader()
465    {
466        $initPosX = 10;
467
468        // Get the longest column name
469        $longestColumnName = '';
470        foreach ($this->reportColumns as $columnName) {
471            if (strlen($columnName) > strlen($longestColumnName)) {
472                $longestColumnName = $columnName;
473            }
474        }
475
476        $columnsCount = count($this->reportColumns);
477        // Computes available column width
478        if ($this->orientation == self::PORTRAIT
479            && $columnsCount <= 3
480        ) {
481            $totalWidth = $this->reportWidthPortrait * 2 / 3;
482        } elseif ($this->orientation == self::LANDSCAPE) {
483            $totalWidth = $this->reportWidthLandscape;
484        } else {
485            $totalWidth = $this->reportWidthPortrait;
486        }
487        $this->totalWidth = $totalWidth;
488        $this->labelCellWidth = max(round(($this->totalWidth / $columnsCount)), $this->minWidthLabelCell);
489        $this->cellWidth = round(($this->totalWidth - $this->labelCellWidth) / ($columnsCount - 1));
490        $this->totalWidth = $this->labelCellWidth + ($columnsCount - 1) * $this->cellWidth;
491
492        $this->TCPDF->SetFillColor($this->tableHeaderBackgroundColor[0], $this->tableHeaderBackgroundColor[1], $this->tableHeaderBackgroundColor[2]);
493        $this->TCPDF->SetTextColor($this->tableHeaderTextColor[0], $this->tableHeaderTextColor[1], $this->tableHeaderTextColor[2]);
494        $this->TCPDF->SetLineWidth(.3);
495        $this->setBorderColor();
496        $this->TCPDF->SetFont($this->reportFont, $this->reportFontStyle);
497        $this->TCPDF->SetFillColor(255);
498        $this->TCPDF->SetTextColor($this->tableHeaderBackgroundColor[0], $this->tableHeaderBackgroundColor[1], $this->tableHeaderBackgroundColor[2]);
499        $this->TCPDF->SetDrawColor(255);
500
501        $posY = $this->TCPDF->GetY();
502        $this->TCPDF->MultiCell($this->cellWidth, $this->cellHeight, $longestColumnName, 1, 'C', true);
503        $maxCellHeight = $this->TCPDF->GetY() - $posY;
504
505        $this->TCPDF->SetFillColor($this->tableHeaderBackgroundColor[0], $this->tableHeaderBackgroundColor[1], $this->tableHeaderBackgroundColor[2]);
506        $this->TCPDF->SetTextColor($this->tableHeaderTextColor[0], $this->tableHeaderTextColor[1], $this->tableHeaderTextColor[2]);
507        $this->TCPDF->SetDrawColor($this->tableCellBorderColor[0], $this->tableCellBorderColor[1], $this->tableCellBorderColor[2]);
508
509        $this->TCPDF->SetXY($initPosX, $posY);
510
511        $countColumns = 0;
512        $posX = $initPosX;
513        foreach ($this->reportColumns as $columnName) {
514            $columnName = $this->formatText($columnName);
515
516            //Label column
517            if ($countColumns == 0) {
518                $this->TCPDF->MultiCell($this->labelCellWidth, $maxCellHeight, $columnName, $border = 0, $align = 'L', true);
519                $this->TCPDF->SetXY($posX + $this->labelCellWidth, $posY);
520            } else {
521                $this->TCPDF->MultiCell($this->cellWidth, $maxCellHeight, $columnName, $border = 0, $align = 'L', true);
522                $this->TCPDF->SetXY($posX + $this->cellWidth, $posY);
523            }
524            $countColumns++;
525            $posX = $this->TCPDF->GetX();
526        }
527        $this->TCPDF->Ln();
528        $this->TCPDF->SetXY($initPosX, $posY + $maxCellHeight);
529    }
530
531    /**
532     * Prints a message
533     *
534     * @param string $message
535     * @return void
536     */
537    private function paintMessage($message)
538    {
539        $this->TCPDF->SetFont($this->reportFont, $this->reportFontStyle, $this->reportSimpleFontSize);
540        $this->TCPDF->SetTextColor($this->reportTextColor[0], $this->reportTextColor[1], $this->reportTextColor[2]);
541        $message = $this->formatText($message);
542        $this->TCPDF->Write("1em", $message);
543        $this->TCPDF->Ln();
544    }
545
546    /**
547     * Get report attachments, ex. graph images
548     *
549     * @param $report
550     * @param $processedReports
551     * @param $prettyDate
552     * @return array
553     */
554    public function getAttachments($report, $processedReports, $prettyDate)
555    {
556        return array();
557    }
558}
559