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