xref: /dokuwiki/inc/common.php (revision 73038c47e3312b4c62c4a0a05ecd5cdcd5eb95b7)
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 = '<input type="hidden" name="sectok" value="'.getSecurityToken().'" />'."\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']);
114    $info['client']     = $_SERVER['REMOTE_USER'];
115
116    // set info about manager/admin status
117    $info['isadmin']   = false;
118    $info['ismanager'] = false;
119    if($info['perm'] == AUTH_ADMIN){
120      $info['isadmin']   = true;
121      $info['ismanager'] = true;
122    }elseif(auth_ismanager()){
123      $info['ismanager'] = true;
124    }
125
126    // if some outside auth were used only REMOTE_USER is set
127    if(!$info['userinfo']['name']){
128      $info['userinfo']['name'] = $_SERVER['REMOTE_USER'];
129    }
130
131  }else{
132    $info['perm']       = auth_aclcheck($ID,'',null);
133    $info['subscribed'] = false;
134    $info['client']     = clientIP(true);
135  }
136
137  $info['namespace'] = getNS($ID);
138  $info['locked']    = checklock($ID);
139  $info['filepath']  = fullpath(wikiFN($ID));
140  $info['exists']    = @file_exists($info['filepath']);
141  if($REV){
142    //check if current revision was meant
143    if($info['exists'] && (@filemtime($info['filepath'])==$REV)){
144      $REV = '';
145    }else{
146      //really use old revision
147      $info['filepath'] = fullpath(wikiFN($ID,$REV));
148      $info['exists']   = @file_exists($info['filepath']);
149    }
150  }
151  $info['rev'] = $REV;
152  if($info['exists']){
153    $info['writable'] = (is_writable($info['filepath']) &&
154                         ($info['perm'] >= AUTH_EDIT));
155  }else{
156    $info['writable'] = ($info['perm'] >= AUTH_CREATE);
157  }
158  $info['editable']  = ($info['writable'] && empty($info['lock']));
159  $info['lastmod']   = @filemtime($info['filepath']);
160
161  //load page meta data
162  $info['meta'] = p_get_metadata($ID);
163
164  //who's the editor
165  if($REV){
166    $revinfo = getRevisionInfo($ID, $REV, 1024);
167  }else{
168    if (is_array($info['meta']['last_change'])) {
169       $revinfo = $info['meta']['last_change'];
170    } else {
171      $revinfo = getRevisionInfo($ID, $info['lastmod'], 1024);
172      // cache most recent changelog line in metadata if missing and still valid
173      if ($revinfo!==false) {
174        $info['meta']['last_change'] = $revinfo;
175        p_set_metadata($ID, array('last_change' => $revinfo));
176      }
177    }
178  }
179  //and check for an external edit
180  if($revinfo!==false && $revinfo['date']!=$info['lastmod']){
181    // cached changelog line no longer valid
182    $revinfo = false;
183    $info['meta']['last_change'] = $revinfo;
184    p_set_metadata($ID, array('last_change' => $revinfo));
185  }
186
187  $info['ip']     = $revinfo['ip'];
188  $info['user']   = $revinfo['user'];
189  $info['sum']    = $revinfo['sum'];
190  // See also $INFO['meta']['last_change'] which is the most recent log line for page $ID.
191  // Use $INFO['meta']['last_change']['type']===DOKU_CHANGE_TYPE_MINOR_EDIT in place of $info['minor'].
192
193  if($revinfo['user']){
194    $info['editor'] = $revinfo['user'];
195  }else{
196    $info['editor'] = $revinfo['ip'];
197  }
198
199  // draft
200  $draft = getCacheName($info['client'].$ID,'.draft');
201  if(@file_exists($draft)){
202    if(@filemtime($draft) < @filemtime(wikiFN($ID))){
203      // remove stale draft
204      @unlink($draft);
205    }else{
206      $info['draft'] = $draft;
207    }
208  }
209
210  return $info;
211}
212
213/**
214 * Build an string of URL parameters
215 *
216 * @author Andreas Gohr
217 */
218function buildURLparams($params, $sep='&amp;'){
219  $url = '';
220  $amp = false;
221  foreach($params as $key => $val){
222    if($amp) $url .= $sep;
223
224    $url .= $key.'=';
225    $url .= rawurlencode($val);
226    $amp = true;
227  }
228  return $url;
229}
230
231/**
232 * Build an string of html tag attributes
233 *
234 * Skips keys starting with '_', values get HTML encoded
235 *
236 * @author Andreas Gohr
237 */
238function buildAttributes($params,$skipempty=false){
239  $url = '';
240  foreach($params as $key => $val){
241    if($key{0} == '_') continue;
242    if($val === '' && $skipempty) continue;
243
244    $url .= $key.'="';
245    $url .= htmlspecialchars ($val);
246    $url .= '" ';
247  }
248  return $url;
249}
250
251
252/**
253 * This builds the breadcrumb trail and returns it as array
254 *
255 * @author Andreas Gohr <andi@splitbrain.org>
256 */
257function breadcrumbs(){
258  // we prepare the breadcrumbs early for quick session closing
259  static $crumbs = null;
260  if($crumbs != null) return $crumbs;
261
262  global $ID;
263  global $ACT;
264  global $conf;
265  $crumbs = $_SESSION[DOKU_COOKIE]['bc'];
266
267  //first visit?
268  if (!is_array($crumbs)){
269    $crumbs = array();
270  }
271  //we only save on show and existing wiki documents
272  $file = wikiFN($ID);
273  if($ACT != 'show' || !@file_exists($file)){
274    $_SESSION[DOKU_COOKIE]['bc'] = $crumbs;
275    return $crumbs;
276  }
277
278  // page names
279  $name = noNSorNS($ID);
280  if ($conf['useheading']) {
281    // get page title
282    $title = p_get_first_heading($ID,true);
283    if ($title) {
284      $name = $title;
285    }
286  }
287
288  //remove ID from array
289  if (isset($crumbs[$ID])) {
290    unset($crumbs[$ID]);
291  }
292
293  //add to array
294  $crumbs[$ID] = $name;
295  //reduce size
296  while(count($crumbs) > $conf['breadcrumbs']){
297    array_shift($crumbs);
298  }
299  //save to session
300  $_SESSION[DOKU_COOKIE]['bc'] = $crumbs;
301  return $crumbs;
302}
303
304/**
305 * Filter for page IDs
306 *
307 * This is run on a ID before it is outputted somewhere
308 * currently used to replace the colon with something else
309 * on Windows systems and to have proper URL encoding
310 *
311 * Urlencoding is ommitted when the second parameter is false
312 *
313 * @author Andreas Gohr <andi@splitbrain.org>
314 */
315function idfilter($id,$ue=true){
316  global $conf;
317  if ($conf['useslash'] && $conf['userewrite']){
318    $id = strtr($id,':','/');
319  }elseif (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' &&
320      $conf['userewrite']) {
321    $id = strtr($id,':',';');
322  }
323  if($ue){
324    $id = rawurlencode($id);
325    $id = str_replace('%3A',':',$id); //keep as colon
326    $id = str_replace('%2F','/',$id); //keep as slash
327  }
328  return $id;
329}
330
331/**
332 * This builds a link to a wikipage
333 *
334 * It handles URL rewriting and adds additional parameter if
335 * given in $more
336 *
337 * @author Andreas Gohr <andi@splitbrain.org>
338 */
339function wl($id='',$more='',$abs=false,$sep='&amp;'){
340  global $conf;
341  if(is_array($more)){
342    $more = buildURLparams($more,$sep);
343  }else{
344    $more = str_replace(',',$sep,$more);
345  }
346
347  $id    = idfilter($id);
348  if($abs){
349    $xlink = DOKU_URL;
350  }else{
351    $xlink = DOKU_BASE;
352  }
353
354  if($conf['userewrite'] == 2){
355    $xlink .= DOKU_SCRIPT.'/'.$id;
356    if($more) $xlink .= '?'.$more;
357  }elseif($conf['userewrite']){
358    $xlink .= $id;
359    if($more) $xlink .= '?'.$more;
360  }elseif($id){
361    $xlink .= DOKU_SCRIPT.'?id='.$id;
362    if($more) $xlink .= $sep.$more;
363  }else{
364    $xlink .= DOKU_SCRIPT;
365    if($more) $xlink .= '?'.$more;
366  }
367
368  return $xlink;
369}
370
371/**
372 * This builds a link to an alternate page format
373 *
374 * Handles URL rewriting if enabled. Follows the style of wl().
375 *
376 * @author Ben Coburn <btcoburn@silicodon.net>
377 */
378function exportlink($id='',$format='raw',$more='',$abs=false,$sep='&amp;'){
379  global $conf;
380  if(is_array($more)){
381    $more = buildURLparams($more,$sep);
382  }else{
383    $more = str_replace(',',$sep,$more);
384  }
385
386  $format = rawurlencode($format);
387  $id = idfilter($id);
388  if($abs){
389    $xlink = DOKU_URL;
390  }else{
391    $xlink = DOKU_BASE;
392  }
393
394  if($conf['userewrite'] == 2){
395    $xlink .= DOKU_SCRIPT.'/'.$id.'?do=export_'.$format;
396    if($more) $xlink .= $sep.$more;
397  }elseif($conf['userewrite'] == 1){
398    $xlink .= '_export/'.$format.'/'.$id;
399    if($more) $xlink .= '?'.$more;
400  }else{
401    $xlink .= DOKU_SCRIPT.'?do=export_'.$format.$sep.'id='.$id;
402    if($more) $xlink .= $sep.$more;
403  }
404
405  return $xlink;
406}
407
408/**
409 * Build a link to a media file
410 *
411 * Will return a link to the detail page if $direct is false
412 */
413function ml($id='',$more='',$direct=true,$sep='&amp;',$abs=false){
414  global $conf;
415  if(is_array($more)){
416    $more = buildURLparams($more,$sep);
417  }else{
418    $more = str_replace(',',$sep,$more);
419  }
420
421  if($abs){
422    $xlink = DOKU_URL;
423  }else{
424    $xlink = DOKU_BASE;
425  }
426
427  // external URLs are always direct without rewriting
428  if(preg_match('#^(https?|ftp)://#i',$id)){
429    $xlink .= 'lib/exe/fetch.php';
430    if($more){
431      $xlink .= '?'.$more;
432      $xlink .= $sep.'media='.rawurlencode($id);
433    }else{
434      $xlink .= '?media='.rawurlencode($id);
435    }
436    return $xlink;
437  }
438
439  $id = idfilter($id);
440
441  // decide on scriptname
442  if($direct){
443    if($conf['userewrite'] == 1){
444      $script = '_media';
445    }else{
446      $script = 'lib/exe/fetch.php';
447    }
448  }else{
449    if($conf['userewrite'] == 1){
450      $script = '_detail';
451    }else{
452      $script = 'lib/exe/detail.php';
453    }
454  }
455
456  // build URL based on rewrite mode
457   if($conf['userewrite']){
458     $xlink .= $script.'/'.$id;
459     if($more) $xlink .= '?'.$more;
460   }else{
461     if($more){
462       $xlink .= $script.'?'.$more;
463       $xlink .= $sep.'media='.$id;
464     }else{
465       $xlink .= $script.'?media='.$id;
466     }
467   }
468
469  return $xlink;
470}
471
472
473
474/**
475 * Just builds a link to a script
476 *
477 * @todo   maybe obsolete
478 * @author Andreas Gohr <andi@splitbrain.org>
479 */
480function script($script='doku.php'){
481#  $link = getBaseURL();
482#  $link .= $script;
483#  return $link;
484  return DOKU_BASE.DOKU_SCRIPT;
485}
486
487/**
488 * Spamcheck against wordlist
489 *
490 * Checks the wikitext against a list of blocked expressions
491 * returns true if the text contains any bad words
492 *
493 * @author Andreas Gohr <andi@splitbrain.org>
494 */
495function checkwordblock(){
496  global $TEXT;
497  global $conf;
498
499  if(!$conf['usewordblock']) return false;
500
501  // we prepare the text a tiny bit to prevent spammers circumventing URL checks
502  $text = preg_replace('!(\b)(www\.[\w.:?\-;,]+?\.[\w.:?\-;,]+?[\w/\#~:.?+=&%@\!\-.:?\-;,]+?)([.:?\-;,]*[^\w/\#~:.?+=&%@\!\-.:?\-;,])!i','\1http://\2 \2\3',$TEXT);
503
504  $wordblocks = getWordblocks();
505  //how many lines to read at once (to work around some PCRE limits)
506  if(version_compare(phpversion(),'4.3.0','<')){
507    //old versions of PCRE define a maximum of parenthesises even if no
508    //backreferences are used - the maximum is 99
509    //this is very bad performancewise and may even be too high still
510    $chunksize = 40;
511  }else{
512    //read file in chunks of 200 - this should work around the
513    //MAX_PATTERN_SIZE in modern PCRE
514    $chunksize = 200;
515  }
516  while($blocks = array_splice($wordblocks,0,$chunksize)){
517    $re = array();
518    #build regexp from blocks
519    foreach($blocks as $block){
520      $block = preg_replace('/#.*$/','',$block);
521      $block = trim($block);
522      if(empty($block)) continue;
523      $re[]  = $block;
524    }
525    if(count($re) && preg_match('#('.join('|',$re).')#si',$text)) {
526      return true;
527    }
528  }
529  return false;
530}
531
532/**
533 * Return the IP of the client
534 *
535 * Honours X-Forwarded-For and X-Real-IP Proxy Headers
536 *
537 * It returns a comma separated list of IPs if the above mentioned
538 * headers are set. If the single parameter is set, it tries to return
539 * a routable public address, prefering the ones suplied in the X
540 * headers
541 *
542 * @param  boolean $single If set only a single IP is returned
543 * @author Andreas Gohr <andi@splitbrain.org>
544 */
545function clientIP($single=false){
546  $ip = array();
547  $ip[] = $_SERVER['REMOTE_ADDR'];
548  if(!empty($_SERVER['HTTP_X_FORWARDED_FOR']))
549    $ip = array_merge($ip,explode(',',$_SERVER['HTTP_X_FORWARDED_FOR']));
550  if(!empty($_SERVER['HTTP_X_REAL_IP']))
551    $ip = array_merge($ip,explode(',',$_SERVER['HTTP_X_REAL_IP']));
552
553  // remove any non-IP stuff
554  $cnt = count($ip);
555  $match = array();
556  for($i=0; $i<$cnt; $i++){
557    if(preg_match('/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/',$ip[$i],$match)) {
558      $ip[$i] = $match[0];
559    } else {
560      $ip[$i] = '';
561    }
562    if(empty($ip[$i])) unset($ip[$i]);
563  }
564  $ip = array_values(array_unique($ip));
565  if(!$ip[0]) $ip[0] = '0.0.0.0'; // for some strange reason we don't have a IP
566
567  if(!$single) return join(',',$ip);
568
569  // decide which IP to use, trying to avoid local addresses
570  $ip = array_reverse($ip);
571  foreach($ip as $i){
572    if(preg_match('/^(127\.|10\.|192\.168\.|172\.((1[6-9])|(2[0-9])|(3[0-1]))\.)/',$i)){
573      continue;
574    }else{
575      return $i;
576    }
577  }
578  // still here? just use the first (last) address
579  return $ip[0];
580}
581
582/**
583 * Convert one or more comma separated IPs to hostnames
584 *
585 * @author Glen Harris <astfgl@iamnota.org>
586 * @returns a comma separated list of hostnames
587 */
588function gethostsbyaddrs($ips){
589  $hosts = array();
590  $ips = explode(',',$ips);
591
592  if(is_array($ips)) {
593    foreach($ips as $ip){
594      $hosts[] = gethostbyaddr(trim($ip));
595    }
596    return join(',',$hosts);
597  } else {
598    return gethostbyaddr(trim($ips));
599  }
600}
601
602/**
603 * Checks if a given page is currently locked.
604 *
605 * removes stale lockfiles
606 *
607 * @author Andreas Gohr <andi@splitbrain.org>
608 */
609function checklock($id){
610  global $conf;
611  $lock = wikiLockFN($id);
612
613  //no lockfile
614  if(!@file_exists($lock)) return false;
615
616  //lockfile expired
617  if((time() - filemtime($lock)) > $conf['locktime']){
618    @unlink($lock);
619    return false;
620  }
621
622  //my own lock
623  $ip = io_readFile($lock);
624  if( ($ip == clientIP()) || ($ip == $_SERVER['REMOTE_USER']) ){
625    return false;
626  }
627
628  return $ip;
629}
630
631/**
632 * Lock a page for editing
633 *
634 * @author Andreas Gohr <andi@splitbrain.org>
635 */
636function lock($id){
637  $lock = wikiLockFN($id);
638  if($_SERVER['REMOTE_USER']){
639    io_saveFile($lock,$_SERVER['REMOTE_USER']);
640  }else{
641    io_saveFile($lock,clientIP());
642  }
643}
644
645/**
646 * Unlock a page if it was locked by the user
647 *
648 * @author Andreas Gohr <andi@splitbrain.org>
649 * @return bool true if a lock was removed
650 */
651function unlock($id){
652  $lock = wikiLockFN($id);
653  if(@file_exists($lock)){
654    $ip = io_readFile($lock);
655    if( ($ip == clientIP()) || ($ip == $_SERVER['REMOTE_USER']) ){
656      @unlink($lock);
657      return true;
658    }
659  }
660  return false;
661}
662
663/**
664 * convert line ending to unix format
665 *
666 * @see    formText() for 2crlf conversion
667 * @author Andreas Gohr <andi@splitbrain.org>
668 */
669function cleanText($text){
670  $text = preg_replace("/(\015\012)|(\015)/","\012",$text);
671  return $text;
672}
673
674/**
675 * Prepares text for print in Webforms by encoding special chars.
676 * It also converts line endings to Windows format which is
677 * pseudo standard for webforms.
678 *
679 * @see    cleanText() for 2unix conversion
680 * @author Andreas Gohr <andi@splitbrain.org>
681 */
682function formText($text){
683  $text = str_replace("\012","\015\012",$text);
684  return htmlspecialchars($text);
685}
686
687/**
688 * Returns the specified local text in raw format
689 *
690 * @author Andreas Gohr <andi@splitbrain.org>
691 */
692function rawLocale($id){
693  return io_readFile(localeFN($id));
694}
695
696/**
697 * Returns the raw WikiText
698 *
699 * @author Andreas Gohr <andi@splitbrain.org>
700 */
701function rawWiki($id,$rev=''){
702  return io_readWikiPage(wikiFN($id, $rev), $id, $rev);
703}
704
705/**
706 * Returns the pagetemplate contents for the ID's namespace
707 *
708 * @author Andreas Gohr <andi@splitbrain.org>
709 */
710function pageTemplate($data){
711  $id = $data[0];
712  global $conf;
713  global $INFO;
714
715  $path = dirname(wikiFN($id));
716
717  if(@file_exists($path.'/_template.txt')){
718    $tpl = io_readFile($path.'/_template.txt');
719  }else{
720    // search upper namespaces for templates
721    $len = strlen(rtrim($conf['datadir'],'/'));
722    while (strlen($path) >= $len){
723      if(@file_exists($path.'/__template.txt')){
724        $tpl = io_readFile($path.'/__template.txt');
725        break;
726      }
727      $path = substr($path, 0, strrpos($path, '/'));
728    }
729  }
730  if(!$tpl) return '';
731
732  // replace placeholders
733  $tpl = str_replace('@ID@',$id,$tpl);
734  $tpl = str_replace('@NS@',getNS($id),$tpl);
735  $tpl = str_replace('@PAGE@',strtr(noNS($id),'_',' '),$tpl);
736  $tpl = str_replace('@USER@',$_SERVER['REMOTE_USER'],$tpl);
737  $tpl = str_replace('@NAME@',$INFO['userinfo']['name'],$tpl);
738  $tpl = str_replace('@MAIL@',$INFO['userinfo']['mail'],$tpl);
739  $tpl = str_replace('@DATE@',$conf['dformat'],$tpl);
740  $tpl = strftime($tpl);
741  return $tpl;
742}
743
744
745/**
746 * Returns the raw Wiki Text in three slices.
747 *
748 * The range parameter needs to have the form "from-to"
749 * and gives the range of the section in bytes - no
750 * UTF-8 awareness is needed.
751 * The returned order is prefix, section and suffix.
752 *
753 * @author Andreas Gohr <andi@splitbrain.org>
754 */
755function rawWikiSlices($range,$id,$rev=''){
756  list($from,$to) = split('-',$range,2);
757  $text = io_readWikiPage(wikiFN($id, $rev), $id, $rev);
758  if(!$from) $from = 0;
759  if(!$to)   $to   = strlen($text)+1;
760
761  $slices[0] = substr($text,0,$from-1);
762  $slices[1] = substr($text,$from-1,$to-$from);
763  $slices[2] = substr($text,$to);
764
765  return $slices;
766}
767
768/**
769 * Joins wiki text slices
770 *
771 * function to join the text slices with correct lineendings again.
772 * When the pretty parameter is set to true it adds additional empty
773 * lines between sections if needed (used on saving).
774 *
775 * @author Andreas Gohr <andi@splitbrain.org>
776 */
777function con($pre,$text,$suf,$pretty=false){
778
779  if($pretty){
780    if($pre && substr($pre,-1) != "\n") $pre .= "\n";
781    if($suf && substr($text,-1) != "\n") $text .= "\n";
782  }
783
784  if($pre) $pre .= "\n";
785  if($suf) $text .= "\n";
786  return $pre.$text.$suf;
787}
788
789/**
790 * Saves a wikitext by calling io_writeWikiPage.
791 * Also directs changelog and attic updates.
792 *
793 * @author Andreas Gohr <andi@splitbrain.org>
794 * @author Ben Coburn <btcoburn@silicodon.net>
795 */
796function saveWikiText($id,$text,$summary,$minor=false){
797  /* Note to developers:
798     This code is subtle and delicate. Test the behavior of
799     the attic and changelog with dokuwiki and external edits
800     after any changes. External edits change the wiki page
801     directly without using php or dokuwiki.
802  */
803  global $conf;
804  global $lang;
805  global $REV;
806  // ignore if no changes were made
807  if($text == rawWiki($id,'')){
808    return;
809  }
810
811  $file = wikiFN($id);
812  $old = @filemtime($file); // from page
813  $wasRemoved = empty($text);
814  $wasCreated = !@file_exists($file);
815  $wasReverted = ($REV==true);
816  $newRev = false;
817  $oldRev = getRevisions($id, -1, 1, 1024); // from changelog
818  $oldRev = (int)(empty($oldRev)?0:$oldRev[0]);
819  if(!@file_exists(wikiFN($id, $old)) && @file_exists($file) && $old>=$oldRev) {
820    // add old revision to the attic if missing
821    saveOldRevision($id);
822    // add a changelog entry if this edit came from outside dokuwiki
823    if ($old>$oldRev) {
824      addLogEntry($old, $id, DOKU_CHANGE_TYPE_EDIT, $lang['external_edit'], '', array('ExternalEdit'=>true));
825      // remove soon to be stale instructions
826      $cache = new cache_instructions($id, $file);
827      $cache->removeCache();
828    }
829  }
830
831  if ($wasRemoved){
832    // Send "update" event with empty data, so plugins can react to page deletion
833    $data = array(array($file, '', false), getNS($id), noNS($id), false);
834    trigger_event('IO_WIKIPAGE_WRITE', $data);
835    // pre-save deleted revision
836    @touch($file);
837    clearstatcache();
838    $newRev = saveOldRevision($id);
839    // remove empty file
840    @unlink($file);
841    // remove old meta info...
842    $mfiles = metaFiles($id);
843    $changelog = metaFN($id, '.changes');
844    foreach ($mfiles as $mfile) {
845      // but keep per-page changelog to preserve page history
846      if (@file_exists($mfile) && $mfile!==$changelog) { @unlink($mfile); }
847    }
848    $del = true;
849    // autoset summary on deletion
850    if(empty($summary)) $summary = $lang['deleted'];
851    // remove empty namespaces
852    io_sweepNS($id, 'datadir');
853    io_sweepNS($id, 'mediadir');
854  }else{
855    // save file (namespace dir is created in io_writeWikiPage)
856    io_writeWikiPage($file, $text, $id);
857    // pre-save the revision, to keep the attic in sync
858    $newRev = saveOldRevision($id);
859    $del = false;
860  }
861
862  // select changelog line type
863  $extra = '';
864  $type = DOKU_CHANGE_TYPE_EDIT;
865  if ($wasReverted) {
866    $type = DOKU_CHANGE_TYPE_REVERT;
867    $extra = $REV;
868  }
869  else if ($wasCreated) { $type = DOKU_CHANGE_TYPE_CREATE; }
870  else if ($wasRemoved) { $type = DOKU_CHANGE_TYPE_DELETE; }
871  else if ($minor && $conf['useacl'] && $_SERVER['REMOTE_USER']) { $type = DOKU_CHANGE_TYPE_MINOR_EDIT; } //minor edits only for logged in users
872
873  addLogEntry($newRev, $id, $type, $summary, $extra);
874  // send notify mails
875  notify($id,'admin',$old,$summary,$minor);
876  notify($id,'subscribers',$old,$summary,$minor);
877
878  // update the purgefile (timestamp of the last time anything within the wiki was changed)
879  io_saveFile($conf['cachedir'].'/purgefile',time());
880}
881
882/**
883 * moves the current version to the attic and returns its
884 * revision date
885 *
886 * @author Andreas Gohr <andi@splitbrain.org>
887 */
888function saveOldRevision($id){
889  global $conf;
890  $oldf = wikiFN($id);
891  if(!@file_exists($oldf)) return '';
892  $date = filemtime($oldf);
893  $newf = wikiFN($id,$date);
894  io_writeWikiPage($newf, rawWiki($id), $id, $date);
895  return $date;
896}
897
898/**
899 * Sends a notify mail on page change
900 *
901 * @param  string  $id       The changed page
902 * @param  string  $who      Who to notify (admin|subscribers)
903 * @param  int     $rev      Old page revision
904 * @param  string  $summary  What changed
905 * @param  boolean $minor    Is this a minor edit?
906 * @param  array   $replace  Additional string substitutions, @KEY@ to be replaced by value
907 *
908 * @author Andreas Gohr <andi@splitbrain.org>
909 */
910function notify($id,$who,$rev='',$summary='',$minor=false,$replace=array()){
911  global $lang;
912  global $conf;
913  global $INFO;
914
915  // decide if there is something to do
916  if($who == 'admin'){
917    if(empty($conf['notify'])) return; //notify enabled?
918    $text = rawLocale('mailtext');
919    $to   = $conf['notify'];
920    $bcc  = '';
921  }elseif($who == 'subscribers'){
922    if(!$conf['subscribers']) return; //subscribers enabled?
923    if($conf['useacl'] && $_SERVER['REMOTE_USER'] && $minor) return; //skip minors
924    $bcc  = subscriber_addresslist($id);
925    if(empty($bcc)) return;
926    $to   = '';
927    $text = rawLocale('subscribermail');
928  }elseif($who == 'register'){
929    if(empty($conf['registernotify'])) return;
930    $text = rawLocale('registermail');
931    $to   = $conf['registernotify'];
932    $bcc  = '';
933  }else{
934    return; //just to be safe
935  }
936
937  $ip   = clientIP();
938  $text = str_replace('@DATE@',strftime($conf['dformat']),$text);
939  $text = str_replace('@BROWSER@',$_SERVER['HTTP_USER_AGENT'],$text);
940  $text = str_replace('@IPADDRESS@',$ip,$text);
941  $text = str_replace('@HOSTNAME@',gethostsbyaddrs($ip),$text);
942  $text = str_replace('@NEWPAGE@',wl($id,'',true,'&'),$text);
943  $text = str_replace('@PAGE@',$id,$text);
944  $text = str_replace('@TITLE@',$conf['title'],$text);
945  $text = str_replace('@DOKUWIKIURL@',DOKU_URL,$text);
946  $text = str_replace('@SUMMARY@',$summary,$text);
947  $text = str_replace('@USER@',$_SERVER['REMOTE_USER'],$text);
948
949  foreach ($replace as $key => $substitution) {
950    $text = str_replace('@'.strtoupper($key).'@',$substitution, $text);
951  }
952
953  if($who == 'register'){
954    $subject = $lang['mail_new_user'].' '.$summary;
955  }elseif($rev){
956    $subject = $lang['mail_changed'].' '.$id;
957    $text = str_replace('@OLDPAGE@',wl($id,"rev=$rev",true,'&'),$text);
958    require_once(DOKU_INC.'inc/DifferenceEngine.php');
959    $df  = new Diff(split("\n",rawWiki($id,$rev)),
960                    split("\n",rawWiki($id)));
961    $dformat = new UnifiedDiffFormatter();
962    $diff    = $dformat->format($df);
963  }else{
964    $subject=$lang['mail_newpage'].' '.$id;
965    $text = str_replace('@OLDPAGE@','none',$text);
966    $diff = rawWiki($id);
967  }
968  $text = str_replace('@DIFF@',$diff,$text);
969  $subject = '['.$conf['title'].'] '.$subject;
970
971  $from = $conf['mailfrom'];
972  $from = str_replace('@USER@',$_SERVER['REMOTE_USER'],$from);
973  $from = str_replace('@NAME@',$INFO['userinfo']['name'],$from);
974  $from = str_replace('@MAIL@',$INFO['userinfo']['mail'],$from);
975
976  mail_send($to,$subject,$text,$from,'',$bcc);
977}
978
979/**
980 * extracts the query from a search engine referrer
981 *
982 * @author Andreas Gohr <andi@splitbrain.org>
983 * @author Todd Augsburger <todd@rollerorgans.com>
984 */
985function getGoogleQuery(){
986  $url = parse_url($_SERVER['HTTP_REFERER']);
987  if(!$url) return '';
988
989  $query = array();
990  parse_str($url['query'],$query);
991  if(isset($query['q']))
992    $q = $query['q'];        // google, live/msn, aol, ask, altavista, alltheweb, gigablast
993  elseif(isset($query['p']))
994    $q = $query['p'];        // yahoo
995  elseif(isset($query['query']))
996    $q = $query['query'];    // lycos, netscape, clusty, hotbot
997  elseif(preg_match("#a9\.com#i",$url['host'])) // a9
998    $q = urldecode(ltrim($url['path'],'/'));
999
1000  if(!$q) return '';
1001  $q = join('|',preg_split('/[\s\'"\\\\`()\]\[?:!\.{};,#+*<>\\/]+/',$q,-1,PREG_SPLIT_NO_EMPTY));
1002  return $q;
1003}
1004
1005/**
1006 * Try to set correct locale
1007 *
1008 * @deprecated No longer used
1009 * @author     Andreas Gohr <andi@splitbrain.org>
1010 */
1011function setCorrectLocale(){
1012  global $conf;
1013  global $lang;
1014
1015  $enc = strtoupper($lang['encoding']);
1016  foreach ($lang['locales'] as $loc){
1017    //try locale
1018    if(@setlocale(LC_ALL,$loc)) return;
1019    //try loceale with encoding
1020    if(@setlocale(LC_ALL,"$loc.$enc")) return;
1021  }
1022  //still here? try to set from environment
1023  @setlocale(LC_ALL,"");
1024}
1025
1026/**
1027 * Return the human readable size of a file
1028 *
1029 * @param       int    $size   A file size
1030 * @param       int    $dec    A number of decimal places
1031 * @author      Martin Benjamin <b.martin@cybernet.ch>
1032 * @author      Aidan Lister <aidan@php.net>
1033 * @version     1.0.0
1034 */
1035function filesize_h($size, $dec = 1){
1036  $sizes = array('B', 'KB', 'MB', 'GB');
1037  $count = count($sizes);
1038  $i = 0;
1039
1040  while ($size >= 1024 && ($i < $count - 1)) {
1041    $size /= 1024;
1042    $i++;
1043  }
1044
1045  return round($size, $dec) . ' ' . $sizes[$i];
1046}
1047
1048/**
1049 * return an obfuscated email address in line with $conf['mailguard'] setting
1050 *
1051 * @author Harry Fuecks <hfuecks@gmail.com>
1052 * @author Christopher Smith <chris@jalakai.co.uk>
1053 */
1054function obfuscate($email) {
1055  global $conf;
1056
1057  switch ($conf['mailguard']) {
1058    case 'visible' :
1059      $obfuscate = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] ');
1060      return strtr($email, $obfuscate);
1061
1062    case 'hex' :
1063      $encode = '';
1064      for ($x=0; $x < strlen($email); $x++) $encode .= '&#x' . bin2hex($email{$x}).';';
1065      return $encode;
1066
1067    case 'none' :
1068    default :
1069      return $email;
1070  }
1071}
1072
1073/**
1074 * Let us know if a user is tracking a page
1075 *
1076 * @author Andreas Gohr <andi@splitbrain.org>
1077 */
1078function is_subscribed($id,$uid){
1079  $file=metaFN($id,'.mlist');
1080  if (@file_exists($file)) {
1081    $mlist = file($file);
1082    $pos = array_search($uid."\n",$mlist);
1083    return is_int($pos);
1084  }
1085
1086  return false;
1087}
1088
1089/**
1090 * Return a string with the email addresses of all the
1091 * users subscribed to a page
1092 *
1093 * @author Steven Danz <steven-danz@kc.rr.com>
1094 */
1095function subscriber_addresslist($id){
1096  global $conf;
1097  global $auth;
1098
1099  $emails = '';
1100
1101  if (!$conf['subscribers']) return;
1102
1103  $mlist = array();
1104  $file=metaFN($id,'.mlist');
1105  if (@file_exists($file)) {
1106    $mlist = file($file);
1107  }
1108  if(count($mlist) > 0) {
1109    foreach ($mlist as $who) {
1110      $who = rtrim($who);
1111      $info = $auth->getUserData($who);
1112      if($info === false) continue;
1113      $level = auth_aclcheck($id,$who,$info['grps']);
1114      if ($level >= AUTH_READ) {
1115        if (strcasecmp($info['mail'],$conf['notify']) != 0) {
1116          if (empty($emails)) {
1117            $emails = $info['mail'];
1118          } else {
1119            $emails = "$emails,".$info['mail'];
1120          }
1121        }
1122      }
1123    }
1124  }
1125
1126  return $emails;
1127}
1128
1129/**
1130 * Removes quoting backslashes
1131 *
1132 * @author Andreas Gohr <andi@splitbrain.org>
1133 */
1134function unslash($string,$char="'"){
1135  return str_replace('\\'.$char,$char,$string);
1136}
1137
1138/**
1139 * Convert php.ini shorthands to byte
1140 *
1141 * @author <gilthans dot NO dot SPAM at gmail dot com>
1142 * @link   http://de3.php.net/manual/en/ini.core.php#79564
1143 */
1144function php_to_byte($v){
1145    $l = substr($v, -1);
1146    $ret = substr($v, 0, -1);
1147    switch(strtoupper($l)){
1148        case 'P':
1149            $ret *= 1024;
1150        case 'T':
1151            $ret *= 1024;
1152        case 'G':
1153            $ret *= 1024;
1154        case 'M':
1155            $ret *= 1024;
1156        case 'K':
1157            $ret *= 1024;
1158        break;
1159    }
1160    return $ret;
1161}
1162
1163
1164
1165//Setup VIM: ex: et ts=2 enc=utf-8 :
1166