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