xref: /dokuwiki/inc/common.php (revision 7d644fc84a081164fcbedbe2b6aad7127ac4e4d0)
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  // we need the callback to work around strftime's char limit
742  $tpl = preg_replace_callback('/%./',create_function('$m','return strftime($m[0]);'),$tpl);
743
744  return $tpl;
745}
746
747
748/**
749 * Returns the raw Wiki Text in three slices.
750 *
751 * The range parameter needs to have the form "from-to"
752 * and gives the range of the section in bytes - no
753 * UTF-8 awareness is needed.
754 * The returned order is prefix, section and suffix.
755 *
756 * @author Andreas Gohr <andi@splitbrain.org>
757 */
758function rawWikiSlices($range,$id,$rev=''){
759  list($from,$to) = split('-',$range,2);
760  $text = io_readWikiPage(wikiFN($id, $rev), $id, $rev);
761  if(!$from) $from = 0;
762  if(!$to)   $to   = strlen($text)+1;
763
764  $slices[0] = substr($text,0,$from-1);
765  $slices[1] = substr($text,$from-1,$to-$from);
766  $slices[2] = substr($text,$to);
767
768  return $slices;
769}
770
771/**
772 * Joins wiki text slices
773 *
774 * function to join the text slices with correct lineendings again.
775 * When the pretty parameter is set to true it adds additional empty
776 * lines between sections if needed (used on saving).
777 *
778 * @author Andreas Gohr <andi@splitbrain.org>
779 */
780function con($pre,$text,$suf,$pretty=false){
781
782  if($pretty){
783    if($pre && substr($pre,-1) != "\n") $pre .= "\n";
784    if($suf && substr($text,-1) != "\n") $text .= "\n";
785  }
786
787  if($pre) $pre .= "\n";
788  if($suf) $text .= "\n";
789  return $pre.$text.$suf;
790}
791
792/**
793 * Saves a wikitext by calling io_writeWikiPage.
794 * Also directs changelog and attic updates.
795 *
796 * @author Andreas Gohr <andi@splitbrain.org>
797 * @author Ben Coburn <btcoburn@silicodon.net>
798 */
799function saveWikiText($id,$text,$summary,$minor=false){
800  /* Note to developers:
801     This code is subtle and delicate. Test the behavior of
802     the attic and changelog with dokuwiki and external edits
803     after any changes. External edits change the wiki page
804     directly without using php or dokuwiki.
805  */
806  global $conf;
807  global $lang;
808  global $REV;
809  // ignore if no changes were made
810  if($text == rawWiki($id,'')){
811    return;
812  }
813
814  $file = wikiFN($id);
815  $old = @filemtime($file); // from page
816  $wasRemoved = empty($text);
817  $wasCreated = !@file_exists($file);
818  $wasReverted = ($REV==true);
819  $newRev = false;
820  $oldRev = getRevisions($id, -1, 1, 1024); // from changelog
821  $oldRev = (int)(empty($oldRev)?0:$oldRev[0]);
822  if(!@file_exists(wikiFN($id, $old)) && @file_exists($file) && $old>=$oldRev) {
823    // add old revision to the attic if missing
824    saveOldRevision($id);
825    // add a changelog entry if this edit came from outside dokuwiki
826    if ($old>$oldRev) {
827      addLogEntry($old, $id, DOKU_CHANGE_TYPE_EDIT, $lang['external_edit'], '', array('ExternalEdit'=>true));
828      // remove soon to be stale instructions
829      $cache = new cache_instructions($id, $file);
830      $cache->removeCache();
831    }
832  }
833
834  if ($wasRemoved){
835    // Send "update" event with empty data, so plugins can react to page deletion
836    $data = array(array($file, '', false), getNS($id), noNS($id), false);
837    trigger_event('IO_WIKIPAGE_WRITE', $data);
838    // pre-save deleted revision
839    @touch($file);
840    clearstatcache();
841    $newRev = saveOldRevision($id);
842    // remove empty file
843    @unlink($file);
844    // remove old meta info...
845    $mfiles = metaFiles($id);
846    $changelog = metaFN($id, '.changes');
847    $metadata  = metaFN($id, '.meta');
848    foreach ($mfiles as $mfile) {
849      // but keep per-page changelog to preserve page history and keep meta data
850      if (@file_exists($mfile) && $mfile!==$changelog && $mfile!==$metadata) { @unlink($mfile); }
851    }
852    // purge meta data
853    p_purge_metadata($id);
854    $del = true;
855    // autoset summary on deletion
856    if(empty($summary)) $summary = $lang['deleted'];
857    // remove empty namespaces
858    io_sweepNS($id, 'datadir');
859    io_sweepNS($id, 'mediadir');
860  }else{
861    // save file (namespace dir is created in io_writeWikiPage)
862    io_writeWikiPage($file, $text, $id);
863    // pre-save the revision, to keep the attic in sync
864    $newRev = saveOldRevision($id);
865    $del = false;
866  }
867
868  // select changelog line type
869  $extra = '';
870  $type = DOKU_CHANGE_TYPE_EDIT;
871  if ($wasReverted) {
872    $type = DOKU_CHANGE_TYPE_REVERT;
873    $extra = $REV;
874  }
875  else if ($wasCreated) { $type = DOKU_CHANGE_TYPE_CREATE; }
876  else if ($wasRemoved) { $type = DOKU_CHANGE_TYPE_DELETE; }
877  else if ($minor && $conf['useacl'] && $_SERVER['REMOTE_USER']) { $type = DOKU_CHANGE_TYPE_MINOR_EDIT; } //minor edits only for logged in users
878
879  addLogEntry($newRev, $id, $type, $summary, $extra);
880  // send notify mails
881  notify($id,'admin',$old,$summary,$minor);
882  notify($id,'subscribers',$old,$summary,$minor);
883
884  // update the purgefile (timestamp of the last time anything within the wiki was changed)
885  io_saveFile($conf['cachedir'].'/purgefile',time());
886}
887
888/**
889 * moves the current version to the attic and returns its
890 * revision date
891 *
892 * @author Andreas Gohr <andi@splitbrain.org>
893 */
894function saveOldRevision($id){
895  global $conf;
896  $oldf = wikiFN($id);
897  if(!@file_exists($oldf)) return '';
898  $date = filemtime($oldf);
899  $newf = wikiFN($id,$date);
900  io_writeWikiPage($newf, rawWiki($id), $id, $date);
901  return $date;
902}
903
904/**
905 * Sends a notify mail on page change
906 *
907 * @param  string  $id       The changed page
908 * @param  string  $who      Who to notify (admin|subscribers)
909 * @param  int     $rev      Old page revision
910 * @param  string  $summary  What changed
911 * @param  boolean $minor    Is this a minor edit?
912 * @param  array   $replace  Additional string substitutions, @KEY@ to be replaced by value
913 *
914 * @author Andreas Gohr <andi@splitbrain.org>
915 */
916function notify($id,$who,$rev='',$summary='',$minor=false,$replace=array()){
917  global $lang;
918  global $conf;
919  global $INFO;
920
921  // decide if there is something to do
922  if($who == 'admin'){
923    if(empty($conf['notify'])) return; //notify enabled?
924    $text = rawLocale('mailtext');
925    $to   = $conf['notify'];
926    $bcc  = '';
927  }elseif($who == 'subscribers'){
928    if(!$conf['subscribers']) return; //subscribers enabled?
929    if($conf['useacl'] && $_SERVER['REMOTE_USER'] && $minor) return; //skip minors
930    $bcc  = subscriber_addresslist($id);
931    if(empty($bcc)) return;
932    $to   = '';
933    $text = rawLocale('subscribermail');
934  }elseif($who == 'register'){
935    if(empty($conf['registernotify'])) return;
936    $text = rawLocale('registermail');
937    $to   = $conf['registernotify'];
938    $bcc  = '';
939  }else{
940    return; //just to be safe
941  }
942
943  $ip   = clientIP();
944  $text = str_replace('@DATE@',strftime($conf['dformat']),$text);
945  $text = str_replace('@BROWSER@',$_SERVER['HTTP_USER_AGENT'],$text);
946  $text = str_replace('@IPADDRESS@',$ip,$text);
947  $text = str_replace('@HOSTNAME@',gethostsbyaddrs($ip),$text);
948  $text = str_replace('@NEWPAGE@',wl($id,'',true,'&'),$text);
949  $text = str_replace('@PAGE@',$id,$text);
950  $text = str_replace('@TITLE@',$conf['title'],$text);
951  $text = str_replace('@DOKUWIKIURL@',DOKU_URL,$text);
952  $text = str_replace('@SUMMARY@',$summary,$text);
953  $text = str_replace('@USER@',$_SERVER['REMOTE_USER'],$text);
954
955  foreach ($replace as $key => $substitution) {
956    $text = str_replace('@'.strtoupper($key).'@',$substitution, $text);
957  }
958
959  if($who == 'register'){
960    $subject = $lang['mail_new_user'].' '.$summary;
961  }elseif($rev){
962    $subject = $lang['mail_changed'].' '.$id;
963    $text = str_replace('@OLDPAGE@',wl($id,"rev=$rev",true,'&'),$text);
964    require_once(DOKU_INC.'inc/DifferenceEngine.php');
965    $df  = new Diff(split("\n",rawWiki($id,$rev)),
966                    split("\n",rawWiki($id)));
967    $dformat = new UnifiedDiffFormatter();
968    $diff    = $dformat->format($df);
969  }else{
970    $subject=$lang['mail_newpage'].' '.$id;
971    $text = str_replace('@OLDPAGE@','none',$text);
972    $diff = rawWiki($id);
973  }
974  $text = str_replace('@DIFF@',$diff,$text);
975  $subject = '['.$conf['title'].'] '.$subject;
976
977  $from = $conf['mailfrom'];
978  $from = str_replace('@USER@',$_SERVER['REMOTE_USER'],$from);
979  $from = str_replace('@NAME@',$INFO['userinfo']['name'],$from);
980  $from = str_replace('@MAIL@',$INFO['userinfo']['mail'],$from);
981
982  mail_send($to,$subject,$text,$from,'',$bcc);
983}
984
985/**
986 * extracts the query from a search engine referrer
987 *
988 * @author Andreas Gohr <andi@splitbrain.org>
989 * @author Todd Augsburger <todd@rollerorgans.com>
990 */
991function getGoogleQuery(){
992  $url = parse_url($_SERVER['HTTP_REFERER']);
993  if(!$url) return '';
994
995  $query = array();
996  parse_str($url['query'],$query);
997  if(isset($query['q']))
998    $q = $query['q'];        // google, live/msn, aol, ask, altavista, alltheweb, gigablast
999  elseif(isset($query['p']))
1000    $q = $query['p'];        // yahoo
1001  elseif(isset($query['query']))
1002    $q = $query['query'];    // lycos, netscape, clusty, hotbot
1003  elseif(preg_match("#a9\.com#i",$url['host'])) // a9
1004    $q = urldecode(ltrim($url['path'],'/'));
1005
1006  if(!$q) return '';
1007  $q = preg_split('/[\s\'"\\\\`()\]\[?:!\.{};,#+*<>\\/]+/',$q,-1,PREG_SPLIT_NO_EMPTY);
1008  return $q;
1009}
1010
1011/**
1012 * Try to set correct locale
1013 *
1014 * @deprecated No longer used
1015 * @author     Andreas Gohr <andi@splitbrain.org>
1016 */
1017function setCorrectLocale(){
1018  global $conf;
1019  global $lang;
1020
1021  $enc = strtoupper($lang['encoding']);
1022  foreach ($lang['locales'] as $loc){
1023    //try locale
1024    if(@setlocale(LC_ALL,$loc)) return;
1025    //try loceale with encoding
1026    if(@setlocale(LC_ALL,"$loc.$enc")) return;
1027  }
1028  //still here? try to set from environment
1029  @setlocale(LC_ALL,"");
1030}
1031
1032/**
1033 * Return the human readable size of a file
1034 *
1035 * @param       int    $size   A file size
1036 * @param       int    $dec    A number of decimal places
1037 * @author      Martin Benjamin <b.martin@cybernet.ch>
1038 * @author      Aidan Lister <aidan@php.net>
1039 * @version     1.0.0
1040 */
1041function filesize_h($size, $dec = 1){
1042  $sizes = array('B', 'KB', 'MB', 'GB');
1043  $count = count($sizes);
1044  $i = 0;
1045
1046  while ($size >= 1024 && ($i < $count - 1)) {
1047    $size /= 1024;
1048    $i++;
1049  }
1050
1051  return round($size, $dec) . ' ' . $sizes[$i];
1052}
1053
1054/**
1055 * return an obfuscated email address in line with $conf['mailguard'] setting
1056 *
1057 * @author Harry Fuecks <hfuecks@gmail.com>
1058 * @author Christopher Smith <chris@jalakai.co.uk>
1059 */
1060function obfuscate($email) {
1061  global $conf;
1062
1063  switch ($conf['mailguard']) {
1064    case 'visible' :
1065      $obfuscate = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] ');
1066      return strtr($email, $obfuscate);
1067
1068    case 'hex' :
1069      $encode = '';
1070      for ($x=0; $x < strlen($email); $x++) $encode .= '&#x' . bin2hex($email{$x}).';';
1071      return $encode;
1072
1073    case 'none' :
1074    default :
1075      return $email;
1076  }
1077}
1078
1079/**
1080 * Let us know if a user is tracking a page or a namespace
1081 *
1082 * @author Andreas Gohr <andi@splitbrain.org>
1083 */
1084function is_subscribed($id,$uid,$ns=false){
1085  if(!$ns) {
1086    $file=metaFN($id,'.mlist');
1087  } else {
1088    if(!getNS($id)) {
1089      $file = metaFN(getNS($id),'.mlist');
1090    } else {
1091      $file = metaFN(getNS($id),'/.mlist');
1092    }
1093  }
1094  if (@file_exists($file)) {
1095    $mlist = file($file);
1096    $pos = array_search($uid."\n",$mlist);
1097    return is_int($pos);
1098  }
1099
1100  return false;
1101}
1102
1103/**
1104 * Return a string with the email addresses of all the
1105 * users subscribed to a page
1106 *
1107 * @author Steven Danz <steven-danz@kc.rr.com>
1108 */
1109function subscriber_addresslist($id){
1110  global $conf;
1111  global $auth;
1112
1113  if (!$conf['subscribers']) return '';
1114
1115  $users = array();
1116  $emails = array();
1117
1118  // load the page mlist file content
1119  $mlist = array();
1120  $file=metaFN($id,'.mlist');
1121  if (@file_exists($file)) {
1122    $mlist = file($file);
1123    foreach ($mlist as $who) {
1124      $who = rtrim($who);
1125      $users[$who] = true;
1126    }
1127  }
1128
1129  // load also the namespace mlist file content
1130  $ns = getNS($id);
1131  while ($ns) {
1132    $nsfile = metaFN($ns,'/.mlist');
1133    if (@file_exists($nsfile)) {
1134      $mlist = file($nsfile);
1135      foreach ($mlist as $who) {
1136        $who = rtrim($who);
1137        $users[$who] = true;
1138      }
1139    }
1140    $ns = getNS($ns);
1141  }
1142  // root namespace
1143  $nsfile = metaFN('','.mlist');
1144  if (@file_exists($nsfile)) {
1145    $mlist = file($nsfile);
1146    foreach ($mlist as $who) {
1147      $who = rtrim($who);
1148      $users[$who] = true;
1149    }
1150  }
1151  if(!empty($users)) {
1152    foreach (array_keys($users) as $who) {
1153      $info = $auth->getUserData($who);
1154      if($info === false) continue;
1155      $level = auth_aclcheck($id,$who,$info['grps']);
1156      if ($level >= AUTH_READ) {
1157        if (strcasecmp($info['mail'],$conf['notify']) != 0) {
1158          $emails[] = $info['mail'];
1159        }
1160      }
1161    }
1162  }
1163
1164  return implode(',',$emails);
1165}
1166
1167/**
1168 * Removes quoting backslashes
1169 *
1170 * @author Andreas Gohr <andi@splitbrain.org>
1171 */
1172function unslash($string,$char="'"){
1173  return str_replace('\\'.$char,$char,$string);
1174}
1175
1176/**
1177 * Convert php.ini shorthands to byte
1178 *
1179 * @author <gilthans dot NO dot SPAM at gmail dot com>
1180 * @link   http://de3.php.net/manual/en/ini.core.php#79564
1181 */
1182function php_to_byte($v){
1183    $l = substr($v, -1);
1184    $ret = substr($v, 0, -1);
1185    switch(strtoupper($l)){
1186        case 'P':
1187            $ret *= 1024;
1188        case 'T':
1189            $ret *= 1024;
1190        case 'G':
1191            $ret *= 1024;
1192        case 'M':
1193            $ret *= 1024;
1194        case 'K':
1195            $ret *= 1024;
1196        break;
1197    }
1198    return $ret;
1199}
1200
1201/**
1202 * Wrapper around preg_quote adding the default delimiter
1203 */
1204function preg_quote_cb($string){
1205    return preg_quote($string,'/');
1206}
1207
1208//Setup VIM: ex: et ts=2 enc=utf-8 :
1209