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