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