10234787dSAndreas Gohr<?php 2d6d97f60SAnna Dabrowska 3ba766201SAndreas Gohrnamespace dokuwiki\plugin\struct\types; 494603e42SAndreas Gohr 56a819106SAndreas Gohruse dokuwiki\File\PageResolver; 6a827267cSAndreas Gohruse dokuwiki\plugin\struct\meta\QueryBuilder; 7af993d55SMichael Grosseuse dokuwiki\plugin\struct\meta\QueryBuilderWhere; 8996125f5SAnna Dabrowskause dokuwiki\plugin\struct\meta\StructException; 96a819106SAndreas Gohruse dokuwiki\Utf8\PhpString; 100234787dSAndreas Gohr 110234787dSAndreas Gohr/** 120234787dSAndreas Gohr * Class Page 130234787dSAndreas Gohr * 140234787dSAndreas Gohr * Represents a single page in the wiki. Will be linked in output. 150234787dSAndreas Gohr * 16ba766201SAndreas Gohr * @package dokuwiki\plugin\struct\types 170234787dSAndreas Gohr */ 18d6d97f60SAnna Dabrowskaclass Page extends AbstractMultiBaseType 19d6d97f60SAnna Dabrowska{ 207fe2cdf2SAndreas Gohr protected $config = [ 217fe2cdf2SAndreas Gohr 'usetitles' => false, 227fe2cdf2SAndreas Gohr 'autocomplete' => [ 237fe2cdf2SAndreas Gohr 'mininput' => 2, 247fe2cdf2SAndreas Gohr 'maxresult' => 5, 259c075503SAnna Dabrowska 'filter' => '', 267fe2cdf2SAndreas Gohr ] 277fe2cdf2SAndreas Gohr ]; 2816a4ba5bSAndreas Gohr 290234787dSAndreas Gohr /** 300234787dSAndreas Gohr * Output the stored data 310234787dSAndreas Gohr * 32a827267cSAndreas Gohr * @param string $value the value stored in the database - JSON when titles are used 33797f0dfeSAndreas Gohr * @param \Doku_Renderer $R the renderer currently used to render the data 34797f0dfeSAndreas Gohr * @param string $mode The mode the output is rendered in (eg. XHTML) 35797f0dfeSAndreas Gohr * @return bool true if $mode could be satisfied 360234787dSAndreas Gohr */ 37d6d97f60SAnna Dabrowska public function renderValue($value, \Doku_Renderer $R, $mode) 38d6d97f60SAnna Dabrowska { 39a827267cSAndreas Gohr if ($this->config['usetitles']) { 407234bfb1Ssplitbrain [$id, $title] = \helper_plugin_struct::decodeJson($value); 41a827267cSAndreas Gohr } else { 42a827267cSAndreas Gohr $id = $value; 439dc9943dSAnna Dabrowska $title = $id; // cannot be empty or internallink() might hijack %pageid% and use headings 44a827267cSAndreas Gohr } 454d19af58SAndreas Gohr 46a827267cSAndreas Gohr if (!$id) return true; 47a827267cSAndreas Gohr 48a827267cSAndreas Gohr $R->internallink(":$id", $title); 49797f0dfeSAndreas Gohr return true; 500234787dSAndreas Gohr } 510234787dSAndreas Gohr 5216a4ba5bSAndreas Gohr /** 5306bdb2c2SAndreas Gohr * Cleans the link 5406bdb2c2SAndreas Gohr * 5523169abeSAndreas Gohr * @param string $rawvalue 5606bdb2c2SAndreas Gohr * @return string 5706bdb2c2SAndreas Gohr */ 58d6d97f60SAnna Dabrowska public function validate($rawvalue) 59d6d97f60SAnna Dabrowska { 607234bfb1Ssplitbrain [$page, $fragment] = array_pad(explode('#', $rawvalue, 2), 2, ''); 61eeac4e77SMichael Grosse return cleanID($page) . (strlen(cleanID($fragment)) > 0 ? '#' . cleanID($fragment) : ''); 6206bdb2c2SAndreas Gohr } 6306bdb2c2SAndreas Gohr 6406bdb2c2SAndreas Gohr /** 6516a4ba5bSAndreas Gohr * Autocompletion support for pages 6616a4ba5bSAndreas Gohr * 6716a4ba5bSAndreas Gohr * @return array 6816a4ba5bSAndreas Gohr */ 69d6d97f60SAnna Dabrowska public function handleAjax() 70d6d97f60SAnna Dabrowska { 7116a4ba5bSAndreas Gohr global $INPUT; 7216a4ba5bSAndreas Gohr 7316a4ba5bSAndreas Gohr // check minimum length 7416a4ba5bSAndreas Gohr $lookup = trim($INPUT->str('search')); 757234bfb1Ssplitbrain if (PhpString::strlen($lookup) < $this->config['autocomplete']['mininput']) return []; 7616a4ba5bSAndreas Gohr 7716a4ba5bSAndreas Gohr // results wanted? 7816a4ba5bSAndreas Gohr $max = $this->config['autocomplete']['maxresult']; 797234bfb1Ssplitbrain if ($max <= 0) return []; 8016a4ba5bSAndreas Gohr 8185716d37SAndreas Gohr $data = ft_pageLookup($lookup, true, $this->config['usetitles']); 827234bfb1Ssplitbrain if ($data === []) return []; 8316a4ba5bSAndreas Gohr 849c075503SAnna Dabrowska $filter = $this->config['autocomplete']['filter']; 85c0f1a2d1SAnna Dabrowska 869c075503SAnna Dabrowska // this basically duplicates what we do in ajax_qsearch() but with a filter 877234bfb1Ssplitbrain $result = []; 8816a4ba5bSAndreas Gohr $counter = 0; 8916a4ba5bSAndreas Gohr foreach ($data as $id => $title) { 909c075503SAnna Dabrowska if (!empty($filter) && !$this->filterMatch($id, $filter)) { 91c0f1a2d1SAnna Dabrowska continue; 92c0f1a2d1SAnna Dabrowska } 9385716d37SAndreas Gohr if ($this->config['usetitles']) { 9494603e42SAndreas Gohr $name = $title . ' (' . $id . ')'; 9516a4ba5bSAndreas Gohr } else { 9616a4ba5bSAndreas Gohr $ns = getNS($id); 9716a4ba5bSAndreas Gohr if ($ns) { 9816a4ba5bSAndreas Gohr $name = noNS($id) . ' (' . $ns . ')'; 9916a4ba5bSAndreas Gohr } else { 10016a4ba5bSAndreas Gohr $name = $id; 10116a4ba5bSAndreas Gohr } 10216a4ba5bSAndreas Gohr } 1037067bf5aSAndreas Gohr 1047fe2cdf2SAndreas Gohr $result[] = [ 1057fe2cdf2SAndreas Gohr 'label' => $name, 1067fe2cdf2SAndreas Gohr 'value' => $id 1077fe2cdf2SAndreas Gohr ]; 10816a4ba5bSAndreas Gohr 10916a4ba5bSAndreas Gohr $counter++; 11016a4ba5bSAndreas Gohr if ($counter > $max) break; 11116a4ba5bSAndreas Gohr } 11216a4ba5bSAndreas Gohr 11316a4ba5bSAndreas Gohr return $result; 11416a4ba5bSAndreas Gohr } 115a827267cSAndreas Gohr 116a827267cSAndreas Gohr /** 117a827267cSAndreas Gohr * When using titles, we need ot join the titles table 118a827267cSAndreas Gohr * 119a827267cSAndreas Gohr * @param QueryBuilder $QB 120a827267cSAndreas Gohr * @param string $tablealias 121a827267cSAndreas Gohr * @param string $colname 122a827267cSAndreas Gohr * @param string $alias 123a827267cSAndreas Gohr */ 124d6d97f60SAnna Dabrowska public function select(QueryBuilder $QB, $tablealias, $colname, $alias) 125d6d97f60SAnna Dabrowska { 126a827267cSAndreas Gohr if (!$this->config['usetitles']) { 127a827267cSAndreas Gohr parent::select($QB, $tablealias, $colname, $alias); 128a827267cSAndreas Gohr return; 129a827267cSAndreas Gohr } 130a827267cSAndreas Gohr $rightalias = $QB->generateTableAlias(); 131a827267cSAndreas Gohr $QB->addLeftJoin($tablealias, 'titles', $rightalias, "$tablealias.$colname = $rightalias.pid"); 1320e72ef50SAndreas Gohr $QB->addSelectStatement("STRUCT_JSON($tablealias.$colname, $rightalias.title)", $alias); 133a827267cSAndreas Gohr } 134a827267cSAndreas Gohr 135a827267cSAndreas Gohr /** 136694968d6SAndreas Gohr * When using titles, sort by them first 137694968d6SAndreas Gohr * 138694968d6SAndreas Gohr * @param QueryBuilder $QB 139694968d6SAndreas Gohr * @param string $tablealias 140694968d6SAndreas Gohr * @param string $colname 141694968d6SAndreas Gohr * @param string $order 142694968d6SAndreas Gohr */ 143d6d97f60SAnna Dabrowska public function sort(QueryBuilder $QB, $tablealias, $colname, $order) 144d6d97f60SAnna Dabrowska { 145694968d6SAndreas Gohr if (!$this->config['usetitles']) { 146694968d6SAndreas Gohr parent::sort($QB, $tablealias, $colname, $order); 147694968d6SAndreas Gohr return; 148694968d6SAndreas Gohr } 149694968d6SAndreas Gohr 150694968d6SAndreas Gohr $rightalias = $QB->generateTableAlias(); 151694968d6SAndreas Gohr $QB->addLeftJoin($tablealias, 'titles', $rightalias, "$tablealias.$colname = $rightalias.pid"); 152*f0be5277SAnna Dabrowska $QB->addOrderBy("$rightalias.title COLLATE NOCASE $order"); 153694968d6SAndreas Gohr $QB->addOrderBy("$tablealias.$colname $order"); 154694968d6SAndreas Gohr } 155694968d6SAndreas Gohr 156694968d6SAndreas Gohr /** 157e7ee2b64SAndreas Gohr * Return the pageid only 158e7ee2b64SAndreas Gohr * 159e7ee2b64SAndreas Gohr * @param string $value 160e7ee2b64SAndreas Gohr * @return string 161e7ee2b64SAndreas Gohr */ 162d6d97f60SAnna Dabrowska public function rawValue($value) 163d6d97f60SAnna Dabrowska { 164e7ee2b64SAndreas Gohr if ($this->config['usetitles']) { 1657234bfb1Ssplitbrain [$value] = \helper_plugin_struct::decodeJson($value); 166e7ee2b64SAndreas Gohr } 167e7ee2b64SAndreas Gohr return $value; 168e7ee2b64SAndreas Gohr } 169e7ee2b64SAndreas Gohr 170e7ee2b64SAndreas Gohr /** 1715241ca30SAndreas Gohr * Return the title only 1725241ca30SAndreas Gohr * 1735241ca30SAndreas Gohr * @param string $value 1745241ca30SAndreas Gohr * @return string 1755241ca30SAndreas Gohr */ 176d6d97f60SAnna Dabrowska public function displayValue($value) 177d6d97f60SAnna Dabrowska { 1785241ca30SAndreas Gohr if ($this->config['usetitles']) { 1797234bfb1Ssplitbrain [$pageid, $value] = \helper_plugin_struct::decodeJson($value); 180faa6f42cSMichael Grosse if (blank($value)) { 181faa6f42cSMichael Grosse $value = $pageid; 182faa6f42cSMichael Grosse } 1835241ca30SAndreas Gohr } 1845241ca30SAndreas Gohr return $value; 1855241ca30SAndreas Gohr } 1865241ca30SAndreas Gohr 1875241ca30SAndreas Gohr /** 188fab65495SAndreas Gohr * When using titles, we need to compare against the title table, too 189a827267cSAndreas Gohr * 190af993d55SMichael Grosse * @param QueryBuilderWhere $add 191a827267cSAndreas Gohr * @param string $tablealias 192a827267cSAndreas Gohr * @param string $colname 193a827267cSAndreas Gohr * @param string $comp 194a827267cSAndreas Gohr * @param string $value 195a827267cSAndreas Gohr * @param string $op 196a827267cSAndreas Gohr */ 197d6d97f60SAnna Dabrowska public function filter(QueryBuilderWhere $add, $tablealias, $colname, $comp, $value, $op) 198d6d97f60SAnna Dabrowska { 199a827267cSAndreas Gohr if (!$this->config['usetitles']) { 200af993d55SMichael Grosse parent::filter($add, $tablealias, $colname, $comp, $value, $op); 201a827267cSAndreas Gohr return; 202a827267cSAndreas Gohr } 203a827267cSAndreas Gohr 204af993d55SMichael Grosse $QB = $add->getQB(); 205a827267cSAndreas Gohr $rightalias = $QB->generateTableAlias(); 206a827267cSAndreas Gohr $QB->addLeftJoin($tablealias, 'titles', $rightalias, "$tablealias.$colname = $rightalias.pid"); 207a827267cSAndreas Gohr 208fab65495SAndreas Gohr // compare against page and title 209af993d55SMichael Grosse $sub = $add->where($op); 210a827267cSAndreas Gohr $pl = $QB->addValue($value); 211fab65495SAndreas Gohr $sub->whereOr("$tablealias.$colname $comp $pl"); 212fab65495SAndreas Gohr $pl = $QB->addValue($value); 213fab65495SAndreas Gohr $sub->whereOr("$rightalias.title $comp $pl"); 214a827267cSAndreas Gohr } 215c0f1a2d1SAnna Dabrowska 216c0f1a2d1SAnna Dabrowska /** 2179c075503SAnna Dabrowska * Check if the given id matches a configured filter pattern 218c0f1a2d1SAnna Dabrowska * 219c0f1a2d1SAnna Dabrowska * @param string $id 2209c075503SAnna Dabrowska * @param string $filter 221c0f1a2d1SAnna Dabrowska * @return bool 222c0f1a2d1SAnna Dabrowska */ 2239c075503SAnna Dabrowska public function filterMatch($id, $filter) 224c0f1a2d1SAnna Dabrowska { 225c0f1a2d1SAnna Dabrowska // absolute namespace? 2269c075503SAnna Dabrowska if (PhpString::substr($filter, 0, 1) === ':') { 2279c075503SAnna Dabrowska $filter = '^' . $filter; 228c0f1a2d1SAnna Dabrowska } 229c0f1a2d1SAnna Dabrowska 230996125f5SAnna Dabrowska try { 231996125f5SAnna Dabrowska $check = preg_match('/' . $filter . '/', ':' . $id, $matches); 232996125f5SAnna Dabrowska } catch (\Exception $e) { 233996125f5SAnna Dabrowska throw new StructException("Error processing regular expression '$filter'"); 234996125f5SAnna Dabrowska } 235996125f5SAnna Dabrowska return (bool)$check; 2369c075503SAnna Dabrowska } 2379c075503SAnna Dabrowska 2389c075503SAnna Dabrowska /** 2399c075503SAnna Dabrowska * Merge the current config with the base config of the type. 2409c075503SAnna Dabrowska * 2419c075503SAnna Dabrowska * In contrast to parent, this method does not throw away unknown keys. 2429c075503SAnna Dabrowska * Required to migrate deprecated / obsolete options, no longer part of type config. 2439c075503SAnna Dabrowska * 2449c075503SAnna Dabrowska * @param array $current Current configuration 2459c075503SAnna Dabrowska * @param array $config Base Type configuration 2469c075503SAnna Dabrowska */ 2479c075503SAnna Dabrowska protected function mergeConfig($current, &$config) 2489c075503SAnna Dabrowska { 2499c075503SAnna Dabrowska foreach ($current as $key => $value) { 2509c075503SAnna Dabrowska if (isset($config[$key]) && is_array($config[$key])) { 2519c075503SAnna Dabrowska $this->mergeConfig($value, $config[$key]); 2529c075503SAnna Dabrowska } else { 2539c075503SAnna Dabrowska $config[$key] = $value; 2549c075503SAnna Dabrowska } 2559c075503SAnna Dabrowska } 25645c5f0a9SAnna Dabrowska 25745c5f0a9SAnna Dabrowska // migrate autocomplete options 'namespace' and 'postfix' to 'filter' 25845c5f0a9SAnna Dabrowska if (empty($config['autocomplete']['filter'])) { 25945c5f0a9SAnna Dabrowska if (!empty($config['autocomplete']['namespace'])) { 26045c5f0a9SAnna Dabrowska $config['autocomplete']['filter'] = $config['autocomplete']['namespace']; 26145c5f0a9SAnna Dabrowska unset($config['autocomplete']['namespace']); 26245c5f0a9SAnna Dabrowska } 26345c5f0a9SAnna Dabrowska if (!empty($config['autocomplete']['postfix'])) { 26445c5f0a9SAnna Dabrowska $config['autocomplete']['filter'] .= '.+?' . $config['autocomplete']['postfix'] . '$'; 26545c5f0a9SAnna Dabrowska unset($config['autocomplete']['postfix']); 26645c5f0a9SAnna Dabrowska } 26745c5f0a9SAnna Dabrowska } 268c0f1a2d1SAnna Dabrowska } 2690234787dSAndreas Gohr} 270