1<?php
2
3use dokuwiki\Utf8\PhpString;
4use dokuwiki\Utf8\Sort;
5
6/**
7 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
8 * @author     Esther Brunner <wikidesign@gmail.com>
9 * @author     Gina Häußge <osd@foosel.net>
10 */
11class helper_plugin_pagelist extends DokuWiki_Plugin
12{
13
14    /** @var string table style: 'default', 'table', 'list' */
15    protected $style = '';
16    /** @var bool whether heading line is shown */
17    protected $showheader = false;
18    /** @var bool whether first headline/title is shown in the page column */
19    protected $showfirsthl = false;
20
21    /**
22     * @var array with entries: 'columnname' => bool/int enabled/disable column, something also config setting
23     * @deprecated 2022-08-17 still public, will change to protected. Use addColumn() and modifyColumn() instead.
24     */
25    public $column = [];
26    /**
27     * @var array with entries: 'columnname' => language strings for table headers as html for in th
28     * @deprecated 2022-08-17 still public, will change to protected, use setHeader() instead.
29     */
30    public $header = [];
31
32    /**
33     * Associated array, where the keys are the sortkey.
34     * For each row an array is added which must contain at least the key 'id', can further contain as well :
35     *  'title', 'date', 'user', 'desc', 'summary', 'comments', 'tags', 'status' and 'priority', see addPage() for details
36     * @var array[] array of arrays with the entries: 'columnname' => value, or if plugin the html for in cell
37     */
38    protected $pages = [];
39
40    /**
41     * data of the current processed page row
42     * see addPage() for details
43     *
44     * @var null|array with entries: 'columnname' => value or if plugin html for in cell, null if no lines processed
45     * @deprecated 2022-08-17 still public, will change to protected, use addPage() instead
46     */
47    public $page = null;
48
49    /**
50     * @var bool enables sorting. If no sortkey was given, 'id' is used.
51     * @deprecated 2022-08-17 still public, will change to protected, use setFlags() instead
52     */
53    public $sort = false;
54    /**
55     * @var bool Reverses the sort
56     * @deprecated 2022-08-17 still public, will change to protected, use setFlags()instead
57     */
58    public $rsort = false;
59
60    /**
61     * @var string the item to use as key for sorting
62     */
63    private $sortKey;
64    /**
65     * @var string let plugins set their own default without already enabling sorting
66     */
67    private $defaultSortKey = 'id';
68
69    /**
70     * @var array with entries: 'pluginname' => ['columnname1', 'columnname2'], registers the available columns per plugin
71     */
72    protected $plugins = [];
73
74    /** @var string final html output */
75    protected $doc = '';
76
77    /** @var null|mixed data retrieved from metadata array for the current processed page */
78    protected $meta = null;
79
80    /** @var array @deprecated 2022-08-17 still used by very old plugins */
81    public $_meta = null;
82
83    /** @var helper_plugin_pageimage */
84    protected $pageimage = null;
85    /** @var helper_plugin_discussion */
86    protected $discussion = null;
87    /** @var helper_plugin_linkback */
88    protected $linkback = null;
89    /** @var helper_plugin_tag */
90    protected $tag = null;
91
92    /**
93     * @var int limits the number of rows shown, 0 is all.
94     */
95    private $limit;
96
97    /**
98     * Constructor gets default preferences
99     *
100     * These can be overriden by plugins using this class
101     */
102    public function __construct()
103    {
104        $this->style = $this->getConf('style'); //string
105        $this->showheader = $this->getConf('showheader'); //on-off
106        $this->showfirsthl = $this->getConf('showfirsthl'); //on-off
107        $this->sort = $this->getConf('sort'); //on-off
108        $this->rsort = $this->getConf('rsort'); //on-off
109        $this->sortKey = $this->getConf('sortby'); //string
110        if($this->sortKey) {
111            $this->sort = true;
112        }
113
114        $this->plugins = [
115            'discussion' => ['comments'],
116            'linkback' => ['linkbacks'],
117            'tag' => ['tags'],
118            'pageimage' => ['image'],
119        ];
120
121        $this->column = [
122            'page' => true,
123            'date' => $this->getConf('showdate'), //0,1,2
124            'user' => $this->getConf('showuser'), //0,1,2,3,4
125            'desc' => $this->getConf('showdesc'), //0,160,500
126            'summary' => false,
127            'comments' => $this->getConf('showcomments'), //on-off
128            'linkbacks' => $this->getConf('showlinkbacks'), //on-off
129            'tags' => $this->getConf('showtags'), //on-off
130            'image' => $this->getConf('showimage'), //on-off
131            'diff' => $this->getConf('showdiff'), //on-off
132        ];
133
134        $this->header = [];
135        $this->limit = 0;
136    }
137
138    public function getMethods()
139    {
140        $result = [];
141        $result[] = [
142            'name' => 'addColumn',
143            'desc' => '(optional) adds an extra column for plugin data',
144            'params' => [
145                'plugin name' => 'string',
146                'column key' => 'string'
147            ],
148        ];
149        $result[] = [
150            'name' => 'modifyColumn',
151            'desc' => '(optional) override value of an existing column, value equal to false disables column',
152            'params' => [
153                'column key' => 'string',
154                'value' => 'int|bool'
155            ],
156        ];
157        $result[] = [
158            'name' => 'setHeader',
159            'desc' => '(optional) Provide header data, if not given default values or [plugin]->th() is used',
160            'params' => [
161                'column key' => 'string',
162                'value' => 'int|bool'
163            ],
164        ];
165        $result[] = [
166            'name' => 'setFlags',
167            'desc' => '(optional) overrides default flags, or en/disable existing columns',
168            'params' => ['flags' => 'array'],
169            'return' => ['success' => 'boolean'],
170        ];
171        $result[] = [
172            'name' => 'startList',
173            'desc' => '(required) prepares the table header for the page list',
174        ];
175        $result[] = [
176            'name' => 'addPage',
177            'desc' => '(required) adds a page to the list',
178            'params' => ["page attributes, 'id' required, others optional" => 'array'],
179        ];
180        $result[] = [
181            'name' => 'finishList',
182            'desc' => '(required) returns the XHTML output',
183            'return' => ['xhtml' => 'string'],
184        ];
185        return $result;
186    }
187
188    /**
189     * (optional) Adds an extra column named $col for plugin $plugin.
190     * The data for the extra column is provided via:
191     *    1) extra entry to setHeader([]) or addPage([])
192     *    2) or, alternatively, by the plugin $plugin that implements a helper component with the functions:
193     *  - th($col, &$class=null) or th()
194     *  - td($id, $col=null, &$class=null) or td($id)
195     *
196     *
197     * @param string $plugin plugin name
198     * @param string $col column name. Assumption: unique between all builtin columns and plugin supplied columns
199     */
200    public function addColumn($plugin, $col)
201    {
202        //prevent duplicates if adding a column of already listed plugins
203        if (!isset($this->plugins[$plugin]) || !in_array($col, $this->plugins[$plugin])) {
204            $this->plugins[$plugin][] = $col;
205        }
206        $this->column[$col] = true;
207    }
208
209    /**
210     * (optional) Allow to override the column values e.g. to disable a column
211     *
212     * @param string $col column name
213     * @param int|bool $value must evaluate to false/true for dis/enabling column. Sometimes value is used for specific setting
214     * @see $column
215     */
216    public function modifyColumn($col, $value)
217    {
218        if (isset($this->column[$col])) {
219            $this->column[$col] = $value;
220        }
221    }
222
223    /**
224     * (optional) Provide header data, if not given for built-in columns localized strings are used, or for plugins the th() function
225     * @param array $header entries, if not given default values or plugin->th() is used
226     * @return void
227     * @see $column the keys of $header should match the keys of $column
228     *
229     */
230    public function setHeader($header)
231    {
232        if (is_array($header)) {
233            $this->header = $header;
234        }
235    }
236
237    /**
238     * (Optional) Overrides standard values for style, showheader and show(column) settings
239     *
240     * @param string[] $flags
241     *  possible flags:
242     *     for styling: 'default', 'table', 'list', 'simplelist'
243     *     for dis/enabling header: '(no)header', and show titel for page column with '(no)firsthl',
244     *     for sorting: 'sort', 'rsort', 'nosort', 'sortby=<columnname>'
245     *     for dis/enabling columns: accepts keys of $column, e.g. default: '(no)date', 'user', 'desc', 'summary',
246     *        'comments', 'linkbacks', 'tags', 'image', 'diff'
247     * @return bool, false if no array given
248     */
249    public function setFlags($flags)
250    {
251        if (!is_array($flags)) return false;
252
253        foreach ($flags as $flag) {
254            switch ($flag) {
255                case 'default':
256                    $this->style = 'default';
257                    break;
258                case 'table':
259                    $this->style = 'table';
260                    break;
261                case 'list':
262                    $this->style = 'list';
263                    break;
264                case 'simplelist':
265                    $this->style = 'simplelist'; // Displays pagenames only, no other information
266                    break;
267                case 'header':
268                    $this->showheader = true;
269                    break;
270                case 'noheader':
271                    $this->showheader = false;
272                    break;
273                case 'firsthl':
274                    $this->showfirsthl = true;
275                    break;
276                case 'nofirsthl':
277                    $this->showfirsthl = false;
278                    break;
279                case 'sort':
280                    $this->sort = true; //sort by pageid
281                    $this->rsort = false;
282                    break;
283                case 'rsort':
284                    $this->sort = true; //reverse sort on key, not sure if that is by pageid
285                    $this->rsort = true;
286                    break;
287                case 'nosort':
288                    $this->sort = false;
289                    $this->rsort = false;
290                    break;
291                case 'showdiff':
292                    $flag = 'diff';
293                    break;
294            }
295
296            // it is not required to set the sort flag, rsort flag will reverse.
297            // $flag should be an existing column, not checked here as addColumn() is maybe called later then setFlags()?
298            if (substr($flag, 0, 7) == 'sortby=') {
299                $this->sortKey = substr($flag, 7);
300                $this->sort = true;
301            }
302            //for plugins to propose a default value for the sortby flag
303            if (substr($flag, 0, 14) == 'defaultsortby=') {
304                $this->defaultSortKey = substr($flag, 14);
305            }
306            if (substr($flag, 0, 6) == 'limit=') {
307                $this->limit = (int) substr($flag, 6);
308            }
309
310            /** @see $column array, enable/disable columns */
311            if (substr($flag, 0, 2) == 'no') {
312                $value = false;
313                $flag = substr($flag, 2);
314            } else {
315                $value = true;
316            }
317
318            if (isset($this->column[$flag]) && $flag !== 'page') {
319                $this->column[$flag] = $value;
320            }
321        }
322        if ($this->sortKey === '' && $this->sort) {
323            $this->sortKey = $this->defaultSortKey;
324        }
325        return true;
326    }
327
328    /**
329     * (required) Sets the list header
330     *
331     * @param null|string $callerClass
332     * @return bool
333     */
334    public function startList($callerClass = null)
335    {
336
337        // table style
338        switch ($this->style) {
339            case 'table':
340                $class = 'inline';
341                break;
342            case 'list':
343                $class = 'ul';
344                break;
345            case 'simplelist':
346                $class = false;
347                break;
348            default:
349                $class = 'pagelist';
350        }
351
352        if ($class) {
353            $class .= ' plgn__pglist';
354            if ($callerClass) {
355                $class .= ' ' . $callerClass;
356            }
357            $this->doc = '<div class="table"><table class="' . $class . '">';
358        } else {
359            // Simplelist is enabled; Skip header and firsthl
360            $this->showheader = false;
361            $this->showfirsthl = false;
362
363            $this->doc = '<ul>';
364        }
365
366        $this->page = null;
367        $this->pages = [];
368
369        // check if some plugins are available - if yes, load them!
370        foreach ($this->plugins as $plugin => $columns) {
371            foreach ($columns as $col) {
372                if (!$this->column[$col]) continue;
373
374                if (!$this->$plugin = $this->loadHelper($plugin)) {
375                    $this->column[$col] = false;
376                }
377            }
378        }
379
380        // header row
381        if ($this->showheader) {
382            $this->doc .= '<tr>';
383            $columns = ['page', 'date', 'user', 'desc', 'diff', 'summary'];
384            //image column first
385            if ($this->column['image']) {
386                if (empty($this->header['image'])) {
387                    $this->header['image'] = hsc($this->pageimage->th('image'));
388                }
389                $this->doc .= '<th class="images">' . $this->header['image'] . '</th>';
390            }
391            //pagelist columns
392            foreach ($columns as $col) {
393                if ($this->column[$col]) {
394                    if (empty($this->header[$col])) {
395                        $this->header[$col] = hsc($this->getLang($col));
396                    }
397                    $this->doc .= '<th class="' . $col . '">' . $this->header[$col] . '</th>';
398                }
399            }
400            //plugin columns
401            foreach ($this->plugins as $plugin => $columns) {
402                foreach ($columns as $col) {
403                    if ($this->column[$col] && $col != 'image') {
404                        if (empty($this->header[$col])) {
405                            $this->header[$col] = hsc($this->$plugin->th($col, $class));
406                        }
407                        $this->doc .= '<th class="' . $col . '">' . $this->header[$col] . '</th>';
408                    }
409                }
410            }
411            $this->doc .= '</tr>';
412        }
413        return true;
414    }
415
416    /**
417     * (required) Add page row to the list, call for every row. In the $page array is 'id' required, other entries are optional.
418     *
419     * @param array $page
420     *       'id'     => (required) string page id
421     *       'title'  => string First headline, otherwise page id; exception: if titleimage is used this is used for the image title&alt attribute
422     *       'titleimage' => string media id
423     *       'date'   => int timestamp of creation date, otherwise modification date (e.g. sometimes needed for plugin)
424     *       'user'   => string $meta['creator']
425     *       'desc'   => string $meta['description']['abstract']
426     *       'description' => string description set via pagelist syntax
427     *       'summary' => string summary of the last change of the page $meta['last_change']['sum']
428     *       'exists' => bool page_exists($id)
429     *       'perm'   => int auth_quickaclcheck($id)
430     *       'draft'  => string $meta['type'] set by blog plugin
431     *       'priority' => string priority of task: 'low', 'medium', 'high', 'critical'
432     *       'class'  => string class set for each row
433     *       'file'   => string wikiFN($id)
434     *       'section' => string id of section, added as #ancher to page url
435     *    further key-value pairs for columns set by plugins (optional), if not defined th() and td() of plugin are called
436     * @return bool, false if no id given
437     */
438    public function addPage($page)
439    {
440        $id = $page['id'];
441        if (!$id) return false;
442        $this->page = $page;
443        $this->meta = null; // do all metadata calls in addPage()
444
445        if ($this->style != 'simplelist') {
446            if (!isset($this->page['draft'])) {
447                $this->page['draft'] = $this->getMeta('type') == 'draft';
448            }
449            $this->getPageData($id);
450
451            if (!empty($this->column['date'])) {
452                $this->getDate();
453            }
454            if (!empty($this->column['user'])) {
455                $this->getUser();
456            }
457            if (!empty($this->column['desc'])) {
458                $this->getDescription();
459            }
460            if (!empty($this->column['summary'])) {
461                $this->getSummary();
462            }
463        }
464
465        $sortKey = $this->getSortKey($id);
466        if (!blank($sortKey)) {
467            //unique key needed, otherwise entries are overwritten
468            $sortKey = $this->uniqueKey($sortKey, $this->pages);
469            $this->pages[$sortKey] = $this->page;
470        } else {
471            $this->pages[] = $this->page;
472        }
473        return true;
474    }
475
476    /**
477     * Non-recursive function to check whether an array key is unique
478     *
479     * @param int|string $key
480     * @param array $result
481     * @return float|int|string
482     *
483     * @author    Ilya S. Lebedev <ilya@lebedev.net>
484     * @author    Esther Brunner <wikidesign@gmail.com>
485     */
486    protected function uniqueKey($key, $result)
487    {
488        // increase numeric keys by one
489        if (is_numeric($key)) {
490            while (array_key_exists($key, $result)) {
491                $key++;
492            }
493            return $key;
494
495            // append a number to literal keys
496        } else {
497            $num = 0;
498            $testkey = $key;
499            while (array_key_exists($testkey, $result)) {
500                $testkey = $key . $num;
501                $num++;
502            }
503            return $testkey;
504        }
505    }
506
507    /**
508     * Prints html of a list row, call for every row
509     *
510     * @param array $page see for details @see addPage()
511     * @return void
512     */
513    protected function renderPageRow($page)
514    {
515        $this->page = $page;
516        $this->meta = null; // should not be used here
517
518        $id = $this->page['id'];
519        if ($this->style == 'simplelist') {
520            // simplelist is enabled; just output pagename
521            $this->doc .= '<li>';
522            if (page_exists($id)) {
523                $class = 'wikilink1';
524            } else {
525                $class = 'wikilink2';
526            }
527
528            if (empty($this->page['title'])) {
529                $this->page['title'] = str_replace('_', ' ', noNS($id));
530            }
531            $title = hsc($this->page['title']);
532
533            $content = '<a href="' . wl($id) . '" class="' . $class . '" title="' . $id . '">' . $title . '</a>';
534            $this->doc .= $content;
535            $this->doc .= '</li>';
536            return;
537        }
538        // default pagelist, list or table style:
539
540        // priority and draft
541        $class = '';
542        if (isset($this->page['priority'])) {
543            $class .= 'priority' . $this->page['priority'] . ' ';
544        }
545        if (!empty($this->page['draft'])) {
546            $class .= 'draft ';
547        }
548        if (!empty($this->page['class'])) {
549            $class .= $this->page['class'];
550        }
551
552        if (!empty($class)) {
553            $class = ' class="' . $class . '"';
554        }
555
556        $this->doc .= '<tr' . $class . '>';
557        //image column first
558        if (!empty($this->column['image'])) {
559            $this->printPluginCell('pageimage', 'image', $id);
560        }
561        $this->printPageCell($id);
562
563        if (!empty($this->column['date'])) {
564            $this->printDateCell();
565        }
566        if (!empty($this->column['user'])) {
567            $this->printUserCell();
568        }
569        if (!empty($this->column['desc'])) {
570            $this->printDescriptionCell();
571        }
572        if (!empty($this->column['diff'])) {
573            $this->printDiffCell($id);
574        }
575        if (!empty($this->column['summary'])) {
576            $this->printSummary();
577        }
578        foreach ($this->plugins as $plugin => $columns) {
579            foreach ($columns as $col) {
580                if (!empty($this->column[$col]) && $col != 'image') {
581                    $this->printPluginCell($plugin, $col, $id);
582                }
583            }
584        }
585        $this->doc .= '</tr>';
586    }
587
588    /**
589     * (required) Sort pages and render these.
590     * Sets the list footer, reset helper to defaults
591     *
592     * @return string html
593     */
594    public function finishList()
595    {
596        if ($this->sort) {
597            Sort::ksort($this->pages);
598            if ($this->rsort) {
599                $this->pages = array_reverse($this->pages, true);
600            }
601        }
602
603        $cnt = 0;
604        foreach ($this->pages as $page) {
605            $this->renderPageRow($page);
606
607            $cnt++;
608            if($this->limit > 0 && $cnt >= $this->limit){
609                break;
610            }
611        }
612
613        if ($this->style == 'simplelist') {
614            $this->doc .= '</ul>';
615        } else {
616            if (!isset($this->page)) {
617                $this->doc = '';
618            } else {
619                $this->doc .= '</table></div>';
620            }
621        }
622
623        // reset defaults
624        $this->__construct();
625
626        return $this->doc;
627    }
628
629    /* ---------- Private Methods ---------- */
630
631    /**
632     * Page title / link to page
633     *
634     * @param string $id page id displayed in this table row
635     * @return bool whether empty
636     */
637    protected function printPageCell($id)
638    {
639        if ($this->page['exists']) {
640            $class = 'wikilink1';
641        } else {
642            $class = 'wikilink2';
643        }
644
645        // handle image and text titles
646        if (!empty($this->page['titleimage'])) {
647            $title = '<img src="' . ml($this->page['titleimage']) . '" class="media"';
648            if (!empty($this->page['title'])) {
649                $title .= ' title="' . hsc($this->page['title']) . '" alt="' . hsc($this->page['title']) . '"';
650            }
651            $title .= ' />';
652        } else {
653            $title = hsc($this->page['title']);
654        }
655
656        // produce output
657        $section = !empty($this->page['section']) ? '#' . $this->page['section'] : '';
658        $content = '<a href="' . wl($id) . $section . '" class="' . $class . '" title="' . $id . '"  data-wiki-id="' . $id . '">' . $title . '</a>';
659        if ($this->style == 'list') {
660            $content = '<ul><li>' . $content . '</li></ul>';
661        }
662        return $this->printCell('page', $content);
663    }
664
665    /**
666     * Date - creation or last modification date if not set otherwise
667     *
668     * @return bool whether empty
669     */
670    protected function printDateCell()
671    {
672        global $conf;
673
674        if (empty($this->page['date']) || empty($this->page['exists'])) {
675            return $this->printCell('date', '');
676        } else {
677            return $this->printCell('date', dformat($this->page['date'], $conf['dformat']));
678        }
679    }
680
681    /**
682     * User - page creator or contributors if not set otherwise
683     *
684     * @return bool whether empty
685     */
686    protected function printUserCell()
687    {
688        return $this->printCell('user', $this->page['user']);
689    }
690
691    /**
692     * Internal function to get user column as set in 'showuseras' config option.
693     *
694     * @param string $login_name
695     * @return string whether empty
696     */
697    private function getShowUserAsContent($login_name)
698    {
699        if (function_exists('userlink')) {
700            $content = userlink($login_name);
701        } else {
702            $content = editorinfo($login_name);
703        }
704        return $content;
705    }
706
707    /**
708     * Description - (truncated) auto abstract if not set otherwise
709     *
710     * @return bool whether empty
711     */
712    protected function printDescriptionCell()
713    {
714        $desc = $this->page['desc'];
715
716        $max = $this->column['desc'];
717        if ($max > 1 && PhpString::strlen($desc) > $max) {
718            $desc = PhpString::substr($desc, 0, $max) . '…';
719        }
720        return $this->printCell('desc', hsc($desc));
721    }
722
723    /**
724     * Diff icon / link to diff page
725     *
726     * @param string $id page id displayed in this table row
727     * @return bool whether empty
728     */
729    protected function printDiffCell($id)
730    {
731        // check for page existence
732        if (!isset($this->page['exists'])) {
733            if (!isset($this->page['file'])) {
734                $this->page['file'] = wikiFN($id);
735            }
736            $this->page['exists'] = @file_exists($this->page['file']);
737        }
738
739        // produce output
740        $url_params = [];
741        $url_params ['do'] = 'diff';
742        $url = wl($id, $url_params) . (!empty($this->page['section']) ? '#' . $this->page['section'] : '');
743        $content = '<a href="' . $url . '" class="diff_link">
744                    <img src="' . DOKU_BASE . 'lib/images/diff.png" width="15" height="11"
745                     title="' . hsc($this->getLang('diff_title')) . '" alt="' . hsc($this->getLang('diff_alt')) . '"/>
746                    </a>';
747        return $this->printCell('diff', $content);
748    }
749
750    /**
751     * Print the summary from the last page change
752     */
753    protected function printSummary()
754    {
755        return $this->printCell('summary', hsc($this->page['summary']));
756    }
757
758    /**
759     * Plugins - respective plugins must be installed!
760     *
761     * @param string $plugin pluginname
762     * @param string $col column name. Before not provided to td of plugin. Since 2022. Allows different columns per plugin.
763     * @param string $id page id displayed in this table row
764     * @return bool whether empty
765     */
766    protected function printPluginCell($plugin, $col, $id)
767    {
768        if (!isset($this->page[$col])) {
769            $this->page[$col] = $this->$plugin->td($id, $col);
770        }
771        return $this->printCell($col, $this->page[$col]);
772    }
773
774    /**
775     * Produce XHTML cell output
776     *
777     * @param string $class class per td
778     * @param string $content html
779     * @return bool whether empty
780     */
781    protected function printCell($class, $content)
782    {
783        if (!$content) {
784            $content = '&nbsp;';
785            $empty = true;
786        } else {
787            $empty = false;
788        }
789        $this->doc .= '<td class="' . $class . '">' . $content . '</td>';
790        return $empty;
791    }
792
793
794    /**
795     * Get default value for an unset element
796     *
797     * @param string $key one key of metadata array
798     * @param string $subkey second key as subkey of metadata array
799     * @return false|mixed content of the metadata (sub)array
800     */
801    protected function getMeta($key, $subkey = null)
802    {
803        if (empty($this->page['exists']) || empty($this->page['id'])) {
804            return false;
805        }
806        if (!isset($this->meta)) {
807            $this->meta = p_get_metadata($this->page['id'], '', METADATA_RENDER_USING_CACHE);
808        }
809
810        if ($subkey === null) {
811            return $this->meta[$key] ?? null;
812        } else {
813            return $this->meta[$key][$subkey] ?? null;
814        }
815    }
816
817    /**
818     * Retrieve page related data
819     *
820     * @param string $id page id
821     */
822    private function getPageData($id)
823    {
824        // check for page existence
825        if (!isset($this->page['exists'])) {
826            if (!isset($this->page['file'])) {
827                $this->page['file'] = wikiFN($id);
828            }
829            $this->page['exists'] = @file_exists($this->page['file']);
830        }
831        //retrieve title, but not if titleimage which can have eventually its own title
832        if (empty($this->page['titleimage'])) {
833            //not overwrite titles in earlier provided data
834            if (blank($this->page['title']) && $this->showfirsthl) {
835                $this->page['title'] = $this->getMeta('title');
836            }
837
838            if (blank($this->page['title'])) {
839                $this->page['title'] = str_replace('_', ' ', noNSorNS($id));
840            }
841        }
842    }
843
844    /**
845     * Retrieve description
846     */
847    private function getDescription()
848    {
849        if (array_key_exists('desc', $this->page)) return;
850
851        if (strlen($this->page['description']) > 0) {
852            // This condition will become true, when a page-description is given
853            // inside the pagelist plugin syntax-block
854            $desc = $this->page['description'];
855        } else {
856            //supports meta stored by the Description plugin
857            $desc = $this->getMeta('plugin_description', 'keywords');
858
859            //use otherwise the default dokuwiki abstract
860            if (!$desc) {
861                $desc = $this->getMeta('description', 'abstract');
862            }
863            if (blank($desc)) {
864                $desc = '';
865            }
866        }
867        $this->page['desc'] = $desc;
868    }
869
870    private function getSummary()
871    {
872        if (array_key_exists('summary', $this->page)) return;
873
874        $summary = $this->getMeta('last_change', 'sum');
875        $this->page['summary'] = $summary;
876    }
877    /**
878     * Retrieve user
879     */
880    private function getUser()
881    {
882        if (array_key_exists('user', $this->page)) return;
883
884        $content = null;
885        switch ($this->column['user']) {
886            case 1:
887                $content = $this->getMeta('creator');
888                $content = hsc($content);
889                break;
890            case 2:
891                $users = $this->getMeta('contributor');
892                if (is_array($users)) {
893                    $content = join(', ', $users);
894                    $content = hsc($content);
895                }
896                break;
897            case 3:
898                $content = $this->getShowUserAsContent($this->getMeta('user'));
899                break;
900            case 4:
901                $users = $this->getMeta('contributor');
902                if (is_array($users)) {
903                    $content = '';
904                    $item = 0;
905                    foreach ($users as $userid => $fullname) {
906                        $item++;
907                        $content .= $this->getShowUserAsContent($userid);
908                        if ($item < count($users)) {
909                            $content .= ', ';
910                        }
911                    }
912                }
913                break;
914        }
915        $this->page['user'] = $content;
916    }
917
918    /**
919     * Retrieve date
920     */
921    private function getDate()
922    {
923        if (empty($this->page['date']) && !empty($this->page['exists'])) {
924            if ($this->column['date'] == 2) {
925                $this->page['date'] = $this->getMeta('date', 'modified');
926            } else {
927                $this->page['date'] = $this->getMeta('date', 'created');
928            }
929        }
930    }
931
932    /**
933     * Determines the sortkey if sorting is requested
934     *
935     * @param string $id page id
936     * @return string
937     */
938    private function getSortKey($id)
939    {
940        $sortKey = '';
941        if ($this->sortKey !== '') {
942            $sortKey = $this->page[$this->sortKey] ?? false;
943            if ($sortKey === false) {
944                //entry corresponding to sortKey is not yet set
945                if ($this->sortKey == "draft") {
946                    $this->page['draft'] = $this->getMeta('type') == 'draft';
947                }
948                $this->getPageData($id);
949                if ($this->sortKey == "pagename") {
950                    $this->page['pagename'] = noNS($id);
951                }
952                if ($this->sortKey == "ns") {
953                    // sorts pages before namespaces using a zero byte
954                    // see https://github.com/dokufreaks/plugin-tag/commit/7df7f2cb315c5a3a21b9dfacae89bd3ee661c690
955                    $pos = strrpos($id, ':');
956                    if ($pos === false) {
957                        $sortkey = "\0" . $id;
958                    } else {
959                        $sortkey = substr_replace($id, "\0\0", $pos, 1);
960                    }
961                    $this->page['ns'] = str_replace(':', "\0", $sortkey);
962                }
963                if ($this->sortKey == "date") {
964                    $this->getDate();
965                }
966                if ($this->sortKey == "desc") {
967                    $this->getDescription();
968                }
969                if ($this->sortKey == "summary") {
970                    $this->getSummary();
971                }
972                if ($this->sortKey == "user") {
973                    $this->getUser();
974                }
975                foreach ($this->plugins as $plugin => $columns) {
976                    foreach ($columns as $col) {
977                        if ($this->sortKey == $col) {
978                            if (!isset($this->page[$col])) {
979                                $this->page[$col] = $this->$plugin->td($id, $col);
980                            }
981                        }
982                    }
983                }
984                $sortKey = $this->page[$this->sortKey] ?? 9999999999999999; //TODO mostly used for non-existing pages. 999 works only for dates?
985            }
986        }
987        return $sortKey;
988    }
989
990}
991