xref: /dokuwiki/inc/common.php (revision 91f24e5d953c4894c00992c79cb8a3629bdf93f2)
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
931/**
932 * moves the current version to the attic and returns its
933 * revision date
934 *
935 * @author Andreas Gohr <andi@splitbrain.org>
936 */
937function saveOldRevision($id){
938  global $conf;
939  $oldf = wikiFN($id);
940  if(!@file_exists($oldf)) return '';
941  $date = filemtime($oldf);
942  $newf = wikiFN($id,$date);
943  io_writeWikiPage($newf, rawWiki($id), $id, $date);
944  return $date;
945}
946
947/**
948 * Sends a notify mail on page change
949 *
950 * @param  string  $id       The changed page
951 * @param  string  $who      Who to notify (admin|subscribers)
952 * @param  int     $rev      Old page revision
953 * @param  string  $summary  What changed
954 * @param  boolean $minor    Is this a minor edit?
955 * @param  array   $replace  Additional string substitutions, @KEY@ to be replaced by value
956 *
957 * @author Andreas Gohr <andi@splitbrain.org>
958 */
959function notify($id,$who,$rev='',$summary='',$minor=false,$replace=array()){
960  global $lang;
961  global $conf;
962  global $INFO;
963
964  // decide if there is something to do
965  if($who == 'admin'){
966    if(empty($conf['notify'])) return; //notify enabled?
967    $text = rawLocale('mailtext');
968    $to   = $conf['notify'];
969    $bcc  = '';
970  }elseif($who == 'subscribers'){
971    if(!$conf['subscribers']) return; //subscribers enabled?
972    if($conf['useacl'] && $_SERVER['REMOTE_USER'] && $minor) return; //skip minors
973    $bcc  = subscriber_addresslist($id);
974    if(empty($bcc)) return;
975    $to   = '';
976    $text = rawLocale('subscribermail');
977  }elseif($who == 'register'){
978    if(empty($conf['registernotify'])) return;
979    $text = rawLocale('registermail');
980    $to   = $conf['registernotify'];
981    $bcc  = '';
982  }else{
983    return; //just to be safe
984  }
985
986  $ip   = clientIP();
987  $text = str_replace('@DATE@',strftime($conf['dformat']),$text);
988  $text = str_replace('@BROWSER@',$_SERVER['HTTP_USER_AGENT'],$text);
989  $text = str_replace('@IPADDRESS@',$ip,$text);
990  $text = str_replace('@HOSTNAME@',gethostsbyaddrs($ip),$text);
991  $text = str_replace('@NEWPAGE@',wl($id,'',true,'&'),$text);
992  $text = str_replace('@PAGE@',$id,$text);
993  $text = str_replace('@TITLE@',$conf['title'],$text);
994  $text = str_replace('@DOKUWIKIURL@',DOKU_URL,$text);
995  $text = str_replace('@SUMMARY@',$summary,$text);
996  $text = str_replace('@USER@',$_SERVER['REMOTE_USER'],$text);
997
998  foreach ($replace as $key => $substitution) {
999    $text = str_replace('@'.strtoupper($key).'@',$substitution, $text);
1000  }
1001
1002  if($who == 'register'){
1003    $subject = $lang['mail_new_user'].' '.$summary;
1004  }elseif($rev){
1005    $subject = $lang['mail_changed'].' '.$id;
1006    $text = str_replace('@OLDPAGE@',wl($id,"rev=$rev",true,'&'),$text);
1007    require_once(DOKU_INC.'inc/DifferenceEngine.php');
1008    $df  = new Diff(split("\n",rawWiki($id,$rev)),
1009                    split("\n",rawWiki($id)));
1010    $dformat = new UnifiedDiffFormatter();
1011    $diff    = $dformat->format($df);
1012  }else{
1013    $subject=$lang['mail_newpage'].' '.$id;
1014    $text = str_replace('@OLDPAGE@','none',$text);
1015    $diff = rawWiki($id);
1016  }
1017  $text = str_replace('@DIFF@',$diff,$text);
1018  $subject = '['.$conf['title'].'] '.$subject;
1019
1020  $from = $conf['mailfrom'];
1021  $from = str_replace('@USER@',$_SERVER['REMOTE_USER'],$from);
1022  $from = str_replace('@NAME@',$INFO['userinfo']['name'],$from);
1023  $from = str_replace('@MAIL@',$INFO['userinfo']['mail'],$from);
1024
1025  mail_send($to,$subject,$text,$from,'',$bcc);
1026}
1027
1028/**
1029 * extracts the query from a search engine referrer
1030 *
1031 * @author Andreas Gohr <andi@splitbrain.org>
1032 * @author Todd Augsburger <todd@rollerorgans.com>
1033 */
1034function getGoogleQuery(){
1035  $url = parse_url($_SERVER['HTTP_REFERER']);
1036  if(!$url) return '';
1037
1038  $query = array();
1039  parse_str($url['query'],$query);
1040  if(isset($query['q']))
1041    $q = $query['q'];        // google, live/msn, aol, ask, altavista, alltheweb, gigablast
1042  elseif(isset($query['p']))
1043    $q = $query['p'];        // yahoo
1044  elseif(isset($query['query']))
1045    $q = $query['query'];    // lycos, netscape, clusty, hotbot
1046  elseif(preg_match("#a9\.com#i",$url['host'])) // a9
1047    $q = urldecode(ltrim($url['path'],'/'));
1048
1049  if(!$q) return '';
1050  $q = preg_split('/[\s\'"\\\\`()\]\[?:!\.{};,#+*<>\\/]+/',$q,-1,PREG_SPLIT_NO_EMPTY);
1051  return $q;
1052}
1053
1054/**
1055 * Try to set correct locale
1056 *
1057 * @deprecated No longer used
1058 * @author     Andreas Gohr <andi@splitbrain.org>
1059 */
1060function setCorrectLocale(){
1061  global $conf;
1062  global $lang;
1063
1064  $enc = strtoupper($lang['encoding']);
1065  foreach ($lang['locales'] as $loc){
1066    //try locale
1067    if(@setlocale(LC_ALL,$loc)) return;
1068    //try loceale with encoding
1069    if(@setlocale(LC_ALL,"$loc.$enc")) return;
1070  }
1071  //still here? try to set from environment
1072  @setlocale(LC_ALL,"");
1073}
1074
1075/**
1076 * Return the human readable size of a file
1077 *
1078 * @param       int    $size   A file size
1079 * @param       int    $dec    A number of decimal places
1080 * @author      Martin Benjamin <b.martin@cybernet.ch>
1081 * @author      Aidan Lister <aidan@php.net>
1082 * @version     1.0.0
1083 */
1084function filesize_h($size, $dec = 1){
1085  $sizes = array('B', 'KB', 'MB', 'GB');
1086  $count = count($sizes);
1087  $i = 0;
1088
1089  while ($size >= 1024 && ($i < $count - 1)) {
1090    $size /= 1024;
1091    $i++;
1092  }
1093
1094  return round($size, $dec) . ' ' . $sizes[$i];
1095}
1096
1097/**
1098 * return an obfuscated email address in line with $conf['mailguard'] setting
1099 *
1100 * @author Harry Fuecks <hfuecks@gmail.com>
1101 * @author Christopher Smith <chris@jalakai.co.uk>
1102 */
1103function obfuscate($email) {
1104  global $conf;
1105
1106  switch ($conf['mailguard']) {
1107    case 'visible' :
1108      $obfuscate = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] ');
1109      return strtr($email, $obfuscate);
1110
1111    case 'hex' :
1112      $encode = '';
1113      for ($x=0; $x < strlen($email); $x++) $encode .= '&#x' . bin2hex($email{$x}).';';
1114      return $encode;
1115
1116    case 'none' :
1117    default :
1118      return $email;
1119  }
1120}
1121
1122/**
1123 * Let us know if a user is tracking a page or a namespace
1124 *
1125 * @author Andreas Gohr <andi@splitbrain.org>
1126 */
1127function is_subscribed($id,$uid,$ns=false){
1128  if(!$ns) {
1129    $file=metaFN($id,'.mlist');
1130  } else {
1131    if(!getNS($id)) {
1132      $file = metaFN(getNS($id),'.mlist');
1133    } else {
1134      $file = metaFN(getNS($id),'/.mlist');
1135    }
1136  }
1137  if (@file_exists($file)) {
1138    $mlist = file($file);
1139    $pos = array_search($uid."\n",$mlist);
1140    return is_int($pos);
1141  }
1142
1143  return false;
1144}
1145
1146/**
1147 * Return a string with the email addresses of all the
1148 * users subscribed to a page
1149 *
1150 * @author Steven Danz <steven-danz@kc.rr.com>
1151 */
1152function subscriber_addresslist($id){
1153  global $conf;
1154  global $auth;
1155
1156  if (!$conf['subscribers']) return '';
1157
1158  $users = array();
1159  $emails = array();
1160
1161  // load the page mlist file content
1162  $mlist = array();
1163  $file=metaFN($id,'.mlist');
1164  if (@file_exists($file)) {
1165    $mlist = file($file);
1166    foreach ($mlist as $who) {
1167      $who = rtrim($who);
1168      $users[$who] = true;
1169    }
1170  }
1171
1172  // load also the namespace mlist file content
1173  $ns = getNS($id);
1174  while ($ns) {
1175    $nsfile = metaFN($ns,'/.mlist');
1176    if (@file_exists($nsfile)) {
1177      $mlist = file($nsfile);
1178      foreach ($mlist as $who) {
1179        $who = rtrim($who);
1180        $users[$who] = true;
1181      }
1182    }
1183    $ns = getNS($ns);
1184  }
1185  // root namespace
1186  $nsfile = metaFN('','.mlist');
1187  if (@file_exists($nsfile)) {
1188    $mlist = file($nsfile);
1189    foreach ($mlist as $who) {
1190      $who = rtrim($who);
1191      $users[$who] = true;
1192    }
1193  }
1194  if(!empty($users)) {
1195    foreach (array_keys($users) as $who) {
1196      $info = $auth->getUserData($who);
1197      if($info === false) continue;
1198      $level = auth_aclcheck($id,$who,$info['grps']);
1199      if ($level >= AUTH_READ) {
1200        if (strcasecmp($info['mail'],$conf['notify']) != 0) {
1201          $emails[] = $info['mail'];
1202        }
1203      }
1204    }
1205  }
1206
1207  return implode(',',$emails);
1208}
1209
1210/**
1211 * Removes quoting backslashes
1212 *
1213 * @author Andreas Gohr <andi@splitbrain.org>
1214 */
1215function unslash($string,$char="'"){
1216  return str_replace('\\'.$char,$char,$string);
1217}
1218
1219/**
1220 * Convert php.ini shorthands to byte
1221 *
1222 * @author <gilthans dot NO dot SPAM at gmail dot com>
1223 * @link   http://de3.php.net/manual/en/ini.core.php#79564
1224 */
1225function php_to_byte($v){
1226    $l = substr($v, -1);
1227    $ret = substr($v, 0, -1);
1228    switch(strtoupper($l)){
1229        case 'P':
1230            $ret *= 1024;
1231        case 'T':
1232            $ret *= 1024;
1233        case 'G':
1234            $ret *= 1024;
1235        case 'M':
1236            $ret *= 1024;
1237        case 'K':
1238            $ret *= 1024;
1239        break;
1240    }
1241    return $ret;
1242}
1243
1244/**
1245 * Wrapper around preg_quote adding the default delimiter
1246 */
1247function preg_quote_cb($string){
1248    return preg_quote($string,'/');
1249}
1250
1251/**
1252 * Shorten a given string by removing data from the middle
1253 *
1254 * You can give the string in two parts, teh first part $keep
1255 * will never be shortened. The second part $short will be cut
1256 * in the middle to shorten but only if at least $min chars are
1257 * left to display it. Otherwise it will be left off.
1258 *
1259 * @param string $keep   the part to keep
1260 * @param string $short  the part to shorten
1261 * @param int    $max    maximum chars you want for the whole string
1262 * @param int    $min    minimum number of chars to have left for middle shortening
1263 * @param string $char   the shortening character to use
1264 */
1265function shorten($keep,$short,$max,$min=9,$char='⌇'){
1266    $max = $max - utf8_strlen($keep);
1267   if($max < $min) return $keep;
1268    $len = utf8_strlen($short);
1269    if($len <= $max) return $keep.$short;
1270    $half = floor($max/2);
1271    return $keep.utf8_substr($short,0,$half-1).$char.utf8_substr($short,$len-$half);
1272}
1273
1274/**
1275 * Return the users realname or e-mail address for use
1276 * in page footer and recent changes pages
1277 *
1278 * @author Andy Webber <dokuwiki AT andywebber DOT com>
1279 */
1280function editorinfo($username){
1281    global $conf;
1282    global $auth;
1283
1284    switch($conf['showuseras']){
1285      case 'username':
1286      case 'email':
1287      case 'email_link':
1288        $info = $auth->getUserData($username);
1289        break;
1290      default:
1291        return hsc($username);
1292    }
1293
1294    if(isset($info) && $info) {
1295        switch($conf['showuseras']){
1296          case 'username':
1297            return hsc($info['name']);
1298          case 'email':
1299            return obfuscate($info['mail']);
1300          case 'email_link':
1301            $mail=obfuscate($info['mail']);
1302            return '<a href="mailto:'.$mail.'">'.$mail.'</a>';
1303          default:
1304            return hsc($username);
1305        }
1306    } else {
1307        return hsc($username);
1308    }
1309}
1310
1311//Setup VIM: ex: et ts=2 enc=utf-8 :
1312