xref: /plugin/struct/types/Page.php (revision c0f1a2d1add0af94d83a1f75f04bdfd9ba698611)
1<?php
2
3namespace dokuwiki\plugin\struct\types;
4
5use dokuwiki\File\PageResolver;
6use dokuwiki\plugin\struct\meta\QueryBuilder;
7use dokuwiki\plugin\struct\meta\QueryBuilderWhere;
8use dokuwiki\Utf8\PhpString;
9
10/**
11 * Class Page
12 *
13 * Represents a single page in the wiki. Will be linked in output.
14 *
15 * @package dokuwiki\plugin\struct\types
16 */
17class Page extends AbstractMultiBaseType
18{
19    protected $config = [
20        'usetitles' => false,
21        'autocomplete' => [
22            'mininput' => 2,
23            'maxresult' => 5,
24            'namespace' => '',
25            'postfix' => ''
26        ]
27    ];
28
29    /**
30     * Output the stored data
31     *
32     * @param string $value the value stored in the database - JSON when titles are used
33     * @param \Doku_Renderer $R the renderer currently used to render the data
34     * @param string $mode The mode the output is rendered in (eg. XHTML)
35     * @return bool true if $mode could be satisfied
36     */
37    public function renderValue($value, \Doku_Renderer $R, $mode)
38    {
39        if ($this->config['usetitles']) {
40            [$id, $title] = \helper_plugin_struct::decodeJson($value);
41        } else {
42            $id = $value;
43            $title = $id; // cannot be empty or internallink() might hijack %pageid% and use headings
44        }
45
46        if (!$id) return true;
47
48        $R->internallink(":$id", $title);
49        return true;
50    }
51
52    /**
53     * Cleans the link
54     *
55     * @param string $rawvalue
56     * @return string
57     */
58    public function validate($rawvalue)
59    {
60        [$page, $fragment] = array_pad(explode('#', $rawvalue, 2), 2, '');
61        return cleanID($page) . (strlen(cleanID($fragment)) > 0 ? '#' . cleanID($fragment) : '');
62    }
63
64    /**
65     * Autocompletion support for pages
66     *
67     * @return array
68     */
69    public function handleAjax()
70    {
71        global $INPUT;
72
73        // check minimum length
74        $lookup = trim($INPUT->str('search'));
75        if (PhpString::strlen($lookup) < $this->config['autocomplete']['mininput']) return [];
76
77        // results wanted?
78        $max = $this->config['autocomplete']['maxresult'];
79        if ($max <= 0) return [];
80
81        // apply namespace and postfix
82        $postfix = $this->config['autocomplete']['postfix'];
83
84        $data = ft_pageLookup($lookup, true, $this->config['usetitles']);
85        if ($data === []) return [];
86
87        $namespace = $this->config['autocomplete']['namespace'];
88
89        // this basically duplicates what we do in ajax_qsearch() but with ns filter
90        $result = [];
91        $counter = 0;
92        foreach ($data as $id => $title) {
93            if (!empty($namespace) && !$this->nsMatch($id, $namespace)) {
94                continue;
95            }
96            if ($this->config['usetitles']) {
97                $name = $title . ' (' . $id . ')';
98            } else {
99                $ns = getNS($id);
100                if ($ns) {
101                    $name = noNS($id) . ' (' . $ns . ')';
102                } else {
103                    $name = $id;
104                }
105            }
106
107            // check suffix
108            if ($postfix && substr($id, -1 * strlen($postfix)) != $postfix) {
109                continue; // page does not end in postfix, don't suggest it
110            }
111
112            $result[] = [
113                'label' => $name,
114                'value' => $id
115            ];
116
117            $counter++;
118            if ($counter > $max) break;
119        }
120
121        return $result;
122    }
123
124    /**
125     * When using titles, we need ot join the titles table
126     *
127     * @param QueryBuilder $QB
128     * @param string $tablealias
129     * @param string $colname
130     * @param string $alias
131     */
132    public function select(QueryBuilder $QB, $tablealias, $colname, $alias)
133    {
134        if (!$this->config['usetitles']) {
135            parent::select($QB, $tablealias, $colname, $alias);
136            return;
137        }
138        $rightalias = $QB->generateTableAlias();
139        $QB->addLeftJoin($tablealias, 'titles', $rightalias, "$tablealias.$colname = $rightalias.pid");
140        $QB->addSelectStatement("STRUCT_JSON($tablealias.$colname, $rightalias.title)", $alias);
141    }
142
143    /**
144     * When using titles, sort by them first
145     *
146     * @param QueryBuilder $QB
147     * @param string $tablealias
148     * @param string $colname
149     * @param string $order
150     */
151    public function sort(QueryBuilder $QB, $tablealias, $colname, $order)
152    {
153        if (!$this->config['usetitles']) {
154            parent::sort($QB, $tablealias, $colname, $order);
155            return;
156        }
157
158        $rightalias = $QB->generateTableAlias();
159        $QB->addLeftJoin($tablealias, 'titles', $rightalias, "$tablealias.$colname = $rightalias.pid");
160        $QB->addOrderBy("$rightalias.title $order");
161        $QB->addOrderBy("$tablealias.$colname $order");
162    }
163
164    /**
165     * Return the pageid only
166     *
167     * @param string $value
168     * @return string
169     */
170    public function rawValue($value)
171    {
172        if ($this->config['usetitles']) {
173            [$value] = \helper_plugin_struct::decodeJson($value);
174        }
175        return $value;
176    }
177
178    /**
179     * Return the title only
180     *
181     * @param string $value
182     * @return string
183     */
184    public function displayValue($value)
185    {
186        if ($this->config['usetitles']) {
187            [$pageid, $value] = \helper_plugin_struct::decodeJson($value);
188            if (blank($value)) {
189                $value = $pageid;
190            }
191        }
192        return $value;
193    }
194
195    /**
196     * When using titles, we need to compare against the title table, too
197     *
198     * @param QueryBuilderWhere $add
199     * @param string $tablealias
200     * @param string $colname
201     * @param string $comp
202     * @param string $value
203     * @param string $op
204     */
205    public function filter(QueryBuilderWhere $add, $tablealias, $colname, $comp, $value, $op)
206    {
207        if (!$this->config['usetitles']) {
208            parent::filter($add, $tablealias, $colname, $comp, $value, $op);
209            return;
210        }
211
212        $QB = $add->getQB();
213        $rightalias = $QB->generateTableAlias();
214        $QB->addLeftJoin($tablealias, 'titles', $rightalias, "$tablealias.$colname = $rightalias.pid");
215
216        // compare against page and title
217        $sub = $add->where($op);
218        $pl = $QB->addValue($value);
219        $sub->whereOr("$tablealias.$colname $comp $pl");
220        $pl = $QB->addValue($value);
221        $sub->whereOr("$rightalias.title $comp $pl");
222    }
223
224    /**
225     * Check if the given id matches at configured namespace (pattern):
226     * simple string or regex pattern with delimiter "/"
227     *
228     * @param string $id
229     * @param string $namespace
230     * @return bool
231     */
232    public function nsMatch($id, $namespace)
233    {
234        $searchNS = getNS($id);
235        if (!$searchNS) {
236            return false; // root
237        }
238
239        // prepare any namespace for preg_match()
240        $searchNS = ':' . $searchNS . ':';
241        // absolute namespace?
242        if (PhpString::substr($namespace, 0, 1) === ':') {
243            $namespace = '^' . $namespace;
244        }
245        // non-regex namespace?
246        if (PhpString::substr($namespace, 0, 1) !== '/') {
247            $namespace = '(?::|^)' . $namespace ;
248            $namespace = '/' . $namespace . '/';
249        }
250        preg_match($namespace, $searchNS, $matches);
251
252        return !empty($matches);
253    }
254}
255