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