1<?php 2 3namespace dokuwiki\plugin\versionswitch; 4 5use dokuwiki\File\PageResolver; 6 7/** 8 * Manage the current page's version 9 */ 10class Version 11{ 12 protected $regex = ''; 13 protected $version = ''; 14 protected $namespace = ''; 15 protected $idpart = ''; 16 17 public const DEFAULT_REGEX = '[^:]+'; 18 19 /** 20 * @param string $conf The configuration string containing the namespaces and regexes 21 * @param string $id The current page id 22 */ 23 public function __construct($conf, $id) 24 { 25 $this->match($this->conf2List($conf), $id); 26 } 27 28 /** 29 * The regex that applies to the current namespace 30 * 31 * This will be empty if the current page is not in a versioned namespace 32 * 33 * @return string 34 */ 35 public function getRegex(): string 36 { 37 return $this->regex; 38 } 39 40 /** 41 * The base namespace that applies to the current page 42 * 43 * This will be empty if the current page is not in a versioned namespace 44 * 45 * @return string 46 */ 47 public function getBaseNamespace(): string 48 { 49 return $this->namespace; 50 } 51 52 /** 53 * The version that applies to the current page 54 * 55 * This will be empty if the current page is not in a versioned namespace 56 * 57 * @return string 58 */ 59 public function getVersion(): string 60 { 61 return $this->version; 62 } 63 64 /** 65 * The part of the id that comes after the version namespace 66 * 67 * @return string 68 */ 69 public function getIdPart(): string 70 { 71 return $this->idpart; 72 } 73 74 75 /** 76 * Convert the list of namespaces and regexes into an associative array 77 * 78 * @param string $conf 79 * @return array 80 */ 81 protected function conf2List($conf) 82 { 83 $result = []; 84 $list = explode("\n", $conf); 85 foreach ($list as $line) { 86 $line = trim($line); 87 if ($line == '') { 88 continue; 89 } 90 if ($line[0] == '#') { 91 continue; 92 } 93 [$ns, $re] = sexplode(' ', $line, 2, ''); 94 $ns = ':' . cleanID($ns); 95 $re = trim($re); 96 if ($re === '') $re = self::DEFAULT_REGEX; // default is direct namespaces 97 98 $result[$ns] = $re; 99 } 100 101 return $result; 102 } 103 104 /** 105 * Try the given regexes against the namespace of the given id 106 * 107 * @param array $regexes 108 * @param string $id 109 * @return bool true if a match was found 110 */ 111 protected function match($regexes, $id) 112 { 113 $namespace = ':' . getNS($id); 114 115 foreach ($regexes as $base => $re) { 116 $regex = "/$re/i"; 117 118 // match against base namespace first 119 if (str_starts_with($namespace, $base)) { 120 $namespace = substr($namespace, strlen($base)); 121 122 // match remainder against regex 123 if (preg_match($regex, $namespace, $matches)) { 124 $this->namespace = $base; 125 $this->regex = $re; 126 $this->version = $matches[0]; 127 $this->idpart = substr($id, strlen($base) + strlen($matches[0]) + 1); 128 129 return true; 130 } 131 } 132 } 133 134 return false; 135 } 136 137 /** 138 * Get all versions and their titles 139 * 140 * @return array 141 */ 142 public function getVersions() 143 { 144 $versions = $this->readVersionDirs(); 145 146 // get titles for the versions 147 $resolver = new PageResolver('start'); // context doesn't matter, we always use absolute IDs 148 foreach (array_keys($versions) as $ns) { 149 $startPage = $resolver->resolveId($this->namespace . ':' . $ns . ':'); 150 $title = p_get_first_heading($startPage,); 151 if ($title) $versions[$ns] = $title; 152 } 153 uksort($versions, [$this, 'sortByDepthAndVersion']); 154 155 return $versions; 156 } 157 158 /** 159 * Sort by depth of the namespace and by version 160 * 161 * @param string $a 162 * @param string $b 163 * @return int 164 */ 165 public function sortByDepthAndVersion($a, $b) 166 { 167 $countA = substr_count($a, ':'); 168 $countB = substr_count($b, ':'); 169 if ($countA !== $countB) return $countA - $countB; 170 return version_compare($b, $a); // reverse order 171 } 172 173 /** 174 * Traverse the version directories and find all versions 175 * 176 * @param string $dir The base directory to start in, defaults to the set namespace 177 * @param string $sub The currently traversed sub directory 178 * @return array 179 */ 180 protected function readVersionDirs($dir = '', $sub = '') 181 { 182 if ($dir === '') $dir = dirname(wikiFN($this->namespace . ':somthing', '', false)); 183 $subns = utf8_decodeFN($sub); 184 $regex = '/^' . $this->regex . '$/i'; // anchored regex 185 $versions = []; 186 187 $fh = @opendir($dir . '/' . $sub); 188 if (!$fh) return []; 189 while (($item = readdir($fh)) !== false) { 190 if ($item[0] == '.') continue; 191 if (!is_dir($dir . '/' . $sub . '/' . $item)) continue; 192 193 $itemid = utf8_decodeFN($item); 194 195 // check if this is a version namespace 196 if (preg_match($regex, ltrim("$subns:$itemid", ':'), $match)) { 197 $versions[ltrim("$subns:$itemid", ':')] = $match[0]; 198 } 199 200 // traverse into sub namespace unless default regex is used 201 if ($this->regex !== self::DEFAULT_REGEX) { 202 $versions = array_merge($versions, $this->readVersionDirs($dir, ltrim("$sub/$item", '/'))); 203 } 204 } 205 closedir($fh); 206 return $versions; 207 } 208} 209