1<?php
2
3namespace dokuwiki\plugin\struct\meta;
4
5use dokuwiki\File\PageResolver;
6
7/**
8 * Class SearchConfig
9 *
10 * The same as @see Search but can be initialized by a configuration array
11 *
12 * @package dokuwiki\plugin\struct\meta
13 */
14class SearchConfig extends Search
15{
16    /** @var int default aggregation caching (depends on last struct save) */
17    public static $CACHE_DEFAULT = 1;
18    /** @var int caching depends on current user */
19    public static $CACHE_USER = 2;
20    /** @var int caching depends on current date */
21    public static $CACHE_DATE = 4;
22
23    /**
24     * @var array hold the configuration as parsed and extended by dynamic params
25     */
26    protected $config;
27
28    /**
29     * @var SearchConfigParameters manages dynamic parameters
30     */
31    protected $dynamicParameters;
32
33    /**
34     * @var int the cache flag to use (binary flags)
35     */
36    protected $cacheFlag;
37
38    /**
39     * SearchConfig constructor.
40     * @param array $config The parsed configuration for this search
41     * @param bool $dynamic Should dynamic parameters be applied?
42     */
43    public function __construct($config, $dynamic = true)
44    {
45        parent::__construct();
46
47        // setup schemas and columns
48        if (!empty($config['schemas'])) foreach ($config['schemas'] as $schema) {
49            $this->addSchema($schema[0], $schema[1]);
50        }
51        if (!empty($config['cols'])) foreach ($config['cols'] as $col) {
52            $this->addColumn($col);
53        }
54
55        // cache flag setting
56        $this->cacheFlag = self::$CACHE_DEFAULT;
57        if (!empty($config['filters'])) $this->cacheFlag = $this->determineCacheFlag($config['filters']);
58
59        // configure search from configuration
60        if (!empty($config['filter'])) foreach ($config['filter'] as $filter) {
61            $this->addFilter($filter[0], $this->applyFilterVars($filter[2]), $filter[1], $filter[3]);
62        }
63
64        if (!empty($config['sort'])) foreach ($config['sort'] as $sort) {
65            $this->addSort($sort[0], $sort[1]);
66        }
67
68        if (!empty($config['limit'])) {
69            $this->setLimit($config['limit']);
70        }
71
72        if (!empty($config['offset'])) {
73            $this->setOffset($config['offset']);
74        }
75
76        // prepare dynamic parameters
77        $this->dynamicParameters = new SearchConfigParameters($this);
78        if ($dynamic) {
79            $this->dynamicParameters->apply();
80        }
81
82        $this->config = $config;
83    }
84
85    /**
86     * Set the cache flag accordingly to the set filter placeholders
87     *
88     * @param array $filters
89     * @return int
90     */
91    protected function determineCacheFlag($filters)
92    {
93        $flags = self::$CACHE_DEFAULT;
94
95        foreach ($filters as $filter) {
96            if (is_array($filter)) $filter = $filter[2]; // this is the format we get fro the config parser
97
98            if (str_contains($filter, '$USER$')) {
99                $flags |= self::$CACHE_USER;
100            } elseif (str_contains($filter, '$TODAY$')) {
101                $flags |= self::$CACHE_DATE;
102            }
103        }
104
105        return $flags;
106    }
107
108    /**
109     * Replaces placeholders in the given filter value by the proper value
110     *
111     * @param string $filter
112     * @return string|string[] Result may be an array when a multi column placeholder is used
113     */
114    protected function applyFilterVars($filter)
115    {
116        global $INPUT;
117        global $INFO;
118        if (!isset($INFO['id'])) {
119            $INFO['id'] = '';
120        }
121        $ns = getNS($INFO['id']);
122
123        // apply inexpensive filters first
124        $filter = str_replace(
125            [
126                '$ID$',
127                '$NS$',
128                '$PAGE$',
129                '$USER$',
130                '$TODAY$'
131            ],
132            [
133                $INFO['id'],
134                $ns,
135                noNS($INFO['id']),
136                $INPUT->server->str('REMOTE_USER'),
137                date('Y-m-d')
138            ],
139            $filter
140        );
141
142        // apply namespace or id placeholder #712
143        // returns the namespace for start pages, otherwise the ID
144        if (preg_match('/\$NSORID\$/', $filter)) {
145            $resolver = new PageResolver('');
146
147            $start = $resolver->resolveId($ns . ':');
148            if ($start === $INFO['id']) {
149                // This is a start page, we return the namespace
150                $val = $ns;
151            } else {
152                // This is a normal page, we return the ID
153                $val = $INFO['id'];
154            }
155            $filter = str_replace('$NSORID$', $val, $filter);
156        }
157
158        // apply struct column placeholder (we support only one!)
159        // or apply date formula, given as strtotime
160        if (preg_match('/^(.*?)(?:\$STRUCT\.(.*?)\$)(.*?)$/', $filter, $match)) {
161            $filter = $this->applyFilterVarsStruct($match);
162        } elseif (preg_match('/^(.*?)(?:\$USER\.(.*?)\$)(.*?)$/', $filter, $match)) {
163            $filter = $this->applyFilterVarsUser($match);
164        } elseif (preg_match('/^(.*?)(?:\$DATE\((.*?)\)\$?)(.*?)$/', $filter, $match)) {
165            $toparse = $match[2];
166            if ($toparse == '') {
167                $toparse = 'now';
168            }
169            $timestamp = strtotime($toparse);
170            if ($timestamp === false) {
171                throw new StructException('datefilter', hsc($toparse));
172            } else {
173                $filter = str_replace($filter, date('Y-m-d', $timestamp), $filter);
174            }
175        }
176
177        return $filter;
178    }
179
180    /**
181     * Replaces struct placeholders in the given filter value by the proper value
182     *
183     * @param string $match
184     * @return string|string[] Result may be an array when a multi column placeholder is used
185     */
186    protected function applyFilterVarsStruct($match)
187    {
188        global $INFO;
189
190        $key = $match[2];
191
192        // we try to resolve the key via the assigned schemas first, otherwise take it literally
193        $column = $this->findColumn($key, true);
194        if ($column) {
195            $label = $column->getLabel();
196            $table = $column->getTable();
197        } else {
198            [$table, $label] = sexplode('.', $key, 2, '');
199        }
200
201        // get the data from the current page
202        if ($table && $label) {
203            $schemaData = AccessTable::getPageAccess($table, $INFO['id']);
204            $data = $schemaData->getData();
205            if (!isset($data[$label])) {
206                throw new StructException("column not in table", $label, $table);
207            }
208            $value = $data[$label]->getCompareValue();
209
210            if (is_array($value) && $value === []) {
211                $value = '';
212            }
213        } else {
214            $value = '';
215        }
216
217        // apply any pre and postfixes, even when multi value
218        if (is_array($value)) {
219            $filter = [];
220            foreach ($value as $item) {
221                $filter[] = $match[1] . $item . $match[3];
222            }
223        } else {
224            $filter = $match[1] . $value . $match[3];
225        }
226
227        return $filter;
228    }
229
230    /**
231     * Replaces user placeholders in the given filter value by the proper value
232     *
233     * @param string $match
234     * @return string|string[] String for name and mail, array for grps
235     */
236    protected function applyFilterVarsUser($match)
237    {
238        global $INFO;
239
240        $key = strtolower($match[2]);
241
242        if (!in_array($key, ['name', 'mail', 'grps'])) {
243            throw new StructException('"%s" is not a valid USER key', $key);
244        }
245
246        if (empty($INFO['userinfo'])) {
247            $filter = '';
248        } else {
249            $filter = $INFO['userinfo'][$key];
250        }
251
252        return $filter;
253    }
254
255    /**
256     * @return int cacheflag for this search
257     */
258    public function getCacheFlag()
259    {
260        return $this->cacheFlag;
261    }
262
263    /**
264     * Access the dynamic parameters of this search
265     *
266     * Note: This call returns a clone of the parameters as they were initialized
267     *
268     * @return SearchConfigParameters
269     */
270    public function getDynamicParameters()
271    {
272        return clone $this->dynamicParameters;
273    }
274
275    /**
276     * Get the config this search was initialized with
277     *
278     * Note that the search may have been modified by dynamic parameters or additional member calls
279     *
280     * @return array
281     */
282    public function getConf()
283    {
284        return $this->config;
285    }
286}
287