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