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