xref: /dokuwiki/inc/common.php (revision 20e29859d7134c28e50a97c828f78aa7fe719352)
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  // some IPv4/v6 regexps borrowed from Feyd
598  // see: http://forums.devnetwork.net/viewtopic.php?f=38&t=53479
599  $dec_octet = '(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|[0-9])';
600  $hex_digit = '[A-Fa-f0-9]';
601  $h16 = "{$hex_digit}{1,4}";
602  $IPv4Address = "$dec_octet\\.$dec_octet\\.$dec_octet\\.$dec_octet";
603  $ls32 = "(?:$h16:$h16|$IPv4Address)";
604  $IPv6Address =
605    "(?:(?:{$IPv4Address})|(?:".
606    "(?:$h16:){6}$ls32" .
607    "|::(?:$h16:){5}$ls32" .
608    "|(?:$h16)?::(?:$h16:){4}$ls32" .
609    "|(?:(?:$h16:){0,1}$h16)?::(?:$h16:){3}$ls32" .
610    "|(?:(?:$h16:){0,2}$h16)?::(?:$h16:){2}$ls32" .
611    "|(?:(?:$h16:){0,3}$h16)?::(?:$h16:){1}$ls32" .
612    "|(?:(?:$h16:){0,4}$h16)?::$ls32" .
613    "|(?:(?:$h16:){0,5}$h16)?::$h16" .
614    "|(?:(?:$h16:){0,6}$h16)?::" .
615    ")(?:\\/(?:12[0-8]|1[0-1][0-9]|[1-9][0-9]|[0-9]))?)";
616
617  // remove any non-IP stuff
618  $cnt = count($ip);
619  $match = array();
620  for($i=0; $i<$cnt; $i++){
621    if(preg_match("/^$IPv4Address$/",$ip[$i],$match) || preg_match("/^$IPv6Address$/",$ip[$i],$match)) {
622      $ip[$i] = $match[0];
623    } else {
624      $ip[$i] = '';
625    }
626    if(empty($ip[$i])) unset($ip[$i]);
627  }
628  $ip = array_values(array_unique($ip));
629  if(!$ip[0]) $ip[0] = '0.0.0.0'; // for some strange reason we don't have a IP
630
631  if(!$single) return join(',',$ip);
632
633  // decide which IP to use, trying to avoid local addresses
634  $ip = array_reverse($ip);
635  foreach($ip as $i){
636    if(preg_match('/^(127\.|10\.|192\.168\.|172\.((1[6-9])|(2[0-9])|(3[0-1]))\.)/',$i)){
637      continue;
638    }else{
639      return $i;
640    }
641  }
642  // still here? just use the first (last) address
643  return $ip[0];
644}
645
646/**
647 * Check if the browser is on a mobile device
648 *
649 * Adapted from the example code at url below
650 *
651 * @link http://www.brainhandles.com/2007/10/15/detecting-mobile-browsers/#code
652 */
653function clientismobile(){
654
655    if(isset($_SERVER['HTTP_X_WAP_PROFILE'])) return true;
656
657    if(preg_match('/wap\.|\.wap/i',$_SERVER['HTTP_ACCEPT'])) return true;
658
659    if(!isset($_SERVER['HTTP_USER_AGENT'])) return false;
660
661    $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';
662
663    if(preg_match("/$uamatches/i",$_SERVER['HTTP_USER_AGENT'])) return true;
664
665    return false;
666}
667
668
669/**
670 * Convert one or more comma separated IPs to hostnames
671 *
672 * @author Glen Harris <astfgl@iamnota.org>
673 * @returns a comma separated list of hostnames
674 */
675function gethostsbyaddrs($ips){
676  $hosts = array();
677  $ips = explode(',',$ips);
678
679  if(is_array($ips)) {
680    foreach($ips as $ip){
681      $hosts[] = gethostbyaddr(trim($ip));
682    }
683    return join(',',$hosts);
684  } else {
685    return gethostbyaddr(trim($ips));
686  }
687}
688
689/**
690 * Checks if a given page is currently locked.
691 *
692 * removes stale lockfiles
693 *
694 * @author Andreas Gohr <andi@splitbrain.org>
695 */
696function checklock($id){
697  global $conf;
698  $lock = wikiLockFN($id);
699
700  //no lockfile
701  if(!@file_exists($lock)) return false;
702
703  //lockfile expired
704  if((time() - filemtime($lock)) > $conf['locktime']){
705    @unlink($lock);
706    return false;
707  }
708
709  //my own lock
710  $ip = io_readFile($lock);
711  if( ($ip == clientIP()) || ($ip == $_SERVER['REMOTE_USER']) ){
712    return false;
713  }
714
715  return $ip;
716}
717
718/**
719 * Lock a page for editing
720 *
721 * @author Andreas Gohr <andi@splitbrain.org>
722 */
723function lock($id){
724  $lock = wikiLockFN($id);
725  if($_SERVER['REMOTE_USER']){
726    io_saveFile($lock,$_SERVER['REMOTE_USER']);
727  }else{
728    io_saveFile($lock,clientIP());
729  }
730}
731
732/**
733 * Unlock a page if it was locked by the user
734 *
735 * @author Andreas Gohr <andi@splitbrain.org>
736 * @return bool true if a lock was removed
737 */
738function unlock($id){
739  $lock = wikiLockFN($id);
740  if(@file_exists($lock)){
741    $ip = io_readFile($lock);
742    if( ($ip == clientIP()) || ($ip == $_SERVER['REMOTE_USER']) ){
743      @unlink($lock);
744      return true;
745    }
746  }
747  return false;
748}
749
750/**
751 * convert line ending to unix format
752 *
753 * @see    formText() for 2crlf conversion
754 * @author Andreas Gohr <andi@splitbrain.org>
755 */
756function cleanText($text){
757  $text = preg_replace("/(\015\012)|(\015)/","\012",$text);
758  return $text;
759}
760
761/**
762 * Prepares text for print in Webforms by encoding special chars.
763 * It also converts line endings to Windows format which is
764 * pseudo standard for webforms.
765 *
766 * @see    cleanText() for 2unix conversion
767 * @author Andreas Gohr <andi@splitbrain.org>
768 */
769function formText($text){
770  $text = str_replace("\012","\015\012",$text);
771  return htmlspecialchars($text);
772}
773
774/**
775 * Returns the specified local text in raw format
776 *
777 * @author Andreas Gohr <andi@splitbrain.org>
778 */
779function rawLocale($id){
780  return io_readFile(localeFN($id));
781}
782
783/**
784 * Returns the raw WikiText
785 *
786 * @author Andreas Gohr <andi@splitbrain.org>
787 */
788function rawWiki($id,$rev=''){
789  return io_readWikiPage(wikiFN($id, $rev), $id, $rev);
790}
791
792/**
793 * Returns the pagetemplate contents for the ID's namespace
794 *
795 * @author Andreas Gohr <andi@splitbrain.org>
796 */
797function pageTemplate($data){
798  $id = $data[0];
799  global $conf;
800  global $INFO;
801
802  $path = dirname(wikiFN($id));
803
804  if(@file_exists($path.'/_template.txt')){
805    $tpl = io_readFile($path.'/_template.txt');
806  }else{
807    // search upper namespaces for templates
808    $len = strlen(rtrim($conf['datadir'],'/'));
809    while (strlen($path) >= $len){
810      if(@file_exists($path.'/__template.txt')){
811        $tpl = io_readFile($path.'/__template.txt');
812        break;
813      }
814      $path = substr($path, 0, strrpos($path, '/'));
815    }
816  }
817  if(!$tpl) return '';
818
819  // replace placeholders
820  $tpl = str_replace('@ID@',$id,$tpl);
821  $tpl = str_replace('@NS@',getNS($id),$tpl);
822  $tpl = str_replace('@PAGE@',strtr(noNS($id),'_',' '),$tpl);
823  $tpl = str_replace('@USER@',$_SERVER['REMOTE_USER'],$tpl);
824  $tpl = str_replace('@NAME@',$INFO['userinfo']['name'],$tpl);
825  $tpl = str_replace('@MAIL@',$INFO['userinfo']['mail'],$tpl);
826  $tpl = str_replace('@DATE@',$conf['dformat'],$tpl);
827  // we need the callback to work around strftime's char limit
828  $tpl = preg_replace_callback('/%./',create_function('$m','return strftime($m[0]);'),$tpl);
829
830  return $tpl;
831}
832
833
834/**
835 * Returns the raw Wiki Text in three slices.
836 *
837 * The range parameter needs to have the form "from-to"
838 * and gives the range of the section in bytes - no
839 * UTF-8 awareness is needed.
840 * The returned order is prefix, section and suffix.
841 *
842 * @author Andreas Gohr <andi@splitbrain.org>
843 */
844function rawWikiSlices($range,$id,$rev=''){
845  list($from,$to) = split('-',$range,2);
846  $text = io_readWikiPage(wikiFN($id, $rev), $id, $rev);
847  if(!$from) $from = 0;
848  if(!$to)   $to   = strlen($text)+1;
849
850  $slices[0] = substr($text,0,$from-1);
851  $slices[1] = substr($text,$from-1,$to-$from);
852  $slices[2] = substr($text,$to);
853
854  return $slices;
855}
856
857/**
858 * Joins wiki text slices
859 *
860 * function to join the text slices with correct lineendings again.
861 * When the pretty parameter is set to true it adds additional empty
862 * lines between sections if needed (used on saving).
863 *
864 * @author Andreas Gohr <andi@splitbrain.org>
865 */
866function con($pre,$text,$suf,$pretty=false){
867
868  if($pretty){
869    if($pre && substr($pre,-1) != "\n") $pre .= "\n";
870    if($suf && substr($text,-1) != "\n") $text .= "\n";
871  }
872
873  // Avoid double newline above section when saving section edit
874  //if($pre) $pre .= "\n";
875  if($suf) $text .= "\n";
876  return $pre.$text.$suf;
877}
878
879/**
880 * Saves a wikitext by calling io_writeWikiPage.
881 * Also directs changelog and attic updates.
882 *
883 * @author Andreas Gohr <andi@splitbrain.org>
884 * @author Ben Coburn <btcoburn@silicodon.net>
885 */
886function saveWikiText($id,$text,$summary,$minor=false){
887  /* Note to developers:
888     This code is subtle and delicate. Test the behavior of
889     the attic and changelog with dokuwiki and external edits
890     after any changes. External edits change the wiki page
891     directly without using php or dokuwiki.
892  */
893  global $conf;
894  global $lang;
895  global $REV;
896  // ignore if no changes were made
897  if($text == rawWiki($id,'')){
898    return;
899  }
900
901  $file = wikiFN($id);
902  $old = @filemtime($file); // from page
903  $wasRemoved = empty($text);
904  $wasCreated = !@file_exists($file);
905  $wasReverted = ($REV==true);
906  $newRev = false;
907  $oldRev = getRevisions($id, -1, 1, 1024); // from changelog
908  $oldRev = (int)(empty($oldRev)?0:$oldRev[0]);
909  if(!@file_exists(wikiFN($id, $old)) && @file_exists($file) && $old>=$oldRev) {
910    // add old revision to the attic if missing
911    saveOldRevision($id);
912    // add a changelog entry if this edit came from outside dokuwiki
913    if ($old>$oldRev) {
914      addLogEntry($old, $id, DOKU_CHANGE_TYPE_EDIT, $lang['external_edit'], '', array('ExternalEdit'=>true));
915      // remove soon to be stale instructions
916      $cache = new cache_instructions($id, $file);
917      $cache->removeCache();
918    }
919  }
920
921  if ($wasRemoved){
922    // Send "update" event with empty data, so plugins can react to page deletion
923    $data = array(array($file, '', false), getNS($id), noNS($id), false);
924    trigger_event('IO_WIKIPAGE_WRITE', $data);
925    // pre-save deleted revision
926    @touch($file);
927    clearstatcache();
928    $newRev = saveOldRevision($id);
929    // remove empty file
930    @unlink($file);
931    // remove old meta info...
932    $mfiles = metaFiles($id);
933    $changelog = metaFN($id, '.changes');
934    $metadata  = metaFN($id, '.meta');
935    foreach ($mfiles as $mfile) {
936      // but keep per-page changelog to preserve page history and keep meta data
937      if (@file_exists($mfile) && $mfile!==$changelog && $mfile!==$metadata) { @unlink($mfile); }
938    }
939    // purge meta data
940    p_purge_metadata($id);
941    $del = true;
942    // autoset summary on deletion
943    if(empty($summary)) $summary = $lang['deleted'];
944    // remove empty namespaces
945    io_sweepNS($id, 'datadir');
946    io_sweepNS($id, 'mediadir');
947  }else{
948    // save file (namespace dir is created in io_writeWikiPage)
949    io_writeWikiPage($file, $text, $id);
950    // pre-save the revision, to keep the attic in sync
951    $newRev = saveOldRevision($id);
952    $del = false;
953  }
954
955  // select changelog line type
956  $extra = '';
957  $type = DOKU_CHANGE_TYPE_EDIT;
958  if ($wasReverted) {
959    $type = DOKU_CHANGE_TYPE_REVERT;
960    $extra = $REV;
961  }
962  else if ($wasCreated) { $type = DOKU_CHANGE_TYPE_CREATE; }
963  else if ($wasRemoved) { $type = DOKU_CHANGE_TYPE_DELETE; }
964  else if ($minor && $conf['useacl'] && $_SERVER['REMOTE_USER']) { $type = DOKU_CHANGE_TYPE_MINOR_EDIT; } //minor edits only for logged in users
965
966  addLogEntry($newRev, $id, $type, $summary, $extra);
967  // send notify mails
968  notify($id,'admin',$old,$summary,$minor);
969  notify($id,'subscribers',$old,$summary,$minor);
970
971  // update the purgefile (timestamp of the last time anything within the wiki was changed)
972  io_saveFile($conf['cachedir'].'/purgefile',time());
973
974  // if useheading is enabled, purge the cache of all linking pages
975  if($conf['useheading']){
976    require_once(DOKU_INC.'inc/fulltext.php');
977    $pages = ft_backlinks($id);
978    foreach ($pages as $page) {
979      $cache = new cache_renderer($page, wikiFN($page), 'xhtml');
980      $cache->removeCache();
981    }
982  }
983}
984
985/**
986 * moves the current version to the attic and returns its
987 * revision date
988 *
989 * @author Andreas Gohr <andi@splitbrain.org>
990 */
991function saveOldRevision($id){
992  global $conf;
993  $oldf = wikiFN($id);
994  if(!@file_exists($oldf)) return '';
995  $date = filemtime($oldf);
996  $newf = wikiFN($id,$date);
997  io_writeWikiPage($newf, rawWiki($id), $id, $date);
998  return $date;
999}
1000
1001/**
1002 * Sends a notify mail on page change
1003 *
1004 * @param  string  $id       The changed page
1005 * @param  string  $who      Who to notify (admin|subscribers)
1006 * @param  int     $rev      Old page revision
1007 * @param  string  $summary  What changed
1008 * @param  boolean $minor    Is this a minor edit?
1009 * @param  array   $replace  Additional string substitutions, @KEY@ to be replaced by value
1010 *
1011 * @author Andreas Gohr <andi@splitbrain.org>
1012 */
1013function notify($id,$who,$rev='',$summary='',$minor=false,$replace=array()){
1014  global $lang;
1015  global $conf;
1016  global $INFO;
1017
1018  // decide if there is something to do
1019  if($who == 'admin'){
1020    if(empty($conf['notify'])) return; //notify enabled?
1021    $text = rawLocale('mailtext');
1022    $to   = $conf['notify'];
1023    $bcc  = '';
1024  }elseif($who == 'subscribers'){
1025    if(!$conf['subscribers']) return; //subscribers enabled?
1026    if($conf['useacl'] && $_SERVER['REMOTE_USER'] && $minor) return; //skip minors
1027    $bcc  = subscriber_addresslist($id,false);
1028    if(empty($bcc)) return;
1029    $to   = '';
1030    $text = rawLocale('subscribermail');
1031  }elseif($who == 'register'){
1032    if(empty($conf['registernotify'])) return;
1033    $text = rawLocale('registermail');
1034    $to   = $conf['registernotify'];
1035    $bcc  = '';
1036  }else{
1037    return; //just to be safe
1038  }
1039
1040  $ip   = clientIP();
1041  $text = str_replace('@DATE@',strftime($conf['dformat']),$text);
1042  $text = str_replace('@BROWSER@',$_SERVER['HTTP_USER_AGENT'],$text);
1043  $text = str_replace('@IPADDRESS@',$ip,$text);
1044  $text = str_replace('@HOSTNAME@',gethostsbyaddrs($ip),$text);
1045  $text = str_replace('@NEWPAGE@',wl($id,'',true,'&'),$text);
1046  $text = str_replace('@PAGE@',$id,$text);
1047  $text = str_replace('@TITLE@',$conf['title'],$text);
1048  $text = str_replace('@DOKUWIKIURL@',DOKU_URL,$text);
1049  $text = str_replace('@SUMMARY@',$summary,$text);
1050  $text = str_replace('@USER@',$_SERVER['REMOTE_USER'],$text);
1051
1052  foreach ($replace as $key => $substitution) {
1053    $text = str_replace('@'.strtoupper($key).'@',$substitution, $text);
1054  }
1055
1056  if($who == 'register'){
1057    $subject = $lang['mail_new_user'].' '.$summary;
1058  }elseif($rev){
1059    $subject = $lang['mail_changed'].' '.$id;
1060    $text = str_replace('@OLDPAGE@',wl($id,"rev=$rev",true,'&'),$text);
1061    require_once(DOKU_INC.'inc/DifferenceEngine.php');
1062    $df  = new Diff(split("\n",rawWiki($id,$rev)),
1063                    split("\n",rawWiki($id)));
1064    $dformat = new UnifiedDiffFormatter();
1065    $diff    = $dformat->format($df);
1066  }else{
1067    $subject=$lang['mail_newpage'].' '.$id;
1068    $text = str_replace('@OLDPAGE@','none',$text);
1069    $diff = rawWiki($id);
1070  }
1071  $text = str_replace('@DIFF@',$diff,$text);
1072  $subject = '['.$conf['title'].'] '.$subject;
1073
1074  $from = $conf['mailfrom'];
1075  $from = str_replace('@USER@',$_SERVER['REMOTE_USER'],$from);
1076  $from = str_replace('@NAME@',$INFO['userinfo']['name'],$from);
1077  $from = str_replace('@MAIL@',$INFO['userinfo']['mail'],$from);
1078
1079  mail_send($to,$subject,$text,$from,'',$bcc);
1080}
1081
1082/**
1083 * extracts the query from a search engine referrer
1084 *
1085 * @author Andreas Gohr <andi@splitbrain.org>
1086 * @author Todd Augsburger <todd@rollerorgans.com>
1087 */
1088function getGoogleQuery(){
1089  $url = parse_url($_SERVER['HTTP_REFERER']);
1090  if(!$url) return '';
1091
1092  $query = array();
1093  parse_str($url['query'],$query);
1094  if(isset($query['q']))
1095    $q = $query['q'];        // google, live/msn, aol, ask, altavista, alltheweb, gigablast
1096  elseif(isset($query['p']))
1097    $q = $query['p'];        // yahoo
1098  elseif(isset($query['query']))
1099    $q = $query['query'];    // lycos, netscape, clusty, hotbot
1100  elseif(preg_match("#a9\.com#i",$url['host'])) // a9
1101    $q = urldecode(ltrim($url['path'],'/'));
1102
1103  if(!$q) return '';
1104  $q = preg_split('/[\s\'"\\\\`()\]\[?:!\.{};,#+*<>\\/]+/',$q,-1,PREG_SPLIT_NO_EMPTY);
1105  return $q;
1106}
1107
1108/**
1109 * Try to set correct locale
1110 *
1111 * @deprecated No longer used
1112 * @author     Andreas Gohr <andi@splitbrain.org>
1113 */
1114function setCorrectLocale(){
1115  global $conf;
1116  global $lang;
1117
1118  $enc = strtoupper($lang['encoding']);
1119  foreach ($lang['locales'] as $loc){
1120    //try locale
1121    if(@setlocale(LC_ALL,$loc)) return;
1122    //try loceale with encoding
1123    if(@setlocale(LC_ALL,"$loc.$enc")) return;
1124  }
1125  //still here? try to set from environment
1126  @setlocale(LC_ALL,"");
1127}
1128
1129/**
1130 * Return the human readable size of a file
1131 *
1132 * @param       int    $size   A file size
1133 * @param       int    $dec    A number of decimal places
1134 * @author      Martin Benjamin <b.martin@cybernet.ch>
1135 * @author      Aidan Lister <aidan@php.net>
1136 * @version     1.0.0
1137 */
1138function filesize_h($size, $dec = 1){
1139  $sizes = array('B', 'KB', 'MB', 'GB');
1140  $count = count($sizes);
1141  $i = 0;
1142
1143  while ($size >= 1024 && ($i < $count - 1)) {
1144    $size /= 1024;
1145    $i++;
1146  }
1147
1148  return round($size, $dec) . ' ' . $sizes[$i];
1149}
1150
1151/**
1152 * return an obfuscated email address in line with $conf['mailguard'] setting
1153 *
1154 * @author Harry Fuecks <hfuecks@gmail.com>
1155 * @author Christopher Smith <chris@jalakai.co.uk>
1156 */
1157function obfuscate($email) {
1158  global $conf;
1159
1160  switch ($conf['mailguard']) {
1161    case 'visible' :
1162      $obfuscate = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] ');
1163      return strtr($email, $obfuscate);
1164
1165    case 'hex' :
1166      $encode = '';
1167      for ($x=0; $x < strlen($email); $x++) $encode .= '&#x' . bin2hex($email{$x}).';';
1168      return $encode;
1169
1170    case 'none' :
1171    default :
1172      return $email;
1173  }
1174}
1175
1176/**
1177 * Let us know if a user is tracking a page or a namespace
1178 *
1179 * @author Andreas Gohr <andi@splitbrain.org>
1180 */
1181function is_subscribed($id,$uid,$ns=false){
1182  if(!$ns) {
1183    $file=metaFN($id,'.mlist');
1184  } else {
1185    if(!getNS($id)) {
1186      $file = metaFN(getNS($id),'.mlist');
1187    } else {
1188      $file = metaFN(getNS($id),'/.mlist');
1189    }
1190  }
1191  if (@file_exists($file)) {
1192    $mlist = file($file);
1193    $pos = array_search($uid."\n",$mlist);
1194    return is_int($pos);
1195  }
1196
1197  return false;
1198}
1199
1200/**
1201 * Return a string with the email addresses of all the
1202 * users subscribed to a page
1203 *
1204 * @author Steven Danz <steven-danz@kc.rr.com>
1205 */
1206function subscriber_addresslist($id,$self=true){
1207  global $conf;
1208  global $auth;
1209
1210  if (!$conf['subscribers']) return '';
1211
1212  $users = array();
1213  $emails = array();
1214
1215  // load the page mlist file content
1216  $mlist = array();
1217  $file=metaFN($id,'.mlist');
1218  if (@file_exists($file)) {
1219    $mlist = file($file);
1220    foreach ($mlist as $who) {
1221      $who = rtrim($who);
1222      if(!$self && $who == $_SERVER['REMOTE_USER']) continue;
1223      $users[$who] = true;
1224    }
1225  }
1226
1227  // load also the namespace mlist file content
1228  $ns = getNS($id);
1229  while ($ns) {
1230    $nsfile = metaFN($ns,'/.mlist');
1231    if (@file_exists($nsfile)) {
1232      $mlist = file($nsfile);
1233      foreach ($mlist as $who) {
1234        $who = rtrim($who);
1235        if(!$self && $who == $_SERVER['REMOTE_USER']) continue;
1236        $users[$who] = true;
1237      }
1238    }
1239    $ns = getNS($ns);
1240  }
1241  // root namespace
1242  $nsfile = metaFN('','.mlist');
1243  if (@file_exists($nsfile)) {
1244    $mlist = file($nsfile);
1245    foreach ($mlist as $who) {
1246      $who = rtrim($who);
1247      if(!$self && $who == $_SERVER['REMOTE_USER']) continue;
1248      $users[$who] = true;
1249    }
1250  }
1251  if(!empty($users)) {
1252    foreach (array_keys($users) as $who) {
1253      $info = $auth->getUserData($who);
1254      if($info === false) continue;
1255      $level = auth_aclcheck($id,$who,$info['grps']);
1256      if ($level >= AUTH_READ) {
1257        if (strcasecmp($info['mail'],$conf['notify']) != 0) {
1258          $emails[] = $info['mail'];
1259        }
1260      }
1261    }
1262  }
1263
1264  return implode(',',$emails);
1265}
1266
1267/**
1268 * Removes quoting backslashes
1269 *
1270 * @author Andreas Gohr <andi@splitbrain.org>
1271 */
1272function unslash($string,$char="'"){
1273  return str_replace('\\'.$char,$char,$string);
1274}
1275
1276/**
1277 * Convert php.ini shorthands to byte
1278 *
1279 * @author <gilthans dot NO dot SPAM at gmail dot com>
1280 * @link   http://de3.php.net/manual/en/ini.core.php#79564
1281 */
1282function php_to_byte($v){
1283    $l = substr($v, -1);
1284    $ret = substr($v, 0, -1);
1285    switch(strtoupper($l)){
1286        case 'P':
1287            $ret *= 1024;
1288        case 'T':
1289            $ret *= 1024;
1290        case 'G':
1291            $ret *= 1024;
1292        case 'M':
1293            $ret *= 1024;
1294        case 'K':
1295            $ret *= 1024;
1296        break;
1297    }
1298    return $ret;
1299}
1300
1301/**
1302 * Wrapper around preg_quote adding the default delimiter
1303 */
1304function preg_quote_cb($string){
1305    return preg_quote($string,'/');
1306}
1307
1308/**
1309 * Shorten a given string by removing data from the middle
1310 *
1311 * You can give the string in two parts, teh first part $keep
1312 * will never be shortened. The second part $short will be cut
1313 * in the middle to shorten but only if at least $min chars are
1314 * left to display it. Otherwise it will be left off.
1315 *
1316 * @param string $keep   the part to keep
1317 * @param string $short  the part to shorten
1318 * @param int    $max    maximum chars you want for the whole string
1319 * @param int    $min    minimum number of chars to have left for middle shortening
1320 * @param string $char   the shortening character to use
1321 */
1322function shorten($keep,$short,$max,$min=9,$char='⌇'){
1323    $max = $max - utf8_strlen($keep);
1324   if($max < $min) return $keep;
1325    $len = utf8_strlen($short);
1326    if($len <= $max) return $keep.$short;
1327    $half = floor($max/2);
1328    return $keep.utf8_substr($short,0,$half-1).$char.utf8_substr($short,$len-$half);
1329}
1330
1331/**
1332 * Return the users realname or e-mail address for use
1333 * in page footer and recent changes pages
1334 *
1335 * @author Andy Webber <dokuwiki AT andywebber DOT com>
1336 */
1337function editorinfo($username){
1338    global $conf;
1339    global $auth;
1340
1341    switch($conf['showuseras']){
1342      case 'username':
1343      case 'email':
1344      case 'email_link':
1345        $info = $auth->getUserData($username);
1346        break;
1347      default:
1348        return hsc($username);
1349    }
1350
1351    if(isset($info) && $info) {
1352        switch($conf['showuseras']){
1353          case 'username':
1354            return hsc($info['name']);
1355          case 'email':
1356            return obfuscate($info['mail']);
1357          case 'email_link':
1358            $mail=obfuscate($info['mail']);
1359            return '<a href="mailto:'.$mail.'">'.$mail.'</a>';
1360          default:
1361            return hsc($username);
1362        }
1363    } else {
1364        return hsc($username);
1365    }
1366}
1367
1368/**
1369 * Returns the path to a image file for the currently chosen license.
1370 * When no image exists, returns an empty string
1371 *
1372 * @author Andreas Gohr <andi@splitbrain.org>
1373 * @param  string $type - type of image 'badge' or 'button'
1374 */
1375function license_img($type){
1376    global $license;
1377    global $conf;
1378    if(!$conf['license']) return '';
1379    if(!is_array($license[$conf['license']])) return '';
1380    $lic = $license[$conf['license']];
1381    $try = array();
1382    $try[] = 'lib/images/license/'.$type.'/'.$conf['license'].'.png';
1383    $try[] = 'lib/images/license/'.$type.'/'.$conf['license'].'.gif';
1384    if(substr($conf['license'],0,3) == 'cc-'){
1385        $try[] = 'lib/images/license/'.$type.'/cc.png';
1386    }
1387    foreach($try as $src){
1388        if(@file_exists(DOKU_INC.$src)) return $src;
1389    }
1390    return '';
1391}
1392
1393//Setup VIM: ex: et ts=2 enc=utf-8 :
1394