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