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 = ['usetitles' => false, 'autocomplete' => ['mininput' => 2, 'maxresult' => 5, 'namespace' => '', 'postfix' => '']]; 20 21 /** 22 * Output the stored data 23 * 24 * @param string $value the value stored in the database - JSON when titles are used 25 * @param \Doku_Renderer $R the renderer currently used to render the data 26 * @param string $mode The mode the output is rendered in (eg. XHTML) 27 * @return bool true if $mode could be satisfied 28 */ 29 public function renderValue($value, \Doku_Renderer $R, $mode) 30 { 31 if ($this->config['usetitles']) { 32 [$id, $title] = \helper_plugin_struct::decodeJson($value); 33 } else { 34 $id = $value; 35 $title = $id; // cannot be empty or internallink() might hijack %pageid% and use headings 36 } 37 38 if (!$id) return true; 39 40 $R->internallink(":$id", $title); 41 return true; 42 } 43 44 /** 45 * Cleans the link 46 * 47 * @param string $rawvalue 48 * @return string 49 */ 50 public function validate($rawvalue) 51 { 52 [$page, $fragment] = array_pad(explode('#', $rawvalue, 2), 2, ''); 53 return cleanID($page) . (strlen(cleanID($fragment)) > 0 ? '#' . cleanID($fragment) : ''); 54 } 55 56 /** 57 * Autocompletion support for pages 58 * 59 * @return array 60 */ 61 public function handleAjax() 62 { 63 global $INPUT; 64 65 // check minimum length 66 $lookup = trim($INPUT->str('search')); 67 if (PhpString::strlen($lookup) < $this->config['autocomplete']['mininput']) return []; 68 69 // results wanted? 70 $max = $this->config['autocomplete']['maxresult']; 71 if ($max <= 0) return []; 72 73 // lookup with namespace and postfix applied 74 $namespace = $this->config['autocomplete']['namespace']; 75 if ($namespace) { 76 // namespace may be relative, resolve in current context 77 $namespace .= ':foo'; // resolve expects pageID 78 $resolver = new PageResolver($INPUT->str('ns') . ':foo'); // resolve relative to current namespace 79 $namespace = $resolver->resolveId($namespace); 80 $namespace = getNS($namespace); 81 } 82 $postfix = $this->config['autocomplete']['postfix']; 83 if ($namespace) $lookup .= ' @' . $namespace; 84 85 $data = ft_pageLookup($lookup, true, $this->config['usetitles']); 86 if ($data === []) return []; 87 88 // this basically duplicates what we do in ajax_qsearch() 89 $result = []; 90 $counter = 0; 91 foreach ($data as $id => $title) { 92 if ($this->config['usetitles']) { 93 $name = $title . ' (' . $id . ')'; 94 } else { 95 $ns = getNS($id); 96 if ($ns) { 97 $name = noNS($id) . ' (' . $ns . ')'; 98 } else { 99 $name = $id; 100 } 101 } 102 103 // check suffix 104 if ($postfix && substr($id, -1 * strlen($postfix)) != $postfix) { 105 continue; // page does not end in postfix, don't suggest it 106 } 107 108 $result[] = ['label' => $name, 'value' => $id]; 109 110 $counter++; 111 if ($counter > $max) break; 112 } 113 114 return $result; 115 } 116 117 /** 118 * When using titles, we need ot join the titles table 119 * 120 * @param QueryBuilder $QB 121 * @param string $tablealias 122 * @param string $colname 123 * @param string $alias 124 */ 125 public function select(QueryBuilder $QB, $tablealias, $colname, $alias) 126 { 127 if (!$this->config['usetitles']) { 128 parent::select($QB, $tablealias, $colname, $alias); 129 return; 130 } 131 $rightalias = $QB->generateTableAlias(); 132 $QB->addLeftJoin($tablealias, 'titles', $rightalias, "$tablealias.$colname = $rightalias.pid"); 133 $QB->addSelectStatement("STRUCT_JSON($tablealias.$colname, $rightalias.title)", $alias); 134 } 135 136 /** 137 * When using titles, sort by them first 138 * 139 * @param QueryBuilder $QB 140 * @param string $tablealias 141 * @param string $colname 142 * @param string $order 143 */ 144 public function sort(QueryBuilder $QB, $tablealias, $colname, $order) 145 { 146 if (!$this->config['usetitles']) { 147 parent::sort($QB, $tablealias, $colname, $order); 148 return; 149 } 150 151 $rightalias = $QB->generateTableAlias(); 152 $QB->addLeftJoin($tablealias, 'titles', $rightalias, "$tablealias.$colname = $rightalias.pid"); 153 $QB->addOrderBy("$rightalias.title $order"); 154 $QB->addOrderBy("$tablealias.$colname $order"); 155 } 156 157 /** 158 * Return the pageid only 159 * 160 * @param string $value 161 * @return string 162 */ 163 public function rawValue($value) 164 { 165 if ($this->config['usetitles']) { 166 [$value] = \helper_plugin_struct::decodeJson($value); 167 } 168 return $value; 169 } 170 171 /** 172 * Return the title only 173 * 174 * @param string $value 175 * @return string 176 */ 177 public function displayValue($value) 178 { 179 if ($this->config['usetitles']) { 180 [$pageid, $value] = \helper_plugin_struct::decodeJson($value); 181 if (blank($value)) { 182 $value = $pageid; 183 } 184 } 185 return $value; 186 } 187 188 /** 189 * When using titles, we need to compare against the title table, too 190 * 191 * @param QueryBuilderWhere $add 192 * @param string $tablealias 193 * @param string $colname 194 * @param string $comp 195 * @param string $value 196 * @param string $op 197 */ 198 public function filter(QueryBuilderWhere $add, $tablealias, $colname, $comp, $value, $op) 199 { 200 if (!$this->config['usetitles']) { 201 parent::filter($add, $tablealias, $colname, $comp, $value, $op); 202 return; 203 } 204 205 $QB = $add->getQB(); 206 $rightalias = $QB->generateTableAlias(); 207 $QB->addLeftJoin($tablealias, 'titles', $rightalias, "$tablealias.$colname = $rightalias.pid"); 208 209 // compare against page and title 210 $sub = $add->where($op); 211 $pl = $QB->addValue($value); 212 $sub->whereOr("$tablealias.$colname $comp $pl"); 213 $pl = $QB->addValue($value); 214 $sub->whereOr("$rightalias.title $comp $pl"); 215 } 216} 217