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 * 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 $data = ft_pageLookup($lookup, true, $this->config['usetitles']); 82 if ($data === []) return []; 83 84 $filter = $this->config['autocomplete']['filter']; 85 86 // this basically duplicates what we do in ajax_qsearch() but with a filter 87 $result = []; 88 $counter = 0; 89 foreach ($data as $id => $title) { 90 if (!empty($filter) && !$this->filterMatch($id, $filter)) { 91 continue; 92 } 93 if ($this->config['usetitles']) { 94 $name = $title . ' (' . $id . ')'; 95 } else { 96 $ns = getNS($id); 97 if ($ns) { 98 $name = noNS($id) . ' (' . $ns . ')'; 99 } else { 100 $name = $id; 101 } 102 } 103 104 $result[] = [ 105 'label' => $name, 106 'value' => $id 107 ]; 108 109 $counter++; 110 if ($counter > $max) break; 111 } 112 113 return $result; 114 } 115 116 /** 117 * When using titles, we need ot join the titles table 118 * 119 * @param QueryBuilder $QB 120 * @param string $tablealias 121 * @param string $colname 122 * @param string $alias 123 */ 124 public function select(QueryBuilder $QB, $tablealias, $colname, $alias) 125 { 126 if (!$this->config['usetitles']) { 127 parent::select($QB, $tablealias, $colname, $alias); 128 return; 129 } 130 $rightalias = $QB->generateTableAlias(); 131 $QB->addLeftJoin($tablealias, 'titles', $rightalias, "$tablealias.$colname = $rightalias.pid"); 132 $QB->addSelectStatement("STRUCT_JSON($tablealias.$colname, $rightalias.title)", $alias); 133 } 134 135 /** 136 * When using titles, sort by them first 137 * 138 * @param QueryBuilder $QB 139 * @param string $tablealias 140 * @param string $colname 141 * @param string $order 142 */ 143 public function sort(QueryBuilder $QB, $tablealias, $colname, $order) 144 { 145 if (!$this->config['usetitles']) { 146 parent::sort($QB, $tablealias, $colname, $order); 147 return; 148 } 149 150 $rightalias = $QB->generateTableAlias(); 151 $QB->addLeftJoin($tablealias, 'titles', $rightalias, "$tablealias.$colname = $rightalias.pid"); 152 $QB->addOrderBy("$rightalias.title COLLATE NOCASE $order"); 153 $QB->addOrderBy("$tablealias.$colname $order"); 154 } 155 156 /** 157 * Return the pageid only 158 * 159 * @param string $value 160 * @return string 161 */ 162 public function rawValue($value) 163 { 164 if ($this->config['usetitles']) { 165 [$value] = \helper_plugin_struct::decodeJson($value); 166 } 167 return $value; 168 } 169 170 /** 171 * Return the title only 172 * 173 * @param string $value 174 * @return string 175 */ 176 public function displayValue($value) 177 { 178 if ($this->config['usetitles']) { 179 [$pageid, $value] = \helper_plugin_struct::decodeJson($value); 180 if (blank($value)) { 181 $value = $pageid; 182 } 183 } 184 return $value; 185 } 186 187 /** 188 * When using titles, we need to compare against the title table, too 189 * 190 * @param QueryBuilderWhere $add 191 * @param string $tablealias 192 * @param string $colname 193 * @param string $comp 194 * @param string $value 195 * @param string $op 196 */ 197 public function filter(QueryBuilderWhere $add, $tablealias, $colname, $comp, $value, $op) 198 { 199 if (!$this->config['usetitles']) { 200 parent::filter($add, $tablealias, $colname, $comp, $value, $op); 201 return; 202 } 203 204 $QB = $add->getQB(); 205 $rightalias = $QB->generateTableAlias(); 206 $QB->addLeftJoin($tablealias, 'titles', $rightalias, "$tablealias.$colname = $rightalias.pid"); 207 208 // compare against page and title 209 $sub = $add->where($op); 210 $pl = $QB->addValue($value); 211 $sub->whereOr("$tablealias.$colname $comp $pl"); 212 $pl = $QB->addValue($value); 213 $sub->whereOr("$rightalias.title $comp $pl"); 214 } 215 216 /** 217 * Check if the given id matches a configured filter pattern 218 * 219 * @param string $id 220 * @param string $filter 221 * @return bool 222 */ 223 public function filterMatch($id, $filter) 224 { 225 // absolute namespace? 226 if (PhpString::substr($filter, 0, 1) === ':') { 227 $filter = '^' . $filter; 228 } 229 230 try { 231 $check = preg_match('/' . $filter . '/', ':' . $id, $matches); 232 } catch (\Exception $e) { 233 throw new StructException("Error processing regular expression '$filter'"); 234 } 235 return (bool)$check; 236 } 237 238 /** 239 * Merge the current config with the base config of the type. 240 * 241 * In contrast to parent, this method does not throw away unknown keys. 242 * Required to migrate deprecated / obsolete options, no longer part of type config. 243 * 244 * @param array $current Current configuration 245 * @param array $config Base Type configuration 246 */ 247 protected function mergeConfig($current, &$config) 248 { 249 foreach ($current as $key => $value) { 250 if (isset($config[$key]) && is_array($config[$key])) { 251 $this->mergeConfig($value, $config[$key]); 252 } else { 253 $config[$key] = $value; 254 } 255 } 256 257 // migrate autocomplete options 'namespace' and 'postfix' to 'filter' 258 if (empty($config['autocomplete']['filter'])) { 259 if (!empty($config['autocomplete']['namespace'])) { 260 $config['autocomplete']['filter'] = $config['autocomplete']['namespace']; 261 unset($config['autocomplete']['namespace']); 262 } 263 if (!empty($config['autocomplete']['postfix'])) { 264 $config['autocomplete']['filter'] .= '.+?' . $config['autocomplete']['postfix'] . '$'; 265 unset($config['autocomplete']['postfix']); 266 } 267 } 268 } 269} 270