xref: /dokuwiki/inc/common.php (revision 2eccbdaacee808c072a68b7bfd961978025b2bcb)
1<?php
2/**
3 * Common DokuWiki functions
4 *
5 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
6 * @author     Andreas Gohr <andi@splitbrain.org>
7 */
8
9if(!defined('DOKU_INC')) define('DOKU_INC',fullpath(dirname(__FILE__).'/../').'/');
10require_once(DOKU_CONF.'dokuwiki.php');
11require_once(DOKU_INC.'inc/io.php');
12require_once(DOKU_INC.'inc/changelog.php');
13require_once(DOKU_INC.'inc/utf8.php');
14require_once(DOKU_INC.'inc/mail.php');
15require_once(DOKU_INC.'inc/parserutils.php');
16require_once(DOKU_INC.'inc/infoutils.php');
17
18/**
19 * These constants are used with the recents function
20 */
21define('RECENTS_SKIP_DELETED',2);
22define('RECENTS_SKIP_MINORS',4);
23define('RECENTS_SKIP_SUBSPACES',8);
24
25/**
26 * Wrapper around htmlspecialchars()
27 *
28 * @author Andreas Gohr <andi@splitbrain.org>
29 * @see    htmlspecialchars()
30 */
31function hsc($string){
32  return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
33}
34
35/**
36 * print a newline terminated string
37 *
38 * You can give an indention as optional parameter
39 *
40 * @author Andreas Gohr <andi@splitbrain.org>
41 */
42function ptln($string,$indent=0){
43  echo str_repeat(' ', $indent)."$string\n";
44}
45
46/**
47 * strips control characters (<32) from the given string
48 *
49 * @author Andreas Gohr <andi@splitbrain.org>
50 */
51function stripctl($string){
52  return preg_replace('/[\x00-\x1F]+/s','',$string);
53}
54
55/**
56 * Return a secret token to be used for CSRF attack prevention
57 *
58 * @author  Andreas Gohr <andi@splitbrain.org>
59 * @link    http://en.wikipedia.org/wiki/Cross-site_request_forgery
60 * @link    http://christ1an.blogspot.com/2007/04/preventing-csrf-efficiently.html
61 * @return  string
62 */
63function getSecurityToken(){
64  return md5(auth_cookiesalt().session_id());
65}
66
67/**
68 * Check the secret CSRF token
69 */
70function checkSecurityToken($token=null){
71  if(is_null($token)) $token = $_REQUEST['sectok'];
72  if(getSecurityToken() != $token){
73    msg('Security Token did not match. Possible CSRF attack.',-1);
74    return false;
75  }
76  return true;
77}
78
79/**
80 * Print a hidden form field with a secret CSRF token
81 *
82 * @author  Andreas Gohr <andi@splitbrain.org>
83 */
84function formSecurityToken($print=true){
85  $ret = '<div class="no"><input type="hidden" name="sectok" value="'.getSecurityToken().'" /></div>'."\n";
86  if($print){
87    echo $ret;
88  }else{
89    return $ret;
90  }
91}
92
93/**
94 * Return info about the current document as associative
95 * array.
96 *
97 * @author Andreas Gohr <andi@splitbrain.org>
98 */
99function pageinfo(){
100  global $ID;
101  global $REV;
102  global $USERINFO;
103  global $conf;
104
105  // include ID & REV not redundant, as some parts of DokuWiki may temporarily change $ID, e.g. p_wiki_xhtml
106  // FIXME ... perhaps it would be better to ensure the temporary changes weren't necessary
107  $info['id'] = $ID;
108  $info['rev'] = $REV;
109
110  if($_SERVER['REMOTE_USER']){
111    $info['userinfo']     = $USERINFO;
112    $info['perm']         = auth_quickaclcheck($ID);
113    $info['subscribed']   = is_subscribed($ID,$_SERVER['REMOTE_USER'],false);
114    $info['subscribedns'] = is_subscribed($ID,$_SERVER['REMOTE_USER'],true);
115    $info['client']       = $_SERVER['REMOTE_USER'];
116
117    // set info about manager/admin status
118    $info['isadmin']   = false;
119    $info['ismanager'] = false;
120    if($info['perm'] == AUTH_ADMIN){
121      $info['isadmin']   = true;
122      $info['ismanager'] = true;
123    }elseif(auth_ismanager()){
124      $info['ismanager'] = true;
125    }
126
127    // if some outside auth were used only REMOTE_USER is set
128    if(!$info['userinfo']['name']){
129      $info['userinfo']['name'] = $_SERVER['REMOTE_USER'];
130    }
131
132  }else{
133    $info['perm']       = auth_aclcheck($ID,'',null);
134    $info['subscribed'] = false;
135    $info['client']     = clientIP(true);
136  }
137
138  $info['namespace'] = getNS($ID);
139  $info['locked']    = checklock($ID);
140  $info['filepath']  = fullpath(wikiFN($ID));
141  $info['exists']    = @file_exists($info['filepath']);
142  if($REV){
143    //check if current revision was meant
144    if($info['exists'] && (@filemtime($info['filepath'])==$REV)){
145      $REV = '';
146    }else{
147      //really use old revision
148      $info['filepath'] = fullpath(wikiFN($ID,$REV));
149      $info['exists']   = @file_exists($info['filepath']);
150    }
151  }
152  $info['rev'] = $REV;
153  if($info['exists']){
154    $info['writable'] = (is_writable($info['filepath']) &&
155                         ($info['perm'] >= AUTH_EDIT));
156  }else{
157    $info['writable'] = ($info['perm'] >= AUTH_CREATE);
158  }
159  $info['editable']  = ($info['writable'] && empty($info['lock']));
160  $info['lastmod']   = @filemtime($info['filepath']);
161
162  //load page meta data
163  $info['meta'] = p_get_metadata($ID);
164
165  //who's the editor
166  if($REV){
167    $revinfo = getRevisionInfo($ID, $REV, 1024);
168  }else{
169    if (is_array($info['meta']['last_change'])) {
170       $revinfo = $info['meta']['last_change'];
171    } else {
172      $revinfo = getRevisionInfo($ID, $info['lastmod'], 1024);
173      // cache most recent changelog line in metadata if missing and still valid
174      if ($revinfo!==false) {
175        $info['meta']['last_change'] = $revinfo;
176        p_set_metadata($ID, array('last_change' => $revinfo));
177      }
178    }
179  }
180  //and check for an external edit
181  if($revinfo!==false && $revinfo['date']!=$info['lastmod']){
182    // cached changelog line no longer valid
183    $revinfo = false;
184    $info['meta']['last_change'] = $revinfo;
185    p_set_metadata($ID, array('last_change' => $revinfo));
186  }
187
188  $info['ip']     = $revinfo['ip'];
189  $info['user']   = $revinfo['user'];
190  $info['sum']    = $revinfo['sum'];
191  // See also $INFO['meta']['last_change'] which is the most recent log line for page $ID.
192  // Use $INFO['meta']['last_change']['type']===DOKU_CHANGE_TYPE_MINOR_EDIT in place of $info['minor'].
193
194  if($revinfo['user']){
195    $info['editor'] = $revinfo['user'];
196  }else{
197    $info['editor'] = $revinfo['ip'];
198  }
199
200  // draft
201  $draft = getCacheName($info['client'].$ID,'.draft');
202  if(@file_exists($draft)){
203    if(@filemtime($draft) < @filemtime(wikiFN($ID))){
204      // remove stale draft
205      @unlink($draft);
206    }else{
207      $info['draft'] = $draft;
208    }
209  }
210
211  // mobile detection
212  $info['ismobile'] = clientismobile();
213
214  return $info;
215}
216
217/**
218 * Build an string of URL parameters
219 *
220 * @author Andreas Gohr
221 */
222function buildURLparams($params, $sep='&amp;'){
223  $url = '';
224  $amp = false;
225  foreach($params as $key => $val){
226    if($amp) $url .= $sep;
227
228    $url .= $key.'=';
229    $url .= rawurlencode((string)$val);
230    $amp = true;
231  }
232  return $url;
233}
234
235/**
236 * Build an string of html tag attributes
237 *
238 * Skips keys starting with '_', values get HTML encoded
239 *
240 * @author Andreas Gohr
241 */
242function buildAttributes($params,$skipempty=false){
243  $url = '';
244  foreach($params as $key => $val){
245    if($key{0} == '_') continue;
246    if($val === '' && $skipempty) continue;
247
248    $url .= $key.'="';
249    $url .= htmlspecialchars ($val);
250    $url .= '" ';
251  }
252  return $url;
253}
254
255
256/**
257 * This builds the breadcrumb trail and returns it as array
258 *
259 * @author Andreas Gohr <andi@splitbrain.org>
260 */
261function breadcrumbs(){
262  // we prepare the breadcrumbs early for quick session closing
263  static $crumbs = null;
264  if($crumbs != null) return $crumbs;
265
266  global $ID;
267  global $ACT;
268  global $conf;
269  $crumbs = $_SESSION[DOKU_COOKIE]['bc'];
270
271  //first visit?
272  if (!is_array($crumbs)){
273    $crumbs = array();
274  }
275  //we only save on show and existing wiki documents
276  $file = wikiFN($ID);
277  if($ACT != 'show' || !@file_exists($file)){
278    $_SESSION[DOKU_COOKIE]['bc'] = $crumbs;
279    return $crumbs;
280  }
281
282  // page names
283  $name = noNSorNS($ID);
284  if ($conf['useheading']) {
285    // get page title
286    $title = p_get_first_heading($ID,true);
287    if ($title) {
288      $name = $title;
289    }
290  }
291
292  //remove ID from array
293  if (isset($crumbs[$ID])) {
294    unset($crumbs[$ID]);
295  }
296
297  //add to array
298  $crumbs[$ID] = $name;
299  //reduce size
300  while(count($crumbs) > $conf['breadcrumbs']){
301    array_shift($crumbs);
302  }
303  //save to session
304  $_SESSION[DOKU_COOKIE]['bc'] = $crumbs;
305  return $crumbs;
306}
307
308/**
309 * Filter for page IDs
310 *
311 * This is run on a ID before it is outputted somewhere
312 * currently used to replace the colon with something else
313 * on Windows systems and to have proper URL encoding
314 *
315 * Urlencoding is ommitted when the second parameter is false
316 *
317 * @author Andreas Gohr <andi@splitbrain.org>
318 */
319function idfilter($id,$ue=true){
320  global $conf;
321  if ($conf['useslash'] && $conf['userewrite']){
322    $id = strtr($id,':','/');
323  }elseif (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' &&
324      $conf['userewrite']) {
325    $id = strtr($id,':',';');
326  }
327  if($ue){
328    $id = rawurlencode($id);
329    $id = str_replace('%3A',':',$id); //keep as colon
330    $id = str_replace('%2F','/',$id); //keep as slash
331  }
332  return $id;
333}
334
335/**
336 * This builds a link to a wikipage
337 *
338 * It handles URL rewriting and adds additional parameter if
339 * given in $more
340 *
341 * @author Andreas Gohr <andi@splitbrain.org>
342 */
343function wl($id='',$more='',$abs=false,$sep='&amp;'){
344  global $conf;
345  if(is_array($more)){
346    $more = buildURLparams($more,$sep);
347  }else{
348    $more = str_replace(',',$sep,$more);
349  }
350
351  $id    = idfilter($id);
352  if($abs){
353    $xlink = DOKU_URL;
354  }else{
355    $xlink = DOKU_BASE;
356  }
357
358  if($conf['userewrite'] == 2){
359    $xlink .= DOKU_SCRIPT.'/'.$id;
360    if($more) $xlink .= '?'.$more;
361  }elseif($conf['userewrite']){
362    $xlink .= $id;
363    if($more) $xlink .= '?'.$more;
364  }elseif($id){
365    $xlink .= DOKU_SCRIPT.'?id='.$id;
366    if($more) $xlink .= $sep.$more;
367  }else{
368    $xlink .= DOKU_SCRIPT;
369    if($more) $xlink .= '?'.$more;
370  }
371
372  return $xlink;
373}
374
375/**
376 * This builds a link to an alternate page format
377 *
378 * Handles URL rewriting if enabled. Follows the style of wl().
379 *
380 * @author Ben Coburn <btcoburn@silicodon.net>
381 */
382function exportlink($id='',$format='raw',$more='',$abs=false,$sep='&amp;'){
383  global $conf;
384  if(is_array($more)){
385    $more = buildURLparams($more,$sep);
386  }else{
387    $more = str_replace(',',$sep,$more);
388  }
389
390  $format = rawurlencode($format);
391  $id = idfilter($id);
392  if($abs){
393    $xlink = DOKU_URL;
394  }else{
395    $xlink = DOKU_BASE;
396  }
397
398  if($conf['userewrite'] == 2){
399    $xlink .= DOKU_SCRIPT.'/'.$id.'?do=export_'.$format;
400    if($more) $xlink .= $sep.$more;
401  }elseif($conf['userewrite'] == 1){
402    $xlink .= '_export/'.$format.'/'.$id;
403    if($more) $xlink .= '?'.$more;
404  }else{
405    $xlink .= DOKU_SCRIPT.'?do=export_'.$format.$sep.'id='.$id;
406    if($more) $xlink .= $sep.$more;
407  }
408
409  return $xlink;
410}
411
412/**
413 * Build a link to a media file
414 *
415 * Will return a link to the detail page if $direct is false
416 *
417 * The $more parameter should always be given as array, the function then
418 * will strip default parameters to produce even cleaner URLs
419 *
420 * @param string  $id     - the media file id or URL
421 * @param mixed   $more   - string or array with additional parameters
422 * @param boolean $direct - link to detail page if false
423 * @param string  $sep    - URL parameter separator
424 * @param boolean $abs    - Create an absolute URL
425 */
426function ml($id='',$more='',$direct=true,$sep='&amp;',$abs=false){
427  global $conf;
428  if(is_array($more)){
429    // strip defaults for shorter URLs
430    if(isset($more['cache']) && $more['cache'] == 'cache') unset($more['cache']);
431    if(!$more['w']) unset($more['w']);
432    if(!$more['h']) unset($more['h']);
433    if(isset($more['id']) && $direct) unset($more['id']);
434    $more = buildURLparams($more,$sep);
435  }else{
436    $more = str_replace('cache=cache','',$more); //skip default
437    $more = str_replace(',,',',',$more);
438    $more = str_replace(',',$sep,$more);
439  }
440
441  if($abs){
442    $xlink = DOKU_URL;
443  }else{
444    $xlink = DOKU_BASE;
445  }
446
447  // external URLs are always direct without rewriting
448  if(preg_match('#^(https?|ftp)://#i',$id)){
449    $xlink .= 'lib/exe/fetch.php';
450    if($more){
451      $xlink .= '?'.$more;
452      $xlink .= $sep.'media='.rawurlencode($id);
453    }else{
454      $xlink .= '?media='.rawurlencode($id);
455    }
456    return $xlink;
457  }
458
459  $id = idfilter($id);
460
461  // decide on scriptname
462  if($direct){
463    if($conf['userewrite'] == 1){
464      $script = '_media';
465    }else{
466      $script = 'lib/exe/fetch.php';
467    }
468  }else{
469    if($conf['userewrite'] == 1){
470      $script = '_detail';
471    }else{
472      $script = 'lib/exe/detail.php';
473    }
474  }
475
476  // build URL based on rewrite mode
477   if($conf['userewrite']){
478     $xlink .= $script.'/'.$id;
479     if($more) $xlink .= '?'.$more;
480   }else{
481     if($more){
482       $xlink .= $script.'?'.$more;
483       $xlink .= $sep.'media='.$id;
484     }else{
485       $xlink .= $script.'?media='.$id;
486     }
487   }
488
489  return $xlink;
490}
491
492
493
494/**
495 * Just builds a link to a script
496 *
497 * @todo   maybe obsolete
498 * @author Andreas Gohr <andi@splitbrain.org>
499 */
500function script($script='doku.php'){
501#  $link = getBaseURL();
502#  $link .= $script;
503#  return $link;
504  return DOKU_BASE.DOKU_SCRIPT;
505}
506
507/**
508 * Spamcheck against wordlist
509 *
510 * Checks the wikitext against a list of blocked expressions
511 * returns true if the text contains any bad words
512 *
513 * @author Andreas Gohr <andi@splitbrain.org>
514 */
515function checkwordblock(){
516  global $TEXT;
517  global $conf;
518
519  if(!$conf['usewordblock']) return false;
520
521  // we prepare the text a tiny bit to prevent spammers circumventing URL checks
522  $text = preg_replace('!(\b)(www\.[\w.:?\-;,]+?\.[\w.:?\-;,]+?[\w/\#~:.?+=&%@\!\-.:?\-;,]+?)([.:?\-;,]*[^\w/\#~:.?+=&%@\!\-.:?\-;,])!i','\1http://\2 \2\3',$TEXT);
523
524  $wordblocks = getWordblocks();
525  //how many lines to read at once (to work around some PCRE limits)
526  if(version_compare(phpversion(),'4.3.0','<')){
527    //old versions of PCRE define a maximum of parenthesises even if no
528    //backreferences are used - the maximum is 99
529    //this is very bad performancewise and may even be too high still
530    $chunksize = 40;
531  }else{
532    //read file in chunks of 200 - this should work around the
533    //MAX_PATTERN_SIZE in modern PCRE
534    $chunksize = 200;
535  }
536  while($blocks = array_splice($wordblocks,0,$chunksize)){
537    $re = array();
538    #build regexp from blocks
539    foreach($blocks as $block){
540      $block = preg_replace('/#.*$/','',$block);
541      $block = trim($block);
542      if(empty($block)) continue;
543      $re[]  = $block;
544    }
545    if(count($re) && preg_match('#('.join('|',$re).')#si',$text)) {
546      return true;
547    }
548  }
549  return false;
550}
551
552/**
553 * Return the IP of the client
554 *
555 * Honours X-Forwarded-For and X-Real-IP Proxy Headers
556 *
557 * It returns a comma separated list of IPs if the above mentioned
558 * headers are set. If the single parameter is set, it tries to return
559 * a routable public address, prefering the ones suplied in the X
560 * headers
561 *
562 * @param  boolean $single If set only a single IP is returned
563 * @author Andreas Gohr <andi@splitbrain.org>
564 */
565function clientIP($single=false){
566  $ip = array();
567  $ip[] = $_SERVER['REMOTE_ADDR'];
568  if(!empty($_SERVER['HTTP_X_FORWARDED_FOR']))
569    $ip = array_merge($ip,explode(',',$_SERVER['HTTP_X_FORWARDED_FOR']));
570  if(!empty($_SERVER['HTTP_X_REAL_IP']))
571    $ip = array_merge($ip,explode(',',$_SERVER['HTTP_X_REAL_IP']));
572
573  // remove any non-IP stuff
574  $cnt = count($ip);
575  $match = array();
576  for($i=0; $i<$cnt; $i++){
577    if(preg_match('/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/',$ip[$i],$match)) {
578      $ip[$i] = $match[0];
579    } else {
580      $ip[$i] = '';
581    }
582    if(empty($ip[$i])) unset($ip[$i]);
583  }
584  $ip = array_values(array_unique($ip));
585  if(!$ip[0]) $ip[0] = '0.0.0.0'; // for some strange reason we don't have a IP
586
587  if(!$single) return join(',',$ip);
588
589  // decide which IP to use, trying to avoid local addresses
590  $ip = array_reverse($ip);
591  foreach($ip as $i){
592    if(preg_match('/^(127\.|10\.|192\.168\.|172\.((1[6-9])|(2[0-9])|(3[0-1]))\.)/',$i)){
593      continue;
594    }else{
595      return $i;
596    }
597  }
598  // still here? just use the first (last) address
599  return $ip[0];
600}
601
602/**
603 * Check if the browser is on a mobile device
604 *
605 * Adapted from the example code at url below
606 *
607 * @link http://www.brainhandles.com/2007/10/15/detecting-mobile-browsers/#code
608 */
609function clientismobile(){
610
611    if(isset($_SERVER['HTTP_X_WAP_PROFILE'])) return true;
612
613    if(preg_match('/wap\.|\.wap/i',$_SERVER['HTTP_ACCEPT'])) return true;
614
615    if(!isset($_SERVER['HTTP_USER_AGENT'])) return false;
616
617    $uamatches = 'midp|j2me|avantg|docomo|novarra|palmos|palmsource|240x320|opwv|chtml|pda|windows ce|mmp\/|blackberry|mib\/|symbian|wireless|nokia|hand|mobi|phone|cdm|up\.b|audio|SIE\-|SEC\-|samsung|HTC|mot\-|mitsu|sagem|sony|alcatel|lg|erics|vx|NEC|philips|mmm|xx|panasonic|sharp|wap|sch|rover|pocket|benq|java|pt|pg|vox|amoi|bird|compal|kg|voda|sany|kdd|dbt|sendo|sgh|gradi|jb|\d\d\di|moto';
618
619    if(preg_match("/$uamatches/i",$_SERVER['HTTP_USER_AGENT'])) return true;
620
621    return false;
622}
623
624
625/**
626 * Convert one or more comma separated IPs to hostnames
627 *
628 * @author Glen Harris <astfgl@iamnota.org>
629 * @returns a comma separated list of hostnames
630 */
631function gethostsbyaddrs($ips){
632  $hosts = array();
633  $ips = explode(',',$ips);
634
635  if(is_array($ips)) {
636    foreach($ips as $ip){
637      $hosts[] = gethostbyaddr(trim($ip));
638    }
639    return join(',',$hosts);
640  } else {
641    return gethostbyaddr(trim($ips));
642  }
643}
644
645/**
646 * Checks if a given page is currently locked.
647 *
648 * removes stale lockfiles
649 *
650 * @author Andreas Gohr <andi@splitbrain.org>
651 */
652function checklock($id){
653  global $conf;
654  $lock = wikiLockFN($id);
655
656  //no lockfile
657  if(!@file_exists($lock)) return false;
658
659  //lockfile expired
660  if((time() - filemtime($lock)) > $conf['locktime']){
661    @unlink($lock);
662    return false;
663  }
664
665  //my own lock
666  $ip = io_readFile($lock);
667  if( ($ip == clientIP()) || ($ip == $_SERVER['REMOTE_USER']) ){
668    return false;
669  }
670
671  return $ip;
672}
673
674/**
675 * Lock a page for editing
676 *
677 * @author Andreas Gohr <andi@splitbrain.org>
678 */
679function lock($id){
680  $lock = wikiLockFN($id);
681  if($_SERVER['REMOTE_USER']){
682    io_saveFile($lock,$_SERVER['REMOTE_USER']);
683  }else{
684    io_saveFile($lock,clientIP());
685  }
686}
687
688/**
689 * Unlock a page if it was locked by the user
690 *
691 * @author Andreas Gohr <andi@splitbrain.org>
692 * @return bool true if a lock was removed
693 */
694function unlock($id){
695  $lock = wikiLockFN($id);
696  if(@file_exists($lock)){
697    $ip = io_readFile($lock);
698    if( ($ip == clientIP()) || ($ip == $_SERVER['REMOTE_USER']) ){
699      @unlink($lock);
700      return true;
701    }
702  }
703  return false;
704}
705
706/**
707 * convert line ending to unix format
708 *
709 * @see    formText() for 2crlf conversion
710 * @author Andreas Gohr <andi@splitbrain.org>
711 */
712function cleanText($text){
713  $text = preg_replace("/(\015\012)|(\015)/","\012",$text);
714  return $text;
715}
716
717/**
718 * Prepares text for print in Webforms by encoding special chars.
719 * It also converts line endings to Windows format which is
720 * pseudo standard for webforms.
721 *
722 * @see    cleanText() for 2unix conversion
723 * @author Andreas Gohr <andi@splitbrain.org>
724 */
725function formText($text){
726  $text = str_replace("\012","\015\012",$text);
727  return htmlspecialchars($text);
728}
729
730/**
731 * Returns the specified local text in raw format
732 *
733 * @author Andreas Gohr <andi@splitbrain.org>
734 */
735function rawLocale($id){
736  return io_readFile(localeFN($id));
737}
738
739/**
740 * Returns the raw WikiText
741 *
742 * @author Andreas Gohr <andi@splitbrain.org>
743 */
744function rawWiki($id,$rev=''){
745  return io_readWikiPage(wikiFN($id, $rev), $id, $rev);
746}
747
748/**
749 * Returns the pagetemplate contents for the ID's namespace
750 *
751 * @author Andreas Gohr <andi@splitbrain.org>
752 */
753function pageTemplate($data){
754  $id = $data[0];
755  global $conf;
756  global $INFO;
757
758  $path = dirname(wikiFN($id));
759
760  if(@file_exists($path.'/_template.txt')){
761    $tpl = io_readFile($path.'/_template.txt');
762  }else{
763    // search upper namespaces for templates
764    $len = strlen(rtrim($conf['datadir'],'/'));
765    while (strlen($path) >= $len){
766      if(@file_exists($path.'/__template.txt')){
767        $tpl = io_readFile($path.'/__template.txt');
768        break;
769      }
770      $path = substr($path, 0, strrpos($path, '/'));
771    }
772  }
773  if(!$tpl) return '';
774
775  // replace placeholders
776  $tpl = str_replace('@ID@',$id,$tpl);
777  $tpl = str_replace('@NS@',getNS($id),$tpl);
778  $tpl = str_replace('@PAGE@',strtr(noNS($id),'_',' '),$tpl);
779  $tpl = str_replace('@USER@',$_SERVER['REMOTE_USER'],$tpl);
780  $tpl = str_replace('@NAME@',$INFO['userinfo']['name'],$tpl);
781  $tpl = str_replace('@MAIL@',$INFO['userinfo']['mail'],$tpl);
782  $tpl = str_replace('@DATE@',$conf['dformat'],$tpl);
783  // we need the callback to work around strftime's char limit
784  $tpl = preg_replace_callback('/%./',create_function('$m','return strftime($m[0]);'),$tpl);
785
786  return $tpl;
787}
788
789
790/**
791 * Returns the raw Wiki Text in three slices.
792 *
793 * The range parameter needs to have the form "from-to"
794 * and gives the range of the section in bytes - no
795 * UTF-8 awareness is needed.
796 * The returned order is prefix, section and suffix.
797 *
798 * @author Andreas Gohr <andi@splitbrain.org>
799 */
800function rawWikiSlices($range,$id,$rev=''){
801  list($from,$to) = split('-',$range,2);
802  $text = io_readWikiPage(wikiFN($id, $rev), $id, $rev);
803  if(!$from) $from = 0;
804  if(!$to)   $to   = strlen($text)+1;
805
806  $slices[0] = substr($text,0,$from-1);
807  $slices[1] = substr($text,$from-1,$to-$from);
808  $slices[2] = substr($text,$to);
809
810  return $slices;
811}
812
813/**
814 * Joins wiki text slices
815 *
816 * function to join the text slices with correct lineendings again.
817 * When the pretty parameter is set to true it adds additional empty
818 * lines between sections if needed (used on saving).
819 *
820 * @author Andreas Gohr <andi@splitbrain.org>
821 */
822function con($pre,$text,$suf,$pretty=false){
823
824  if($pretty){
825    if($pre && substr($pre,-1) != "\n") $pre .= "\n";
826    if($suf && substr($text,-1) != "\n") $text .= "\n";
827  }
828
829  // Avoid double newline above section when saving section edit
830  //if($pre) $pre .= "\n";
831  if($suf) $text .= "\n";
832  return $pre.$text.$suf;
833}
834
835/**
836 * Saves a wikitext by calling io_writeWikiPage.
837 * Also directs changelog and attic updates.
838 *
839 * @author Andreas Gohr <andi@splitbrain.org>
840 * @author Ben Coburn <btcoburn@silicodon.net>
841 */
842function saveWikiText($id,$text,$summary,$minor=false){
843  /* Note to developers:
844     This code is subtle and delicate. Test the behavior of
845     the attic and changelog with dokuwiki and external edits
846     after any changes. External edits change the wiki page
847     directly without using php or dokuwiki.
848  */
849  global $conf;
850  global $lang;
851  global $REV;
852  // ignore if no changes were made
853  if($text == rawWiki($id,'')){
854    return;
855  }
856
857  $file = wikiFN($id);
858  $old = @filemtime($file); // from page
859  $wasRemoved = empty($text);
860  $wasCreated = !@file_exists($file);
861  $wasReverted = ($REV==true);
862  $newRev = false;
863  $oldRev = getRevisions($id, -1, 1, 1024); // from changelog
864  $oldRev = (int)(empty($oldRev)?0:$oldRev[0]);
865  if(!@file_exists(wikiFN($id, $old)) && @file_exists($file) && $old>=$oldRev) {
866    // add old revision to the attic if missing
867    saveOldRevision($id);
868    // add a changelog entry if this edit came from outside dokuwiki
869    if ($old>$oldRev) {
870      addLogEntry($old, $id, DOKU_CHANGE_TYPE_EDIT, $lang['external_edit'], '', array('ExternalEdit'=>true));
871      // remove soon to be stale instructions
872      $cache = new cache_instructions($id, $file);
873      $cache->removeCache();
874    }
875  }
876
877  if ($wasRemoved){
878    // Send "update" event with empty data, so plugins can react to page deletion
879    $data = array(array($file, '', false), getNS($id), noNS($id), false);
880    trigger_event('IO_WIKIPAGE_WRITE', $data);
881    // pre-save deleted revision
882    @touch($file);
883    clearstatcache();
884    $newRev = saveOldRevision($id);
885    // remove empty file
886    @unlink($file);
887    // remove old meta info...
888    $mfiles = metaFiles($id);
889    $changelog = metaFN($id, '.changes');
890    $metadata  = metaFN($id, '.meta');
891    foreach ($mfiles as $mfile) {
892      // but keep per-page changelog to preserve page history and keep meta data
893      if (@file_exists($mfile) && $mfile!==$changelog && $mfile!==$metadata) { @unlink($mfile); }
894    }
895    // purge meta data
896    p_purge_metadata($id);
897    $del = true;
898    // autoset summary on deletion
899    if(empty($summary)) $summary = $lang['deleted'];
900    // remove empty namespaces
901    io_sweepNS($id, 'datadir');
902    io_sweepNS($id, 'mediadir');
903  }else{
904    // save file (namespace dir is created in io_writeWikiPage)
905    io_writeWikiPage($file, $text, $id);
906    // pre-save the revision, to keep the attic in sync
907    $newRev = saveOldRevision($id);
908    $del = false;
909  }
910
911  // select changelog line type
912  $extra = '';
913  $type = DOKU_CHANGE_TYPE_EDIT;
914  if ($wasReverted) {
915    $type = DOKU_CHANGE_TYPE_REVERT;
916    $extra = $REV;
917  }
918  else if ($wasCreated) { $type = DOKU_CHANGE_TYPE_CREATE; }
919  else if ($wasRemoved) { $type = DOKU_CHANGE_TYPE_DELETE; }
920  else if ($minor && $conf['useacl'] && $_SERVER['REMOTE_USER']) { $type = DOKU_CHANGE_TYPE_MINOR_EDIT; } //minor edits only for logged in users
921
922  addLogEntry($newRev, $id, $type, $summary, $extra);
923  // send notify mails
924  notify($id,'admin',$old,$summary,$minor);
925  notify($id,'subscribers',$old,$summary,$minor);
926
927  // update the purgefile (timestamp of the last time anything within the wiki was changed)
928  io_saveFile($conf['cachedir'].'/purgefile',time());
929
930  // if useheading is enabled, purge the cache of all linking pages
931  if($conf['useheading']){
932    require_once(DOKU_INC.'inc/fulltext.php');
933    $pages = ft_backlinks($id);
934    foreach ($pages as $page) {
935      $cache = new cache_renderer($page, wikiFN($page), 'xhtml');
936      $cache->removeCache();
937    }
938  }
939}
940
941/**
942 * moves the current version to the attic and returns its
943 * revision date
944 *
945 * @author Andreas Gohr <andi@splitbrain.org>
946 */
947function saveOldRevision($id){
948  global $conf;
949  $oldf = wikiFN($id);
950  if(!@file_exists($oldf)) return '';
951  $date = filemtime($oldf);
952  $newf = wikiFN($id,$date);
953  io_writeWikiPage($newf, rawWiki($id), $id, $date);
954  return $date;
955}
956
957/**
958 * Sends a notify mail on page change
959 *
960 * @param  string  $id       The changed page
961 * @param  string  $who      Who to notify (admin|subscribers)
962 * @param  int     $rev      Old page revision
963 * @param  string  $summary  What changed
964 * @param  boolean $minor    Is this a minor edit?
965 * @param  array   $replace  Additional string substitutions, @KEY@ to be replaced by value
966 *
967 * @author Andreas Gohr <andi@splitbrain.org>
968 */
969function notify($id,$who,$rev='',$summary='',$minor=false,$replace=array()){
970  global $lang;
971  global $conf;
972  global $INFO;
973
974  // decide if there is something to do
975  if($who == 'admin'){
976    if(empty($conf['notify'])) return; //notify enabled?
977    $text = rawLocale('mailtext');
978    $to   = $conf['notify'];
979    $bcc  = '';
980  }elseif($who == 'subscribers'){
981    if(!$conf['subscribers']) return; //subscribers enabled?
982    if($conf['useacl'] && $_SERVER['REMOTE_USER'] && $minor) return; //skip minors
983    $bcc  = subscriber_addresslist($id);
984    if(empty($bcc)) return;
985    $to   = '';
986    $text = rawLocale('subscribermail');
987  }elseif($who == 'register'){
988    if(empty($conf['registernotify'])) return;
989    $text = rawLocale('registermail');
990    $to   = $conf['registernotify'];
991    $bcc  = '';
992  }else{
993    return; //just to be safe
994  }
995
996  $ip   = clientIP();
997  $text = str_replace('@DATE@',strftime($conf['dformat']),$text);
998  $text = str_replace('@BROWSER@',$_SERVER['HTTP_USER_AGENT'],$text);
999  $text = str_replace('@IPADDRESS@',$ip,$text);
1000  $text = str_replace('@HOSTNAME@',gethostsbyaddrs($ip),$text);
1001  $text = str_replace('@NEWPAGE@',wl($id,'',true,'&'),$text);
1002  $text = str_replace('@PAGE@',$id,$text);
1003  $text = str_replace('@TITLE@',$conf['title'],$text);
1004  $text = str_replace('@DOKUWIKIURL@',DOKU_URL,$text);
1005  $text = str_replace('@SUMMARY@',$summary,$text);
1006  $text = str_replace('@USER@',$_SERVER['REMOTE_USER'],$text);
1007
1008  foreach ($replace as $key => $substitution) {
1009    $text = str_replace('@'.strtoupper($key).'@',$substitution, $text);
1010  }
1011
1012  if($who == 'register'){
1013    $subject = $lang['mail_new_user'].' '.$summary;
1014  }elseif($rev){
1015    $subject = $lang['mail_changed'].' '.$id;
1016    $text = str_replace('@OLDPAGE@',wl($id,"rev=$rev",true,'&'),$text);
1017    require_once(DOKU_INC.'inc/DifferenceEngine.php');
1018    $df  = new Diff(split("\n",rawWiki($id,$rev)),
1019                    split("\n",rawWiki($id)));
1020    $dformat = new UnifiedDiffFormatter();
1021    $diff    = $dformat->format($df);
1022  }else{
1023    $subject=$lang['mail_newpage'].' '.$id;
1024    $text = str_replace('@OLDPAGE@','none',$text);
1025    $diff = rawWiki($id);
1026  }
1027  $text = str_replace('@DIFF@',$diff,$text);
1028  $subject = '['.$conf['title'].'] '.$subject;
1029
1030  $from = $conf['mailfrom'];
1031  $from = str_replace('@USER@',$_SERVER['REMOTE_USER'],$from);
1032  $from = str_replace('@NAME@',$INFO['userinfo']['name'],$from);
1033  $from = str_replace('@MAIL@',$INFO['userinfo']['mail'],$from);
1034
1035  mail_send($to,$subject,$text,$from,'',$bcc);
1036}
1037
1038/**
1039 * extracts the query from a search engine referrer
1040 *
1041 * @author Andreas Gohr <andi@splitbrain.org>
1042 * @author Todd Augsburger <todd@rollerorgans.com>
1043 */
1044function getGoogleQuery(){
1045  $url = parse_url($_SERVER['HTTP_REFERER']);
1046  if(!$url) return '';
1047
1048  $query = array();
1049  parse_str($url['query'],$query);
1050  if(isset($query['q']))
1051    $q = $query['q'];        // google, live/msn, aol, ask, altavista, alltheweb, gigablast
1052  elseif(isset($query['p']))
1053    $q = $query['p'];        // yahoo
1054  elseif(isset($query['query']))
1055    $q = $query['query'];    // lycos, netscape, clusty, hotbot
1056  elseif(preg_match("#a9\.com#i",$url['host'])) // a9
1057    $q = urldecode(ltrim($url['path'],'/'));
1058
1059  if(!$q) return '';
1060  $q = preg_split('/[\s\'"\\\\`()\]\[?:!\.{};,#+*<>\\/]+/',$q,-1,PREG_SPLIT_NO_EMPTY);
1061  return $q;
1062}
1063
1064/**
1065 * Try to set correct locale
1066 *
1067 * @deprecated No longer used
1068 * @author     Andreas Gohr <andi@splitbrain.org>
1069 */
1070function setCorrectLocale(){
1071  global $conf;
1072  global $lang;
1073
1074  $enc = strtoupper($lang['encoding']);
1075  foreach ($lang['locales'] as $loc){
1076    //try locale
1077    if(@setlocale(LC_ALL,$loc)) return;
1078    //try loceale with encoding
1079    if(@setlocale(LC_ALL,"$loc.$enc")) return;
1080  }
1081  //still here? try to set from environment
1082  @setlocale(LC_ALL,"");
1083}
1084
1085/**
1086 * Return the human readable size of a file
1087 *
1088 * @param       int    $size   A file size
1089 * @param       int    $dec    A number of decimal places
1090 * @author      Martin Benjamin <b.martin@cybernet.ch>
1091 * @author      Aidan Lister <aidan@php.net>
1092 * @version     1.0.0
1093 */
1094function filesize_h($size, $dec = 1){
1095  $sizes = array('B', 'KB', 'MB', 'GB');
1096  $count = count($sizes);
1097  $i = 0;
1098
1099  while ($size >= 1024 && ($i < $count - 1)) {
1100    $size /= 1024;
1101    $i++;
1102  }
1103
1104  return round($size, $dec) . ' ' . $sizes[$i];
1105}
1106
1107/**
1108 * return an obfuscated email address in line with $conf['mailguard'] setting
1109 *
1110 * @author Harry Fuecks <hfuecks@gmail.com>
1111 * @author Christopher Smith <chris@jalakai.co.uk>
1112 */
1113function obfuscate($email) {
1114  global $conf;
1115
1116  switch ($conf['mailguard']) {
1117    case 'visible' :
1118      $obfuscate = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] ');
1119      return strtr($email, $obfuscate);
1120
1121    case 'hex' :
1122      $encode = '';
1123      for ($x=0; $x < strlen($email); $x++) $encode .= '&#x' . bin2hex($email{$x}).';';
1124      return $encode;
1125
1126    case 'none' :
1127    default :
1128      return $email;
1129  }
1130}
1131
1132/**
1133 * Let us know if a user is tracking a page or a namespace
1134 *
1135 * @author Andreas Gohr <andi@splitbrain.org>
1136 */
1137function is_subscribed($id,$uid,$ns=false){
1138  if(!$ns) {
1139    $file=metaFN($id,'.mlist');
1140  } else {
1141    if(!getNS($id)) {
1142      $file = metaFN(getNS($id),'.mlist');
1143    } else {
1144      $file = metaFN(getNS($id),'/.mlist');
1145    }
1146  }
1147  if (@file_exists($file)) {
1148    $mlist = file($file);
1149    $pos = array_search($uid."\n",$mlist);
1150    return is_int($pos);
1151  }
1152
1153  return false;
1154}
1155
1156/**
1157 * Return a string with the email addresses of all the
1158 * users subscribed to a page
1159 *
1160 * @author Steven Danz <steven-danz@kc.rr.com>
1161 */
1162function subscriber_addresslist($id){
1163  global $conf;
1164  global $auth;
1165
1166  if (!$conf['subscribers']) return '';
1167
1168  $users = array();
1169  $emails = array();
1170
1171  // load the page mlist file content
1172  $mlist = array();
1173  $file=metaFN($id,'.mlist');
1174  if (@file_exists($file)) {
1175    $mlist = file($file);
1176    foreach ($mlist as $who) {
1177      $who = rtrim($who);
1178      $users[$who] = true;
1179    }
1180  }
1181
1182  // load also the namespace mlist file content
1183  $ns = getNS($id);
1184  while ($ns) {
1185    $nsfile = metaFN($ns,'/.mlist');
1186    if (@file_exists($nsfile)) {
1187      $mlist = file($nsfile);
1188      foreach ($mlist as $who) {
1189        $who = rtrim($who);
1190        $users[$who] = true;
1191      }
1192    }
1193    $ns = getNS($ns);
1194  }
1195  // root namespace
1196  $nsfile = metaFN('','.mlist');
1197  if (@file_exists($nsfile)) {
1198    $mlist = file($nsfile);
1199    foreach ($mlist as $who) {
1200      $who = rtrim($who);
1201      $users[$who] = true;
1202    }
1203  }
1204  if(!empty($users)) {
1205    foreach (array_keys($users) as $who) {
1206      $info = $auth->getUserData($who);
1207      if($info === false) continue;
1208      $level = auth_aclcheck($id,$who,$info['grps']);
1209      if ($level >= AUTH_READ) {
1210        if (strcasecmp($info['mail'],$conf['notify']) != 0) {
1211          $emails[] = $info['mail'];
1212        }
1213      }
1214    }
1215  }
1216
1217  return implode(',',$emails);
1218}
1219
1220/**
1221 * Removes quoting backslashes
1222 *
1223 * @author Andreas Gohr <andi@splitbrain.org>
1224 */
1225function unslash($string,$char="'"){
1226  return str_replace('\\'.$char,$char,$string);
1227}
1228
1229/**
1230 * Convert php.ini shorthands to byte
1231 *
1232 * @author <gilthans dot NO dot SPAM at gmail dot com>
1233 * @link   http://de3.php.net/manual/en/ini.core.php#79564
1234 */
1235function php_to_byte($v){
1236    $l = substr($v, -1);
1237    $ret = substr($v, 0, -1);
1238    switch(strtoupper($l)){
1239        case 'P':
1240            $ret *= 1024;
1241        case 'T':
1242            $ret *= 1024;
1243        case 'G':
1244            $ret *= 1024;
1245        case 'M':
1246            $ret *= 1024;
1247        case 'K':
1248            $ret *= 1024;
1249        break;
1250    }
1251    return $ret;
1252}
1253
1254/**
1255 * Wrapper around preg_quote adding the default delimiter
1256 */
1257function preg_quote_cb($string){
1258    return preg_quote($string,'/');
1259}
1260
1261/**
1262 * Shorten a given string by removing data from the middle
1263 *
1264 * You can give the string in two parts, teh first part $keep
1265 * will never be shortened. The second part $short will be cut
1266 * in the middle to shorten but only if at least $min chars are
1267 * left to display it. Otherwise it will be left off.
1268 *
1269 * @param string $keep   the part to keep
1270 * @param string $short  the part to shorten
1271 * @param int    $max    maximum chars you want for the whole string
1272 * @param int    $min    minimum number of chars to have left for middle shortening
1273 * @param string $char   the shortening character to use
1274 */
1275function shorten($keep,$short,$max,$min=9,$char='⌇'){
1276    $max = $max - utf8_strlen($keep);
1277   if($max < $min) return $keep;
1278    $len = utf8_strlen($short);
1279    if($len <= $max) return $keep.$short;
1280    $half = floor($max/2);
1281    return $keep.utf8_substr($short,0,$half-1).$char.utf8_substr($short,$len-$half);
1282}
1283
1284/**
1285 * Return the users realname or e-mail address for use
1286 * in page footer and recent changes pages
1287 *
1288 * @author Andy Webber <dokuwiki AT andywebber DOT com>
1289 */
1290function editorinfo($username){
1291    global $conf;
1292    global $auth;
1293
1294    switch($conf['showuseras']){
1295      case 'username':
1296      case 'email':
1297      case 'email_link':
1298        $info = $auth->getUserData($username);
1299        break;
1300      default:
1301        return hsc($username);
1302    }
1303
1304    if(isset($info) && $info) {
1305        switch($conf['showuseras']){
1306          case 'username':
1307            return hsc($info['name']);
1308          case 'email':
1309            return obfuscate($info['mail']);
1310          case 'email_link':
1311            $mail=obfuscate($info['mail']);
1312            return '<a href="mailto:'.$mail.'">'.$mail.'</a>';
1313          default:
1314            return hsc($username);
1315        }
1316    } else {
1317        return hsc($username);
1318    }
1319}
1320
1321//Setup VIM: ex: et ts=2 enc=utf-8 :
1322