1<?php 2/** 3 * Utilities for handling pagenames 4 * 5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 6 * @author Andreas Gohr <andi@splitbrain.org> 7 * @todo Combine similar functions like {wiki,media,meta}FN() 8 */ 9 10/** 11 * Fetch the an ID from request 12 * 13 * Uses either standard $_REQUEST variable or extracts it from 14 * the full request URI when userewrite is set to 2 15 * 16 * For $param='id' $conf['start'] is returned if no id was found. 17 * If the second parameter is true (default) the ID is cleaned. 18 * 19 * @author Andreas Gohr <andi@splitbrain.org> 20 */ 21function getID($param='id',$clean=true){ 22 global $conf; 23 24 $id = isset($_REQUEST[$param]) ? $_REQUEST[$param] : null; 25 26 //construct page id from request URI 27 if(empty($id) && $conf['userewrite'] == 2){ 28 //get the script URL 29 if($conf['basedir']){ 30 $relpath = ''; 31 if($param != 'id') { 32 $relpath = 'lib/exe/'; 33 } 34 $script = $conf['basedir'].$relpath.basename($_SERVER['SCRIPT_FILENAME']); 35 }elseif($_SERVER['DOCUMENT_ROOT'] && $_SERVER['SCRIPT_FILENAME']){ 36 $script = preg_replace ('/^'.preg_quote($_SERVER['DOCUMENT_ROOT'],'/').'/','', 37 $_SERVER['SCRIPT_FILENAME']); 38 $script = '/'.$script; 39 }else{ 40 $script = $_SERVER['SCRIPT_NAME']; 41 } 42 43 //clean script and request (fixes a windows problem) 44 $script = preg_replace('/\/\/+/','/',$script); 45 $request = preg_replace('/\/\/+/','/',$_SERVER['REQUEST_URI']); 46 47 //remove script URL and Querystring to gain the id 48 if(preg_match('/^'.preg_quote($script,'/').'(.*)/',$request, $match)){ 49 $id = preg_replace ('/\?.*/','',$match[1]); 50 } 51 $id = urldecode($id); 52 //strip leading slashes 53 $id = preg_replace('!^/+!','',$id); 54 } 55 56 // Namespace autolinking from URL 57 if(substr($id,-1) == ':' || ($conf['useslash'] && substr($id,-1) == '/')){ 58 if(@file_exists(wikiFN($id.$conf['start']))){ 59 // start page inside namespace 60 $id = $id.$conf['start']; 61 }elseif(@file_exists(wikiFN($id.noNS(cleanID($id))))){ 62 // page named like the NS inside the NS 63 $id = $id.noNS(cleanID($id)); 64 }elseif(@file_exists(wikiFN($id))){ 65 // page like namespace exists 66 $id = $id; 67 }else{ 68 // fall back to default 69 $id = $id.$conf['start']; 70 } 71 header("Location: ".wl($id,'',true)); 72 } 73 74 if($clean) $id = cleanID($id); 75 if(empty($id) && $param=='id') $id = $conf['start']; 76 77 return $id; 78} 79 80/** 81 * Remove unwanted chars from ID 82 * 83 * Cleans a given ID to only use allowed characters. Accented characters are 84 * converted to unaccented ones 85 * 86 * @author Andreas Gohr <andi@splitbrain.org> 87 * @param string $raw_id The pageid to clean 88 * @param boolean $ascii Force ASCII 89 */ 90function cleanID($raw_id,$ascii=false){ 91 global $conf; 92 global $lang; 93 static $sepcharpat = null; 94 95 global $cache_cleanid; 96 $cache = & $cache_cleanid; 97 98 // check if it's already in the memory cache 99 if (isset($cache[$raw_id])) { 100 return $cache[$raw_id]; 101 } 102 103 $sepchar = $conf['sepchar']; 104 if($sepcharpat == null) // build string only once to save clock cycles 105 $sepcharpat = '#\\'.$sepchar.'+#'; 106 107 $id = trim($raw_id); 108 $id = utf8_strtolower($id); 109 110 //alternative namespace seperator 111 $id = strtr($id,';',':'); 112 if($conf['useslash']){ 113 $id = strtr($id,'/',':'); 114 }else{ 115 $id = strtr($id,'/',$sepchar); 116 } 117 118 if($conf['deaccent'] == 2 || $ascii) $id = utf8_romanize($id); 119 if($conf['deaccent'] || $ascii) $id = utf8_deaccent($id,-1); 120 121 //remove specials 122 $id = utf8_stripspecials($id,$sepchar,'\*'); 123 124 if($ascii) $id = utf8_strip($id); 125 126 //clean up 127 $id = preg_replace($sepcharpat,$sepchar,$id); 128 $id = preg_replace('#:+#',':',$id); 129 $id = trim($id,':._-'); 130 $id = preg_replace('#:[:\._\-]+#',':',$id); 131 132 $cache[$raw_id] = $id; 133 return($id); 134} 135 136/** 137 * Return namespacepart of a wiki ID 138 * 139 * @author Andreas Gohr <andi@splitbrain.org> 140 */ 141function getNS($id){ 142 $pos = strrpos($id,':'); 143 if($pos!==false){ 144 return substr($id,0,$pos); 145 } 146 return false; 147} 148 149/** 150 * Returns the ID without the namespace 151 * 152 * @author Andreas Gohr <andi@splitbrain.org> 153 */ 154function noNS($id) { 155 $pos = strrpos($id, ':'); 156 if ($pos!==false) { 157 return substr($id, $pos+1); 158 } else { 159 return $id; 160 } 161} 162 163/** 164 * returns the full path to the datafile specified by ID and 165 * optional revision 166 * 167 * The filename is URL encoded to protect Unicode chars 168 * 169 * @author Andreas Gohr <andi@splitbrain.org> 170 */ 171function wikiFN($raw_id,$rev='',$clean=true){ 172 global $conf; 173 174 global $cache_wikifn; 175 $cache = & $cache_wikifn; 176 177 if (isset($cache[$raw_id]) && isset($cache[$raw_id][$rev])) { 178 return $cache[$raw_id][$rev]; 179 } 180 181 $id = $raw_id; 182 183 if ($clean) $id = cleanID($id); 184 $id = str_replace(':','/',$id); 185 if(empty($rev)){ 186 $fn = $conf['datadir'].'/'.utf8_encodeFN($id).'.txt'; 187 }else{ 188 $fn = $conf['olddir'].'/'.utf8_encodeFN($id).'.'.$rev.'.txt'; 189 if($conf['compression']){ 190 //test for extensions here, we want to read both compressions 191 if (@file_exists($fn . '.gz')){ 192 $fn .= '.gz'; 193 }else if(@file_exists($fn . '.bz2')){ 194 $fn .= '.bz2'; 195 }else{ 196 //file doesnt exist yet, so we take the configured extension 197 $fn .= '.' . $conf['compression']; 198 } 199 } 200 } 201 202 if (!isset($cache[$raw_id])) { $cache[$raw_id] = array(); } 203 $cache[$raw_id][$rev] = $fn; 204 return $fn; 205} 206 207/** 208 * Returns the full path to the file for locking the page while editing. 209 * 210 * @author Ben Coburn <btcoburn@silicodon.net> 211 */ 212function wikiLockFN($id) { 213 global $conf; 214 return $conf['lockdir'].'/'.md5(cleanID($id)).'.lock'; 215} 216 217 218/** 219 * returns the full path to the meta file specified by ID and extension 220 * 221 * The filename is URL encoded to protect Unicode chars 222 * 223 * @author Steven Danz <steven-danz@kc.rr.com> 224 */ 225function metaFN($id,$ext){ 226 global $conf; 227 $id = cleanID($id); 228 $id = str_replace(':','/',$id); 229 $fn = $conf['metadir'].'/'.utf8_encodeFN($id).$ext; 230 return $fn; 231} 232 233/** 234 * returns an array of full paths to all metafiles of a given ID 235 * 236 * @author Esther Brunner <esther@kaffeehaus.ch> 237 */ 238function metaFiles($id){ 239 $name = noNS($id); 240 $dir = metaFN(getNS($id),''); 241 $files = array(); 242 243 $dh = @opendir($dir); 244 if(!$dh) return $files; 245 while(($file = readdir($dh)) !== false){ 246 if(strpos($file,$name.'.') === 0 && !is_dir($dir.$file)) 247 $files[] = $dir.$file; 248 } 249 closedir($dh); 250 251 return $files; 252} 253 254/** 255 * returns the full path to the mediafile specified by ID 256 * 257 * The filename is URL encoded to protect Unicode chars 258 * 259 * @author Andreas Gohr <andi@splitbrain.org> 260 */ 261function mediaFN($id){ 262 global $conf; 263 $id = cleanID($id); 264 $id = str_replace(':','/',$id); 265 $fn = $conf['mediadir'].'/'.utf8_encodeFN($id); 266 return $fn; 267} 268 269/** 270 * Returns the full filepath to a localized textfile if local 271 * version isn't found the english one is returned 272 * 273 * @author Andreas Gohr <andi@splitbrain.org> 274 */ 275function localeFN($id){ 276 global $conf; 277 $file = DOKU_INC.'inc/lang/'.$conf['lang'].'/'.$id.'.txt'; 278 if(!@file_exists($file)){ 279 //fall back to english 280 $file = DOKU_INC.'inc/lang/en/'.$id.'.txt'; 281 } 282 return $file; 283} 284 285/** 286 * Resolve relative paths in IDs 287 * 288 * Do not call directly use resolve_mediaid or resolve_pageid 289 * instead 290 * 291 * Partyly based on a cleanPath function found at 292 * http://www.php.net/manual/en/function.realpath.php#57016 293 * 294 * @author <bart at mediawave dot nl> 295 */ 296function resolve_id($ns,$id,$clean=true){ 297 // if the id starts with a dot we need to handle the 298 // relative stuff 299 if($id{0} == '.'){ 300 // normalize initial dots without a colon 301 $id = preg_replace('/^(\.+)(?=[^:\.])/','\1:',$id); 302 // prepend the current namespace 303 $id = $ns.':'.$id; 304 305 // cleanup relatives 306 $result = array(); 307 $pathA = explode(':', $id); 308 if (!$pathA[0]) $result[] = ''; 309 foreach ($pathA AS $key => $dir) { 310 if ($dir == '..') { 311 if (end($result) == '..') { 312 $result[] = '..'; 313 } elseif (!array_pop($result)) { 314 $result[] = '..'; 315 } 316 } elseif ($dir && $dir != '.') { 317 $result[] = $dir; 318 } 319 } 320 if (!end($pathA)) $result[] = ''; 321 $id = implode(':', $result); 322 }elseif($ns !== false && strpos($id,':') === false){ 323 //if link contains no namespace. add current namespace (if any) 324 $id = $ns.':'.$id; 325 } 326 327 if($clean) $id = cleanID($id); 328 return $id; 329} 330 331/** 332 * Returns a full media id 333 * 334 * @author Andreas Gohr <andi@splitbrain.org> 335 */ 336function resolve_mediaid($ns,&$page,&$exists){ 337 $page = resolve_id($ns,$page); 338 $file = mediaFN($page); 339 $exists = @file_exists($file); 340} 341 342/** 343 * Returns a full page id 344 * 345 * @author Andreas Gohr <andi@splitbrain.org> 346 */ 347function resolve_pageid($ns,&$page,&$exists){ 348 global $conf; 349 $exists = false; 350 351 //keep hashlink if exists then clean both parts 352 if (strpos($page,'#')) { 353 list($page,$hash) = split('#',$page,2); 354 } else { 355 $hash = ''; 356 } 357 $hash = cleanID($hash); 358 $page = resolve_id($ns,$page,false); // resolve but don't clean, yet 359 360 // get filename (calls clean itself) 361 $file = wikiFN($page); 362 363 // if ends with colon or slash we have a namespace link 364 if(substr($page,-1) == ':' || ($conf['useslash'] && substr($page,-1) == '/')){ 365 if(@file_exists(wikiFN($page.$conf['start']))){ 366 // start page inside namespace 367 $page = $page.$conf['start']; 368 $exists = true; 369 }elseif(@file_exists(wikiFN($page.noNS(cleanID($page))))){ 370 // page named like the NS inside the NS 371 $page = $page.noNS(cleanID($page)); 372 $exists = true; 373 }elseif(@file_exists(wikiFN($page))){ 374 // page like namespace exists 375 $page = $page; 376 $exists = true; 377 }else{ 378 // fall back to default 379 $page = $page.$conf['start']; 380 } 381 }else{ 382 //check alternative plural/nonplural form 383 if(!@file_exists($file)){ 384 if( $conf['autoplural'] ){ 385 if(substr($page,-1) == 's'){ 386 $try = substr($page,0,-1); 387 }else{ 388 $try = $page.'s'; 389 } 390 if(@file_exists(wikiFN($try))){ 391 $page = $try; 392 $exists = true; 393 } 394 } 395 }else{ 396 $exists = true; 397 } 398 } 399 400 // now make sure we have a clean page 401 $page = cleanID($page); 402 403 //add hash if any 404 if(!empty($hash)) $page .= '#'.$hash; 405} 406 407/** 408 * Returns the name of a cachefile from given data 409 * 410 * The needed directory is created by this function! 411 * 412 * @author Andreas Gohr <andi@splitbrain.org> 413 * 414 * @param string $data This data is used to create a unique md5 name 415 * @param string $ext This is appended to the filename if given 416 * @return string The filename of the cachefile 417 */ 418function getCacheName($data,$ext=''){ 419 global $conf; 420 $md5 = md5($data); 421 $file = $conf['cachedir'].'/'.$md5{0}.'/'.$md5.$ext; 422 io_makeFileDir($file); 423 return $file; 424} 425 426/** 427 * Checks a pageid against $conf['hidepages'] 428 * 429 * @author Andreas Gohr <gohr@cosmocode.de> 430 */ 431function isHiddenPage($id){ 432 global $conf; 433 if(empty($conf['hidepages'])) return false; 434 435 if(preg_match('/'.$conf['hidepages'].'/ui',':'.$id)){ 436 return true; 437 } 438 return false; 439} 440 441/** 442 * Reverse of isHiddenPage 443 * 444 * @author Andreas Gohr <gohr@cosmocode.de> 445 */ 446function isVisiblePage($id){ 447 return !isHiddenPage($id); 448} 449 450/** 451 * Checks and sets HTTP headers for conditional HTTP requests 452 * 453 * @author Simon Willison <swillison@gmail.com> 454 * @link http://simon.incutio.com/archive/2003/04/23/conditionalGet 455 * @param timestamp $timestamp lastmodified time of the cache file 456 * @returns void or void with previously header() commands executed 457 */ 458function http_conditionalRequest($timestamp){ 459 // A PHP implementation of conditional get, see 460 // http://fishbowl.pastiche.org/archives/001132.html 461 $last_modified = substr(gmdate('r', $timestamp), 0, -5).'GMT'; 462 $etag = '"'.md5($last_modified).'"'; 463 // Send the headers 464 header("Last-Modified: $last_modified"); 465 header("ETag: $etag"); 466 // See if the client has provided the required headers 467 if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])){ 468 $if_modified_since = stripslashes($_SERVER['HTTP_IF_MODIFIED_SINCE']); 469 }else{ 470 $if_modified_since = false; 471 } 472 473 if (isset($_SERVER['HTTP_IF_NONE_MATCH'])){ 474 $if_none_match = stripslashes($_SERVER['HTTP_IF_NONE_MATCH']); 475 }else{ 476 $if_none_match = false; 477 } 478 479 if (!$if_modified_since && !$if_none_match){ 480 return; 481 } 482 483 // At least one of the headers is there - check them 484 if ($if_none_match && $if_none_match != $etag) { 485 return; // etag is there but doesn't match 486 } 487 488 if ($if_modified_since && $if_modified_since != $last_modified) { 489 return; // if-modified-since is there but doesn't match 490 } 491 492 // Nothing has changed since their last request - serve a 304 and exit 493 header('HTTP/1.0 304 Not Modified'); 494 exit; 495} 496 497//Setup VIM: ex: et ts=2 enc=utf-8 : 498