Mereged updates from DokuWiki 38
[sudaraka-org:dokuwiki-mods.git] / inc / pageutils.php
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  */
21 function getID($param='id',$clean=true){
22     global $INPUT;
23     global $conf;
24
25     $id = $INPUT->str($param);
26
27     //construct page id from request URI
28     if(empty($id) && $conf['userewrite'] == 2){
29         $request = $_SERVER['REQUEST_URI'];
30         $script = '';
31
32         //get the script URL
33         if($conf['basedir']){
34             $relpath = '';
35             if($param != 'id') {
36                 $relpath = 'lib/exe/';
37             }
38             $script = $conf['basedir'].$relpath.utf8_basename($_SERVER['SCRIPT_FILENAME']);
39
40         }elseif($_SERVER['PATH_INFO']){
41             $request = $_SERVER['PATH_INFO'];
42         }elseif($_SERVER['SCRIPT_NAME']){
43             $script = $_SERVER['SCRIPT_NAME'];
44         }elseif($_SERVER['DOCUMENT_ROOT'] && $_SERVER['SCRIPT_FILENAME']){
45             $script = preg_replace ('/^'.preg_quote($_SERVER['DOCUMENT_ROOT'],'/').'/','',
46                     $_SERVER['SCRIPT_FILENAME']);
47             $script = '/'.$script;
48         }
49
50         //clean script and request (fixes a windows problem)
51         $script  = preg_replace('/\/\/+/','/',$script);
52         $request = preg_replace('/\/\/+/','/',$request);
53
54         //remove script URL and Querystring to gain the id
55         if(preg_match('/^'.preg_quote($script,'/').'(.*)/',$request, $match)){
56             $id = preg_replace ('/\?.*/','',$match[1]);
57         }
58         $id = urldecode($id);
59         //strip leading slashes
60         $id = preg_replace('!^/+!','',$id);
61     }
62
63     // Namespace autolinking from URL
64     if(substr($id,-1) == ':' || ($conf['useslash'] && substr($id,-1) == '/')){
65         if(page_exists($id.$conf['start'])){
66             // start page inside namespace
67             $id = $id.$conf['start'];
68         }elseif(page_exists($id.noNS(cleanID($id)))){
69             // page named like the NS inside the NS
70             $id = $id.noNS(cleanID($id));
71         }elseif(page_exists($id)){
72             // page like namespace exists
73             $id = substr($id,0,-1);
74         }else{
75             // fall back to default
76             $id = $id.$conf['start'];
77         }
78         send_redirect(wl($id,'',true));
79     }
80
81     if($clean) $id = cleanID($id);
82     if(empty($id) && $param=='id') $id = $conf['start'];
83
84     return $id;
85 }
86
87 /**
88  * Remove unwanted chars from ID
89  *
90  * Cleans a given ID to only use allowed characters. Accented characters are
91  * converted to unaccented ones
92  *
93  * @author Andreas Gohr <andi@splitbrain.org>
94  * @param  string  $raw_id    The pageid to clean
95  * @param  boolean $ascii     Force ASCII
96  * @param  boolean $media     DEPRECATED
97  */
98 function cleanID($raw_id,$ascii=false,$media=false){
99     global $conf;
100     static $sepcharpat = null;
101
102     global $cache_cleanid;
103     $cache = & $cache_cleanid;
104
105     // check if it's already in the memory cache
106     if (isset($cache[(string)$raw_id])) {
107         return $cache[(string)$raw_id];
108     }
109
110     $sepchar = $conf['sepchar'];
111     if($sepcharpat == null) // build string only once to save clock cycles
112         $sepcharpat = '#\\'.$sepchar.'+#';
113
114     $id = trim((string)$raw_id);
115     $id = utf8_strtolower($id);
116
117     //alternative namespace seperator
118     $id = strtr($id,';',':');
119     if($conf['useslash']){
120         $id = strtr($id,'/',':');
121     }else{
122         $id = strtr($id,'/',$sepchar);
123     }
124
125     if($conf['deaccent'] == 2 || $ascii) $id = utf8_romanize($id);
126     if($conf['deaccent'] || $ascii) $id = utf8_deaccent($id,-1);
127
128     //remove specials
129     $id = utf8_stripspecials($id,$sepchar,'\*');
130
131     if($ascii) $id = utf8_strip($id);
132
133     //clean up
134     $id = preg_replace($sepcharpat,$sepchar,$id);
135     $id = preg_replace('#:+#',':',$id);
136     $id = trim($id,':._-');
137     $id = preg_replace('#:[:\._\-]+#',':',$id);
138     $id = preg_replace('#[:\._\-]+:#',':',$id);
139
140     $cache[(string)$raw_id] = $id;
141     return($id);
142 }
143
144 /**
145  * Return namespacepart of a wiki ID
146  *
147  * @author Andreas Gohr <andi@splitbrain.org>
148  */
149 function getNS($id){
150     $pos = strrpos((string)$id,':');
151     if($pos!==false){
152         return substr((string)$id,0,$pos);
153     }
154     return false;
155 }
156
157 /**
158  * Returns the ID without the namespace
159  *
160  * @author Andreas Gohr <andi@splitbrain.org>
161  */
162 function noNS($id) {
163     $pos = strrpos($id, ':');
164     if ($pos!==false) {
165         return substr($id, $pos+1);
166     } else {
167         return $id;
168     }
169 }
170
171 /**
172  * Returns the current namespace
173  *
174  * @author Nathan Fritz <fritzn@crown.edu>
175  */
176 function curNS($id) {
177     return noNS(getNS($id));
178 }
179
180 /**
181  * Returns the ID without the namespace or current namespace for 'start' pages
182  *
183  * @author Nathan Fritz <fritzn@crown.edu>
184  */
185 function noNSorNS($id) {
186     global $conf;
187
188     $p = noNS($id);
189     if ($p == $conf['start'] || $p == false) {
190         $p = curNS($id);
191         if ($p == false) {
192             return $conf['start'];
193         }
194     }
195     return $p;
196 }
197
198 /**
199  * Creates a XHTML valid linkid from a given headline title
200  *
201  * @param string  $title   The headline title
202  * @param array|bool   $check   Existing IDs (title => number)
203  * @return string the title
204  * @author Andreas Gohr <andi@splitbrain.org>
205  */
206 function sectionID($title,&$check) {
207     $title = str_replace(array(':','.'),'',cleanID($title));
208     $new = ltrim($title,'0123456789_-');
209     if(empty($new)){
210         $title = 'section'.preg_replace('/[^0-9]+/','',$title); //keep numbers from headline
211     }else{
212         $title = $new;
213     }
214
215     if(is_array($check)){
216         // make sure tiles are unique
217         if (!array_key_exists ($title,$check)) {
218             $check[$title] = 0;
219         } else {
220             $title .= ++ $check[$title];
221         }
222     }
223
224     return $title;
225 }
226
227
228 /**
229  * Wiki page existence check
230  *
231  * parameters as for wikiFN
232  *
233  * @author Chris Smith <chris@jalakai.co.uk>
234  */
235 function page_exists($id,$rev='',$clean=true) {
236     return @file_exists(wikiFN($id,$rev,$clean));
237 }
238
239 /**
240  * returns the full path to the datafile specified by ID and optional revision
241  *
242  * The filename is URL encoded to protect Unicode chars
243  *
244  * @param  $raw_id  string   id of wikipage
245  * @param  $rev     string   page revision, empty string for current
246  * @param  $clean   bool     flag indicating that $raw_id should be cleaned.  Only set to false
247  *                           when $id is guaranteed to have been cleaned already.
248  *
249  * @author Andreas Gohr <andi@splitbrain.org>
250  */
251 function wikiFN($raw_id,$rev='',$clean=true){
252     global $conf;
253
254     global $cache_wikifn;
255     $cache = & $cache_wikifn;
256
257     if (isset($cache[$raw_id]) && isset($cache[$raw_id][$rev])) {
258         return $cache[$raw_id][$rev];
259     }
260
261     $id = $raw_id;
262
263     if ($clean) $id = cleanID($id);
264     $id = str_replace(':','/',$id);
265     if(empty($rev)){
266         $fn = $conf['datadir'].'/'.utf8_encodeFN($id).'.txt';
267     }else{
268         $fn = $conf['olddir'].'/'.utf8_encodeFN($id).'.'.$rev.'.txt';
269         if($conf['compression']){
270             //test for extensions here, we want to read both compressions
271             if (@file_exists($fn . '.gz')){
272                 $fn .= '.gz';
273             }else if(@file_exists($fn . '.bz2')){
274                 $fn .= '.bz2';
275             }else{
276                 //file doesnt exist yet, so we take the configured extension
277                 $fn .= '.' . $conf['compression'];
278             }
279         }
280     }
281
282     if (!isset($cache[$raw_id])) { $cache[$raw_id] = array(); }
283     $cache[$raw_id][$rev] = $fn;
284     return $fn;
285 }
286
287 /**
288  * Returns the full path to the file for locking the page while editing.
289  *
290  * @author Ben Coburn <btcoburn@silicodon.net>
291  */
292 function wikiLockFN($id) {
293     global $conf;
294     return $conf['lockdir'].'/'.md5(cleanID($id)).'.lock';
295 }
296
297
298 /**
299  * returns the full path to the meta file specified by ID and extension
300  *
301  * @author Steven Danz <steven-danz@kc.rr.com>
302  */
303 function metaFN($id,$ext){
304     global $conf;
305     $id = cleanID($id);
306     $id = str_replace(':','/',$id);
307     $fn = $conf['metadir'].'/'.utf8_encodeFN($id).$ext;
308     return $fn;
309 }
310
311 /**
312  * returns the full path to the media's meta file specified by ID and extension
313  *
314  * @author Kate Arzamastseva <pshns@ukr.net>
315  */
316 function mediaMetaFN($id,$ext){
317     global $conf;
318     $id = cleanID($id);
319     $id = str_replace(':','/',$id);
320     $fn = $conf['mediametadir'].'/'.utf8_encodeFN($id).$ext;
321     return $fn;
322 }
323
324 /**
325  * returns an array of full paths to all metafiles of a given ID
326  *
327  * @author Esther Brunner <esther@kaffeehaus.ch>
328  * @author Michael Hamann <michael@content-space.de>
329  */
330 function metaFiles($id){
331     $basename = metaFN($id, '');
332     $files    = glob($basename.'.*', GLOB_MARK);
333     // filter files like foo.bar.meta when $id == 'foo'
334     return    $files ? preg_grep('/^'.preg_quote($basename, '/').'\.[^.\/]*$/u', $files) : array();
335 }
336
337 /**
338  * returns the full path to the mediafile specified by ID
339  *
340  * The filename is URL encoded to protect Unicode chars
341  *
342  * @author Andreas Gohr <andi@splitbrain.org>
343  * @author Kate Arzamastseva <pshns@ukr.net>
344  */
345 function mediaFN($id, $rev=''){
346     global $conf;
347     $id = cleanID($id);
348     $id = str_replace(':','/',$id);
349     if(empty($rev)){
350         $fn = $conf['mediadir'].'/'.utf8_encodeFN($id);
351     }else{
352         $ext = mimetype($id);
353         $name = substr($id,0, -1*strlen($ext[0])-1);
354         $fn = $conf['mediaolddir'].'/'.utf8_encodeFN($name .'.'.( (int) $rev ).'.'.$ext[0]);
355     }
356     return $fn;
357 }
358
359 /**
360  * Returns the full filepath to a localized file if local
361  * version isn't found the english one is returned
362  *
363  * @param  string $id  The id of the local file
364  * @param  string $ext The file extension (usually txt)
365  * @author Andreas Gohr <andi@splitbrain.org>
366  */
367 function localeFN($id,$ext='txt'){
368     global $conf;
369     $file = DOKU_CONF.'/lang/'.$conf['lang'].'/'.$id.'.'.$ext;
370     if(!@file_exists($file)){
371         $file = DOKU_INC.'inc/lang/'.$conf['lang'].'/'.$id.'.'.$ext;
372         if(!@file_exists($file)){
373             //fall back to english
374             $file = DOKU_INC.'inc/lang/en/'.$id.'.'.$ext;
375         }
376     }
377     return $file;
378 }
379
380 /**
381  * Resolve relative paths in IDs
382  *
383  * Do not call directly use resolve_mediaid or resolve_pageid
384  * instead
385  *
386  * Partyly based on a cleanPath function found at
387  * http://www.php.net/manual/en/function.realpath.php#57016
388  *
389  * @author <bart at mediawave dot nl>
390  */
391 function resolve_id($ns,$id,$clean=true){
392     global $conf;
393
394     // some pre cleaning for useslash:
395     if($conf['useslash']) $id = str_replace('/',':',$id);
396
397     // if the id starts with a dot we need to handle the
398     // relative stuff
399     if($id{0} == '.'){
400         // normalize initial dots without a colon
401         $id = preg_replace('/^(\.+)(?=[^:\.])/','\1:',$id);
402         // prepend the current namespace
403         $id = $ns.':'.$id;
404
405         // cleanup relatives
406         $result = array();
407         $pathA  = explode(':', $id);
408         if (!$pathA[0]) $result[] = '';
409         foreach ($pathA AS $key => $dir) {
410             if ($dir == '..') {
411                 if (end($result) == '..') {
412                     $result[] = '..';
413                 } elseif (!array_pop($result)) {
414                     $result[] = '..';
415                 }
416             } elseif ($dir && $dir != '.') {
417                 $result[] = $dir;
418             }
419         }
420         if (!end($pathA)) $result[] = '';
421         $id = implode(':', $result);
422     }elseif($ns !== false && strpos($id,':') === false){
423         //if link contains no namespace. add current namespace (if any)
424         $id = $ns.':'.$id;
425     }
426
427     if($clean) $id = cleanID($id);
428     return $id;
429 }
430
431 /**
432  * Returns a full media id
433  *
434  * @author Andreas Gohr <andi@splitbrain.org>
435  */
436 function resolve_mediaid($ns,&$page,&$exists){
437     $page   = resolve_id($ns,$page);
438     $file   = mediaFN($page);
439     $exists = @file_exists($file);
440 }
441
442 /**
443  * Returns a full page id
444  *
445  * @author Andreas Gohr <andi@splitbrain.org>
446  */
447 function resolve_pageid($ns,&$page,&$exists){
448     global $conf;
449     global $ID;
450     $exists = false;
451
452     //empty address should point to current page
453     if ($page === "") {
454         $page = $ID;
455     }
456
457     //keep hashlink if exists then clean both parts
458     if (strpos($page,'#')) {
459         list($page,$hash) = explode('#',$page,2);
460     } else {
461         $hash = '';
462     }
463     $hash = cleanID($hash);
464     $page = resolve_id($ns,$page,false); // resolve but don't clean, yet
465
466     // get filename (calls clean itself)
467     $file = wikiFN($page);
468
469     // if ends with colon or slash we have a namespace link
470     if(in_array(substr($page,-1), array(':', ';')) ||
471        ($conf['useslash'] && substr($page,-1) == '/')){
472         if(page_exists($page.$conf['start'])){
473             // start page inside namespace
474             $page = $page.$conf['start'];
475             $exists = true;
476         }elseif(page_exists($page.noNS(cleanID($page)))){
477             // page named like the NS inside the NS
478             $page = $page.noNS(cleanID($page));
479             $exists = true;
480         }elseif(page_exists($page)){
481             // page like namespace exists
482             $page = $page;
483             $exists = true;
484         }else{
485             // fall back to default
486             $page = $page.$conf['start'];
487         }
488     }else{
489         //check alternative plural/nonplural form
490         if(!@file_exists($file)){
491             if( $conf['autoplural'] ){
492                 if(substr($page,-1) == 's'){
493                     $try = substr($page,0,-1);
494                 }else{
495                     $try = $page.'s';
496                 }
497                 if(page_exists($try)){
498                     $page   = $try;
499                     $exists = true;
500                 }
501             }
502         }else{
503             $exists = true;
504         }
505     }
506
507     // now make sure we have a clean page
508     $page = cleanID($page);
509
510     //add hash if any
511     if(!empty($hash)) $page .= '#'.$hash;
512 }
513
514 /**
515  * Returns the name of a cachefile from given data
516  *
517  * The needed directory is created by this function!
518  *
519  * @author Andreas Gohr <andi@splitbrain.org>
520  *
521  * @param string $data  This data is used to create a unique md5 name
522  * @param string $ext   This is appended to the filename if given
523  * @return string       The filename of the cachefile
524  */
525 function getCacheName($data,$ext=''){
526     global $conf;
527     $md5  = md5($data);
528     $file = $conf['cachedir'].'/'.$md5{0}.'/'.$md5.$ext;
529     io_makeFileDir($file);
530     return $file;
531 }
532
533 /**
534  * Checks a pageid against $conf['hidepages']
535  *
536  * @author Andreas Gohr <gohr@cosmocode.de>
537  */
538 function isHiddenPage($id){
539     global $conf;
540     global $ACT;
541     if(empty($conf['hidepages'])) return false;
542     if($ACT == 'admin') return false;
543
544     if(preg_match('/'.$conf['hidepages'].'/ui',':'.$id)){
545         return true;
546     }
547     return false;
548 }
549
550 /**
551  * Reverse of isHiddenPage
552  *
553  * @author Andreas Gohr <gohr@cosmocode.de>
554  */
555 function isVisiblePage($id){
556     return !isHiddenPage($id);
557 }
558
559 /**
560  * Format an id for output to a user
561  *
562  * Namespaces are denoted by a trailing “:*”. The root namespace is
563  * “*”. Output is escaped.
564  *
565  * @author Adrian Lang <lang@cosmocode.de>
566  */
567
568 function prettyprint_id($id) {
569     if (!$id || $id === ':') {
570         return '*';
571     }
572     if ((substr($id, -1, 1) === ':')) {
573         $id .= '*';
574     }
575     return hsc($id);
576 }
577
578 /**
579  * Encode a UTF-8 filename to use on any filesystem
580  *
581  * Uses the 'fnencode' option to determine encoding
582  *
583  * When the second parameter is true the string will
584  * be encoded only if non ASCII characters are detected -
585  * This makes it safe to run it multiple times on the
586  * same string (default is true)
587  *
588  * @author Andreas Gohr <andi@splitbrain.org>
589  * @see    urlencode
590  */
591 function utf8_encodeFN($file,$safe=true){
592     global $conf;
593     if($conf['fnencode'] == 'utf-8') return $file;
594
595     if($safe && preg_match('#^[a-zA-Z0-9/_\-\.%]+$#',$file)){
596         return $file;
597     }
598
599     if($conf['fnencode'] == 'safe'){
600         return SafeFN::encode($file);
601     }
602
603     $file = urlencode($file);
604     $file = str_replace('%2F','/',$file);
605     return $file;
606 }
607
608 /**
609  * Decode a filename back to UTF-8
610  *
611  * Uses the 'fnencode' option to determine encoding
612  *
613  * @author Andreas Gohr <andi@splitbrain.org>
614  * @see    urldecode
615  */
616 function utf8_decodeFN($file){
617     global $conf;
618     if($conf['fnencode'] == 'utf-8') return $file;
619
620     if($conf['fnencode'] == 'safe'){
621         return SafeFN::decode($file);
622     }
623
624     return urldecode($file);
625 }
626
627 /**
628  * Find a page in the current namespace (determined from $ID) or any
629  * higher namespace
630  *
631  * Used for sidebars, but can be used other stuff as well
632  *
633  * @todo   add event hook
634  * @param  string $page the pagename you're looking for
635  * @return string|false the full page id of the found page, false if any
636  */
637 function page_findnearest($page){
638     global $ID;
639
640     $ns = $ID;
641     do {
642         $ns = getNS($ns);
643         $pageid = ltrim("$ns:$page",':');
644         if(page_exists($pageid)){
645             return $pageid;
646         }
647     } while($ns);
648
649     return false;
650 }