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