Merge pull request #4167 from dokuwiki-translate/lang_update_800_1706385011
[dokuwiki.git] / inc / common.php
blob167caddda0083c91d24cb947f3a44c587cda8e85
1 <?php
3 /**
4 * Common DokuWiki functions
6 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
7 * @author Andreas Gohr <andi@splitbrain.org>
8 */
10 use dokuwiki\PassHash;
11 use dokuwiki\Draft;
12 use dokuwiki\Utf8\Clean;
13 use dokuwiki\Utf8\PhpString;
14 use dokuwiki\Utf8\Conversion;
15 use dokuwiki\Cache\CacheRenderer;
16 use dokuwiki\ChangeLog\PageChangeLog;
17 use dokuwiki\File\PageFile;
18 use dokuwiki\Subscriptions\PageSubscriptionSender;
19 use dokuwiki\Subscriptions\SubscriberManager;
20 use dokuwiki\Extension\AuthPlugin;
21 use dokuwiki\Extension\Event;
23 /**
24 * Wrapper around htmlspecialchars()
26 * @author Andreas Gohr <andi@splitbrain.org>
27 * @see htmlspecialchars()
29 * @param string $string the string being converted
30 * @return string converted string
32 function hsc($string)
34 return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, 'UTF-8');
37 /**
38 * A safer explode for fixed length lists
40 * This works just like explode(), but will always return the wanted number of elements.
41 * If the $input string does not contain enough elements, the missing elements will be
42 * filled up with the $default value. If the input string contains more elements, the last
43 * one will NOT be split up and will still contain $separator
45 * @param string $separator The boundary string
46 * @param string $string The input string
47 * @param int $limit The number of expected elements
48 * @param mixed $default The value to use when filling up missing elements
49 * @see explode
50 * @return array
52 function sexplode($separator, $string, $limit, $default = null)
54 return array_pad(explode($separator, $string, $limit), $limit, $default);
57 /**
58 * Checks if the given input is blank
60 * This is similar to empty() but will return false for "0".
62 * Please note: when you pass uninitialized variables, they will implicitly be created
63 * with a NULL value without warning.
65 * To avoid this it's recommended to guard the call with isset like this:
67 * (isset($foo) && !blank($foo))
68 * (!isset($foo) || blank($foo))
70 * @param $in
71 * @param bool $trim Consider a string of whitespace to be blank
72 * @return bool
74 function blank(&$in, $trim = false)
76 if (is_null($in)) return true;
77 if (is_array($in)) return $in === [];
78 if ($in === "\0") return true;
79 if ($trim && trim($in) === '') return true;
80 if (strlen($in) > 0) return false;
81 return empty($in);
84 /**
85 * strips control characters (<32) from the given string
87 * @author Andreas Gohr <andi@splitbrain.org>
89 * @param string $string being stripped
90 * @return string
92 function stripctl($string)
94 return preg_replace('/[\x00-\x1F]+/s', '', $string);
97 /**
98 * Return a secret token to be used for CSRF attack prevention
100 * @author Andreas Gohr <andi@splitbrain.org>
101 * @link http://en.wikipedia.org/wiki/Cross-site_request_forgery
102 * @link http://christ1an.blogspot.com/2007/04/preventing-csrf-efficiently.html
104 * @return string
106 function getSecurityToken()
108 /** @var Input $INPUT */
109 global $INPUT;
111 $user = $INPUT->server->str('REMOTE_USER');
112 $session = session_id();
114 // CSRF checks are only for logged in users - do not generate for anonymous
115 if (trim($user) == '' || trim($session) == '') return '';
116 return PassHash::hmac('md5', $session . $user, auth_cookiesalt());
120 * Check the secret CSRF token
122 * @param null|string $token security token or null to read it from request variable
123 * @return bool success if the token matched
125 function checkSecurityToken($token = null)
127 /** @var Input $INPUT */
128 global $INPUT;
129 if (!$INPUT->server->str('REMOTE_USER')) return true; // no logged in user, no need for a check
131 if (is_null($token)) $token = $INPUT->str('sectok');
132 if (getSecurityToken() != $token) {
133 msg('Security Token did not match. Possible CSRF attack.', -1);
134 return false;
136 return true;
140 * Print a hidden form field with a secret CSRF token
142 * @author Andreas Gohr <andi@splitbrain.org>
144 * @param bool $print if true print the field, otherwise html of the field is returned
145 * @return string html of hidden form field
147 function formSecurityToken($print = true)
149 $ret = '<div class="no"><input type="hidden" name="sectok" value="' . getSecurityToken() . '" /></div>' . "\n";
150 if ($print) echo $ret;
151 return $ret;
155 * Determine basic information for a request of $id
157 * @author Andreas Gohr <andi@splitbrain.org>
158 * @author Chris Smith <chris@jalakai.co.uk>
160 * @param string $id pageid
161 * @param bool $htmlClient add info about whether is mobile browser
162 * @return array with info for a request of $id
165 function basicinfo($id, $htmlClient = true)
167 global $USERINFO;
168 /* @var Input $INPUT */
169 global $INPUT;
171 // set info about manager/admin status.
172 $info = [];
173 $info['isadmin'] = false;
174 $info['ismanager'] = false;
175 if ($INPUT->server->has('REMOTE_USER')) {
176 $info['userinfo'] = $USERINFO;
177 $info['perm'] = auth_quickaclcheck($id);
178 $info['client'] = $INPUT->server->str('REMOTE_USER');
180 if ($info['perm'] == AUTH_ADMIN) {
181 $info['isadmin'] = true;
182 $info['ismanager'] = true;
183 } elseif (auth_ismanager()) {
184 $info['ismanager'] = true;
187 // if some outside auth were used only REMOTE_USER is set
188 if (empty($info['userinfo']['name'])) {
189 $info['userinfo']['name'] = $INPUT->server->str('REMOTE_USER');
191 } else {
192 $info['perm'] = auth_aclcheck($id, '', null);
193 $info['client'] = clientIP(true);
196 $info['namespace'] = getNS($id);
198 // mobile detection
199 if ($htmlClient) {
200 $info['ismobile'] = clientismobile();
203 return $info;
207 * Return info about the current document as associative
208 * array.
210 * @return array with info about current document
211 * @throws Exception
213 * @author Andreas Gohr <andi@splitbrain.org>
215 function pageinfo()
217 global $ID;
218 global $REV;
219 global $RANGE;
220 global $lang;
222 $info = basicinfo($ID);
224 // include ID & REV not redundant, as some parts of DokuWiki may temporarily change $ID, e.g. p_wiki_xhtml
225 // FIXME ... perhaps it would be better to ensure the temporary changes weren't necessary
226 $info['id'] = $ID;
227 $info['rev'] = $REV;
229 $subManager = new SubscriberManager();
230 $info['subscribed'] = $subManager->userSubscription();
232 $info['locked'] = checklock($ID);
233 $info['filepath'] = wikiFN($ID);
234 $info['exists'] = file_exists($info['filepath']);
235 $info['currentrev'] = @filemtime($info['filepath']);
237 if ($REV) {
238 //check if current revision was meant
239 if ($info['exists'] && ($info['currentrev'] == $REV)) {
240 $REV = '';
241 } elseif ($RANGE) {
242 //section editing does not work with old revisions!
243 $REV = '';
244 $RANGE = '';
245 msg($lang['nosecedit'], 0);
246 } else {
247 //really use old revision
248 $info['filepath'] = wikiFN($ID, $REV);
249 $info['exists'] = file_exists($info['filepath']);
252 $info['rev'] = $REV;
253 if ($info['exists']) {
254 $info['writable'] = (is_writable($info['filepath']) && $info['perm'] >= AUTH_EDIT);
255 } else {
256 $info['writable'] = ($info['perm'] >= AUTH_CREATE);
258 $info['editable'] = ($info['writable'] && empty($info['locked']));
259 $info['lastmod'] = @filemtime($info['filepath']);
261 //load page meta data
262 $info['meta'] = p_get_metadata($ID);
264 //who's the editor
265 $pagelog = new PageChangeLog($ID, 1024);
266 if ($REV) {
267 $revinfo = $pagelog->getRevisionInfo($REV);
268 } elseif (!empty($info['meta']['last_change']) && is_array($info['meta']['last_change'])) {
269 $revinfo = $info['meta']['last_change'];
270 } else {
271 $revinfo = $pagelog->getRevisionInfo($info['lastmod']);
272 // cache most recent changelog line in metadata if missing and still valid
273 if ($revinfo !== false) {
274 $info['meta']['last_change'] = $revinfo;
275 p_set_metadata($ID, ['last_change' => $revinfo]);
278 //and check for an external edit
279 if ($revinfo !== false && $revinfo['date'] != $info['lastmod']) {
280 // cached changelog line no longer valid
281 $revinfo = false;
282 $info['meta']['last_change'] = $revinfo;
283 p_set_metadata($ID, ['last_change' => $revinfo]);
286 if ($revinfo !== false) {
287 $info['ip'] = $revinfo['ip'];
288 $info['user'] = $revinfo['user'];
289 $info['sum'] = $revinfo['sum'];
290 // See also $INFO['meta']['last_change'] which is the most recent log line for page $ID.
291 // Use $INFO['meta']['last_change']['type']===DOKU_CHANGE_TYPE_MINOR_EDIT in place of $info['minor'].
293 $info['editor'] = $revinfo['user'] ?: $revinfo['ip'];
294 } else {
295 $info['ip'] = null;
296 $info['user'] = null;
297 $info['sum'] = null;
298 $info['editor'] = null;
301 // draft
302 $draft = new Draft($ID, $info['client']);
303 if ($draft->isDraftAvailable()) {
304 $info['draft'] = $draft->getDraftFilename();
307 return $info;
311 * Initialize and/or fill global $JSINFO with some basic info to be given to javascript
313 function jsinfo()
315 global $JSINFO, $ID, $INFO, $ACT;
317 if (!is_array($JSINFO)) {
318 $JSINFO = [];
320 //export minimal info to JS, plugins can add more
321 $JSINFO['id'] = $ID;
322 $JSINFO['namespace'] = isset($INFO) ? (string) $INFO['namespace'] : '';
323 $JSINFO['ACT'] = act_clean($ACT);
324 $JSINFO['useHeadingNavigation'] = (int) useHeading('navigation');
325 $JSINFO['useHeadingContent'] = (int) useHeading('content');
329 * Return information about the current media item as an associative array.
331 * @return array with info about current media item
333 function mediainfo()
335 global $NS;
336 global $IMG;
338 $info = basicinfo("$NS:*");
339 $info['image'] = $IMG;
341 return $info;
345 * Build an string of URL parameters
347 * @author Andreas Gohr
349 * @param array $params array with key-value pairs
350 * @param string $sep series of pairs are separated by this character
351 * @return string query string
353 function buildURLparams($params, $sep = '&amp;')
355 $url = '';
356 $amp = false;
357 foreach ($params as $key => $val) {
358 if ($amp) $url .= $sep;
360 $url .= rawurlencode($key) . '=';
361 $url .= rawurlencode((string) $val);
362 $amp = true;
364 return $url;
368 * Build an string of html tag attributes
370 * Skips keys starting with '_', values get HTML encoded
372 * @author Andreas Gohr
374 * @param array $params array with (attribute name-attribute value) pairs
375 * @param bool $skipEmptyStrings skip empty string values?
376 * @return string
378 function buildAttributes($params, $skipEmptyStrings = false)
380 $url = '';
381 $white = false;
382 foreach ($params as $key => $val) {
383 if ($key[0] == '_') continue;
384 if ($val === '' && $skipEmptyStrings) continue;
385 if ($white) $url .= ' ';
387 $url .= $key . '="';
388 $url .= hsc($val);
389 $url .= '"';
390 $white = true;
392 return $url;
396 * This builds the breadcrumb trail and returns it as array
398 * @author Andreas Gohr <andi@splitbrain.org>
400 * @return string[] with the data: array(pageid=>name, ... )
402 function breadcrumbs()
404 // we prepare the breadcrumbs early for quick session closing
405 static $crumbs = null;
406 if ($crumbs != null) return $crumbs;
408 global $ID;
409 global $ACT;
410 global $conf;
411 global $INFO;
413 //first visit?
414 $crumbs = $_SESSION[DOKU_COOKIE]['bc'] ?? [];
415 //we only save on show and existing visible readable wiki documents
416 $file = wikiFN($ID);
417 if ($ACT != 'show' || $INFO['perm'] < AUTH_READ || isHiddenPage($ID) || !file_exists($file)) {
418 $_SESSION[DOKU_COOKIE]['bc'] = $crumbs;
419 return $crumbs;
422 // page names
423 $name = noNSorNS($ID);
424 if (useHeading('navigation')) {
425 // get page title
426 $title = p_get_first_heading($ID, METADATA_RENDER_USING_SIMPLE_CACHE);
427 if ($title) {
428 $name = $title;
432 //remove ID from array
433 if (isset($crumbs[$ID])) {
434 unset($crumbs[$ID]);
437 //add to array
438 $crumbs[$ID] = $name;
439 //reduce size
440 while (count($crumbs) > $conf['breadcrumbs']) {
441 array_shift($crumbs);
443 //save to session
444 $_SESSION[DOKU_COOKIE]['bc'] = $crumbs;
445 return $crumbs;
449 * Filter for page IDs
451 * This is run on a ID before it is outputted somewhere
452 * currently used to replace the colon with something else
453 * on Windows (non-IIS) systems and to have proper URL encoding
455 * See discussions at https://github.com/dokuwiki/dokuwiki/pull/84 and
456 * https://github.com/dokuwiki/dokuwiki/pull/173 why we use a whitelist of
457 * unaffected servers instead of blacklisting affected servers here.
459 * Urlencoding is ommitted when the second parameter is false
461 * @author Andreas Gohr <andi@splitbrain.org>
463 * @param string $id pageid being filtered
464 * @param bool $ue apply urlencoding?
465 * @return string
467 function idfilter($id, $ue = true)
469 global $conf;
470 /* @var Input $INPUT */
471 global $INPUT;
473 $id = (string) $id;
475 if ($conf['useslash'] && $conf['userewrite']) {
476 $id = strtr($id, ':', '/');
477 } elseif (
478 str_starts_with(strtoupper(PHP_OS), 'WIN') &&
479 $conf['userewrite'] &&
480 strpos($INPUT->server->str('SERVER_SOFTWARE'), 'Microsoft-IIS') === false
482 $id = strtr($id, ':', ';');
484 if ($ue) {
485 $id = rawurlencode($id);
486 $id = str_replace('%3A', ':', $id); //keep as colon
487 $id = str_replace('%3B', ';', $id); //keep as semicolon
488 $id = str_replace('%2F', '/', $id); //keep as slash
490 return $id;
494 * This builds a link to a wikipage
496 * It handles URL rewriting and adds additional parameters
498 * @author Andreas Gohr <andi@splitbrain.org>
500 * @param string $id page id, defaults to start page
501 * @param string|array $urlParameters URL parameters, associative array recommended
502 * @param bool $absolute request an absolute URL instead of relative
503 * @param string $separator parameter separator
504 * @return string
506 function wl($id = '', $urlParameters = '', $absolute = false, $separator = '&amp;')
508 global $conf;
509 if (is_array($urlParameters)) {
510 if (isset($urlParameters['rev']) && !$urlParameters['rev']) unset($urlParameters['rev']);
511 if (isset($urlParameters['at']) && $conf['date_at_format']) {
512 $urlParameters['at'] = date($conf['date_at_format'], $urlParameters['at']);
514 $urlParameters = buildURLparams($urlParameters, $separator);
515 } else {
516 $urlParameters = str_replace(',', $separator, $urlParameters);
518 if ($id === '') {
519 $id = $conf['start'];
521 $id = idfilter($id);
522 if ($absolute) {
523 $xlink = DOKU_URL;
524 } else {
525 $xlink = DOKU_BASE;
528 if ($conf['userewrite'] == 2) {
529 $xlink .= DOKU_SCRIPT . '/' . $id;
530 if ($urlParameters) $xlink .= '?' . $urlParameters;
531 } elseif ($conf['userewrite']) {
532 $xlink .= $id;
533 if ($urlParameters) $xlink .= '?' . $urlParameters;
534 } elseif ($id !== '') {
535 $xlink .= DOKU_SCRIPT . '?id=' . $id;
536 if ($urlParameters) $xlink .= $separator . $urlParameters;
537 } else {
538 $xlink .= DOKU_SCRIPT;
539 if ($urlParameters) $xlink .= '?' . $urlParameters;
542 return $xlink;
546 * This builds a link to an alternate page format
548 * Handles URL rewriting if enabled. Follows the style of wl().
550 * @author Ben Coburn <btcoburn@silicodon.net>
551 * @param string $id page id, defaults to start page
552 * @param string $format the export renderer to use
553 * @param string|array $urlParameters URL parameters, associative array recommended
554 * @param bool $abs request an absolute URL instead of relative
555 * @param string $sep parameter separator
556 * @return string
558 function exportlink($id = '', $format = 'raw', $urlParameters = '', $abs = false, $sep = '&amp;')
560 global $conf;
561 if (is_array($urlParameters)) {
562 $urlParameters = buildURLparams($urlParameters, $sep);
563 } else {
564 $urlParameters = str_replace(',', $sep, $urlParameters);
567 $format = rawurlencode($format);
568 $id = idfilter($id);
569 if ($abs) {
570 $xlink = DOKU_URL;
571 } else {
572 $xlink = DOKU_BASE;
575 if ($conf['userewrite'] == 2) {
576 $xlink .= DOKU_SCRIPT . '/' . $id . '?do=export_' . $format;
577 if ($urlParameters) $xlink .= $sep . $urlParameters;
578 } elseif ($conf['userewrite'] == 1) {
579 $xlink .= '_export/' . $format . '/' . $id;
580 if ($urlParameters) $xlink .= '?' . $urlParameters;
581 } else {
582 $xlink .= DOKU_SCRIPT . '?do=export_' . $format . $sep . 'id=' . $id;
583 if ($urlParameters) $xlink .= $sep . $urlParameters;
586 return $xlink;
590 * Build a link to a media file
592 * Will return a link to the detail page if $direct is false
594 * The $more parameter should always be given as array, the function then
595 * will strip default parameters to produce even cleaner URLs
597 * @param string $id the media file id or URL
598 * @param mixed $more string or array with additional parameters
599 * @param bool $direct link to detail page if false
600 * @param string $sep URL parameter separator
601 * @param bool $abs Create an absolute URL
602 * @return string
604 function ml($id = '', $more = '', $direct = true, $sep = '&amp;', $abs = false)
606 global $conf;
607 $isexternalimage = media_isexternal($id);
608 if (!$isexternalimage) {
609 $id = cleanID($id);
612 if (is_array($more)) {
613 // add token for resized images
614 $w = $more['w'] ?? null;
615 $h = $more['h'] ?? null;
616 if ($w || $h || $isexternalimage) {
617 $more['tok'] = media_get_token($id, $w, $h);
619 // strip defaults for shorter URLs
620 if (isset($more['cache']) && $more['cache'] == 'cache') unset($more['cache']);
621 if (empty($more['w'])) unset($more['w']);
622 if (empty($more['h'])) unset($more['h']);
623 if (isset($more['id']) && $direct) unset($more['id']);
624 if (isset($more['rev']) && !$more['rev']) unset($more['rev']);
625 $more = buildURLparams($more, $sep);
626 } else {
627 $matches = [];
628 if (preg_match_all('/\b(w|h)=(\d*)\b/', $more, $matches, PREG_SET_ORDER) || $isexternalimage) {
629 $resize = ['w' => 0, 'h' => 0];
630 foreach ($matches as $match) {
631 $resize[$match[1]] = $match[2];
633 $more .= $more === '' ? '' : $sep;
634 $more .= 'tok=' . media_get_token($id, $resize['w'], $resize['h']);
636 $more = str_replace('cache=cache', '', $more); //skip default
637 $more = str_replace(',,', ',', $more);
638 $more = str_replace(',', $sep, $more);
641 if ($abs) {
642 $xlink = DOKU_URL;
643 } else {
644 $xlink = DOKU_BASE;
647 // external URLs are always direct without rewriting
648 if ($isexternalimage) {
649 $xlink .= 'lib/exe/fetch.php';
650 $xlink .= '?' . $more;
651 $xlink .= $sep . 'media=' . rawurlencode($id);
652 return $xlink;
655 $id = idfilter($id);
657 // decide on scriptname
658 if ($direct) {
659 if ($conf['userewrite'] == 1) {
660 $script = '_media';
661 } else {
662 $script = 'lib/exe/fetch.php';
664 } elseif ($conf['userewrite'] == 1) {
665 $script = '_detail';
666 } else {
667 $script = 'lib/exe/detail.php';
670 // build URL based on rewrite mode
671 if ($conf['userewrite']) {
672 $xlink .= $script . '/' . $id;
673 if ($more) $xlink .= '?' . $more;
674 } elseif ($more) {
675 $xlink .= $script . '?' . $more;
676 $xlink .= $sep . 'media=' . $id;
677 } else {
678 $xlink .= $script . '?media=' . $id;
681 return $xlink;
685 * Returns the URL to the DokuWiki base script
687 * Consider using wl() instead, unless you absoutely need the doku.php endpoint
689 * @author Andreas Gohr <andi@splitbrain.org>
691 * @return string
693 function script()
695 return DOKU_BASE . DOKU_SCRIPT;
699 * Spamcheck against wordlist
701 * Checks the wikitext against a list of blocked expressions
702 * returns true if the text contains any bad words
704 * Triggers COMMON_WORDBLOCK_BLOCKED
706 * Action Plugins can use this event to inspect the blocked data
707 * and gain information about the user who was blocked.
709 * Event data:
710 * data['matches'] - array of matches
711 * data['userinfo'] - information about the blocked user
712 * [ip] - ip address
713 * [user] - username (if logged in)
714 * [mail] - mail address (if logged in)
715 * [name] - real name (if logged in)
717 * @author Andreas Gohr <andi@splitbrain.org>
718 * @author Michael Klier <chi@chimeric.de>
720 * @param string $text - optional text to check, if not given the globals are used
721 * @return bool - true if a spam word was found
723 function checkwordblock($text = '')
725 global $TEXT;
726 global $PRE;
727 global $SUF;
728 global $SUM;
729 global $conf;
730 global $INFO;
731 /* @var Input $INPUT */
732 global $INPUT;
734 if (!$conf['usewordblock']) return false;
736 if (!$text) $text = "$PRE $TEXT $SUF $SUM";
738 // we prepare the text a tiny bit to prevent spammers circumventing URL checks
739 // phpcs:disable Generic.Files.LineLength.TooLong
740 $text = preg_replace(
741 '!(\b)(www\.[\w.:?\-;,]+?\.[\w.:?\-;,]+?[\w/\#~:.?+=&%@\!\-.:?\-;,]+?)([.:?\-;,]*[^\w/\#~:.?+=&%@\!\-.:?\-;,])!i',
742 '\1http://\2 \2\3',
743 $text
745 // phpcs:enable
747 $wordblocks = getWordblocks();
748 // read file in chunks of 200 - this should work around the
749 // MAX_PATTERN_SIZE in modern PCRE
750 $chunksize = 200;
752 while ($blocks = array_splice($wordblocks, 0, $chunksize)) {
753 $re = [];
754 // build regexp from blocks
755 foreach ($blocks as $block) {
756 $block = preg_replace('/#.*$/', '', $block);
757 $block = trim($block);
758 if (empty($block)) continue;
759 $re[] = $block;
761 if (count($re) && preg_match('#(' . implode('|', $re) . ')#si', $text, $matches)) {
762 // prepare event data
763 $data = [];
764 $data['matches'] = $matches;
765 $data['userinfo']['ip'] = $INPUT->server->str('REMOTE_ADDR');
766 if ($INPUT->server->str('REMOTE_USER')) {
767 $data['userinfo']['user'] = $INPUT->server->str('REMOTE_USER');
768 $data['userinfo']['name'] = $INFO['userinfo']['name'];
769 $data['userinfo']['mail'] = $INFO['userinfo']['mail'];
771 $callback = static fn() => true;
772 return Event::createAndTrigger('COMMON_WORDBLOCK_BLOCKED', $data, $callback, true);
775 return false;
779 * Return the IP of the client
781 * Honours X-Forwarded-For and X-Real-IP Proxy Headers
783 * It returns a comma separated list of IPs if the above mentioned
784 * headers are set. If the single parameter is set, it tries to return
785 * a routable public address, prefering the ones suplied in the X
786 * headers
788 * @author Andreas Gohr <andi@splitbrain.org>
790 * @param boolean $single If set only a single IP is returned
791 * @return string
793 function clientIP($single = false)
795 /* @var Input $INPUT */
796 global $INPUT, $conf;
798 $ip = [];
799 $ip[] = $INPUT->server->str('REMOTE_ADDR');
800 if ($INPUT->server->str('HTTP_X_FORWARDED_FOR')) {
801 $ip = array_merge($ip, explode(',', str_replace(' ', '', $INPUT->server->str('HTTP_X_FORWARDED_FOR'))));
803 if ($INPUT->server->str('HTTP_X_REAL_IP')) {
804 $ip = array_merge($ip, explode(',', str_replace(' ', '', $INPUT->server->str('HTTP_X_REAL_IP'))));
807 // remove any non-IP stuff
808 $cnt = count($ip);
809 for ($i = 0; $i < $cnt; $i++) {
810 if (filter_var($ip[$i], FILTER_VALIDATE_IP) === false) {
811 unset($ip[$i]);
814 $ip = array_values(array_unique($ip));
815 if ($ip === [] || !$ip[0]) $ip[0] = '0.0.0.0'; // for some strange reason we don't have a IP
817 if (!$single) return implode(',', $ip);
819 // skip trusted local addresses
820 foreach ($ip as $i) {
821 if (!empty($conf['trustedproxy']) && preg_match('/' . $conf['trustedproxy'] . '/', $i)) {
822 continue;
823 } else {
824 return $i;
828 // still here? just use the last address
829 // this case all ips in the list are trusted
830 return $ip[count($ip) - 1];
834 * Check if the browser is on a mobile device
836 * Adapted from the example code at url below
838 * @link http://www.brainhandles.com/2007/10/15/detecting-mobile-browsers/#code
840 * @deprecated 2018-04-27 you probably want media queries instead anyway
841 * @return bool if true, client is mobile browser; otherwise false
843 function clientismobile()
845 /* @var Input $INPUT */
846 global $INPUT;
848 if ($INPUT->server->has('HTTP_X_WAP_PROFILE')) return true;
850 if (preg_match('/wap\.|\.wap/i', $INPUT->server->str('HTTP_ACCEPT'))) return true;
852 if (!$INPUT->server->has('HTTP_USER_AGENT')) return false;
854 $uamatches = implode(
855 '|',
857 'midp', 'j2me', 'avantg', 'docomo', 'novarra', 'palmos', 'palmsource', '240x320', 'opwv',
858 'chtml', 'pda', 'windows ce', 'mmp\/', 'blackberry', 'mib\/', 'symbian', 'wireless', 'nokia',
859 'hand', 'mobi', 'phone', 'cdm', 'up\.b', 'audio', 'SIE\-', 'SEC\-', 'samsung', 'HTC', 'mot\-',
860 'mitsu', 'sagem', 'sony', 'alcatel', 'lg', 'erics', 'vx', 'NEC', 'philips', 'mmm', 'xx',
861 'panasonic', 'sharp', 'wap', 'sch', 'rover', 'pocket', 'benq', 'java', 'pt', 'pg', 'vox',
862 'amoi', 'bird', 'compal', 'kg', 'voda', 'sany', 'kdd', 'dbt', 'sendo', 'sgh', 'gradi', 'jb',
863 '\d\d\di', 'moto'
867 if (preg_match("/$uamatches/i", $INPUT->server->str('HTTP_USER_AGENT'))) return true;
869 return false;
873 * check if a given link is interwiki link
875 * @param string $link the link, e.g. "wiki>page"
876 * @return bool
878 function link_isinterwiki($link)
880 if (preg_match('/^[a-zA-Z0-9\.]+>/u', $link)) return true;
881 return false;
885 * Convert one or more comma separated IPs to hostnames
887 * If $conf['dnslookups'] is disabled it simply returns the input string
889 * @author Glen Harris <astfgl@iamnota.org>
891 * @param string $ips comma separated list of IP addresses
892 * @return string a comma separated list of hostnames
894 function gethostsbyaddrs($ips)
896 global $conf;
897 if (!$conf['dnslookups']) return $ips;
899 $hosts = [];
900 $ips = explode(',', $ips);
902 if (is_array($ips)) {
903 foreach ($ips as $ip) {
904 $hosts[] = gethostbyaddr(trim($ip));
906 return implode(',', $hosts);
907 } else {
908 return gethostbyaddr(trim($ips));
913 * Checks if a given page is currently locked.
915 * removes stale lockfiles
917 * @author Andreas Gohr <andi@splitbrain.org>
919 * @param string $id page id
920 * @return bool page is locked?
922 function checklock($id)
924 global $conf;
925 /* @var Input $INPUT */
926 global $INPUT;
928 $lock = wikiLockFN($id);
930 //no lockfile
931 if (!file_exists($lock)) return false;
933 //lockfile expired
934 if ((time() - filemtime($lock)) > $conf['locktime']) {
935 @unlink($lock);
936 return false;
939 //my own lock
940 [$ip, $session] = sexplode("\n", io_readFile($lock), 2);
941 if ($ip == $INPUT->server->str('REMOTE_USER') || (session_id() && $session === session_id())) {
942 return false;
945 return $ip;
949 * Lock a page for editing
951 * @author Andreas Gohr <andi@splitbrain.org>
953 * @param string $id page id to lock
955 function lock($id)
957 global $conf;
958 /* @var Input $INPUT */
959 global $INPUT;
961 if ($conf['locktime'] == 0) {
962 return;
965 $lock = wikiLockFN($id);
966 if ($INPUT->server->str('REMOTE_USER')) {
967 io_saveFile($lock, $INPUT->server->str('REMOTE_USER'));
968 } else {
969 io_saveFile($lock, clientIP() . "\n" . session_id());
974 * Unlock a page if it was locked by the user
976 * @author Andreas Gohr <andi@splitbrain.org>
978 * @param string $id page id to unlock
979 * @return bool true if a lock was removed
981 function unlock($id)
983 /* @var Input $INPUT */
984 global $INPUT;
986 $lock = wikiLockFN($id);
987 if (file_exists($lock)) {
988 @[$ip, $session] = explode("\n", io_readFile($lock));
989 if ($ip == $INPUT->server->str('REMOTE_USER') || $session == session_id()) {
990 @unlink($lock);
991 return true;
994 return false;
998 * convert line ending to unix format
1000 * also makes sure the given text is valid UTF-8
1002 * @see formText() for 2crlf conversion
1003 * @author Andreas Gohr <andi@splitbrain.org>
1005 * @param string $text
1006 * @return string
1008 function cleanText($text)
1010 $text = preg_replace("/(\015\012)|(\015)/", "\012", $text);
1012 // if the text is not valid UTF-8 we simply assume latin1
1013 // this won't break any worse than it breaks with the wrong encoding
1014 // but might actually fix the problem in many cases
1015 if (!Clean::isUtf8($text)) $text = utf8_encode($text);
1017 return $text;
1021 * Prepares text for print in Webforms by encoding special chars.
1022 * It also converts line endings to Windows format which is
1023 * pseudo standard for webforms.
1025 * @see cleanText() for 2unix conversion
1026 * @author Andreas Gohr <andi@splitbrain.org>
1028 * @param string $text
1029 * @return string
1031 function formText($text)
1033 $text = str_replace("\012", "\015\012", $text ?? '');
1034 return htmlspecialchars($text);
1038 * Returns the specified local text in raw format
1040 * @author Andreas Gohr <andi@splitbrain.org>
1042 * @param string $id page id
1043 * @param string $ext extension of file being read, default 'txt'
1044 * @return string
1046 function rawLocale($id, $ext = 'txt')
1048 return io_readFile(localeFN($id, $ext));
1052 * Returns the raw WikiText
1054 * @author Andreas Gohr <andi@splitbrain.org>
1056 * @param string $id page id
1057 * @param string|int $rev timestamp when a revision of wikitext is desired
1058 * @return string
1060 function rawWiki($id, $rev = '')
1062 return io_readWikiPage(wikiFN($id, $rev), $id, $rev);
1066 * Returns the pagetemplate contents for the ID's namespace
1068 * @triggers COMMON_PAGETPL_LOAD
1069 * @author Andreas Gohr <andi@splitbrain.org>
1071 * @param string $id the id of the page to be created
1072 * @return string parsed pagetemplate content
1074 function pageTemplate($id)
1076 global $conf;
1078 if (is_array($id)) $id = $id[0];
1080 // prepare initial event data
1081 $data = [
1082 'id' => $id, // the id of the page to be created
1083 'tpl' => '', // the text used as template
1084 'tplfile' => '', // the file above text was/should be loaded from
1085 'doreplace' => true,
1088 $evt = new Event('COMMON_PAGETPL_LOAD', $data);
1089 if ($evt->advise_before(true)) {
1090 // the before event might have loaded the content already
1091 if (empty($data['tpl'])) {
1092 // if the before event did not set a template file, try to find one
1093 if (empty($data['tplfile'])) {
1094 $path = dirname(wikiFN($id));
1095 if (file_exists($path . '/_template.txt')) {
1096 $data['tplfile'] = $path . '/_template.txt';
1097 } else {
1098 // search upper namespaces for templates
1099 $len = strlen(rtrim($conf['datadir'], '/'));
1100 while (strlen($path) >= $len) {
1101 if (file_exists($path . '/__template.txt')) {
1102 $data['tplfile'] = $path . '/__template.txt';
1103 break;
1105 $path = substr($path, 0, strrpos($path, '/'));
1109 // load the content
1110 $data['tpl'] = io_readFile($data['tplfile']);
1112 if ($data['doreplace']) parsePageTemplate($data);
1114 $evt->advise_after();
1115 unset($evt);
1117 return $data['tpl'];
1121 * Performs common page template replacements
1122 * This works on data from COMMON_PAGETPL_LOAD
1124 * @author Andreas Gohr <andi@splitbrain.org>
1126 * @param array $data array with event data
1127 * @return string
1129 function parsePageTemplate(&$data)
1132 * @var string $id the id of the page to be created
1133 * @var string $tpl the text used as template
1134 * @var string $tplfile the file above text was/should be loaded from
1135 * @var bool $doreplace should wildcard replacements be done on the text?
1137 extract($data);
1139 global $USERINFO;
1140 global $conf;
1141 /* @var Input $INPUT */
1142 global $INPUT;
1144 // replace placeholders
1145 $file = noNS($id);
1146 $page = strtr($file, $conf['sepchar'], ' ');
1148 $tpl = str_replace(
1150 '@ID@',
1151 '@NS@',
1152 '@CURNS@',
1153 '@!CURNS@',
1154 '@!!CURNS@',
1155 '@!CURNS!@',
1156 '@FILE@',
1157 '@!FILE@',
1158 '@!FILE!@',
1159 '@PAGE@',
1160 '@!PAGE@',
1161 '@!!PAGE@',
1162 '@!PAGE!@',
1163 '@USER@',
1164 '@NAME@',
1165 '@MAIL@',
1166 '@DATE@'
1169 $id,
1170 getNS($id),
1171 curNS($id),
1172 PhpString::ucfirst(curNS($id)),
1173 PhpString::ucwords(curNS($id)),
1174 PhpString::strtoupper(curNS($id)),
1175 $file,
1176 PhpString::ucfirst($file),
1177 PhpString::strtoupper($file),
1178 $page,
1179 PhpString::ucfirst($page),
1180 PhpString::ucwords($page),
1181 PhpString::strtoupper($page),
1182 $INPUT->server->str('REMOTE_USER'),
1183 $USERINFO ? $USERINFO['name'] : '',
1184 $USERINFO ? $USERINFO['mail'] : '',
1185 $conf['dformat']
1187 $tpl
1190 // we need the callback to work around strftime's char limit
1191 $tpl = preg_replace_callback(
1192 '/%./',
1193 static fn($m) => dformat(null, $m[0]),
1194 $tpl
1196 $data['tpl'] = $tpl;
1197 return $tpl;
1201 * Returns the raw Wiki Text in three slices.
1203 * The range parameter needs to have the form "from-to"
1204 * and gives the range of the section in bytes - no
1205 * UTF-8 awareness is needed.
1206 * The returned order is prefix, section and suffix.
1208 * @author Andreas Gohr <andi@splitbrain.org>
1210 * @param string $range in form "from-to"
1211 * @param string $id page id
1212 * @param string $rev optional, the revision timestamp
1213 * @return string[] with three slices
1215 function rawWikiSlices($range, $id, $rev = '')
1217 $text = io_readWikiPage(wikiFN($id, $rev), $id, $rev);
1219 // Parse range
1220 [$from, $to] = sexplode('-', $range, 2);
1221 // Make range zero-based, use defaults if marker is missing
1222 $from = $from ? $from - 1 : (0);
1223 $to = $to ? $to - 1 : (strlen($text));
1225 $slices = [];
1226 $slices[0] = substr($text, 0, $from);
1227 $slices[1] = substr($text, $from, $to - $from);
1228 $slices[2] = substr($text, $to);
1229 return $slices;
1233 * Joins wiki text slices
1235 * function to join the text slices.
1236 * When the pretty parameter is set to true it adds additional empty
1237 * lines between sections if needed (used on saving).
1239 * @author Andreas Gohr <andi@splitbrain.org>
1241 * @param string $pre prefix
1242 * @param string $text text in the middle
1243 * @param string $suf suffix
1244 * @param bool $pretty add additional empty lines between sections
1245 * @return string
1247 function con($pre, $text, $suf, $pretty = false)
1249 if ($pretty) {
1250 if (
1251 $pre !== '' && !str_ends_with($pre, "\n") &&
1252 !str_starts_with($text, "\n")
1254 $pre .= "\n";
1256 if (
1257 $suf !== '' && !str_ends_with($text, "\n") &&
1258 !str_starts_with($suf, "\n")
1260 $text .= "\n";
1264 return $pre . $text . $suf;
1268 * Checks if the current page version is newer than the last entry in the page's
1269 * changelog. If so, we assume it has been an external edit and we create an
1270 * attic copy and add a proper changelog line.
1272 * This check is only executed when the page is about to be saved again from the
1273 * wiki, triggered in @see saveWikiText()
1275 * @param string $id the page ID
1276 * @deprecated 2021-11-28
1278 function detectExternalEdit($id)
1280 dbg_deprecated(PageFile::class . '::detectExternalEdit()');
1281 (new PageFile($id))->detectExternalEdit();
1285 * Saves a wikitext by calling io_writeWikiPage.
1286 * Also directs changelog and attic updates.
1288 * @author Andreas Gohr <andi@splitbrain.org>
1289 * @author Ben Coburn <btcoburn@silicodon.net>
1291 * @param string $id page id
1292 * @param string $text wikitext being saved
1293 * @param string $summary summary of text update
1294 * @param bool $minor mark this saved version as minor update
1296 function saveWikiText($id, $text, $summary, $minor = false)
1299 // get COMMON_WIKIPAGE_SAVE event data
1300 $data = (new PageFile($id))->saveWikiText($text, $summary, $minor);
1301 if (!$data) return; // save was cancelled (for no changes or by a plugin)
1303 // send notify mails
1304 ['oldRevision' => $rev, 'newRevision' => $new_rev, 'summary' => $summary] = $data;
1305 notify($id, 'admin', $rev, $summary, $minor, $new_rev);
1306 notify($id, 'subscribers', $rev, $summary, $minor, $new_rev);
1308 // if useheading is enabled, purge the cache of all linking pages
1309 if (useHeading('content')) {
1310 $pages = ft_backlinks($id, true);
1311 foreach ($pages as $page) {
1312 $cache = new CacheRenderer($page, wikiFN($page), 'xhtml');
1313 $cache->removeCache();
1319 * moves the current version to the attic and returns its revision date
1321 * @author Andreas Gohr <andi@splitbrain.org>
1323 * @param string $id page id
1324 * @return int|string revision timestamp
1325 * @deprecated 2021-11-28
1327 function saveOldRevision($id)
1329 dbg_deprecated(PageFile::class . '::saveOldRevision()');
1330 return (new PageFile($id))->saveOldRevision();
1334 * Sends a notify mail on page change or registration
1336 * @param string $id The changed page
1337 * @param string $who Who to notify (admin|subscribers|register)
1338 * @param int|string $rev Old page revision
1339 * @param string $summary What changed
1340 * @param boolean $minor Is this a minor edit?
1341 * @param string[] $replace Additional string substitutions, @KEY@ to be replaced by value
1342 * @param int|string $current_rev New page revision
1343 * @return bool
1345 * @author Andreas Gohr <andi@splitbrain.org>
1347 function notify($id, $who, $rev = '', $summary = '', $minor = false, $replace = [], $current_rev = false)
1349 global $conf;
1350 /* @var Input $INPUT */
1351 global $INPUT;
1353 // decide if there is something to do, eg. whom to mail
1354 if ($who == 'admin') {
1355 if (empty($conf['notify'])) return false; //notify enabled?
1356 $tpl = 'mailtext';
1357 $to = $conf['notify'];
1358 } elseif ($who == 'subscribers') {
1359 if (!actionOK('subscribe')) return false; //subscribers enabled?
1360 if ($conf['useacl'] && $INPUT->server->str('REMOTE_USER') && $minor) return false; //skip minors
1361 $data = ['id' => $id, 'addresslist' => '', 'self' => false, 'replacements' => $replace];
1362 Event::createAndTrigger(
1363 'COMMON_NOTIFY_ADDRESSLIST',
1364 $data,
1365 [new SubscriberManager(), 'notifyAddresses']
1367 $to = $data['addresslist'];
1368 if (empty($to)) return false;
1369 $tpl = 'subscr_single';
1370 } else {
1371 return false; //just to be safe
1374 // prepare content
1375 $subscription = new PageSubscriptionSender();
1376 return $subscription->sendPageDiff($to, $tpl, $id, $rev, $summary, $current_rev);
1380 * extracts the query from a search engine referrer
1382 * @author Andreas Gohr <andi@splitbrain.org>
1383 * @author Todd Augsburger <todd@rollerorgans.com>
1385 * @return array|string
1387 function getGoogleQuery()
1389 /* @var Input $INPUT */
1390 global $INPUT;
1392 if (!$INPUT->server->has('HTTP_REFERER')) {
1393 return '';
1395 $url = parse_url($INPUT->server->str('HTTP_REFERER'));
1397 // only handle common SEs
1398 if (!array_key_exists('host', $url)) return '';
1399 if (!preg_match('/(google|bing|yahoo|ask|duckduckgo|babylon|aol|yandex)/', $url['host'])) return '';
1401 $query = [];
1402 if (!array_key_exists('query', $url)) return '';
1403 parse_str($url['query'], $query);
1405 $q = '';
1406 if (isset($query['q'])) {
1407 $q = $query['q'];
1408 } elseif (isset($query['p'])) {
1409 $q = $query['p'];
1410 } elseif (isset($query['query'])) {
1411 $q = $query['query'];
1413 $q = trim($q);
1415 if (!$q) return '';
1416 // ignore if query includes a full URL
1417 if (strpos($q, '//') !== false) return '';
1418 $q = preg_split('/[\s\'"\\\\`()\]\[?:!\.{};,#+*<>\\/]+/', $q, -1, PREG_SPLIT_NO_EMPTY);
1419 return $q;
1423 * Return the human readable size of a file
1425 * @param int $size A file size
1426 * @param int $dec A number of decimal places
1427 * @return string human readable size
1429 * @author Martin Benjamin <b.martin@cybernet.ch>
1430 * @author Aidan Lister <aidan@php.net>
1431 * @version 1.0.0
1433 function filesize_h($size, $dec = 1)
1435 $sizes = ['B', 'KB', 'MB', 'GB'];
1436 $count = count($sizes);
1437 $i = 0;
1439 while ($size >= 1024 && ($i < $count - 1)) {
1440 $size /= 1024;
1441 $i++;
1444 return round($size, $dec) . "\xC2\xA0" . $sizes[$i]; //non-breaking space
1448 * Return the given timestamp as human readable, fuzzy age
1450 * @author Andreas Gohr <gohr@cosmocode.de>
1452 * @param int $dt timestamp
1453 * @return string
1455 function datetime_h($dt)
1457 global $lang;
1459 $ago = time() - $dt;
1460 if ($ago > 24 * 60 * 60 * 30 * 12 * 2) {
1461 return sprintf($lang['years'], round($ago / (24 * 60 * 60 * 30 * 12)));
1463 if ($ago > 24 * 60 * 60 * 30 * 2) {
1464 return sprintf($lang['months'], round($ago / (24 * 60 * 60 * 30)));
1466 if ($ago > 24 * 60 * 60 * 7 * 2) {
1467 return sprintf($lang['weeks'], round($ago / (24 * 60 * 60 * 7)));
1469 if ($ago > 24 * 60 * 60 * 2) {
1470 return sprintf($lang['days'], round($ago / (24 * 60 * 60)));
1472 if ($ago > 60 * 60 * 2) {
1473 return sprintf($lang['hours'], round($ago / (60 * 60)));
1475 if ($ago > 60 * 2) {
1476 return sprintf($lang['minutes'], round($ago / (60)));
1478 return sprintf($lang['seconds'], $ago);
1482 * Wraps around strftime but provides support for fuzzy dates
1484 * The format default to $conf['dformat']. It is passed to
1485 * strftime - %f can be used to get the value from datetime_h()
1487 * @see datetime_h
1488 * @author Andreas Gohr <gohr@cosmocode.de>
1490 * @param int|null $dt timestamp when given, null will take current timestamp
1491 * @param string $format empty default to $conf['dformat'], or provide format as recognized by strftime()
1492 * @return string
1494 function dformat($dt = null, $format = '')
1496 global $conf;
1498 if (is_null($dt)) $dt = time();
1499 $dt = (int) $dt;
1500 if (!$format) $format = $conf['dformat'];
1502 $format = str_replace('%f', datetime_h($dt), $format);
1503 return strftime($format, $dt);
1507 * Formats a timestamp as ISO 8601 date
1509 * @author <ungu at terong dot com>
1510 * @link http://php.net/manual/en/function.date.php#54072
1512 * @param int $int_date current date in UNIX timestamp
1513 * @return string
1515 function date_iso8601($int_date)
1517 $date_mod = date('Y-m-d\TH:i:s', $int_date);
1518 $pre_timezone = date('O', $int_date);
1519 $time_zone = substr($pre_timezone, 0, 3) . ":" . substr($pre_timezone, 3, 2);
1520 $date_mod .= $time_zone;
1521 return $date_mod;
1525 * return an obfuscated email address in line with $conf['mailguard'] setting
1527 * @author Harry Fuecks <hfuecks@gmail.com>
1528 * @author Christopher Smith <chris@jalakai.co.uk>
1530 * @param string $email email address
1531 * @return string
1533 function obfuscate($email)
1535 global $conf;
1537 switch ($conf['mailguard']) {
1538 case 'visible':
1539 $obfuscate = ['@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] '];
1540 return strtr($email, $obfuscate);
1542 case 'hex':
1543 return Conversion::toHtml($email, true);
1545 case 'none':
1546 default:
1547 return $email;
1552 * Removes quoting backslashes
1554 * @author Andreas Gohr <andi@splitbrain.org>
1556 * @param string $string
1557 * @param string $char backslashed character
1558 * @return string
1560 function unslash($string, $char = "'")
1562 return str_replace('\\' . $char, $char, $string);
1566 * Convert php.ini shorthands to byte
1568 * On 32 bit systems values >= 2GB will fail!
1570 * -1 (infinite size) will be reported as -1
1572 * @link https://www.php.net/manual/en/faq.using.php#faq.using.shorthandbytes
1573 * @param string $value PHP size shorthand
1574 * @return int
1576 function php_to_byte($value)
1578 switch (strtoupper(substr($value, -1))) {
1579 case 'G':
1580 $ret = (int) substr($value, 0, -1) * 1024 * 1024 * 1024;
1581 break;
1582 case 'M':
1583 $ret = (int) substr($value, 0, -1) * 1024 * 1024;
1584 break;
1585 case 'K':
1586 $ret = (int) substr($value, 0, -1) * 1024;
1587 break;
1588 default:
1589 $ret = (int) $value;
1590 break;
1592 return $ret;
1596 * Wrapper around preg_quote adding the default delimiter
1598 * @param string $string
1599 * @return string
1601 function preg_quote_cb($string)
1603 return preg_quote($string, '/');
1607 * Shorten a given string by removing data from the middle
1609 * You can give the string in two parts, the first part $keep
1610 * will never be shortened. The second part $short will be cut
1611 * in the middle to shorten but only if at least $min chars are
1612 * left to display it. Otherwise it will be left off.
1614 * @param string $keep the part to keep
1615 * @param string $short the part to shorten
1616 * @param int $max maximum chars you want for the whole string
1617 * @param int $min minimum number of chars to have left for middle shortening
1618 * @param string $char the shortening character to use
1619 * @return string
1621 function shorten($keep, $short, $max, $min = 9, $char = '…')
1623 $max -= PhpString::strlen($keep);
1624 if ($max < $min) return $keep;
1625 $len = PhpString::strlen($short);
1626 if ($len <= $max) return $keep . $short;
1627 $half = floor($max / 2);
1628 return $keep .
1629 PhpString::substr($short, 0, $half - 1) .
1630 $char .
1631 PhpString::substr($short, $len - $half);
1635 * Return the users real name or e-mail address for use
1636 * in page footer and recent changes pages
1638 * @param string|null $username or null when currently logged-in user should be used
1639 * @param bool $textonly true returns only plain text, true allows returning html
1640 * @return string html or plain text(not escaped) of formatted user name
1642 * @author Andy Webber <dokuwiki AT andywebber DOT com>
1644 function editorinfo($username, $textonly = false)
1646 return userlink($username, $textonly);
1650 * Returns users realname w/o link
1652 * @param string|null $username or null when currently logged-in user should be used
1653 * @param bool $textonly true returns only plain text, true allows returning html
1654 * @return string html or plain text(not escaped) of formatted user name
1656 * @triggers COMMON_USER_LINK
1658 function userlink($username = null, $textonly = false)
1660 global $conf, $INFO;
1661 /** @var AuthPlugin $auth */
1662 global $auth;
1663 /** @var Input $INPUT */
1664 global $INPUT;
1666 // prepare initial event data
1667 $data = [
1668 'username' => $username, // the unique user name
1669 'name' => '',
1670 'link' => [
1671 //setting 'link' to false disables linking
1672 'target' => '',
1673 'pre' => '',
1674 'suf' => '',
1675 'style' => '',
1676 'more' => '',
1677 'url' => '',
1678 'title' => '',
1679 'class' => '',
1681 'userlink' => '', // formatted user name as will be returned
1682 'textonly' => $textonly,
1684 if ($username === null) {
1685 $data['username'] = $username = $INPUT->server->str('REMOTE_USER');
1686 if ($textonly) {
1687 $data['name'] = $INFO['userinfo']['name'] . ' (' . $INPUT->server->str('REMOTE_USER') . ')';
1688 } else {
1689 $data['name'] = '<bdi>' . hsc($INFO['userinfo']['name']) . '</bdi> ' .
1690 '(<bdi>' . hsc($INPUT->server->str('REMOTE_USER')) . '</bdi>)';
1694 $evt = new Event('COMMON_USER_LINK', $data);
1695 if ($evt->advise_before(true)) {
1696 if (empty($data['name'])) {
1697 if ($auth instanceof AuthPlugin) {
1698 $info = $auth->getUserData($username);
1700 if ($conf['showuseras'] != 'loginname' && isset($info) && $info) {
1701 switch ($conf['showuseras']) {
1702 case 'username':
1703 case 'username_link':
1704 $data['name'] = $textonly ? $info['name'] : hsc($info['name']);
1705 break;
1706 case 'email':
1707 case 'email_link':
1708 $data['name'] = obfuscate($info['mail']);
1709 break;
1711 } else {
1712 $data['name'] = $textonly ? $data['username'] : hsc($data['username']);
1716 /** @var Doku_Renderer_xhtml $xhtml_renderer */
1717 static $xhtml_renderer = null;
1719 if (!$data['textonly'] && empty($data['link']['url'])) {
1720 if (in_array($conf['showuseras'], ['email_link', 'username_link'])) {
1721 if (!isset($info) && $auth instanceof AuthPlugin) {
1722 $info = $auth->getUserData($username);
1724 if (isset($info) && $info) {
1725 if ($conf['showuseras'] == 'email_link') {
1726 $data['link']['url'] = 'mailto:' . obfuscate($info['mail']);
1727 } else {
1728 if (is_null($xhtml_renderer)) {
1729 $xhtml_renderer = p_get_renderer('xhtml');
1731 if (empty($xhtml_renderer->interwiki)) {
1732 $xhtml_renderer->interwiki = getInterwiki();
1734 $shortcut = 'user';
1735 $exists = null;
1736 $data['link']['url'] = $xhtml_renderer->_resolveInterWiki($shortcut, $username, $exists);
1737 $data['link']['class'] .= ' interwiki iw_user';
1738 if ($exists !== null) {
1739 if ($exists) {
1740 $data['link']['class'] .= ' wikilink1';
1741 } else {
1742 $data['link']['class'] .= ' wikilink2';
1743 $data['link']['rel'] = 'nofollow';
1747 } else {
1748 $data['textonly'] = true;
1750 } else {
1751 $data['textonly'] = true;
1755 if ($data['textonly']) {
1756 $data['userlink'] = $data['name'];
1757 } else {
1758 $data['link']['name'] = $data['name'];
1759 if (is_null($xhtml_renderer)) {
1760 $xhtml_renderer = p_get_renderer('xhtml');
1762 $data['userlink'] = $xhtml_renderer->_formatLink($data['link']);
1765 $evt->advise_after();
1766 unset($evt);
1768 return $data['userlink'];
1772 * Returns the path to a image file for the currently chosen license.
1773 * When no image exists, returns an empty string
1775 * @author Andreas Gohr <andi@splitbrain.org>
1777 * @param string $type - type of image 'badge' or 'button'
1778 * @return string
1780 function license_img($type)
1782 global $license;
1783 global $conf;
1784 if (!$conf['license']) return '';
1785 if (!is_array($license[$conf['license']])) return '';
1786 $try = [];
1787 $try[] = 'lib/images/license/' . $type . '/' . $conf['license'] . '.png';
1788 $try[] = 'lib/images/license/' . $type . '/' . $conf['license'] . '.gif';
1789 if (str_starts_with($conf['license'], 'cc-')) {
1790 $try[] = 'lib/images/license/' . $type . '/cc.png';
1792 foreach ($try as $src) {
1793 if (file_exists(DOKU_INC . $src)) return $src;
1795 return '';
1799 * Checks if the given amount of memory is available
1801 * If the memory_get_usage() function is not available the
1802 * function just assumes $bytes of already allocated memory
1804 * @author Filip Oscadal <webmaster@illusionsoftworks.cz>
1805 * @author Andreas Gohr <andi@splitbrain.org>
1807 * @param int $mem Size of memory you want to allocate in bytes
1808 * @param int $bytes already allocated memory (see above)
1809 * @return bool
1811 function is_mem_available($mem, $bytes = 1_048_576)
1813 $limit = trim(ini_get('memory_limit'));
1814 if (empty($limit)) return true; // no limit set!
1815 if ($limit == -1) return true; // unlimited
1817 // parse limit to bytes
1818 $limit = php_to_byte($limit);
1820 // get used memory if possible
1821 if (function_exists('memory_get_usage')) {
1822 $used = memory_get_usage();
1823 } else {
1824 $used = $bytes;
1827 if ($used + $mem > $limit) {
1828 return false;
1831 return true;
1835 * Send a HTTP redirect to the browser
1837 * Works arround Microsoft IIS cookie sending bug. Exits the script.
1839 * @link http://support.microsoft.com/kb/q176113/
1840 * @author Andreas Gohr <andi@splitbrain.org>
1842 * @param string $url url being directed to
1844 function send_redirect($url)
1846 $url = stripctl($url); // defend against HTTP Response Splitting
1848 /* @var Input $INPUT */
1849 global $INPUT;
1851 //are there any undisplayed messages? keep them in session for display
1852 global $MSG;
1853 if (isset($MSG) && count($MSG) && !defined('NOSESSION')) {
1854 //reopen session, store data and close session again
1855 @session_start();
1856 $_SESSION[DOKU_COOKIE]['msg'] = $MSG;
1859 // always close the session
1860 session_write_close();
1862 // check if running on IIS < 6 with CGI-PHP
1863 if (
1864 $INPUT->server->has('SERVER_SOFTWARE') && $INPUT->server->has('GATEWAY_INTERFACE') &&
1865 (strpos($INPUT->server->str('GATEWAY_INTERFACE'), 'CGI') !== false) &&
1866 (preg_match('|^Microsoft-IIS/(\d)\.\d$|', trim($INPUT->server->str('SERVER_SOFTWARE')), $matches)) &&
1867 $matches[1] < 6
1869 header('Refresh: 0;url=' . $url);
1870 } else {
1871 header('Location: ' . $url);
1874 // no exits during unit tests
1875 if (defined('DOKU_UNITTEST')) {
1876 // pass info about the redirect back to the test suite
1877 $testRequest = TestRequest::getRunning();
1878 if ($testRequest !== null) {
1879 $testRequest->addData('send_redirect', $url);
1881 return;
1884 exit;
1888 * Validate a value using a set of valid values
1890 * This function checks whether a specified value is set and in the array
1891 * $valid_values. If not, the function returns a default value or, if no
1892 * default is specified, throws an exception.
1894 * @param string $param The name of the parameter
1895 * @param array $valid_values A set of valid values; Optionally a default may
1896 * be marked by the key “default”.
1897 * @param array $array The array containing the value (typically $_POST
1898 * or $_GET)
1899 * @param string $exc The text of the raised exception
1901 * @throws Exception
1902 * @return mixed
1903 * @author Adrian Lang <lang@cosmocode.de>
1905 function valid_input_set($param, $valid_values, $array, $exc = '')
1907 if (isset($array[$param]) && in_array($array[$param], $valid_values)) {
1908 return $array[$param];
1909 } elseif (isset($valid_values['default'])) {
1910 return $valid_values['default'];
1911 } else {
1912 throw new Exception($exc);
1917 * Read a preference from the DokuWiki cookie
1918 * (remembering both keys & values are urlencoded)
1920 * @param string $pref preference key
1921 * @param mixed $default value returned when preference not found
1922 * @return string preference value
1924 function get_doku_pref($pref, $default)
1926 $enc_pref = urlencode($pref);
1927 if (isset($_COOKIE['DOKU_PREFS']) && strpos($_COOKIE['DOKU_PREFS'], $enc_pref) !== false) {
1928 $parts = explode('#', $_COOKIE['DOKU_PREFS']);
1929 $cnt = count($parts);
1931 // due to #2721 there might be duplicate entries,
1932 // so we read from the end
1933 for ($i = $cnt - 2; $i >= 0; $i -= 2) {
1934 if ($parts[$i] === $enc_pref) {
1935 return urldecode($parts[$i + 1]);
1939 return $default;
1943 * Add a preference to the DokuWiki cookie
1944 * (remembering $_COOKIE['DOKU_PREFS'] is urlencoded)
1945 * Remove it by setting $val to false
1947 * @param string $pref preference key
1948 * @param string $val preference value
1950 function set_doku_pref($pref, $val)
1952 global $conf;
1953 $orig = get_doku_pref($pref, false);
1954 $cookieVal = '';
1956 if ($orig !== false && ($orig !== $val)) {
1957 $parts = explode('#', $_COOKIE['DOKU_PREFS']);
1958 $cnt = count($parts);
1959 // urlencode $pref for the comparison
1960 $enc_pref = rawurlencode($pref);
1961 $seen = false;
1962 for ($i = 0; $i < $cnt; $i += 2) {
1963 if ($parts[$i] === $enc_pref) {
1964 if (!$seen) {
1965 if ($val !== false) {
1966 $parts[$i + 1] = rawurlencode($val ?? '');
1967 } else {
1968 unset($parts[$i]);
1969 unset($parts[$i + 1]);
1971 $seen = true;
1972 } else {
1973 // no break because we want to remove duplicate entries
1974 unset($parts[$i]);
1975 unset($parts[$i + 1]);
1979 $cookieVal = implode('#', $parts);
1980 } elseif ($orig === false && $val !== false) {
1981 $cookieVal = (isset($_COOKIE['DOKU_PREFS']) ? $_COOKIE['DOKU_PREFS'] . '#' : '') .
1982 rawurlencode($pref) . '#' . rawurlencode($val);
1985 $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
1986 if (defined('DOKU_UNITTEST')) {
1987 $_COOKIE['DOKU_PREFS'] = $cookieVal;
1988 } else {
1989 setcookie('DOKU_PREFS', $cookieVal, [
1990 'expires' => time() + 365 * 24 * 3600,
1991 'path' => $cookieDir,
1992 'secure' => ($conf['securecookie'] && is_ssl()),
1993 'samesite' => 'Lax'
1999 * Strips source mapping declarations from given text #601
2001 * @param string &$text reference to the CSS or JavaScript code to clean
2003 function stripsourcemaps(&$text)
2005 $text = preg_replace('/^(\/\/|\/\*)[@#]\s+sourceMappingURL=.*?(\*\/)?$/im', '\\1\\2', $text);
2009 * Returns the contents of a given SVG file for embedding
2011 * Inlining SVGs saves on HTTP requests and more importantly allows for styling them through
2012 * CSS. However it should used with small SVGs only. The $maxsize setting ensures only small
2013 * files are embedded.
2015 * This strips unneeded headers, comments and newline. The result is not a vaild standalone SVG!
2017 * @param string $file full path to the SVG file
2018 * @param int $maxsize maximum allowed size for the SVG to be embedded
2019 * @return string|false the SVG content, false if the file couldn't be loaded
2021 function inlineSVG($file, $maxsize = 2048)
2023 $file = trim($file);
2024 if ($file === '') return false;
2025 if (!file_exists($file)) return false;
2026 if (filesize($file) > $maxsize) return false;
2027 if (!is_readable($file)) return false;
2028 $content = file_get_contents($file);
2029 $content = preg_replace('/<!--.*?(-->)/s', '', $content); // comments
2030 $content = preg_replace('/<\?xml .*?\?>/i', '', $content); // xml header
2031 $content = preg_replace('/<!DOCTYPE .*?>/i', '', $content); // doc type
2032 $content = preg_replace('/>\s+</s', '><', $content); // newlines between tags
2033 $content = trim($content);
2034 if (!str_starts_with($content, '<svg ')) return false;
2035 return $content;
2038 //Setup VIM: ex: et ts=2 :