1<?php
2/* vim: set expandtab sw=4 ts=4 sts=4: */
3/**
4 * PDF schema handling
5 *
6 * @package PhpMyAdmin
7 */
8namespace PhpMyAdmin\Plugins\Schema\Pdf;
9
10use PhpMyAdmin\Pdf as PdfLib;
11use PhpMyAdmin\Plugins\Schema\ExportRelationSchema;
12use PhpMyAdmin\Relation;
13use PhpMyAdmin\Transformations;
14use PhpMyAdmin\Util;
15
16/**
17 * Skip the plugin if TCPDF is not available.
18 */
19if (! class_exists('TCPDF')) {
20    $GLOBALS['skip_import'] = true;
21    return;
22}
23
24/**
25 * block attempts to directly run this script
26 */
27if (getcwd() == dirname(__FILE__)) {
28    die('Attack stopped');
29}
30
31/**
32 * Pdf Relation Schema Class
33 *
34 * Purpose of this class is to generate the PDF Document. PDF is widely
35 * used format for documenting text,fonts,images and 3d vector graphics.
36 *
37 * This class inherits ExportRelationSchema class has common functionality added
38 * to this class
39 *
40 * @name Pdf_Relation_Schema
41 * @package PhpMyAdmin
42 */
43class PdfRelationSchema extends ExportRelationSchema
44{
45    /**
46     * Defines properties
47     */
48    private $_showGrid;
49    private $_withDoc;
50    private $_tableOrder;
51
52    /**
53     * @var TableStatsPdf[]
54     */
55    private $_tables = array();
56    private $_ff = PdfLib::PMA_PDF_FONT;
57    private $_xMax = 0;
58    private $_yMax = 0;
59    private $_scale;
60    private $_xMin = 100000;
61    private $_yMin = 100000;
62    private $_topMargin = 10;
63    private $_bottomMargin = 10;
64    private $_leftMargin = 10;
65    private $_rightMargin = 10;
66    private $_tablewidth;
67
68    /**
69     * @var RelationStatsPdf[]
70     */
71    protected $relations = array();
72
73    /**
74     * The "PdfRelationSchema" constructor
75     *
76     * @param string $db database name
77     *
78     * @see PMA_Schema_PDF
79     */
80    public function __construct($db)
81    {
82        $this->setShowGrid(isset($_REQUEST['pdf_show_grid']));
83        $this->setShowColor(isset($_REQUEST['pdf_show_color']));
84        $this->setShowKeys(isset($_REQUEST['pdf_show_keys']));
85        $this->setTableDimension(isset($_REQUEST['pdf_show_table_dimension']));
86        $this->setAllTablesSameWidth(isset($_REQUEST['pdf_all_tables_same_width']));
87        $this->setWithDataDictionary(isset($_REQUEST['pdf_with_doc']));
88        $this->setTableOrder($_REQUEST['pdf_table_order']);
89        $this->setOrientation($_REQUEST['pdf_orientation']);
90        $this->setPaper($_REQUEST['pdf_paper']);
91
92        // Initializes a new document
93        parent::__construct(
94            $db,
95            new Pdf(
96                $this->orientation, 'mm', $this->paper,
97                $this->pageNumber, $this->_withDoc, $db
98            )
99        );
100        $this->diagram->SetTitle(
101            sprintf(
102                __('Schema of the %s database'),
103                $this->db
104            )
105        );
106        $this->diagram->setCMargin(0);
107        $this->diagram->Open();
108        $this->diagram->SetAutoPageBreak('auto');
109        $this->diagram->setOffline($this->offline);
110
111        $alltables = $this->getTablesFromRequest();
112        if ($this->getTableOrder() == 'name_asc') {
113            sort($alltables);
114        } elseif ($this->getTableOrder() == 'name_desc') {
115            rsort($alltables);
116        }
117
118        if ($this->_withDoc) {
119            $this->diagram->SetAutoPageBreak('auto', 15);
120            $this->diagram->setCMargin(1);
121            $this->dataDictionaryDoc($alltables);
122            $this->diagram->SetAutoPageBreak('auto');
123            $this->diagram->setCMargin(0);
124        }
125
126        $this->diagram->Addpage();
127
128        if ($this->_withDoc) {
129            $this->diagram->SetLink($this->diagram->PMA_links['RT']['-'], -1);
130            $this->diagram->Bookmark(__('Relational schema'));
131            $this->diagram->setAlias('{00}', $this->diagram->PageNo());
132            $this->_topMargin = 28;
133            $this->_bottomMargin = 28;
134        }
135
136        /* snip */
137        foreach ($alltables as $table) {
138            if (! isset($this->_tables[$table])) {
139                $this->_tables[$table] = new TableStatsPdf(
140                    $this->diagram,
141                    $this->db,
142                    $table,
143                    null,
144                    $this->pageNumber,
145                    $this->_tablewidth,
146                    $this->showKeys,
147                    $this->tableDimension,
148                    $this->offline
149                );
150            }
151            if ($this->sameWide) {
152                $this->_tables[$table]->width = $this->_tablewidth;
153            }
154            $this->_setMinMax($this->_tables[$table]);
155        }
156
157        // Defines the scale factor
158        $innerWidth = $this->diagram->getPageWidth() - $this->_rightMargin
159            - $this->_leftMargin;
160        $innerHeight = $this->diagram->getPageHeight() - $this->_topMargin
161            - $this->_bottomMargin;
162        $this->_scale = ceil(
163            max(
164                ($this->_xMax - $this->_xMin) / $innerWidth,
165                ($this->_yMax - $this->_yMin) / $innerHeight
166            ) * 100
167        ) / 100;
168
169        $this->diagram->setScale(
170            $this->_scale,
171            $this->_xMin,
172            $this->_yMin,
173            $this->_leftMargin,
174            $this->_topMargin
175        );
176        // Builds and save the PDF document
177        $this->diagram->setLineWidthScale(0.1);
178
179        if ($this->_showGrid) {
180            $this->diagram->SetFontSize(10);
181            $this->_strokeGrid();
182        }
183        $this->diagram->setFontSizeScale(14);
184        // previous logic was checking master tables and foreign tables
185        // but I think that looping on every table of the pdf page as a master
186        // and finding its foreigns is OK (then we can support innodb)
187        $seen_a_relation = false;
188        foreach ($alltables as $one_table) {
189            $exist_rel = $this->relation->getForeigners($this->db, $one_table, '', 'both');
190            if (!$exist_rel) {
191                continue;
192            }
193
194            $seen_a_relation = true;
195            foreach ($exist_rel as $master_field => $rel) {
196                // put the foreign table on the schema only if selected
197                // by the user
198                // (do not use array_search() because we would have to
199                // to do a === false and this is not PHP3 compatible)
200                if ($master_field != 'foreign_keys_data') {
201                    if (in_array($rel['foreign_table'], $alltables)) {
202                        $this->_addRelation(
203                            $one_table,
204                            $master_field,
205                            $rel['foreign_table'],
206                            $rel['foreign_field']
207                        );
208                    }
209                    continue;
210                }
211
212                foreach ($rel as $one_key) {
213                    if (!in_array($one_key['ref_table_name'], $alltables)) {
214                        continue;
215                    }
216
217                    foreach ($one_key['index_list']
218                        as $index => $one_field
219                    ) {
220                        $this->_addRelation(
221                            $one_table,
222                            $one_field,
223                            $one_key['ref_table_name'],
224                            $one_key['ref_index_list'][$index]
225                        );
226                    }
227                }
228            } // end while
229        } // end while
230
231        if ($seen_a_relation) {
232            $this->_drawRelations();
233        }
234        $this->_drawTables();
235    }
236
237    /**
238     * Set Show Grid
239     *
240     * @param boolean $value show grid of the document or not
241     *
242     * @return void
243     */
244    public function setShowGrid($value)
245    {
246        $this->_showGrid = $value;
247    }
248
249    /**
250     * Returns whether to show grid
251     *
252     * @return boolean whether to show grid
253     */
254    public function isShowGrid()
255    {
256        return $this->_showGrid;
257    }
258
259    /**
260     * Set Data Dictionary
261     *
262     * @param boolean $value show selected database data dictionary or not
263     *
264     * @return void
265     */
266    public function setWithDataDictionary($value)
267    {
268        $this->_withDoc = $value;
269    }
270
271    /**
272     * Return whether to show selected database data dictionary or not
273     *
274     * @return boolean whether to show selected database data dictionary or not
275     */
276    public function isWithDataDictionary()
277    {
278        return $this->_withDoc;
279    }
280
281    /**
282     * Sets the order of the table in data dictionary
283     *
284     * @param string $value table order
285     *
286     * @return void
287     */
288    public function setTableOrder($value)
289    {
290        $this->_tableOrder = $value;
291    }
292
293    /**
294     * Returns the order of the table in data dictionary
295     *
296     * @return string table order
297     */
298    public function getTableOrder()
299    {
300        return $this->_tableOrder;
301    }
302
303    /**
304     * Output Pdf Document for download
305     *
306     * @return void
307     */
308    public function showOutput()
309    {
310        $this->diagram->download($this->getFileName('.pdf'));
311    }
312
313    /**
314     * Sets X and Y minimum and maximum for a table cell
315     *
316     * @param TableStatsPdf $table The table name of which sets XY co-ordinates
317     *
318     * @return void
319     */
320    private function _setMinMax($table)
321    {
322        $this->_xMax = max($this->_xMax, $table->x + $table->width);
323        $this->_yMax = max($this->_yMax, $table->y + $table->height);
324        $this->_xMin = min($this->_xMin, $table->x);
325        $this->_yMin = min($this->_yMin, $table->y);
326    }
327
328    /**
329     * Defines relation objects
330     *
331     * @param string $masterTable  The master table name
332     * @param string $masterField  The relation field in the master table
333     * @param string $foreignTable The foreign table name
334     * @param string $foreignField The relation field in the foreign table
335     *
336     * @return void
337     *
338     * @see _setMinMax
339     */
340    private function _addRelation($masterTable, $masterField, $foreignTable,
341        $foreignField
342    ) {
343        if (! isset($this->_tables[$masterTable])) {
344            $this->_tables[$masterTable] = new TableStatsPdf(
345                $this->diagram,
346                $this->db,
347                $masterTable,
348                null,
349                $this->pageNumber,
350                $this->_tablewidth,
351                $this->showKeys,
352                $this->tableDimension
353            );
354            $this->_setMinMax($this->_tables[$masterTable]);
355        }
356        if (! isset($this->_tables[$foreignTable])) {
357            $this->_tables[$foreignTable] = new TableStatsPdf(
358                $this->diagram,
359                $this->db,
360                $foreignTable,
361                null,
362                $this->pageNumber,
363                $this->_tablewidth,
364                $this->showKeys,
365                $this->tableDimension
366            );
367            $this->_setMinMax($this->_tables[$foreignTable]);
368        }
369        $this->relations[] = new RelationStatsPdf(
370            $this->diagram,
371            $this->_tables[$masterTable],
372            $masterField,
373            $this->_tables[$foreignTable],
374            $foreignField
375        );
376    }
377
378    /**
379     * Draws the grid
380     *
381     * @return void
382     *
383     * @see PMA_Schema_PDF
384     */
385    private function _strokeGrid()
386    {
387        $gridSize = 10;
388        $labelHeight = 4;
389        $labelWidth = 5;
390        if ($this->_withDoc) {
391            $topSpace = 6;
392            $bottomSpace = 15;
393        } else {
394            $topSpace = 0;
395            $bottomSpace = 0;
396        }
397
398        $this->diagram->SetMargins(0, 0);
399        $this->diagram->SetDrawColor(200, 200, 200);
400        // Draws horizontal lines
401        $innerHeight = $this->diagram->getPageHeight() - $topSpace - $bottomSpace;
402        for ($l = 0,
403            $size = intval($innerHeight / $gridSize);
404            $l <= $size;
405            $l++
406        ) {
407            $this->diagram->line(
408                0, $l * $gridSize + $topSpace,
409                $this->diagram->getPageWidth(), $l * $gridSize + $topSpace
410            );
411            // Avoid duplicates
412            if ($l > 0
413                && $l <= intval(($innerHeight - $labelHeight) / $gridSize)
414            ) {
415                $this->diagram->SetXY(0, $l * $gridSize + $topSpace);
416                $label = (string) sprintf(
417                    '%.0f',
418                    ($l * $gridSize + $topSpace - $this->_topMargin)
419                    * $this->_scale + $this->_yMin
420                );
421                $this->diagram->Cell($labelWidth, $labelHeight, ' ' . $label);
422            } // end if
423        } // end for
424        // Draws vertical lines
425        for (
426            $j = 0, $size = intval($this->diagram->getPageWidth() / $gridSize);
427            $j <= $size;
428            $j++
429        ) {
430            $this->diagram->line(
431                $j * $gridSize,
432                $topSpace,
433                $j * $gridSize,
434                $this->diagram->getPageHeight() - $bottomSpace
435            );
436            $this->diagram->SetXY($j * $gridSize, $topSpace);
437            $label = (string) sprintf(
438                '%.0f',
439                ($j * $gridSize - $this->_leftMargin) * $this->_scale + $this->_xMin
440            );
441            $this->diagram->Cell($labelWidth, $labelHeight, $label);
442        }
443    }
444
445    /**
446     * Draws relation arrows
447     *
448     * @return void
449     *
450     * @see Relation_Stats_Pdf::relationdraw()
451     */
452    private function _drawRelations()
453    {
454        $i = 0;
455        foreach ($this->relations as $relation) {
456            $relation->relationDraw($this->showColor, $i);
457            $i++;
458        }
459    }
460
461    /**
462     * Draws tables
463     *
464     * @return void
465     *
466     * @see Table_Stats_Pdf::tableDraw()
467     */
468    private function _drawTables()
469    {
470        foreach ($this->_tables as $table) {
471            $table->tableDraw(null, $this->_withDoc, $this->showColor);
472        }
473    }
474
475    /**
476     * Generates data dictionary pages.
477     *
478     * @param array $alltables Tables to document.
479     *
480     * @return void
481     */
482    public function dataDictionaryDoc(array $alltables)
483    {
484         // TOC
485        $this->diagram->addpage($this->orientation);
486        $this->diagram->Cell(0, 9, __('Table of contents'), 1, 0, 'C');
487        $this->diagram->Ln(15);
488        $i = 1;
489        foreach ($alltables as $table) {
490            $this->diagram->PMA_links['doc'][$table]['-']
491                = $this->diagram->AddLink();
492            $this->diagram->SetX(10);
493            // $this->diagram->Ln(1);
494            $this->diagram->Cell(
495                0, 6, __('Page number:') . ' {' . sprintf("%02d", $i) . '}', 0, 0,
496                'R', 0, $this->diagram->PMA_links['doc'][$table]['-']
497            );
498            $this->diagram->SetX(10);
499            $this->diagram->Cell(
500                0, 6, $i . ' ' . $table, 0, 1,
501                'L', 0, $this->diagram->PMA_links['doc'][$table]['-']
502            );
503            // $this->diagram->Ln(1);
504            $fields = $GLOBALS['dbi']->getColumns($this->db, $table);
505            foreach ($fields as $row) {
506                $this->diagram->SetX(20);
507                $field_name = $row['Field'];
508                $this->diagram->PMA_links['doc'][$table][$field_name]
509                    = $this->diagram->AddLink();
510                //$this->diagram->Cell(
511                //    0, 6, $field_name, 0, 1,
512                //    'L', 0, $this->diagram->PMA_links['doc'][$table][$field_name]
513                //);
514            }
515            $i++;
516        }
517        $this->diagram->PMA_links['RT']['-'] = $this->diagram->AddLink();
518        $this->diagram->SetX(10);
519        $this->diagram->Cell(
520            0, 6, __('Page number:') . ' {00}', 0, 0,
521            'R', 0, $this->diagram->PMA_links['RT']['-']
522        );
523        $this->diagram->SetX(10);
524        $this->diagram->Cell(
525            0, 6, $i . ' ' . __('Relational schema'), 0, 1,
526            'L', 0, $this->diagram->PMA_links['RT']['-']
527        );
528        $z = 0;
529        foreach ($alltables as $table) {
530            $z++;
531            $this->diagram->SetAutoPageBreak(true, 15);
532            $this->diagram->addpage($this->orientation);
533            $this->diagram->Bookmark($table);
534            $this->diagram->setAlias(
535                '{' . sprintf("%02d", $z) . '}', $this->diagram->PageNo()
536            );
537            $this->diagram->PMA_links['RT'][$table]['-']
538                = $this->diagram->AddLink();
539            $this->diagram->SetLink(
540                $this->diagram->PMA_links['doc'][$table]['-'], -1
541            );
542            $this->diagram->SetFont($this->_ff, 'B', 18);
543            $this->diagram->Cell(
544                0, 8, $z . ' ' . $table, 1, 1,
545                'C', 0, $this->diagram->PMA_links['RT'][$table]['-']
546            );
547            $this->diagram->SetFont($this->_ff, '', 8);
548            $this->diagram->ln();
549
550            $cfgRelation = $this->relation->getRelationsParam();
551            $comments = $this->relation->getComments($this->db, $table);
552            if ($cfgRelation['mimework']) {
553                $mime_map = Transformations::getMIME($this->db, $table, true);
554            }
555
556            /**
557             * Gets table information
558             */
559            $showtable = $GLOBALS['dbi']->getTable($this->db, $table)
560                ->getStatusInfo();
561            $show_comment = isset($showtable['Comment'])
562                ? $showtable['Comment']
563                : '';
564            $create_time  = isset($showtable['Create_time'])
565                ? Util::localisedDate(
566                    strtotime($showtable['Create_time'])
567                )
568                : '';
569            $update_time  = isset($showtable['Update_time'])
570                ? Util::localisedDate(
571                    strtotime($showtable['Update_time'])
572                )
573                : '';
574            $check_time   = isset($showtable['Check_time'])
575                ? Util::localisedDate(
576                    strtotime($showtable['Check_time'])
577                )
578                : '';
579
580            /**
581             * Gets fields properties
582             */
583            $columns = $GLOBALS['dbi']->getColumns($this->db, $table);
584
585            // Find which tables are related with the current one and write it in
586            // an array
587            $res_rel = $this->relation->getForeigners($this->db, $table);
588
589            /**
590             * Displays the comments of the table if MySQL >= 3.23
591             */
592
593            $break = false;
594            if (! empty($show_comment)) {
595                $this->diagram->Cell(
596                    0, 3, __('Table comments:') . ' ' . $show_comment, 0, 1
597                );
598                $break = true;
599            }
600
601            if (! empty($create_time)) {
602                $this->diagram->Cell(
603                    0, 3, __('Creation:') . ' ' . $create_time, 0, 1
604                );
605                $break = true;
606            }
607
608            if (! empty($update_time)) {
609                $this->diagram->Cell(
610                    0, 3, __('Last update:') . ' ' . $update_time, 0, 1
611                );
612                $break = true;
613            }
614
615            if (! empty($check_time)) {
616                $this->diagram->Cell(
617                    0, 3, __('Last check:') . ' ' . $check_time, 0, 1
618                );
619                $break = true;
620            }
621
622            if ($break == true) {
623                $this->diagram->Cell(0, 3, '', 0, 1);
624                $this->diagram->Ln();
625            }
626
627            $this->diagram->SetFont($this->_ff, 'B');
628            if (isset($this->orientation) && $this->orientation == 'L') {
629                $this->diagram->Cell(25, 8, __('Column'), 1, 0, 'C');
630                $this->diagram->Cell(20, 8, __('Type'), 1, 0, 'C');
631                $this->diagram->Cell(20, 8, __('Attributes'), 1, 0, 'C');
632                $this->diagram->Cell(10, 8, __('Null'), 1, 0, 'C');
633                $this->diagram->Cell(20, 8, __('Default'), 1, 0, 'C');
634                $this->diagram->Cell(25, 8, __('Extra'), 1, 0, 'C');
635                $this->diagram->Cell(45, 8, __('Links to'), 1, 0, 'C');
636
637                if ($this->paper == 'A4') {
638                    $comments_width = 67;
639                } else {
640                    // this is really intended for 'letter'
641                    /**
642                     * @todo find optimal width for all formats
643                     */
644                    $comments_width = 50;
645                }
646                $this->diagram->Cell($comments_width, 8, __('Comments'), 1, 0, 'C');
647                $this->diagram->Cell(45, 8, 'MIME', 1, 1, 'C');
648                $this->diagram->setWidths(
649                    array(25, 20, 20, 10, 20, 25, 45, $comments_width, 45)
650                );
651            } else {
652                $this->diagram->Cell(20, 8, __('Column'), 1, 0, 'C');
653                $this->diagram->Cell(20, 8, __('Type'), 1, 0, 'C');
654                $this->diagram->Cell(20, 8, __('Attributes'), 1, 0, 'C');
655                $this->diagram->Cell(10, 8, __('Null'), 1, 0, 'C');
656                $this->diagram->Cell(15, 8, __('Default'), 1, 0, 'C');
657                $this->diagram->Cell(15, 8, __('Extra'), 1, 0, 'C');
658                $this->diagram->Cell(30, 8, __('Links to'), 1, 0, 'C');
659                $this->diagram->Cell(30, 8, __('Comments'), 1, 0, 'C');
660                $this->diagram->Cell(30, 8, 'MIME', 1, 1, 'C');
661                $this->diagram->setWidths(array(20, 20, 20, 10, 15, 15, 30, 30, 30));
662            }
663            $this->diagram->SetFont($this->_ff, '');
664
665            foreach ($columns as $row) {
666                $extracted_columnspec
667                    = Util::extractColumnSpec($row['Type']);
668                $type                = $extracted_columnspec['print_type'];
669                $attribute           = $extracted_columnspec['attribute'];
670                if (! isset($row['Default'])) {
671                    if ($row['Null'] != '' && $row['Null'] != 'NO') {
672                        $row['Default'] = 'NULL';
673                    }
674                }
675                $field_name = $row['Field'];
676                // $this->diagram->Ln();
677                $this->diagram->PMA_links['RT'][$table][$field_name]
678                    = $this->diagram->AddLink();
679                $this->diagram->Bookmark($field_name, 1, -1);
680                $this->diagram->SetLink(
681                    $this->diagram->PMA_links['doc'][$table][$field_name], -1
682                );
683                $foreigner = $this->relation->searchColumnInForeigners($res_rel, $field_name);
684
685                $linksTo = '';
686                if ($foreigner) {
687                    $linksTo = '-> ';
688                    if ($foreigner['foreign_db'] != $this->db) {
689                        $linksTo .= $foreigner['foreign_db'] . '.';
690                    }
691                    $linksTo .= $foreigner['foreign_table']
692                        . '.' . $foreigner['foreign_field'];
693
694                    if (isset($foreigner['on_update'])) { // not set for internal
695                        $linksTo .= "\n" . 'ON UPDATE ' . $foreigner['on_update'];
696                        $linksTo .= "\n" . 'ON DELETE ' . $foreigner['on_delete'];
697                    }
698                }
699
700                $this->diagram_row = array(
701                    $field_name,
702                    $type,
703                    $attribute,
704                    (($row['Null'] == '' || $row['Null'] == 'NO')
705                        ? __('No')
706                        : __('Yes')),
707                    (isset($row['Default']) ? $row['Default'] : ''),
708                    $row['Extra'],
709                    $linksTo,
710                    (isset($comments[$field_name])
711                        ? $comments[$field_name]
712                        : ''),
713                    (isset($mime_map) && isset($mime_map[$field_name])
714                        ? str_replace('_', '/', $mime_map[$field_name]['mimetype'])
715                        : '')
716                );
717                $links = array();
718                $links[0] = $this->diagram->PMA_links['RT'][$table][$field_name];
719                if ($foreigner
720                    && isset($this->diagram->PMA_links['doc'][$foreigner['foreign_table']][$foreigner['foreign_field']])
721                ) {
722                    $links[6] = $this->diagram->PMA_links['doc']
723                        [$foreigner['foreign_table']][$foreigner['foreign_field']];
724                } else {
725                    unset($links[6]);
726                }
727                $this->diagram->row($this->diagram_row, $links);
728            } // end foreach
729            $this->diagram->SetFont($this->_ff, '', 14);
730        } //end each
731    }
732}
733