1<?php
2# Copyright (c) 2003-2005, Jannis Hermanns (on behalf the Serendipity Developer Team)
3# All rights reserved.  See LICENSE file for licensing details
4
5if (IN_serendipity !== true) {
6    die ("Don't hack!");
7}
8
9if (defined('S9Y_FRAMEWORK_FUNCTIONS')) {
10    return;
11}
12@define('S9Y_FRAMEWORK_FUNCTIONS', true);
13
14$serendipity['imageList'] = array();
15
16include_once(S9Y_INCLUDE_PATH . 'include/db/db.inc.php');
17include_once(S9Y_INCLUDE_PATH . 'include/compat.inc.php');
18include_once(S9Y_INCLUDE_PATH . 'include/functions_config.inc.php');
19include_once(S9Y_INCLUDE_PATH . 'include/plugin_api.inc.php');
20include_once(S9Y_INCLUDE_PATH . 'include/functions_images.inc.php');
21include_once(S9Y_INCLUDE_PATH . 'include/functions_installer.inc.php');
22include_once(S9Y_INCLUDE_PATH . 'include/functions_entries.inc.php');
23include_once(S9Y_INCLUDE_PATH . 'include/functions_comments.inc.php');
24include_once(S9Y_INCLUDE_PATH . 'include/functions_permalinks.inc.php');
25include_once(S9Y_INCLUDE_PATH . 'include/functions_smarty.inc.php');
26
27/**
28 * Truncate a string to a specific length, multibyte aware. Appends '...' if successfully truncated
29 *
30 * @access public
31 * @param   string  Input string
32 * @param   int     Length the final string should have
33 * @return  string  Truncated string
34 */
35function serendipity_truncateString($s, $len) {
36    if ( strlen($s) > ($len+3) ) {
37        $s = serendipity_mb('substr', $s, 0, $len) . '...';
38    }
39    return $s;
40}
41
42/**
43 * Optionally turn on GZip Compression, if configured
44 *
45 * @access public
46 */
47function serendipity_gzCompression() {
48    global $serendipity;
49    if (isset($serendipity['useGzip']) && serendipity_db_bool($serendipity['useGzip'])
50        && function_exists('ob_gzhandler') && extension_loaded('zlib')
51        && serendipity_ini_bool(ini_get('zlib.output_compression')) == false
52        && serendipity_ini_bool(ini_get('session.use_trans_sid')) == false) {
53        ob_start("ob_gzhandler");
54    }
55}
56
57/**
58 * Returns a timestamp formatted according to the current Server timezone offset
59 *
60 * @access public
61 * @param  int      The timestamp you want to convert into the current server timezone. Defaults to "now".
62 * @param  boolean  A toggle to indicate, if the timezone offset should be ADDED or SUBSTRACTED from the timezone. Substracting is required to restore original time when posting an entry.
63 * @return int      The final timestamp
64 */
65function serendipity_serverOffsetHour($timestamp = null, $negative = false) {
66    global $serendipity;
67
68    if ($timestamp === null) {
69        $timestamp = time();
70    }
71
72    if (empty($serendipity['serverOffsetHours']) || !is_numeric($serendipity['serverOffsetHours']) || $serendipity['serverOffsetHours'] == 0) {
73        return $timestamp;
74    } else {
75        return $timestamp + (($negative ? -$serendipity['serverOffsetHours'] : $serendipity['serverOffsetHours']) * 60 * 60);
76    }
77}
78
79/* Converts a date string (DD.MM.YYYY, YYYY-MM-DD, MM/DD/YYYY) into a unix timestamp
80 *
81 * @access public
82 * @param  string  The input date
83 * @return int     The output unix timestamp
84 */
85function &serendipity_convertToTimestamp($in) {
86    if (preg_match('@([0-9]+)([/\.-])([0-9]+)([/\.-])([0-9]+)@', $in, $m)) {
87        if ($m[2] != $m[4]) {
88            return $in;
89        }
90
91        switch($m[2]) {
92            case '.':
93                return mktime(0, 0, 0, /* month */ $m[3], /* day */ $m[1], /* year */ $m[5]);
94                break;
95
96            case '/':
97                return mktime(0, 0, 0, /* month */ $m[1], /* day */ $m[3], /* year */ $m[5]);
98                break;
99
100            case '-':
101                return mktime(0, 0, 0, /* month */ $m[3], /* day */ $m[5], /* year */ $m[1]);
102                break;
103        }
104
105        return $in;
106    }
107
108    return $in;
109}
110
111/**
112 * Format a timestamp
113 *
114 * This function can convert an input timestamp into specific PHP strftime() outputs, including applying necessary timezone calculations.
115 *
116 * @access public
117 * @param   string      Output format for the timestamp
118 * @param   int         Timestamp to use for displaying
119 * @param   boolean     Indicates, if timezone calculations shall be used.
120 * @param   boolean     Whether to use strftime or simply date
121 * @return  string      The formatted timestamp
122 */
123function serendipity_strftime($format, $timestamp = null, $useOffset = true, $useDate = false) {
124    global $serendipity;
125    static $is_win_utf = null;
126
127    if ($is_win_utf === null) {
128        // Windows does not have UTF-8 locales.
129        $is_win_utf = (LANG_CHARSET == 'UTF-8' && strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' ? true : false);
130    }
131
132    if ($useDate) {
133        $out = date($format, $timestamp);
134    } else {
135        switch($serendipity['calendar']) {
136            default:
137            case 'gregorian':
138                if ($timestamp == null) {
139                    $timestamp = serendipity_serverOffsetHour();
140                } elseif ($useOffset) {
141                    $timestamp = serendipity_serverOffsetHour($timestamp);
142                }
143                $out = strftime($format, $timestamp);
144                break;
145
146            case 'persian-utf8':
147                if ($timestamp == null) {
148                    $timestamp = serendipity_serverOffsetHour();
149                } elseif ($useOffset) {
150                    $timestamp = serendipity_serverOffsetHour($timestamp);
151                }
152
153                require_once S9Y_INCLUDE_PATH . 'include/functions_calendars.inc.php';
154                $out = persian_strftime_utf($format, $timestamp);
155                break;
156        }
157    }
158
159    if ($is_win_utf && (empty($serendipity['calendar']) || $serendipity['calendar'] == 'gregorian')) {
160        $out = utf8_encode($out);
161    }
162
163    return $out;
164}
165
166/**
167 * A wrapper function call for formatting Timestamps.
168 *
169 * Utilizes serendipity_strftime() and prepares the output timestamp with a few tweaks, and applies automatic uppercasing of the return.
170 *
171 * @see serendipity_strftime()
172 * @param   string      Output format for the timestamp
173 * @param   int         Timestamp to use for displaying
174 * @param   boolean     Indicates, if timezone calculations shall be used.
175 * @param   boolean     Whether to use strftime or simply date
176 * @return  string      The formatted timestamp
177 */
178function serendipity_formatTime($format, $time, $useOffset = true, $useDate = false) {
179    static $cache;
180    if (!isset($cache)) {
181        $cache = array();
182    }
183
184    if (!isset($cache[$format])) {
185        $cache[$format] = $format;
186        if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
187            $cache[$format] = str_replace('%e', '%d', $cache[$format]);
188        }
189    }
190
191    return serendipity_mb('ucfirst', serendipity_strftime($cache[$format], (int)$time, $useOffset, $useDate));
192}
193
194/**
195 * Fetches the list of available templates/themes/styles.
196 *
197 * @access public
198 * @param   string  Directory to search for a template [recursive use]
199 * @return  array   Sorted array of available template names
200 */
201function serendipity_fetchTemplates($dir = '') {
202    global $serendipity;
203
204    $cdir = @opendir($serendipity['serendipityPath'] . $serendipity['templatePath'] . $dir);
205    $rv   = array();
206    if (!$cdir) {
207        return $rv;
208    }
209    while (($file = readdir($cdir)) !== false) {
210        if (is_dir($serendipity['serendipityPath'] . $serendipity['templatePath'] . $dir . $file) && !preg_match('@^(\.|CVS)@i', $file) && !file_exists($serendipity['serendipityPath'] . $serendipity['templatePath'] . $dir . $file . '/inactive.txt')) {
211            if (file_exists($serendipity['serendipityPath'] . $serendipity['templatePath'] . $dir . $file . '/info.txt')) {
212                $key = strtolower($file);
213                if (isset($rv[$key])) {
214                    $key = $dir . $key;
215                }
216                $rv[$key] = $dir . $file;
217            } else {
218                $temp = serendipity_fetchTemplates($dir . $file . '/');
219                if (count($temp) > 0) {
220                    $rv = array_merge($rv, $temp);
221                }
222            }
223        }
224    }
225    closedir($cdir);
226    ksort($rv);
227    return $rv;
228}
229
230/**
231 * Get information about a specific theme/template/style
232 *
233 * @access public
234 * @param   string  Directory name of a theme
235 * @param   string  Absolute path to the templates [for use on CVS mounted directories]
236 * @return  array   Associative array if template information
237 */
238function serendipity_fetchTemplateInfo($theme, $abspath = null) {
239    global $serendipity;
240
241    if ($abspath === null) {
242        $abspath = $serendipity['serendipityPath'] . $serendipity['templatePath'];
243    }
244
245    $lines = @file($abspath . $theme . '/info.txt');
246    if ( !$lines ) {
247        return array();
248    }
249
250    for($x=0; $x<count($lines); $x++) {
251        $j = preg_split('/([^\:]+)\:/', $lines[$x], -1, PREG_SPLIT_DELIM_CAPTURE);
252        if ($j[2]) {
253            $currSec = $j[1];
254            $data[strtolower($currSec)][] = trim($j[2]);
255        } else {
256            $data[strtolower($currSec)][] = trim($j[0]);
257        }
258    }
259
260    foreach ($data as $k => $v) {
261        $data[$k] = implode("\n", $v);
262    }
263
264    if (@is_file($serendipity['templatePath'] . $theme . '/config.inc.php')) {
265        $data['custom_config'] = YES;
266        $data['custom_config_engine'] = $theme;
267    }
268
269    // Templates can depend on a possible "Engine" (i.e. "Engine: 2k11").
270    // We support the fallback chain also of a template's configuration, so let's check each engine for a config file.
271    if (!empty($data['engine'])) {
272        $engines = explode(',', $data['engine']);
273        foreach($engines AS $engine) {
274            $engine = trim($engine);
275            if (empty($engine)) continue;
276
277            if (@is_file($serendipity['templatePath'] . $engine . '/config.inc.php')) {
278                $data['custom_config'] = YES;
279                $data['custom_config_engine'] = $engine;
280            }
281        }
282    }
283
284    if ( $theme != 'default' && $theme != 'default-rtl'
285      && @is_dir($serendipity['templatePath'] . $theme . '/admin')
286      && strtolower($data['backend']) == 'yes' ) {
287
288        $data['custom_admin_interface'] = YES;
289    } else {
290        $data['custom_admin_interface'] = NO;
291    }
292
293    return $data;
294}
295
296/**
297 * Recursively walks an 1-dimensional array to map parent IDs and depths, depending on the nested array set.
298 *
299 * Used for sorting a list of comments, for example. The list of comment is iterated, and the nesting level is calculated, and the array will be sorted to represent the amount of nesting.
300 *
301 * @access public
302 * @param   array   Input array to investigate [consecutively sliced for recursive calls]
303 * @param   string  Array index name to indicate the ID value of an array index
304 * @param   string  Array index name to indicate the PARENT ID value of an array index, matched against the $child_name value
305 * @param   int     The parent id to check an element against for recursive nesting
306 * @param   int     The current depth of the cycled array
307 * @return  array   The sorted and shiny polished result array
308 */
309function serendipity_walkRecursive($ary, $child_name = 'id', $parent_name = 'parent_id', $parentid = 0, $depth = 0) {
310    global $serendipity;
311    static $_resArray;
312    static $_remain;
313
314    if (!is_array($ary) || sizeof($ary) == 0) {
315        return array();
316    }
317
318    if ($parentid === VIEWMODE_THREADED) {
319        $parentid = 0;
320    }
321
322    if ($depth == 0) {
323        $_resArray = array();
324        $_remain   = $ary;
325    }
326
327    foreach($ary AS $key => $data) {
328        if ($parentid === VIEWMODE_LINEAR || !isset($data[$parent_name]) || $data[$parent_name] == $parentid) {
329            $data['depth'] = $depth;
330            $_resArray[]   = $data;
331            unset($_remain[$key]);
332            if ($data[$child_name] && $parentid !== VIEWMODE_LINEAR ) {
333                serendipity_walkRecursive($ary, $child_name, $parent_name, $data[$child_name], ($depth+1));
334            }
335        }
336    }
337
338    /* We are inside a recusive child, and we need to break out */
339    if ($depth !== 0) {
340        return true;
341    }
342
343    if (count($_remain) > 0) {
344        // Remaining items need to be appended
345        foreach($_remain AS $key => $data) {
346            $data['depth'] = 0;
347            $_resArray[]   = $data;
348        }
349    }
350
351    return $_resArray;
352}
353
354/**
355 * Fetch the list of Serendipity Authors
356 *
357 * @access public
358 * @param   int     Fetch only a specific User
359 * @param   array   Can contain an array of group IDs you only want to fetch authors of.
360 * @param   boolean If set to TRUE, the amount of entries per author will also be returned
361 * @return  array   Result of the SQL query
362 */
363function serendipity_fetchUsers($user = '', $group = null, $is_count = false) {
364    global $serendipity;
365
366    $where = '';
367    if (!empty($user)) {
368        $where = "WHERE a.authorid = '" . (int)$user ."'";
369    }
370
371    $query_select   = '';
372    $query_join     = '';
373    $query_group    = '';
374    $query_distinct = '';
375    if ($is_count) {
376        $query_select = ", count(e.authorid) as artcount";
377        $query_join   = "LEFT OUTER JOIN {$serendipity['dbPrefix']}entries AS e
378                                      ON (a.authorid = e.authorid AND e.isdraft = 'false')";
379    }
380
381    if ($is_count || $group != null) {
382        if ($serendipity['dbType'] == 'postgres' ||
383            $serendipity['dbType'] == 'pdo-postgres') {
384            // Why does PostgreSQL keep doing this to us? :-)
385            $query_group    = 'GROUP BY a.authorid, a.realname, a.username, a.password, a.hashtype, a.mail_comments, a.mail_trackbacks, a.email, a.userlevel, a.right_publish';
386            $query_distinct = 'DISTINCT';
387        } else {
388            $query_group    = 'GROUP BY a.authorid';
389            $query_distinct = '';
390        }
391    }
392
393
394    if ($group === null) {
395        $querystring = "SELECT $query_distinct
396                               a.authorid,
397                               a.realname,
398                               a.username,
399                               a.password,
400                               a.hashtype,
401                               a.mail_comments,
402                               a.mail_trackbacks,
403                               a.email,
404                               a.userlevel,
405                               a.right_publish
406                               $query_select
407                          FROM {$serendipity['dbPrefix']}authors AS a
408                               $query_join
409                               $where
410                               $query_group
411                      ORDER BY a.realname ASC";
412    } else {
413
414        if ($group === 'hidden') {
415            $query_join .= "LEFT OUTER JOIN {$serendipity['dbPrefix']}groupconfig AS gc
416                                         ON (gc.property = 'hiddenGroup' AND gc.id = ag.groupid AND gc.value = 'true')";
417            $where .= " AND gc.id IS NULL ";
418        } elseif (is_array($group)) {
419            foreach($group AS $idx => $groupid) {
420                $group[$idx] = (int)$groupid;
421            }
422            $group_sql = implode(', ', $group);
423        } else {
424            $group_sql = (int)$group;
425        }
426
427        $querystring = "SELECT $query_distinct
428                               a.authorid,
429                               a.realname,
430                               a.username,
431                               a.password,
432                               a.hashtype,
433                               a.mail_comments,
434                               a.mail_trackbacks,
435                               a.email,
436                               a.userlevel,
437                               a.right_publish
438                               $query_select
439                          FROM {$serendipity['dbPrefix']}authors AS a
440               LEFT OUTER JOIN {$serendipity['dbPrefix']}authorgroups AS ag
441                            ON a.authorid = ag.authorid
442               LEFT OUTER JOIN {$serendipity['dbPrefix']}groups AS g
443                            ON ag.groupid  = g.id
444                               $query_join
445                         WHERE " . ($group_sql ? "g.id IN ($group_sql)" : '1=1') . "
446                               $where
447                               $query_group
448                      ORDER BY a.realname ASC";
449    }
450
451    return serendipity_db_query($querystring);
452}
453
454
455/**
456 * Sends a Mail with Serendipity formatting
457 *
458 * @access public
459 * @param   string  The recipient address of the mail
460 * @param   string  The subject of the mail
461 * @param   string  The body of the mail
462 * @param   string  The sender mail address of the mail
463 * @param   array   additional headers to pass to the E-Mail
464 * @param   string  The name of the sender
465 * @return  int     Return code of the PHP mail() function
466 */
467function serendipity_sendMail($to, $subject, $message, $fromMail, $headers = NULL, $fromName = NULL) {
468    global $serendipity;
469
470    if (!is_null($headers) && !is_array($headers)) {
471        trigger_error(__FUNCTION__ . ': $headers must be either an array or null', E_USER_ERROR);
472    }
473
474    if (is_null($fromName) || empty($fromName)) {
475        $fromName = $serendipity['blogTitle'];
476    }
477
478    if (is_null($fromMail) || empty($fromMail)) {
479        $fromMail = $to;
480    }
481
482    if (is_null($headers)) {
483        $headers = array();
484    }
485
486    // Fix special characters
487    $fromName = str_replace(array('"', "\r", "\n"), array("'", '', ''), $fromName);
488    $fromMail = str_replace(array("\r","\n"), array('', ''), $fromMail);
489
490    // Prefix all mail with weblog title
491    $subject = '['. $serendipity['blogTitle'] . '] '.  $subject;
492
493    // Append signature to every mail
494    $message .= "\n" . sprintf(SIGNATURE, $serendipity['blogTitle']);
495
496    $maildata = array(
497        'to'       => &$to,
498        'subject'  => &$subject,
499        'fromName' => &$fromName,
500        'fromMail' => &$fromMail,
501        'blogMail' => $serendipity['blogMail'],
502        'version'  => 'Serendipity' . ($serendipity['expose_s9y'] ? '/' . $serendipity['version'] : ''),
503        'legacy'   => true,
504        'headers'  => &$headers,
505        'message'  => &$message
506    );
507
508    serendipity_plugin_api::hook_event('backend_sendmail', $maildata, LANG_CHARSET);
509
510    // This routine can be overridden by a plugin.
511    if ($maildata['legacy']) {
512        // Check for mb_* function, and use it to encode headers etc. */
513        if (function_exists('mb_encode_mimeheader')) {
514            // mb_encode_mimeheader function insertes linebreaks after 74 chars.
515            // Usually this is according to spec, but for us it caused more trouble than
516            // it prevented.
517            // Regards to Mark Kronsbein for finding this issue!
518            $maildata['subject'] = str_replace(array("\n", "\r"), array('', ''), mb_encode_mimeheader($maildata['subject'], LANG_CHARSET));
519            $maildata['fromName'] = str_replace(array("\n", "\r"), array('', ''), mb_encode_mimeheader($maildata['fromName'], LANG_CHARSET));
520        }
521
522
523        // Always add these headers
524        if (!empty($maildata['blogMail'])) {
525            $maildata['headers'][] = 'From: "'. $maildata['fromName'] .'" <'. $maildata['blogMail'] .'>';
526        }
527        $maildata['headers'][] = 'Reply-To: "'. $maildata['fromName'] .'" <'. $maildata['fromMail'] .'>';
528        if ($serendipity['expose_s9y']) {
529            $maildata['headers'][] = 'X-Mailer: ' . $maildata['version'];
530            $maildata['headers'][] = 'X-Engine: PHP/'. phpversion();
531        }
532        $maildata['headers'][] = 'Message-ID: <'. md5(microtime() . uniqid(time())) .'@'. $_SERVER['HTTP_HOST'] .'>';
533        $maildata['headers'][] = 'MIME-Version: 1.0';
534        $maildata['headers'][] = 'Precedence: bulk';
535        $maildata['headers'][] = 'Content-Type: text/plain; charset=' . LANG_CHARSET;
536        $maildata['headers'][] = 'Auto-Submitted: auto-generated';
537
538        if (LANG_CHARSET == 'UTF-8') {
539            if (function_exists('imap_8bit') && !$serendipity['forceBase64']) {
540                $maildata['headers'][] = 'Content-Transfer-Encoding: quoted-printable';
541                $maildata['message']   = str_replace("\r\n","\n",imap_8bit($maildata['message']));
542            } else {
543                $maildata['headers'][] = 'Content-Transfer-Encoding: base64';
544                $maildata['message']   = chunk_split(base64_encode($maildata['message']));
545            }
546        }
547    }
548
549    if ($serendipity['dumpMail']) {
550        $fp = fopen($serendipity['serendipityPath'] . '/templates_c/mail.log', 'a');
551        fwrite($fp, date('Y-m-d H:i') . "\n" . print_r($maildata, true));
552        fclose($fp);
553    }
554
555    if (!isset($maildata['skip_native']) && !empty($maildata['to'])) {
556        return mail($maildata['to'], $maildata['subject'], $maildata['message'], implode("\n", $maildata['headers']));
557    }
558}
559
560/**
561 * Fetch all references (links) from a given entry ID
562 *
563 * @access public
564 * @param   int     The entry ID
565 * @return  array   The SQL result containing the references/links of an entry
566 */
567function serendipity_fetchReferences($id) {
568    global $serendipity;
569
570    $query = "SELECT name,link FROM {$serendipity['dbPrefix']}references WHERE entry_id = '" . (int)$id . "' AND type = ''";
571
572    return serendipity_db_query($query);
573}
574
575
576/**
577 * Encode a string to UTF-8, if not already in UTF-8 format.
578 *
579 * @access public
580 * @param   string  The input string
581 * @return  string  The output string
582 */
583function serendipity_utf8_encode($string) {
584    if (strtolower(LANG_CHARSET) != 'utf-8') {
585        if (function_exists('iconv')) {
586            $new = iconv(LANG_CHARSET, 'UTF-8', $string);
587            if ($new !== false) {
588                return $new;
589            } else {
590                return utf8_encode($string);
591            }
592        } else if (function_exists('mb_convert_encoding')) {
593            return mb_convert_encoding($string, 'UTF-8', LANG_CHARSET);
594        } else {
595            return utf8_encode($string);
596        }
597    } else {
598        return $string;
599    }
600}
601
602/**
603 * Create a link that can be used within a RSS feed to indicate a permalink for an entry or comment
604 *
605 * @access public
606 * @param   array       The input entry array
607 * @param   boolean     Toggle whether the link will be for a COMMENT [true] or an ENTRY [false]
608 * @return  string      A permalink for the given entry
609 */
610function serendipity_rss_getguid($entry, $comments = false) {
611    global $serendipity;
612
613    $id = (isset($entry['entryid']) && $entry['entryid'] != '' ? $entry['entryid'] : $entry['id']);
614
615    // When using %id%, we can make the GUID shorter and independent from the title.
616    // If not using %id%, the entryid needs to be used for uniqueness.
617    if (stristr($serendipity['permalinkStructure'], '%id%') !== FALSE) {
618        $title = 'guid';
619    } else {
620        $title = $id;
621    }
622
623    $guid = serendipity_archiveURL(
624        $id,
625        $title,
626        'baseURL',
627        true,
628        array('timestamp' => $entry['timestamp'])
629    );
630
631    if ($comments == true) {
632        $guid .= '#c' . $entry['commentid'];
633    }
634
635    return $guid;
636}
637
638/**
639 * Perform some replacement calls to make valid XHTML content
640 *
641 * jbalcorn: starter function to clean up xhtml for atom feed.  Add things to this as we find common
642 * mistakes, unless someone finds a better way to do this.
643 *      DONE:
644 *          since someone encoded all the urls, we can now assume any amp followed by
645 *              whitespace or a HTML tag (i.e. &<br /> )should be
646 *              encoded and most not with a space are intentional
647 *
648 * @access public
649 * @param   string  Input HTML code
650 * @return  string  Cleaned HTML code
651 */
652function xhtml_cleanup($html) {
653    static $p = array(
654        '/\&([\s\<])/',                 // ampersand followed by whitespace or tag
655        '/\&$/',                        // ampersand at end of body
656        '/<(br|hr|img)([^\/>]*)>/i',    // unclosed br tag - attributes included
657        '/\&nbsp;/'                     // Protect whitespace
658    );
659
660    static $r = array(
661        '&amp;\1',
662        '&amp;',
663        '<\1\2 />',
664        '&#160;'
665    );
666
667    return preg_replace($p, $r, $html);
668}
669
670/**
671 * Fetch user data for a specific Serendipity author
672 *
673 * @access public
674 * @param   int     The requested author id
675 * @return  array   The SQL result array
676 */
677function serendipity_fetchAuthor($author) {
678    global $serendipity;
679
680    return serendipity_db_query("SELECT * FROM {$serendipity['dbPrefix']}authors WHERE " . (is_numeric($author) ? "authorid={$author};" : "username='" . serendipity_db_escape_string($author) . "';"));
681}
682
683/**
684 * Split a filename into basename and extension parts
685 *
686 * @access public
687 * @param   string  Filename
688 * @return  array   Return array containing the basename and file extension
689 */
690function serendipity_parseFileName($file) {
691    $x = explode('.', $file);
692    if (count($x)>1){
693        $suf = array_pop($x);
694        $f   = @implode('.', $x);
695        return array($f, $suf);
696    }
697    else {
698        return array($file,'');
699    }
700}
701
702/**
703 * Track the referer to a specific Entry ID
704 *
705 * @access public
706 * @param   int     Entry ID
707 * @return  null
708 */
709function serendipity_track_referrer($entry = 0) {
710    global $serendipity;
711
712    // Tracking disabled.
713    if ($serendipity['trackReferrer'] === false) {
714        return;
715    }
716
717    if (isset($_SERVER['HTTP_REFERER'])) {
718        if (stristr($_SERVER['HTTP_REFERER'], $serendipity['baseURL']) !== false) {
719            return;
720        }
721
722        if (!isset($serendipity['_blockReferer']) || !is_array($serendipity['_blockReferer'])) {
723            // Only generate an array once per call
724            $serendipity['_blockReferer'] = array();
725            $serendipity['_blockReferer'] = @explode(';', $serendipity['blockReferer']);
726        }
727
728        $url_parts  = parse_url($_SERVER['HTTP_REFERER']);
729        $host_parts = explode('.', $url_parts['host']);
730        if (!$url_parts['host'] ||
731            strstr($url_parts['host'], $_SERVER['SERVER_NAME'])) {
732            return;
733        }
734
735        foreach($serendipity['_blockReferer'] AS $idx => $hostname) {
736            if (@strstr($url_parts['host'], $hostname)) {
737                return;
738            }
739        }
740
741        if (rand(0, 100) < 1) {
742            serendipity_track_referrer_gc();
743        }
744
745        $ts       = serendipity_db_get_interval('ts');
746        $interval = serendipity_db_get_interval('interval', 900);
747
748        $url_parts['query'] = substr($url_parts['query'], 0, 255);
749
750        $suppressq = "SELECT count(1)
751                      FROM $serendipity[dbPrefix]suppress
752                      WHERE ip = '" . serendipity_db_escape_string($_SERVER['REMOTE_ADDR']) . "'
753                      AND scheme = '" . serendipity_db_escape_string($url_parts['scheme']) . "'
754                      AND port = '" . serendipity_db_escape_string($url_parts['port']) . "'
755                      AND host = '" . serendipity_db_escape_string($url_parts['host']) . "'
756                      AND path = '" . serendipity_db_escape_string($url_parts['path']) . "'
757                      AND query = '" . serendipity_db_escape_string($url_parts['query']) . "'
758                      AND last > $ts - $interval";
759
760        $suppressp = "DELETE FROM $serendipity[dbPrefix]suppress
761                      WHERE ip = '" . serendipity_db_escape_string($_SERVER['REMOTE_ADDR']) . "'
762                      AND scheme = '" . serendipity_db_escape_string($url_parts['scheme']) . "'
763                      AND host = '" . serendipity_db_escape_string($url_parts['host']) . "'
764                      AND port = '" . serendipity_db_escape_string($url_parts['port']) . "'
765                      AND query = '" . serendipity_db_escape_string($url_parts['query']) . "'
766                      AND path = '" . serendipity_db_escape_string($url_parts['path']) . "'";
767        $suppressu = "INSERT INTO $serendipity[dbPrefix]suppress
768                      (ip, last, scheme, host, port, path, query)
769                      VALUES (
770                      '" . serendipity_db_escape_string($_SERVER['REMOTE_ADDR']) . "',
771                      $ts,
772                      '" . serendipity_db_escape_string($url_parts['scheme']) . "',
773                      '" . serendipity_db_escape_string($url_parts['host']) . "',
774                      '" . serendipity_db_escape_string($url_parts['port']) . "',
775                      '" . serendipity_db_escape_string($url_parts['path']) . "',
776                      '" . serendipity_db_escape_string($url_parts['query']) . "'
777                      )";
778
779        $count = serendipity_db_query($suppressq, true);
780
781        if ($count[0] == 0) {
782            serendipity_db_query($suppressu);
783            return;
784        }
785
786        serendipity_db_query($suppressp);
787        serendipity_db_query($suppressu);
788
789        serendipity_track_url('referrers', $_SERVER['HTTP_REFERER'], $entry);
790    }
791}
792
793/**
794 * Garbage Collection for suppressed referrers
795 *
796 * "Bad" referrers, that only occurred once to your entry are put within a
797 * SUPPRESS database table. Entries contained there will be cleaned up eventually.
798 *
799 * @access public
800 * @return null
801 */
802function serendipity_track_referrer_gc() {
803    global $serendipity;
804
805    $ts       = serendipity_db_get_interval('ts');
806    $interval = serendipity_db_get_interval('interval', 900);
807    $gc = "DELETE FROM $serendipity[dbPrefix]suppress WHERE last <= $ts - $interval";
808    serendipity_db_query($gc);
809}
810
811/**
812 * Track a URL used in your Blog (Exit-Tracking)
813 *
814 * @access public
815 * @param  string   Name of the DB table where to store the link (exits|referrers)
816 * @param  string   The URL to track
817 * @param  int      The Entry ID to relate the track to
818 * @return null
819 */
820function serendipity_track_url($list, $url, $entry_id = 0) {
821    global $serendipity;
822
823    $url_parts = parse_url($url);
824    $url_parts['query'] = substr($url_parts['query'], 0, 255);
825
826    serendipity_db_query(
827      @sprintf(
828        "UPDATE %s%s
829            SET count = count + 1
830          WHERE scheme = '%s'
831            AND host   = '%s'
832            AND port   = '%s'
833            AND path   = '%s'
834            AND query  = '%s'
835            AND day    = '%s'
836            %s",
837
838        $serendipity['dbPrefix'],
839        $list,
840        serendipity_db_escape_string($url_parts['scheme']),
841        serendipity_db_escape_string($url_parts['host']),
842        serendipity_db_escape_string($url_parts['port']),
843        serendipity_db_escape_string($url_parts['path']),
844        serendipity_db_escape_string($url_parts['query']),
845        date('Y-m-d'),
846        ($entry_id != 0) ? "AND entry_id = '". (int)$entry_id ."'" : ''
847      )
848    );
849
850    if (serendipity_db_affected_rows() == 0) {
851        serendipity_db_query(
852          sprintf(
853            "INSERT INTO %s%s
854                    (entry_id, day, count, scheme, host, port, path, query)
855             VALUES (%d, '%s', 1, '%s', '%s', '%s', '%s', '%s')",
856
857            $serendipity['dbPrefix'],
858            $list,
859            (int)$entry_id,
860            date('Y-m-d'),
861            serendipity_db_escape_string($url_parts['scheme']),
862            serendipity_db_escape_string($url_parts['host']),
863            serendipity_db_escape_string($url_parts['port']),
864            serendipity_db_escape_string($url_parts['path']),
865            serendipity_db_escape_string($url_parts['query'])
866          )
867        );
868    }
869}
870
871/**
872 * Display the list of top referrers
873 *
874 * @access public
875 * @see serendipity_displayTopUrlList()
876 * @param  int      Number of referrers to show
877 * @param  boolean  Whether to use HTML links for URLs
878 * @param  int      Interval for which the top referrers are aggregated
879 * @return string   List of Top referrers
880 */
881function serendipity_displayTopReferrers($limit = 10, $use_links = true, $interval = 7) {
882    return serendipity_displayTopUrlList('referrers', $limit, $use_links, $interval);
883}
884
885/**
886 * Display the list of top exits
887 *
888 * @access public
889 * @see serendipity_displayTopUrlList()
890 * @param  int      Number of exits to show
891 * @param  boolean  Whether to use HTML links for URLs
892 * @param  int      Interval for which the top exits are aggregated
893 * @return string   List of Top exits
894 */
895function serendipity_displayTopExits($limit = 10, $use_links = true, $interval = 7) {
896    return serendipity_displayTopUrlList('exits', $limit, $use_links, $interval);
897}
898
899/**
900 * Display HTML output data of a Exit/Referrer list
901 *
902 * @access public
903 * @see serendipity_displayTopExits()
904 * @see serendipity_displayTopReferrers()
905 * @param   string      Name of the DB table to show data from (exits|referrers)
906 * @param  boolean  Whether to use HTML links for URLs
907 * @param  int      Interval for which the top exits are aggregated
908 * @return
909 */
910function serendipity_displayTopUrlList($list, $limit, $use_links = true, $interval = 7) {
911    global $serendipity;
912
913    if ($limit){
914        $limit = serendipity_db_limit_sql($limit);
915    }
916
917    /* HACK */
918    if (preg_match('/^mysqli?/', $serendipity['dbType'])) {
919        /* Nonportable SQL due to MySQL date functions,
920         * but produces rolling 7 day totals, which is more
921         * interesting
922         */
923        $query = "SELECT scheme, host, SUM(count) AS total
924                  FROM {$serendipity['dbPrefix']}$list
925                  WHERE day > date_sub(current_date, interval " . (int)$interval . " day)
926                  GROUP BY host
927                  ORDER BY total DESC, host
928                  $limit";
929    } else {
930        /* Portable version of the same query */
931        $query = "SELECT scheme, host, SUM(count) AS total
932                  FROM {$serendipity['dbPrefix']}$list
933                  GROUP BY scheme, host
934                  ORDER BY total DESC, host
935                  $limit";
936    }
937
938    $rows = serendipity_db_query($query);
939    $output = "<span class='serendipityReferer'>";
940    if (is_array($rows)) {
941        foreach ($rows as $row) {
942            if ($use_links) {
943                $output .= sprintf(
944                    '<span class="block_level"><a href="%1$s://%2$s" title="%2$s" >%2$s</a> (%3$s) </span>',
945                    serendipity_specialchars($row['scheme']),
946                    serendipity_specialchars($row['host']),
947                    serendipity_specialchars($row['total'])
948                );
949            } else {
950                $output .= sprintf(
951                    '<span class="block_level">%1$s (%2$s) </span>',
952                    serendipity_specialchars($row['host']),
953                    serendipity_specialchars($row['total'])
954                );
955            }
956        }
957    }
958    $output .= "</span>";
959    return $output;
960}
961
962/**
963 * Return either HTML or XHTML code for an '<a target...> attribute.
964 *
965 * @access public
966 * @param   string  The target to use (_blank, _parent, ...)
967 * @return  string  HTML string containig the valid markup for the target attribute.
968 */
969function serendipity_xhtml_target($target) {
970    global $serendipity;
971
972    if ($serendipity['enablePopup'] != true)
973        return "";
974
975    return ' onclick="window.open(this.href, \'target' . time() . '\'); return false;" ';
976}
977
978/**
979 * Parse a URI portion to return which RSS Feed version was requested
980 *
981 * @access public
982 * @param  string  Name of the core URI part
983 * @param  string  File extension name of the URI
984 * @return string  RSS feed type/version
985 */
986function serendipity_discover_rss($name, $ext) {
987    static $default = '2.0';
988
989    /* Detect type */
990    if ($name == 'comments') {
991        $type = 'comments';
992    } elseif ($name == 'comments_and_trackbacks') {
993        $type = 'comments_and_trackbacks';
994    } elseif ($name == 'trackbacks') {
995        $type = 'trackbacks';
996    } else {
997        $type = 'content';
998    }
999
1000    /* Detect version */
1001    if ($name == 'atom' || $name == 'atom10' || $ext == 'atom') {
1002        $ver = 'atom1.0';
1003    } elseif ($name == 'atom03') {
1004        $ver = 'atom0.3';
1005    } elseif ($name == 'opml' || $ext == 'opml') {
1006        $ver = 'opml1.0';
1007    } elseif ($ext == 'rss') {
1008        $ver = '0.91';
1009    } elseif ($ext == 'rss1') {
1010        $ver = '1.0';
1011    } else {
1012        $ver = $default;
1013    }
1014
1015    return array($ver, $type);
1016}
1017
1018/**
1019 * Check whether an input string contains "evil" characters used for HTTP Response Splitting
1020 *
1021 * @access public
1022 * @param   string      String to check for evil characters
1023 * @return  boolean     Return true on success, false on failure
1024 */
1025function serendipity_isResponseClean($d) {
1026    return (strpos($d, "\r") === false && strpos($d, "\n") === false && stripos($d, "%0A") === false && stripos($d, "%0D") === false);
1027}
1028
1029/**
1030 * Create a new Category
1031 *
1032 * @access public
1033 * @param   string  The new category name
1034 * @param   string  The new category description
1035 * @param   int     The category owner
1036 * @param   string  An icon representing the category
1037 * @param   int     A possible parentid to a category
1038 * @return  int     The new category's ID
1039 */
1040function serendipity_addCategory($name, $desc, $authorid, $icon, $parentid) {
1041    global $serendipity;
1042    $query = "INSERT INTO {$serendipity['dbPrefix']}category
1043                    (category_name, category_description, authorid, category_icon, parentid, category_left, category_right)
1044                  VALUES
1045                    ('". serendipity_db_escape_string($name) ."',
1046                     '". serendipity_db_escape_string($desc) ."',
1047                      ". (int)$authorid .",
1048                     '". serendipity_db_escape_string($icon) ."',
1049                      ". (int)$parentid .",
1050                       0,
1051                       0)";
1052
1053    serendipity_db_query($query);
1054    $cid = serendipity_db_insert_id('category', 'categoryid');
1055    serendipity_plugin_api::hook_event('backend_category_addNew', $cid);
1056
1057    $data = array(
1058        'categoryid'           => $cid,
1059        'category_name'        => $name,
1060        'category_description' => $desc
1061    );
1062
1063    serendipity_insertPermalink($data, 'category');
1064    return $cid;
1065}
1066
1067/**
1068 * Update an existing category
1069 *
1070 * @access public
1071 * @param   int     Category ID to update
1072 * @param   string  The new category name
1073 * @param   string  The new category description
1074 * @param   int     The new category owner
1075 * @param   string  The new category icon
1076 * @param   int     The new category parent ID
1077 * @param   int     The new category sort order
1078 * @param   int     The new category subcat hiding
1079 * @return null
1080 */
1081function serendipity_updateCategory($cid, $name, $desc, $authorid, $icon, $parentid, $sort_order = 0, $hide_sub = 0, $admin_category = '') {
1082    global $serendipity;
1083
1084    $query = "UPDATE {$serendipity['dbPrefix']}category
1085                    SET category_name = '". serendipity_db_escape_string($name) ."',
1086                        category_description = '". serendipity_db_escape_string($desc) ."',
1087                        authorid = ". (int)$authorid .",
1088                        category_icon = '". serendipity_db_escape_string($icon) ."',
1089                        parentid = ". (int)$parentid .",
1090                        sort_order = ". (int)$sort_order . ",
1091                        hide_sub = ". (int)$hide_sub . "
1092                    WHERE categoryid = ". (int)$cid ."
1093                        $admin_category";
1094    serendipity_db_query($query);
1095    serendipity_plugin_api::hook_event('backend_category_update', $cid);
1096
1097    $data = array(
1098        'id'                   => $cid,
1099        'categoryid'           => $cid,
1100        'category_name'        => $name,
1101        'category_description' => $desc
1102    );
1103
1104    serendipity_updatePermalink($data, 'category');
1105}
1106
1107/**
1108 * Ends a session, so that while a file requests happens, Serendipity can work on in that session
1109 */
1110function serendipity_request_start() {
1111    @session_write_close();
1112    return true;
1113}
1114
1115/**
1116 * Continues a session after a file request
1117 */
1118function serendipity_request_end() {
1119    @session_start();
1120    return true;
1121}
1122
1123/* Request the contents of an URL, API wrapper
1124 * @param $uri string               The URL to fetch
1125 * @param $method string            HTTP method (GET/POST/PUT/OPTIONS...)
1126 * @param $contenttype string       optional HTTP content type
1127 * @param $contenttype mixed        optional extra data (i.e. POST body), can be an array
1128 * @param $extra_options array      Extra options for HTTP_Request $options array (can override defaults)
1129 * @param $addData string           possible extra event addData declaration for 'backend_http_request' hook
1130 * @param $auth array               Array with 'user' and 'pass' for HTTP Auth
1131 * @return $content string          The URL contents
1132 */
1133
1134function serendipity_request_url($uri, $method = 'GET', $contenttype = null, $data = null, $extra_options = null, $addData = null, $auth = null) {
1135    global $serendipity;
1136
1137    require_once S9Y_PEAR_PATH . 'HTTP/Request2.php';
1138    $options = array('follow_redirects' => true, 'max_redirects' => 5);
1139
1140    if (is_array($extra_options)) {
1141        foreach($extra_options AS $okey => $oval) {
1142            $options[$okey] = $oval;
1143        }
1144    }
1145    serendipity_plugin_api::hook_event('backend_http_request', $options, $addData);
1146    serendipity_request_start();
1147    if (version_compare(PHP_VERSION, '5.6.0', '<')) {
1148        // On earlier PHP versions, the certificate validation fails. We deactivate it on them to restore the functionality we had with HTTP/Request1
1149        $options['ssl_verify_peer'] = false;
1150    }
1151
1152    switch(strtoupper($method)) {
1153        case 'GET':
1154            $http_method = HTTP_Request2::METHOD_GET;
1155            break;
1156
1157        case 'PUT':
1158            $http_method = HTTP_Request2::METHOD_PUT;
1159            break;
1160
1161        case 'OPTIONS':
1162            $http_method = HTTP_Request2::METHOD_OPTIONS;
1163            break;
1164
1165        case 'HEAD':
1166            $http_method = HTTP_Request2::METHOD_HEAD;
1167            break;
1168
1169        case 'DELETE':
1170            $http_method = HTTP_Request2::METHOD_DELETE;
1171            break;
1172
1173        case 'TRACE':
1174            $http_method = HTTP_Request2::METHOD_TRACE;
1175            break;
1176
1177        case 'CONNECT':
1178            $http_method = HTTP_Request2::METHOD_CONNECT;
1179            break;
1180
1181        default:
1182        case 'POST':
1183            $http_method = HTTP_Request2::METHOD_POST;
1184            break;
1185
1186    }
1187
1188    $req = new HTTP_Request2($uri, $http_method, $options);
1189    if (isset($contenttype) && $contenttype !== null) {
1190       $req->setHeader('Content-Type', $contenttype);
1191    }
1192
1193    if (is_array($auth)) {
1194        $req->setAuth($auth['user'], $auth['pass']);
1195    }
1196
1197    if ($data != null) {
1198        if (is_array($data)) {
1199            $req->addPostParameter($data);
1200        } else {
1201            $req->setBody($data);
1202        }
1203    }
1204
1205    try {
1206        $res = $req->send();
1207    } catch (HTTP_Request2_Exception $e) {
1208        serendipity_request_end();
1209        return false;
1210    }
1211
1212
1213    $fContent = $res->getBody();
1214    $serendipity['last_http_request'] = array(
1215        'responseCode' => $res->getStatus(),
1216        'effectiveUrl' => $res->getEffectiveUrl(),
1217        'reasonPhrase' => $res->getReasonPhrase(),
1218        'isRedirect'   => $res->isRedirect(),
1219        'cookies'      => $res->getCookies(),
1220        'version'      => $res->getVersion(),
1221        'header'       => $res->getHeader(),
1222
1223        'object'       => $res // forward compatibility for possible other checks
1224    );
1225
1226    serendipity_request_end();
1227    return $fContent;
1228}
1229
1230
1231if (!function_exists('microtime_float')) {
1232    /**
1233     * Get current timestamp as microseconds
1234     *
1235     * @access public
1236     * @return float    the time
1237     */
1238    function microtime_float() {
1239        list($usec, $sec) = explode(" ", microtime());
1240        return ((float)$usec + (float)$sec);
1241    }
1242}
1243
1244/**
1245 * Converts Array data to be used as a GET string
1246 *
1247 * @access public
1248 * @param   array   The input array
1249 * @param   string  An array prefix
1250 * @param   string  How to join the array
1251 * @return  string  The HTTP query string
1252 */
1253function serendipity_build_query(&$array, $array_prefix = null, $comb_char = '&amp;') {
1254    $ret = array();
1255    if (!is_array($array)) {
1256        return '';
1257    }
1258
1259    foreach ($array as $k => $v) {
1260        $newkey = urlencode($k);
1261        if ($array_prefix) {
1262            $newkey = $array_prefix . '[' . $newkey . ']';
1263        }
1264        if (is_array($v)) {
1265            $ret[] = serendipity_build_query($v, $newkey, $comb_char);
1266        } else {
1267            $ret[] = $newkey . '=' . urlencode($v);
1268        }
1269    }
1270
1271    return implode($comb_char, $ret);
1272}
1273
1274/**
1275 * Picks a specified key from an array and returns it
1276 *
1277 * @access public
1278 * @param   array   The input array
1279 * @param   string  The key to search for
1280 * @param   string  The default value to return when not found
1281 * @return null
1282 */
1283function &serendipity_pickKey(&$array, $key, $default) {
1284    if (!is_array($array)) {
1285        return $default;
1286    }
1287
1288    // array_key_exists() copies array, so is much slower.
1289    if (in_array($key, array_keys($array))) {
1290        if (isset($array[$key])) {
1291            return $array[$key];
1292        }
1293    }
1294    foreach($array AS $child) {
1295        if (is_array($child) && isset($child[$key]) && !empty($child[$key])) {
1296            return $child[$key];
1297        }
1298    }
1299
1300    return $default;
1301}
1302
1303/**
1304 * Retrieves the current timestamp but only deals with minutes to optimize Database caching
1305 * @access public
1306 * @return timestamp
1307 * @author Matthew Groeninger
1308 */
1309function serendipity_db_time() {
1310    static $ts    = null;
1311    static $cache = 300; // Seconds to cache
1312
1313    if ($ts === null) {
1314        $now = time();
1315        $ts = $now - ($now % $cache) + $cache;
1316    }
1317
1318    return $ts;
1319}
1320
1321/**
1322 * Inits the logger.
1323 * @return null
1324 */
1325function serendipity_initLog() {
1326    global $serendipity;
1327
1328    if (isset($serendipity['logLevel']) && $serendipity['logLevel'] !== 'Off') {
1329        if ($serendipity['logLevel'] == 'debug') {
1330            $log_level = Psr\Log\LogLevel::DEBUG;
1331        } else {
1332            $log_level = Psr\Log\LogLevel::ERROR;
1333        }
1334        $serendipity['logger'] = new Katzgrau\KLogger\Logger($serendipity['serendipityPath'] . '/templates_c/logs', $log_level);
1335    }
1336}
1337
1338/**
1339 * Check whether a given URL is valid to be locally requested
1340 * @return boolean
1341 */
1342function serendipity_url_allowed($url) {
1343    global $serendipity;
1344
1345    if ($serendipity['allowLocalURL']) {
1346        return true;
1347    }
1348
1349    $parts = @parse_url($url);
1350    if (!is_array($parts) || empty($parts['host'])) {
1351        return false;
1352    }
1353
1354    $host = trim($parts['host'], '.');
1355    if (preg_match('@^(([1-9]?\d|1\d\d|25[0-5]|2[0-4]\d)\.){3}([1-9]?\d|1\d\d|25[0-5]|2[0-4]\d)$@imsU', $host)) {
1356        $ip = $host;
1357    } else {
1358        $ip = gethostbyname($host);
1359        if ($ip === $host) {
1360            $ip = false;
1361        }
1362    }
1363
1364    if ($ip) {
1365        $ipparts = array_map('intval', explode('.', $ip));
1366        if ( 127 === $ipparts[0] || 10 === $ipparts[0] || 0 === $ipparts[0]
1367            || ( 172 === $ipparts[0] && 16 <= $ipparts[1] && 31 >= $ipparts[1] )
1368            || ( 192 === $ipparts[0] && 168 === $ipparts[1])
1369        ) {
1370            return false;
1371        }
1372    }
1373
1374    return true;
1375}
1376
1377use voku\cache\Cache;
1378// Configure voku/simple-cache to use templates_c as directory for the opcache files, the fallback
1379// when Memcached and Redis are not used. Returns the configured cache object. Used internally by
1380// the other cache functions, you most likely never need to call this.
1381function serendipity_setupCache() {
1382    $cacheManager = new \voku\cache\CacheAdapterAutoManager();
1383
1384    $cacheManager->addAdapter(
1385        \voku\cache\AdapterOpCache::class,
1386        static function () {
1387            global $serendipity;
1388            $cacheDir = $serendipity['serendipityPath'] . '/templates_c/simple_cache';
1389
1390            return $cacheDir;
1391        }
1392    );
1393
1394    $cacheManager->addAdapter(
1395        \voku\cache\AdapterArray::class
1396    );
1397
1398    $cache = new Cache(
1399        null,
1400        null,
1401        false,
1402        true,
1403        false,
1404        false,
1405        false,
1406        false,
1407        '',
1408        $cacheManager,
1409        false
1410    );
1411    return $cache;
1412}
1413
1414function serendipity_cleanCache() {
1415    $cache = serendipity_setupCache();
1416    return $cache->removeAll();
1417}
1418
1419function serendipity_cacheItem($key, $item, $ttl = 3600) {
1420    $cache = serendipity_setupCache();
1421    return $cache->setItem($key, $item, $ttl);
1422}
1423
1424function serendipity_getCacheItem($key) {
1425    $cache = serendipity_setupCache();
1426    return $cache->getItem($key);
1427}
1428
1429define("serendipity_FUNCTIONS_LOADED", true);
1430/* vim: set sts=4 ts=4 expandtab : */
1431