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
9/**
10 * Adds a new author account
11 *
12 * @access public
13 * @param   string  New username
14 * @param   string  New password
15 * @param   string  The realname of the user
16 * @param   string  The email address of the user
17 * @param   int     The userlevel of a user
18 * @return  int     The new user ID of the added author
19 */
20function serendipity_addAuthor($username, $password, $realname, $email, $userlevel=0, $hashtype=2) {
21    global $serendipity;
22    $password = serendipity_hash($password);
23    $query = "INSERT INTO {$serendipity['dbPrefix']}authors (username, password, realname, email, userlevel, hashtype)
24                        VALUES  ('" . serendipity_db_escape_string($username) . "',
25                                 '" . serendipity_db_escape_String($password) . "',
26                                 '" . serendipity_db_escape_String($realname) . "',
27                                 '" . serendipity_db_escape_String($email) . "',
28                                 '" . serendipity_db_escape_String($userlevel) . "',
29                                 '" . serendipity_db_escape_String($hashtype) . "'
30                                 )";
31    serendipity_db_query($query);
32    $cid = serendipity_db_insert_id('authors', 'authorid');
33
34    $data = array(
35        'authorid' => $cid,
36        'username' => $username,
37        'realname' => $realname,
38        'email'    => $email
39    );
40
41    serendipity_insertPermalink($data, 'author');
42    return $cid;
43}
44
45/**
46 * Delete an author account
47 *
48 * (Note, this function does not delete entries by an author)
49 *
50 * @access public
51 * @param   int     The author ID to delete
52 * @return  boolean     True on success, false on error or unsufficient privileges
53 */
54function serendipity_deleteAuthor($authorid) {
55    global $serendipity;
56
57    if (!serendipity_checkPermission('adminUsersDelete')) {
58        return false;
59    }
60
61    if (serendipity_db_query("DELETE FROM {$serendipity['dbPrefix']}authors WHERE authorid=" . (int)$authorid)) {
62        serendipity_db_query("DELETE FROM {$serendipity['dbPrefix']}permalinks WHERE entry_id=" . (int)$authorid ." and type='author'");
63    }
64    return true;
65}
66
67/**
68 * Removes a configuration value from the Serendipity Configuration
69 *
70 * Global config items have the authorid 0, author-specific configuration items have the corresponding authorid.
71 *
72 * @access public
73 * @param   string      The name of the configuration value
74 * @param   int         The ID of the owner of the config value (0: global)
75 * @return null
76 */
77function serendipity_remove_config_var($name, $authorid = 0) {
78    global $serendipity;
79    serendipity_db_query("DELETE FROM {$serendipity['dbPrefix']}config where name='" . serendipity_db_escape_string($name) . "' AND authorid = " . (int)$authorid);
80}
81
82/**
83 * Sets a configuration value for the Serendipity Configuration
84 *
85 * Global config items have the authorid 0, author-specific configuration items have the corresponding authorid.
86 *
87 * @access public
88 * @param   string      The name of the configuration value
89 * @param   string      The value of the configuration item
90 * @param   int         The ID of the owner of the config value (0: global)
91 */
92function serendipity_set_config_var($name, $val, $authorid = 0) {
93    global $serendipity;
94
95    serendipity_db_query("DELETE FROM {$serendipity['dbPrefix']}config where name='" . serendipity_db_escape_string($name) . "' AND authorid = " . (int)$authorid);
96
97    if ($name == 'password' || $name == 'check_password') {
98        return;
99    }
100
101    $r = serendipity_db_insert('config', array('name' => $name, 'value' => $val, 'authorid' => $authorid));
102
103    if ($authorid === 0 || $authorid === $serendipity['authorid']) {
104        if ($val === 'false') {
105            $serendipity[$name] = false;
106        } else {
107            $serendipity[$name] = $val;
108        }
109    }
110
111    if (is_string($r)) {
112        # if $r is a string, it is the error-message from the insert
113        echo $r;
114    }
115}
116
117/**
118 * Retrieve a global configuration value for a specific item of the current Serendipity Configuration
119 *
120 * @access public
121 * @param   string      The name of the configuration value
122 * @param   string      The default value of a configuration item, if not found in the Database
123 * @param   boolean     If set to true, the default value of a configuration item will be returned if the item is set, but empty. If false, an empty configuration value will be returned empty. This is required for getting default values if you do not want to store/allow empty config values.
124 * @return  string      The configuration value content
125 */
126function serendipity_get_config_var($name, $defval = false, $empty = false) {
127    global $serendipity;
128    if (isset($serendipity[$name])) {
129        if ($empty && gettype($serendipity[$name]) == 'string' && $serendipity[$name] === '') {
130            return $defval;
131        } else {
132            return $serendipity[$name];
133        }
134    } else {
135        return $defval;
136    }
137}
138
139/**
140 * Retrieve an author-specific configuration value for an item of the Serendipity Configuration stored in the DB
141 *
142 * Despite the serendipity_get_config_var() function, this will retrieve author-specific values straight from the Database.
143 *
144 * @access public
145 * @param   string      The name of the configuration value
146 * @param   int         The ID of the owner of the config value (0: global)
147 * @param   string      The default value of a configuration option, if not set in the DB
148 * @return  string      The configuration value content
149 */
150function serendipity_get_user_config_var($name, $authorid, $default = '') {
151    global $serendipity;
152
153    $author_sql = '';
154    if (!empty($authorid)) {
155        $author_sql = "authorid = " . (int)$authorid . " AND ";
156    } elseif (isset($serendipity[$name])) {
157        return $serendipity[$name];
158    }
159
160    $r = serendipity_db_query("SELECT value FROM {$serendipity['dbPrefix']}config WHERE $author_sql name = '" . $name . "' LIMIT 1", true);
161
162    if (is_array($r)) {
163        return $r[0];
164    } else {
165        return $default;
166    }
167}
168
169/**
170 * Retrieves an author-specific account value
171 *
172 * This retrieves specific account data from the user configuration, not from the serendipity configuration.
173 *
174 * @access public
175 * @param   string      The name of the configuration value
176 * @param   int         The ID of the author to fetch the configuration for
177 * @param   string      The default value of a configuration option, if not set in the DB
178 * @return  string      The configuration value content
179 */
180function serendipity_get_user_var($name, $authorid, $default) {
181    global $serendipity;
182
183    $r = serendipity_db_query("SELECT $name FROM {$serendipity['dbPrefix']}authors WHERE authorid = " . (int)$authorid, true);
184
185    if (is_array($r)) {
186        return $r[0];
187    } else {
188        return $default;
189    }
190}
191
192/**
193 * Updates data from the author-specific account
194 *
195 * This sets the personal account data of a serendipity user within the 'authors' DB table
196 *
197 * @access public
198 * @param   string      The name of the configuration value
199 * @param   string      The content of the configuration value
200 * @param   int         The ID of the author to set the configuration for
201 * @param   boolean     If set to true, the stored config value will be imported to the Session/current config of the user. This is applied for example when you change your own user's preferences and want it to be immediately reflected in the interface.
202 * @return null
203 */
204function serendipity_set_user_var($name, $val, $authorid, $copy_to_s9y = true) {
205    global $serendipity;
206
207    // When inserting a DB value, this array maps the new values to the corresponding s9y variables
208    static $user_map_array = array(
209        'username'  => 'serendipityUser',
210        'email'     => 'serendipityEmail',
211        'userlevel' => 'serendipityUserlevel'
212    );
213
214    // Special case for inserting a password
215    switch($name) {
216        case 'check_password':
217            //Skip this field.  It doesn't need to be stored.
218            return;
219        case 'password':
220            if (empty($val)) {
221                return;
222            }
223
224            $val = serendipity_hash($val);
225            $copy_to_s9y = false;
226            break;
227
228        case 'right_publish':
229        case 'mail_comments':
230        case 'mail_trackbacks':
231            $val = (serendipity_db_bool($val) ? 1 : '0');
232            break;
233    }
234
235    serendipity_db_query("UPDATE {$serendipity['dbPrefix']}authors SET $name = '" . serendipity_db_escape_string($val) . "' WHERE authorid = " . (int)$authorid);
236
237    if ($copy_to_s9y) {
238        if (isset($user_map_array[$name])) {
239            $key = $user_map_array[$name];
240        } else {
241            $key = 'serendipity' . ucfirst($name);
242        }
243
244        $_SESSION[$key] = $serendipity[$key] = $val;
245    }
246}
247
248/**
249 * Gets the full filename and path of a template/style/theme file
250 *
251 * The returned full path is depending on the second parameter, where you can either fetch a HTTP path, or a realpath.
252 * The file is searched in the current template, and if it is not found there, it is returned from the default template.
253 *
254 * @access public
255 * @param   string      The filename to search for in the selected template
256 * @param   string      The path selector that tells whether to return a HTTP or realpath
257 * @param   bool        Enable to include frontend template fallback chaining (used for wysiwyg Editor custom config files, emoticons, etc)
258 * @return  string      The full path+filename to the requested file
259 */
260function serendipity_getTemplateFile($file, $key = 'serendipityHTTPPath', $force_frontend_fallback = false) {
261    global $serendipity;
262
263    $directories = array();
264    if ((! defined('IN_serendipity_admin')) || $force_frontend_fallback) {
265        $directories[] = $serendipity['template'] . '/';  # In the frontend or when forced (=preview_iframe.tpl), use the frontend theme
266    } else {
267        $directories[] = $serendipity['template_backend'] . '/';    # Since 2.0 s9y can have a independent backend theme
268    }
269    $directories[] = $serendipity['template_engine'] . '/'; # themes can set an engine, which will be used if they do not have the file
270    $directories[] = $serendipity['defaultTemplate'] .'/';  # the default theme is the last place we will look in, serving as pure fallback
271    $directories = array_unique($directories); # save performance by not checking for file existence multiple times in the same directory
272
273    foreach ($directories as $directory) {
274        $templateFile = $serendipity['templatePath'] . $directory . $file;
275        if (file_exists($serendipity['serendipityPath'] . $templateFile)) {
276            return $serendipity[$key] . $templateFile;
277        }
278
279        if (file_exists($serendipity['serendipityPath'] . $templateFile . ".tpl")) {
280            # catch *.tpl files serving as template, used by the backend for serendipity_editor.js.tpl
281            return $serendipity['baseURL'] . 'index.php?/plugin/' . $file;
282        }
283    }
284
285    return false;
286}
287
288/**
289 * Loads all configuration values and imports them to the $serendipity array
290 *
291 * This function may be called twice - once for the global config and once for
292 * user-specific config
293 *
294 * @access public
295 * @param   int     The Authorid to fetch the configuration from (0: global)
296 * @return  null
297 */
298function serendipity_load_configuration($author = null) {
299    global $serendipity;
300    static $config_loaded = array();
301
302    if (isset($config_loaded[$author])) {
303        return true;
304    }
305
306    if (!empty($author)) {
307        // Replace default configuration directives with user-relevant data
308        $rows =& serendipity_db_query("SELECT name,value
309                                        FROM {$serendipity['dbPrefix']}config
310                                        WHERE authorid = '". (int)$author ."'");
311    } else {
312        // Only get default variables, user-independent (frontend)
313        $rows =& serendipity_db_query("SELECT name, value
314                                        FROM {$serendipity['dbPrefix']}config
315                                        WHERE authorid = 0");
316    }
317
318    if (is_array($rows)) {
319        foreach ($rows as $row) {
320            // Convert 'true' and 'false' into booleans
321            $serendipity[$row['name']] = serendipity_get_bool($row['value']);
322        }
323    }
324    $config_loaded[$author] = true;
325
326    // Set baseURL to defaultBaseURL
327    if ((empty($author) || empty($serendipity['baseURL'])) && isset($serendipity['defaultBaseURL'])) {
328        $serendipity['baseURL'] = $serendipity['defaultBaseURL'];
329    }
330
331    // Store default language
332    $serendipity['default_lang'] = $serendipity['lang'];
333}
334
335/**
336 * Perform logout functions (destroys session data)
337 *
338 * @access public
339 * @return null
340 */
341function serendipity_logout() {
342    $_SESSION['serendipityAuthedUser'] = false;
343    serendipity_session_destroy();
344    serendipity_deleteCookie('author_username');
345    serendipity_deleteCookie('author_autologintoken');
346    serendipity_deleteCookie('author_token');
347}
348
349/**
350 * Destroys a session, keeps important stuff intact.
351 * @access public
352 * @return null
353 */
354function serendipity_session_destroy() {
355    $no_smarty = $_SESSION['no_smarty'];
356    @session_destroy();
357    session_start();
358    session_regenerate_id();
359
360    $_SESSION['SERVER_GENERATED_SID'] = true;
361    $_SESSION['no_smarty']            = $no_smarty;
362}
363
364/**
365 * Perform login to Serendipity
366 *
367 * @access public
368 * @param   boolean     If set to true, external plugins will be queried for getting a login
369 * @return  boolean     Return true, if the user is logged in. False if not.
370 */
371function serendipity_login($use_external = true) {
372    global $serendipity;
373
374    if (serendipity_authenticate_author('', '', false, $use_external)) {
375        #The session has this data already
376        #we previously just checked the value of $_SESSION['serendipityAuthedUser'] but
377        #we need the authorid still, so call serendipity_authenticate_author with blank
378        #params
379        return true;
380    }
381
382    // First try login via POST data. If true, the userinformation will be stored in a cookie (optionally)
383    if (serendipity_authenticate_author($serendipity['POST']['user'], $serendipity['POST']['pass'], false, $use_external)) {
384        if (empty($serendipity['POST']['auto'])) {
385            serendipity_deleteCookie('author_information');
386            return false;
387        } else {
388            serendipity_issueAutologin($serendipity['POST']['user']);
389            return true;
390        }
391    // Now try login via COOKIE data
392    } elseif (isset($serendipity['COOKIE']['author_username'])) {
393        $user = $serendipity['COOKIE']['author_username'];
394        $valid_logintoken = serendipity_checkAutologin($user);
395        if ($valid_logintoken === true) {
396            // if we do not tie down the session gere it will be recreated on every page reload, which will fuck op the form token system. That's why we need to load all data that makes the session stick. That's why we call setAuthorToken here.
397            serendipity_setAuthorToken();
398            serendipity_load_userdata($user);
399            return true;
400        } else {
401            serendipity_deleteCookie('author_username');
402            serendipity_deleteCookie('author_autologintoken');
403            return false;
404        }
405    }
406
407    $data = array('ext' => $use_external, 'mode' => 2, 'user' => $serendipity['POST']['user'], 'pass' => $serendipity['POST']['pass']);
408    serendipity_plugin_api::hook_event('backend_loginfail', $data);
409}
410
411/**
412 * Issue a new auto login cookie. For that we store in the options table (name, value, okey) the values (autologin_$username, a random token, the current time).
413 * @param String The username
414 */
415function serendipity_issueAutologin($user) {
416    global $serendipity;
417
418    try {
419        // TODO: Add https://github.com/paragonie/random_compat for PHP < 7.0
420        $random_string = random_bytes(32);
421    } catch (TypeError $e) {
422        // Well, it's an integer, so this IS unexpected.
423        die("An unexpected error has occurred");
424    } catch (Error $e) {
425        // This is also unexpected because 32 is a reasonable integer.
426        die("An unexpected error has occurred");
427    } catch (Exception $e) {
428        // If you get this message, the CSPRNG failed hard.
429        die("Could not generate a random string. Is our OS secure?");
430    }
431
432    $rnd = bin2hex($random_string);
433
434
435    // Delete possible current cookie. Also delete any autologin keys that smell like 3-week-old, dead fish.
436    if (stristr($serendipity['dbType'], 'sqlite')) {
437        $cast = "okey";
438    } elseif (stristr($serendipity['dbType'], 'mysqli')) {
439        // Adds explicit casting for mysql.
440        $cast = "cast(okey as unsigned)";
441    } else {
442        // Adds explicit casting for postgresql and others.
443        $cast = "cast(okey as integer)";
444    }
445
446    serendipity_db_query("DELETE FROM {$serendipity['dbPrefix']}options
447                                WHERE name = 'autologin_" . serendipity_db_escape_string($user) . "'
448                                   OR (name LIKE 'autologin_%' AND $cast < " . (time() - 1814400) . ")");
449
450    // Issue new autologin cookie
451    serendipity_db_query("INSERT INTO {$serendipity['dbPrefix']}options (name, value, okey) VALUES ('autologin_" . serendipity_db_escape_string($user) . "', '" . $rnd  . "', '" . time() . "')");
452    serendipity_setCookie('author_autologintoken', $rnd, true, false, true);
453    serendipity_setCookie('author_username', $user);
454}
455
456/**
457 * Checks a new auto login cookie, by seeing whether the token stored in the cookie is the same as the one stored in the database
458 * @param String The username
459 * @returns bool true if valid, false if not
460 */
461function serendipity_checkAutologin($user) {
462    global $serendipity;
463
464    if (stristr($serendipity['dbType'], 'sqlite')) {
465        $cast = "okey";
466    } elseif (stristr($serendipity['dbType'], 'mysqli')) {
467        // Adds explicit casting for mysql.
468        $cast = "cast(okey as unsigned)";
469    } else {
470        // Adds explicit casting for postgresql and others.
471        $cast = "cast(okey as integer)";
472    }
473
474    // Fetch autologin data from DB
475    $autologin_stored = serendipity_db_query("SELECT name, value, okey FROM {$serendipity['dbPrefix']}options WHERE name = 'autologin_" . serendipity_db_escape_string($user) . "' AND $cast > " . (time() - 1814400) . " LIMIT 1", true, 'assoc');
476
477    if (!is_array($autologin_stored)) {
478        return false;
479    }
480
481    if ($serendipity['COOKIE']['author_autologintoken'] !== $autologin_stored['value']) {
482        return false;
483    }
484
485    if ($autologin_stored['okey'] < (time()-86400)) {
486        // Issued autologin cookie has been issued more than 1 day ago. Re-Issue new cookie, invalidate old one to prevent abuse
487        if ($serendipity['expose_s9y']) serendipity_header('X-ReIssue-Cookie: +' . (time() - $autologin_stored['okey']) . 's');
488        serendipity_issueAutologin($user);
489    }
490
491    return true;
492}
493
494/**
495 * Set a session cookie which can identify a user across http/https boundaries
496 */
497function serendipity_setAuthorToken() {
498    $hash = sha1(uniqid(rand(), true));
499    serendipity_setCookie('author_token', $hash);
500    $_SESSION['author_token'] = $hash;
501}
502
503/**
504 * Perform user authentication routine
505 *
506 * If a user is already authenticated via session data, this bypasses some routines.
507 * After a user has ben authenticated, several SESSION variables ar set.
508 * If the authentication fails, the session is destroyed.
509 *
510 * @access public
511 * @param   string      The username to check
512 * @param   string      The password to check (may contain plaintext or MD5 hash)
513 * @param   boolean     Indicates whether the input password is already in MD5 format (TRUE) or not (FALSE).
514 * @param   boolean     Indicates whether to query external plugins for authentication
515 * @return  boolean     True on success, False on error
516 */
517function serendipity_authenticate_author($username = '', $password = '', $is_hashed = false, $use_external = true) {
518    global $serendipity;
519    static $debug = false;
520    static $debugc = 0;
521
522    if ($debug) {
523        $fp = fopen('login.log', 'a');
524        flock($fp, LOCK_EX);
525        $debugc++;
526        fwrite($fp, date('Y-m-d H:i') . ' - #' . $debugc . ' Login init [' . $username . ',' . $password . ',' . (int)$is_hashed . ',' . (int)$use_external . ']' . ' (' . $_SERVER['REMOTE_ADDR'] . ',' . $_SERVER['REQUEST_URI'] . ', ' . session_id() . ')' . "\n");
527    }
528    if (isset($_SESSION['serendipityUser']) && isset($_SESSION['serendipityPassword']) && isset($_SESSION['serendipityAuthedUser']) && $_SESSION['serendipityAuthedUser'] == true) {
529        $username = $_SESSION['serendipityUser'];
530        $password = $_SESSION['serendipityPassword'];
531        // For safety reasons when multiple blogs are installed on the same host, we need to check the current author each time to not let him log into a different blog with the same sessiondata
532        #$is_hashed = true;
533        if ($debug) fwrite($fp, date('Y-m-d H:i') . ' - Recall from session: ' . $username . ':' . $password . "\n");
534    }
535
536    if ($debug) fwrite($fp, date('Y-m-d H:i') . ' - Login ext check' . "\n");
537    $is_authenticated = false;
538    serendipity_plugin_api::hook_event('backend_login', $is_authenticated, NULL);
539    if ($is_authenticated) {
540        return true;
541    }
542
543    if ($debug) fwrite($fp, date('Y-m-d H:i') . ' - Login username check:' . $username . "\n");
544    if (!empty($username) && is_string($username)) {
545        if ($use_external) {
546            serendipity_plugin_api::hook_event('backend_auth', $is_hashed, array('username' => $username, 'password' => $password));
547        }
548
549        $query = "SELECT DISTINCT
550                    email, password, realname, authorid, userlevel, right_publish, hashtype
551                  FROM
552                    {$serendipity['dbPrefix']}authors
553                  WHERE
554                    username   = '" . serendipity_db_escape_string($username) . "'";
555        if ($debug) fwrite($fp, date('Y-m-d H:i') . ' - Login check (' . serialize($is_hashed) . ', ' . $_SESSION['serendipityPassword'] . '):' . $query . "\n");
556
557        $rows =& serendipity_db_query($query, false, 'assoc');
558        if (is_array($rows)) {
559            foreach($rows AS $row) {
560                if ($is_valid_user) continue;
561                $is_valid_user = false;
562
563                if (empty($row['hashtype']) || $row['hashtype'] == 0) {
564                    // Old MD5 hashing routine. Will convert user.
565                    if (isset($serendipity['hashkey']) && (time() - $serendipity['hashkey']) >= 15768000) {
566                        die('You can no longer login with an old-style MD5 hash to prevent MD5-Hostage abuse.
567                             Please ask the Administrator to set you a new password.');
568                    }
569
570                    if ( ($is_hashed === false && (string)$row['password'] === (string)md5($password)) ||
571                         ($is_hashed !== false && (string)$row['password'] === (string)$password) ) {
572
573                        serendipity_db_query("UPDATE {$serendipity['dbPrefix']}authors
574                                                 SET password = '" . ($is_hashed === false ? serendipity_hash($password) : $password) . "',
575                                                     hashtype = 1
576                                               WHERE authorid = '" . $row['authorid'] . "'");
577                        if ($debug) fwrite($fp, date('Y-m-d H:i') . ' - Migrated user:' . $row['username'] . "\n");
578                        $is_valid_user = true;
579                    } else {
580                        continue;
581                    }
582                } else {
583                    if ($row['hashtype'] == 1) {
584                        // Old sha1 hashing routine. Will convert user.
585                        if ( ($is_hashed === false && (string)$row['password'] === (string)serendipity_sha1_hash($password)) ||
586                             ($is_hashed !== false && (string)$row['password'] === (string)$password) ) {
587
588                            serendipity_db_query("UPDATE {$serendipity['dbPrefix']}authors
589                                                     SET password = '" . ($is_hashed === false ? serendipity_hash($password) : $password) . "',
590                                                         hashtype = 2
591                                                   WHERE authorid = '" . $row['authorid'] . "'");
592                            if ($debug) fwrite($fp, date('Y-m-d H:i') . ' - Migrated user:' . $row['username'] . "\n");
593                            $is_valid_user = true;
594                        } else {
595                            continue;
596                        }
597                    } else {
598                        if ( ($is_hashed === false && password_verify((string)$password, $row['password'])) ||
599                             ($is_hashed !== false && (string)$row['password'] === (string)$password) ) {
600
601                            $is_valid_user = true;
602                            if ($debug) fwrite($fp, date('Y-m-d H:i') . ' - Validated ' . $row['password'] . ' == ' . ($is_hashed === false ? 'unhash:' . serendipity_hash($password) : 'hash:' . $password) . "\n");
603                        } else {
604                            if ($debug) fwrite($fp, date('Y-m-d H:i') . ' - INValidated ' . $row['password'] . ' == ' . ($is_hashed === false ? 'unhash:' . serendipity_hash($password) : 'hash:' . $password) . "\n");
605                            continue;
606                        }
607                    }
608                }
609
610                // This code is only reached if the password before is valid.
611                if ($is_valid_user) {
612                    if ($debug) fwrite($fp, date('Y-m-d H:i') . ' [sid:' . session_id() . '] - Success.' . "\n");
613                    serendipity_setCookie('old_session', session_id(), false);
614                    if (!$is_hashed) {
615                        serendipity_setAuthorToken();
616                        $_SESSION['serendipityPassword']    = $serendipity['serendipityPassword'] = $password;
617                    }
618
619                    return serendipity_load_userdata($username);
620
621
622                }
623            }
624        }
625
626        // Only reached, when proper login did not yet return true.
627        if ($debug) fwrite($fp, date('Y-m-d H:i') . ' - FAIL.' . "\n");
628
629        $_SESSION['serendipityAuthedUser'] = false;
630        serendipity_session_destroy();
631    }
632
633    if ($debug) {
634        fwrite($fp, date('Y-m-d H:i') . ' [sid:' . session_id() . '] - Uninit' . "\n");
635        fclose($fp);
636    }
637
638    return false;
639}
640
641function serendipity_load_userdata($username) {
642    global $serendipity;
643
644    $query = "SELECT DISTINCT
645                    email, password, realname, authorid, userlevel, right_publish, hashtype
646                  FROM
647                    {$serendipity['dbPrefix']}authors
648                  WHERE
649                    username   = '" . serendipity_db_escape_string($username) . "'";
650
651    $rows = serendipity_db_query($query, false, 'assoc');
652    $row = $rows[0];
653
654    $_SESSION['serendipityUser']         = $serendipity['serendipityUser']         = $username;
655    $_SESSION['serendipityRealname']     = $serendipity['serendipityRealname']     = $row['realname'];
656    $_SESSION['serendipityEmail']        = $serendipity['serendipityEmail']        = $row['email'];
657    $_SESSION['serendipityAuthorid']     = $serendipity['authorid']                = $row['authorid'];
658    $_SESSION['serendipityUserlevel']    = $serendipity['serendipityUserlevel']    = $row['userlevel'];
659    $_SESSION['serendipityAuthedUser']   = $serendipity['serendipityAuthedUser']   = true;
660    $_SESSION['serendipityRightPublish'] = $serendipity['serendipityRightPublish'] = $row['right_publish'];
661    $_SESSION['serendipityHashType']     = $serendipity['serendipityHashType']     = $row['hashtype'];
662
663    serendipity_load_configuration($serendipity['authorid']);
664    serendipity_setCookie('userDefLang', $serendipity['lang'], false);
665    return true;
666}
667
668/**
669 * Check if a user is logged in
670 *
671 * @access public
672 * @return boolean  TRUE when logged in, FALSE when not.
673 */
674function serendipity_userLoggedIn() {
675    if ($_SESSION['serendipityAuthedUser'] === true && IS_installed) {
676        return true;
677    } else {
678        return false;
679    }
680}
681
682/**
683 * A clone of an ifsetor() function to set a variable conditional on if the target already exists
684 *
685 * The function sets the contents of $source into the $target variable, but only if $target is not yet set. Eases up some if...else logic or multiple ternary operators
686 *
687 * @access public
688 * @param   mixed   Source variable that should be set into the target variable (reference call!)
689 * @param   mixed   Target variable, that should get the contents of the source variable (reference call!)
690 * @return  boolean True, when $target was not yet set and has been altered. False when no changes where made.
691 */
692function serendipity_restoreVar(&$source, &$target) {
693    global $serendipity;
694
695    if (isset($source) && !isset($target)) {
696        $target = $source;
697        return true;
698    }
699
700    return false;
701}
702
703/**
704 * Set a Cookie via HTTP calls, and update $_COOKIE plus $serendipity['COOKIE'] array.
705 *
706 * @access public
707 * @param   string      The name of the cookie variable
708 * @param   string      The contents of the cookie variable
709 * @param   int         Cookie validity (unix timestamp)
710 * @return null
711 */
712function serendipity_setCookie($name, $value, $securebyprot = true, $custom_timeout = false, $httpOnly = false) {
713    global $serendipity;
714
715    $host = $_SERVER['HTTP_HOST'];
716    if ($securebyprot) {
717        $secure = (strtolower($_SERVER['HTTPS']) == 'on') ? true : false;
718        if ($pos = strpos($host, ":")) {
719            $host = substr($host, 0, $pos);
720        }
721    } else {
722        $secure = false;
723    }
724
725    // If HTTP-Hosts like "localhost" are used, current browsers reject cookies.
726    // In this case, we disregard the HTTP host to be able to set that cookie.
727    if (substr_count($host, '.') < 1) {
728        $host = '';
729    }
730
731    if ($custom_timeout === false) {
732        $custom_timeout = time() + 60*60*24*30;
733    }
734
735    setcookie("serendipity[$name]", $value, $custom_timeout, $serendipity['serendipityHTTPPath'], $host, $secure, $httpOnly);
736    $_COOKIE[$name] = $value;
737    $serendipity['COOKIE'][$name] = $value;
738}
739
740/**
741 * Echo Javascript code to set a cookie variable
742 *
743 * This function is useful if your HTTP headers were already sent, but you still want to set a cookie
744 * Note that contents are echo'd, not return'd. Can be used by plugins.
745 *
746 * @access public
747 * @param   string      The name of the cookie variable
748 * @param   string      The contents of the cookie variable
749 * @return  null
750 */
751function serendipity_JSsetCookie($name, $value) {
752    $name  = serendipity_entities($name);
753    $value = urlencode($value);
754
755    echo '<script type="text/javascript">serendipity.SetCookie("' . $name . '", unescape("' . $value . '"))</script>' . "\n";
756}
757
758
759/**
760 * Deletes an existing cookie value
761 *
762 * LONG
763 *
764 * @access public
765 * @param   string      Name of the cookie to delete
766 * @return
767 */
768function serendipity_deleteCookie($name) {
769    global $serendipity;
770
771    $host = $_SERVER['HTTP_HOST'];
772    if ($pos = strpos($host, ":")) {
773        $host = substr($host, 0, $pos);
774    }
775
776    // If HTTP-Hosts like "localhost" are used, current browsers reject cookies.
777    // In this case, we disregard the HTTP host to be able to set that cookie.
778    if (substr_count($host, '.') < 1) {
779        $host = '';
780    }
781
782    setcookie("serendipity[$name]", '', time()-4000, $serendipity['serendipityHTTPPath'], $host);
783    unset($_COOKIE[$name]);
784    unset($serendipity['COOKIE'][$name]);
785}
786
787/**
788 * Performs a check whether an iframe for the admin section shall be emitted
789 *
790 * The iframe is used for previewing an entry with the stylesheet of the frontend.
791 * It fetches its data from the session input data.
792 *
793 * @access private
794 * @return boolean  True, if iframe was requested, false if not.
795 */
796function serendipity_is_iframe() {
797    global $serendipity;
798
799    if ($serendipity['GET']['is_iframe'] && is_array($_SESSION['save_entry'])) {
800        if (!is_object($serendipity['smarty'])) {
801            // We need smarty also in the iframe to load a template's config.inc.php and register possible event hooks.
802            serendipity_smarty_init();
803        }
804        return true;
805    }
806    return false;
807}
808
809/**
810 * Prints the content of the iframe.
811 *
812 * Called by serendipity_is_iframe, when preview is requested. Fetches data from session.
813 * An iframe is used so that a single s9y page must not timeout on intensive operations,
814 * and so that the frontend stylesheet can be embedded without screwing up the backend.
815 *
816 * @access private
817 * @see serendipity_is_iframe()
818 * @param   mixed   The entry array (comes from session variable)
819 * @param   string  Indicates whether an entry is previewed or saved. Save performs XML-RPC calls.
820 * @param   boolean Use smarty templating?
821 * @return  boolean Indicates whether iframe data was printed
822 */
823function serendipity_iframe(&$entry, $mode = null) {
824    global $serendipity;
825
826    if (empty($mode) || !is_array($entry)) {
827        return false;
828    }
829
830
831    $data = array();
832    $data['is_preview'] =  true;
833    $data['mode'] =  $mode;
834
835    switch ($mode) {
836        case 'save':
837            ob_start();
838            $res = serendipity_updertEntry($entry);
839            $data['updertHooks'] = ob_get_contents();
840            ob_end_clean();
841            if (is_string($res)) {
842                $data['res'] = $res;
843            }
844            if (!empty($serendipity['lastSavedEntry'])) {
845                $data['lastSavedEntry'] = $serendipity['lastSavedEntry'];
846            }
847            $data['entrylink'] = serendipity_archiveURL($res, $entry['title'], 'serendipityHTTPPath', true, array('timestamp' => $entry['timestamp']));
848            break;
849
850        case 'preview':
851            $data['preview'] = serendipity_printEntries(array($entry), ($entry['extended'] != '' ? 1 : 0), true);
852            break;
853    }
854    return serendipity_smarty_show('preview_iframe.tpl', $data);
855}
856
857/**
858 * Creates the necessary session data to be used by later iframe calls
859 *
860 * This function emits the actual <iframe> call.
861 *
862 * @access private
863 * @see serendipity_is_iframe()
864 * @param   string  Indicates whether an entry is previewed or saved. Save performs XML-RPC calls.
865 * @param   mixed   The entry array (comes from HTTP POST request)
866 * @return  boolean Indicates whether iframe data was stored
867 */
868function serendipity_iframe_create($mode, &$entry) {
869    global $serendipity;
870
871    if (!empty($serendipity['POST']['no_save'])) {
872        return true;
873    }
874
875    if (!serendipity_checkFormToken()) {
876        return false;
877    }
878
879    $_SESSION['save_entry']      = $entry;
880    $_SESSION['save_entry_POST'] = $serendipity['POST'];
881
882    $attr = '';
883    switch($mode) {
884        case 'save':
885            $attr = ' height="100" ';
886            break;
887
888        case 'preview':
889            $attr = ' height="300" ';
890            break;
891    }
892
893    return '<iframe src="serendipity_admin.php?serendipity[is_iframe]=true&amp;serendipity[iframe_mode]=' . $mode . '" id="serendipity_iframe" name="serendipity_iframe" ' . $attr . ' width="100%" frameborder="0" marginwidth="0" marginheight="0" scrolling="auto" title="Serendipity">'
894         . IFRAME_WARNING
895         . '</iframe>';
896}
897
898/**
899 * Pre-Checks certain server environments to indicate available options when installing Serendipity
900 *
901 * @access public
902 * @param   string      The name of the configuration option that needs to be checked for environmental data.
903 * @return  array       Returns the array of available options for the requested config option
904 */
905function serendipity_probeInstallation($item) {
906    global $serendipity;
907    $res = NULL;
908
909    switch ( $item ) {
910        case 'dbType' :
911            $res =  array();
912            if (extension_loaded('mysql')) {
913                $res['mysql'] = 'MySQL';
914            }
915            if (extension_loaded('PDO') &&
916                in_array('pgsql', PDO::getAvailableDrivers())) {
917                $res['pdo-postgres'] = 'PDO::PostgreSQL';
918            }
919
920            if (extension_loaded('PDO') &&
921                in_array('sqlite', PDO::getAvailableDrivers())) {
922                $res['pdo-sqlite'] = 'PDO::SQLite';
923                $has_pdo = true;
924            } else {
925                $has_pdo = false;
926            }
927
928            if (extension_loaded('pgsql')) {
929                $res['postgres'] = 'PostgreSQL';
930            }
931            if (extension_loaded('mysqli')) {
932                $res['mysqli'] = 'MySQLi';
933            }
934            if (extension_loaded('sqlite') && function_exists('sqlite_open')) {
935                $res['sqlite'] = 'SQLite';
936            }
937            if (extension_loaded('SQLITE3') && function_exists('sqlite3_open')) {
938                $res['sqlite3'] = 'SQLite3';
939            }
940            if (class_exists('SQLite3')) {
941                if ($has_pdo) {
942                    $res['sqlite3oo'] = 'SQLite3 (OO) (Preferably use PDO-SQlite!)';
943                } else {
944                    $res['sqlite3oo'] = 'SQLite3 (OO)';
945                }
946            }
947            if (function_exists('sqlrcon_alloc')) {
948                $res['sqlrelay'] = 'SQLRelay';
949            }
950            break;
951
952        case 'rewrite' :
953            $res = array();
954            $res['none'] = 'Disable URL Rewriting';
955            $res['errordocs'] = 'Use Apache errorhandling';
956            if( !function_exists('apache_get_modules') || in_array('mod_rewrite', apache_get_modules()) ) {
957                $res['rewrite'] = 'Use Apache mod_rewrite';
958            }
959            if( !function_exists('apache_get_modules') || in_array('mod_rewrite', apache_get_modules()) ) {
960                $res['rewrite2'] = 'Use Apache mod_rewrite (for 1&amp;1 and problematic servers)';
961            }
962
963            break;
964    }
965
966    return $res;
967}
968
969/**
970 * Sets a HTTP header
971 *
972 * @access public
973 * @param   string      The HTTP header to set
974 * @return null
975 */
976function serendipity_header($header) {
977    if (!headers_sent()) {
978        header($header);
979    }
980}
981
982/**
983 * Gets the currently selected language. Either from the browser, or the personal configuration, or the global configuration.
984 *
985 * This function also sets HTTP Headers and cookies to contain the language for follow-up requests
986 * TODO:
987 * This previously was handled inside a plugin with an event hook, but caching
988 * the event plugins that early in sequence created trouble with plugins not
989 * having loaded the right language.
990 * Find a way to let plugins hook into that sequence :-)
991 *
992 * @access public
993 * @return  string      Returns the name of the selected language.
994 */
995function serendipity_getSessionLanguage() {
996    global $serendipity;
997
998    // DISABLE THIS!
999/*
1000    if ($_SESSION['serendipityAuthedUser']) {
1001        serendipity_header('X-Serendipity-InterfaceLangSource: Database');
1002        return $serendipity['lang'];
1003    }
1004*/
1005    if (isset($serendipity['lang']) && !isset($serendipity['languages'][$serendipity['lang']])) {
1006        $serendipity['lang'] = $serendipity['autolang'];
1007    }
1008
1009    if (isset($_REQUEST['user_language']) && (!empty($serendipity['languages'][$_REQUEST['user_language']])) && !headers_sent()) {
1010        serendipity_setCookie('serendipityLanguage', $_REQUEST['user_language'], false);
1011    }
1012
1013    if (isset($serendipity['COOKIE']['serendipityLanguage'])) {
1014        if ($serendipity['expose_s9y']) serendipity_header('X-Serendipity-InterfaceLangSource: Cookie');
1015        $lang = $serendipity['COOKIE']['serendipityLanguage'];
1016    } elseif (isset($serendipity['GET']['lang_selected']) && !empty($serendipity['languages'][$serendipity['GET']['lang_selected']])) {
1017        if ($serendipity['expose_s9y']) serendipity_header('X-Serendipity-InterfaceLangSource: GET');
1018        $lang = $serendipity['GET']['lang_selected'];
1019    } elseif (serendipity_db_bool($serendipity['lang_content_negotiation'])) {
1020        if ($serendipity['expose_s9y']) serendipity_header('X-Serendipity-InterfaceLangSource: Content-Negotiation');
1021        $lang = serendipity_detectLang();
1022    }
1023
1024    if (isset($lang)) {
1025        $serendipity['detected_lang'] = $lang;
1026    } else {
1027        if (! empty($_SESSION['serendipityLanguage'])) {
1028            $lang = $_SESSION['serendipityLanguage'];
1029        } else {
1030            if (isset($serendipity['COOKIE']['userDefLang']) && ! empty($serendipity['COOKIE']['userDefLang'])) {
1031                $lang = $serendipity['COOKIE']['userDefLang'];
1032            } else {
1033                if ($serendipity['lang']) {
1034                    $lang = $serendipity['lang'];
1035                } else {
1036                    // serendipity_load_configuration() failed to load language, go default
1037                    $lang = $serendipity['autolang'];
1038                }
1039            }
1040        }
1041        $serendipity['detected_lang'] = null;
1042    }
1043
1044    if (!isset($serendipity['languages'][$lang])) {
1045        $serendipity['detected_lang'] = null;
1046        return $serendipity['lang'];
1047    } else {
1048        $_SESSION['serendipityLanguage'] = $lang;
1049        if (!is_null($serendipity['detected_lang'])) {
1050            if ($serendipity['expose_s9y']) serendipity_header('X-Serendipity-InterfaceLang: ' . $lang);
1051        }
1052    }
1053
1054    return $lang;
1055}
1056
1057/**
1058 * Gets the selected language from personal configuration if needed
1059 *
1060 * This function also sets HTTP Headers and cookies to contain the language for follow-up requests
1061 *
1062 * @access public
1063 * @return  string      Returns the name of the selected language.
1064 */
1065function serendipity_getPostAuthSessionLanguage() {
1066    global $serendipity;
1067
1068    if (! is_null($serendipity['detected_lang'])) {
1069        return $serendipity['detected_lang'];
1070    }
1071
1072    if ($_SESSION['serendipityAuthedUser']) {
1073        if ($serendipity['expose_s9y']) serendipity_header('X-Serendipity-InterfaceLangSource: Database');
1074        $lang = $serendipity['lang'];
1075    } else {
1076        $lang = (isset($_SESSION['serendipityLanguage'])) ? $_SESSION['serendipityLanguage'] : $serendipity['lang'];
1077    }
1078
1079    if (!isset($serendipity['languages'][$lang])) {
1080        $lang = $serendipity['lang'];
1081    }
1082
1083    $_SESSION['serendipityLanguage'] = $lang;
1084
1085    if ($serendipity['expose_s9y']) serendipity_header('X-Serendipity-InterfaceLang: ' . $lang);
1086
1087    if ($lang != $serendipity['lang']) {
1088        $serendipity['content_lang'] = $lang;
1089    }
1090
1091    return $lang;
1092}
1093
1094/**
1095 * Retrieves an array of applying permissions to an author
1096 *
1097 * The privileges of each group an author is a member of are aggreated
1098 * and stored in a larger array. So both memberships and all aplying
1099 * privileges are returned.
1100 *
1101 * @access public
1102 * @param   int     The ID of the author to fetch permissions/group memberships for
1103 * @return  array   Multi-dimensional associative array which holds a 'membership' and permission name data
1104 */
1105function &serendipity_getPermissions($authorid) {
1106    global $serendipity;
1107
1108        // Get group information
1109        $groups =& serendipity_db_query("SELECT ag.groupid, g.name, gc.property, gc.value
1110                                          FROM {$serendipity['dbPrefix']}authorgroups AS ag
1111                               LEFT OUTER JOIN {$serendipity['dbPrefix']}groups AS g
1112                                            ON ag.groupid = g.id
1113                               LEFT OUTER JOIN {$serendipity['dbPrefix']}groupconfig AS gc
1114                                            ON gc.id = g.id
1115                                         WHERE ag.authorid = " . (int)$authorid);
1116        $perm = array('membership' => array());
1117        if (is_array($groups)) {
1118            foreach($groups AS $group) {
1119                $perm['membership'][$group['groupid']]       = $group['groupid'];
1120                $perm[$group['groupid']][$group['property']] = $group['value'];
1121            }
1122        }
1123        return $perm;
1124}
1125
1126/**
1127 * Returns the list of available internal Serendipity permission field names
1128 *
1129 * This function also mapps which function was available to which userleves in older
1130 * Serendipity versions. Thus if an author does not have a certain privilege he should
1131 * have because of his userlevel, this can be reverse-mapped.
1132 *
1133 * @access public
1134 * @return  array   Multi-dimensional associative array which the list of all permission items plus their userlevel associations
1135 */
1136function serendipity_getPermissionNames() {
1137    return array(
1138        'personalConfiguration'
1139            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF, USERLEVEL_EDITOR),
1140        'personalConfigurationUserlevel'
1141            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF),
1142        'personalConfigurationNoCreate'
1143            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF),
1144        'personalConfigurationRightPublish'
1145            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF),
1146        'siteConfiguration'
1147            => array(USERLEVEL_ADMIN),
1148        'blogConfiguration'
1149            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF),
1150
1151        'adminEntries'
1152            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF, USERLEVEL_EDITOR),
1153        'adminEntriesMaintainOthers'
1154            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF),
1155
1156        'adminImport'
1157            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF),
1158
1159        'adminCategories'
1160            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF, USERLEVEL_EDITOR),
1161        'adminCategoriesMaintainOthers'
1162            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF),
1163        'adminCategoriesDelete'
1164            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF),
1165
1166        'adminUsers'
1167            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF),
1168        'adminUsersDelete'
1169            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF),
1170        'adminUsersEditUserlevel'
1171            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF),
1172        'adminUsersMaintainSame'
1173            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF),
1174        'adminUsersMaintainOthers'
1175            => array(USERLEVEL_ADMIN),
1176        'adminUsersCreateNew'
1177            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF),
1178        'adminUsersGroups'
1179            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF),
1180
1181        'adminPlugins'
1182            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF),
1183        'adminPluginsMaintainOthers'
1184            => array(USERLEVEL_ADMIN),
1185
1186        'adminImages'
1187            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF, USERLEVEL_EDITOR),
1188        'adminImagesDirectories'
1189            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF),
1190        'adminImagesAdd'
1191            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF, USERLEVEL_EDITOR),
1192        'adminImagesDelete'
1193            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF, USERLEVEL_EDITOR),
1194        'adminImagesMaintainOthers'
1195            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF),
1196        'adminImagesViewOthers'
1197            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF, USERLEVEL_EDITOR),
1198        'adminImagesView'
1199            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF, USERLEVEL_EDITOR),
1200        'adminImagesSync'
1201            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF),
1202
1203        'adminComments'
1204            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF),
1205
1206        'adminTemplates'
1207            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF),
1208
1209        'hiddenGroup'
1210            => array(-1)
1211    );
1212}
1213
1214/**
1215 * Checks if a permission is granted to a specific author
1216 *
1217 * This function caches all permission chacks in static function variables to not
1218 * fetch all permissions time and again.
1219 * The permission checks are performed against the values of each group. If a privilege
1220 * is set in one of the groups the author is a user of, the function returns true.
1221 * If a privilege is not set, the userlevel of an author is checked to act for backwards-compatibility.
1222 *
1223 * @access public
1224 * @see serendipity_getPermissionNames()
1225 * @param   string      The name of the permission to check
1226 * @param   int         The authorid for which the permission check should be performed
1227 * @param   boolean     If set to true, all groups that the requested author is a user of will be returned. This bypasses the permission check and mainly acts as a mean to return cached permissions, since those variables are only available within this function.
1228 * @return  mixed       Either returns true if a permission check is performed, or return an array of group memberships. Depends on the $returnMyGroups variable.
1229 */
1230function serendipity_checkPermission($permName, $authorid = null, $returnMyGroups = false) {
1231    global $serendipity;
1232
1233    // Define old serendipity permissions
1234    static $permissions = null;
1235    static $group = null;
1236
1237    if (IS_installed !== true) {
1238        return true;
1239    }
1240
1241    if ($permissions === null) {
1242        $permissions = serendipity_getPermissionNames();
1243    }
1244
1245    if ($group === null) {
1246        $group = array();
1247    }
1248
1249    if ($authorid === null) {
1250        $authorid = $serendipity['authorid'];
1251    }
1252
1253    if (!isset($group[$authorid])) {
1254        $group[$authorid] = serendipity_getPermissions($authorid);
1255    }
1256
1257    if ($returnMyGroups) {
1258        if ($returnMyGroups === 'all') {
1259            return $group[$authorid];
1260        } else {
1261            return $group[$authorid]['membership'];
1262        }
1263    }
1264
1265    if ($authorid == $serendipity['authorid'] && $serendipity['no_create']) {
1266        // This no_create user privilege overrides other permissions.
1267        return false;
1268    }
1269
1270    $return = true;
1271
1272    foreach($group[$authorid] AS $item) {
1273        if (!isset($item[$permName])) {
1274            continue;
1275        }
1276
1277        if ($item[$permName] === 'true') {
1278            return true;
1279        } else {
1280            $return = false;
1281        }
1282    }
1283
1284    // If the function did not yet return it means there's a check for a permission which is not defined anywhere.
1285    // Let's use a backwards compatible way.
1286    if ($return && isset($permissions[$permName]) && in_array($serendipity['serendipityUserlevel'], $permissions[$permName])) {
1287        return true;
1288    }
1289
1290    return false;
1291}
1292
1293/**
1294 * Update author group membership(s)
1295 *
1296 * @access public
1297 * @param   array       The array of groups the author should be a member of. All memberships that were present before and not contained in this array will be removed.
1298 * @param   int         The ID of the author to update
1299 * @param   boolean     If set to true, the groups can only be updated if the user has the adminUsersMaintainOthers privilege. If set to false, group memberships will be changeable for any user.
1300 * @return
1301 */
1302function serendipity_updateGroups($groups, $authorid, $apply_acl = true) {
1303    global $serendipity;
1304
1305    if ($apply_acl && !serendipity_checkPermission('adminUsersMaintainOthers')) {
1306        return false;
1307    }
1308
1309    serendipity_db_query("DELETE FROM {$serendipity['dbPrefix']}authorgroups WHERE authorid = " . (int)$authorid);
1310
1311    foreach($groups AS $group) {
1312        serendipity_db_query("INSERT INTO {$serendipity['dbPrefix']}authorgroups (authorid, groupid) VALUES (" . (int)$authorid . ", " . (int)$group . ")");
1313    }
1314    return true;
1315}
1316
1317/**
1318 * Returns all authorgroups that are available
1319 *
1320 * If a groupname is prefixed with "USERLEVEL_" then the constant of that name is used to
1321 * return the name of the group. This allows inserting the special groups Chief Editor, Editor
1322 * and admin and still being able to use multilingual names for these groups.
1323 *
1324 * @access public
1325 * @param   int     If set to an author ID value, only groups are fetched that this author is a member of. If set to false, all groups are returned, also those that the current user has no access to.
1326 * @return  array   An associative array of group names.
1327 */
1328function &serendipity_getAllGroups($apply_ACL_user = false) {
1329    global $serendipity;
1330
1331    if ($apply_ACL_user) {
1332        $groups =& serendipity_db_query("SELECT g.id   AS confkey,
1333                                                g.name AS confvalue,
1334                                                g.id   AS id,
1335                                                g.name AS name
1336                                           FROM {$serendipity['dbPrefix']}authorgroups AS ag
1337                                LEFT OUTER JOIN {$serendipity['dbPrefix']}groups AS g
1338                                             ON g.id = ag.groupid
1339                                          WHERE ag.authorid = " . (int)$apply_ACL_user . "
1340                                       ORDER BY g.name", false, 'assoc');
1341    } else {
1342        $groups =& serendipity_db_query("SELECT g.id   AS confkey,
1343                                                g.name AS confvalue,
1344                                                g.id   AS id,
1345                                                g.name AS name
1346                                          FROM {$serendipity['dbPrefix']}groups AS g
1347                                      ORDER BY  g.name", false, 'assoc');
1348    }
1349    if (is_array($groups)) {
1350        foreach ($groups as $k => $v) {
1351            if ('USERLEVEL_' == substr($v['confvalue'], 0, 10)) {
1352                $groups[$k]['confvalue'] = $groups[$k]['name'] = constant($v['confvalue']);
1353            }
1354        }
1355    }
1356    return $groups;
1357}
1358
1359/**
1360 * Fetch the permissions of a certain group
1361 *
1362 * @access public
1363 * @param   int     The ID of the group that the permissions are fetched for
1364 * @return  array   The associative array of permissions of a group.
1365 */
1366function &serendipity_fetchGroup($groupid) {
1367    global $serendipity;
1368
1369    $conf = array();
1370    $groups =& serendipity_db_query("SELECT g.id        AS confkey,
1371                                            g.name      AS confvalue,
1372                                            g.id        AS id,
1373                                            g.name      AS name,
1374
1375                                            gc.property AS property,
1376                                            gc.value    AS value
1377                                      FROM {$serendipity['dbPrefix']}groups AS g
1378                           LEFT OUTER JOIN {$serendipity['dbPrefix']}groupconfig AS gc
1379                                        ON g.id = gc.id
1380                                     WHERE g.id = " . (int)$groupid, false, 'assoc');
1381
1382    if (is_array($groups)) {
1383        foreach($groups AS $group) {
1384            $conf[$group['property']] = $group['value'];
1385        }
1386    }
1387
1388    // The following are unique
1389    $conf['name']      = $groups[0]['name'];
1390    $conf['id']        = $groups[0]['id'];
1391    $conf['confkey']   = $groups[0]['confkey'];
1392    $conf['confvalue'] = $groups[0]['confvalue'];
1393
1394    return $conf;
1395}
1396
1397/**
1398 * Gets all groups a user is a member of
1399 *
1400 * @access public
1401 * @param   int         The authorid to fetch groups for
1402 * @param   boolean     Indicate whether the original multi-dimensional DB result array shall be returned (FALSE) or if the array shall be flattened to be 1-dimensional (TRUE).
1403 * @return
1404 */
1405function &serendipity_getGroups($authorid, $sequence = false) {
1406    global $serendipity;
1407
1408    $_groups =& serendipity_db_query("SELECT g.id  AS confkey,
1409                                            g.name AS confvalue,
1410                                            g.id   AS id,
1411                                            g.name AS name
1412                                      FROM {$serendipity['dbPrefix']}authorgroups AS ag
1413                           LEFT OUTER JOIN {$serendipity['dbPrefix']}groups AS g
1414                                        ON g.id = ag.groupid
1415                                     WHERE ag.authorid = " . (int)$authorid, false, 'assoc');
1416    if (!is_array($_groups)) {
1417        $groups = array();
1418    } else {
1419        $groups =& $_groups;
1420    }
1421
1422    if ($sequence) {
1423        $rgroups  = array();
1424        foreach($groups AS $grouprow) {
1425            $rgroups[] = $grouprow['confkey'];
1426        }
1427    } else {
1428        $rgroups =& $groups;
1429    }
1430
1431    return $rgroups;
1432}
1433
1434/**
1435 * Gets all author IDs of a specific group
1436 *
1437 * @access public
1438 * @param   int     The ID of the group to fetch the authors of
1439 * @return  array   The associative array of author IDs and names
1440 */
1441function &serendipity_getGroupUsers($groupid) {
1442    global $serendipity;
1443
1444    $groups =& serendipity_db_query("SELECT g.name     AS name,
1445                                            a.realname AS author,
1446                                            a.authorid AS id
1447                                      FROM {$serendipity['dbPrefix']}authorgroups AS ag
1448                           LEFT OUTER JOIN {$serendipity['dbPrefix']}groups AS g
1449                                        ON g.id = ag.groupid
1450                           LEFT OUTER JOIN {$serendipity['dbPrefix']}authors AS a
1451                                        ON ag.authorid = a.authorid
1452                                     WHERE ag.groupid = " . (int)$groupid, false, 'assoc');
1453    return $groups;
1454}
1455
1456/**
1457 * Deletes a specific author group by ID
1458 *
1459 * @access public
1460 * @param   int     The group ID to delete
1461 * @return  boolean Return true if group could be deleted, false if unsufficient privileges.
1462 */
1463function serendipity_deleteGroup($groupid) {
1464    global $serendipity;
1465
1466    if (!serendipity_checkPermission('adminUsersGroups')) {
1467        return false;
1468    }
1469
1470    if (!serendipity_checkPermission('adminUsersMaintainOthers')) {
1471        // Only groups should be accessible where a user has access rights.
1472        $my_groups = serendipity_getGroups($serendipity['authorid'], true);
1473        if (!in_array($groupid, $my_groups)) {
1474            return false;
1475        }
1476    }
1477
1478    serendipity_db_query("DELETE FROM {$serendipity['dbPrefix']}groups       WHERE id = " . (int)$groupid);
1479    serendipity_db_query("DELETE FROM {$serendipity['dbPrefix']}authorgroups WHERE groupid = " . (int)$groupid);
1480
1481    return true;
1482}
1483
1484/**
1485 * Creates a new author group
1486 *
1487 * @access public
1488 * @param   string      The name of the new group
1489 * @return  int         The id of the created group
1490 */
1491function serendipity_addGroup($name) {
1492    global $serendipity;
1493
1494    serendipity_db_query("INSERT INTO {$serendipity['dbPrefix']}groups (name) VALUES ('" . serendipity_db_escape_string($name) . "')");
1495    $gid = serendipity_db_insert_id('groups', 'id');
1496
1497    return $gid;
1498}
1499
1500/**
1501 * Returns a list of all existing permission names.
1502 *
1503 * Additional plugins might insert specific properties into the groupconfig database to
1504 * handle their own privileges. This call returns an array of all available permission names
1505 * so that it can be intersected with the list of internal permission names (serendipity_getPermissionNames())
1506 * and the be distincted.
1507 *
1508 * @access public
1509 * @see serendipity_getPermissionNames()
1510 * @return  array   associative array of all available permission names
1511 */
1512function &serendipity_getDBPermissionNames() {
1513    global $serendipity;
1514
1515    $config =& serendipity_db_query("SELECT property FROM {$serendipity['dbPrefix']}groupconfig GROUP BY property ORDER BY property", false, 'assoc');
1516
1517    return $config;
1518}
1519
1520/**
1521 * Gets the list of all Permissions and merges the two arrays
1522 *
1523 * The first call will fetch all existing permission names of the database. Then it will
1524 * fetch the list of defined internal permission names, which has an array with extra information
1525 * (like userlevel). This array will be merged with those permission names only found in the
1526 * database. The returned array will then hold as much information about permission names as is
1527 * available.
1528 * TODO Might need further pushing and/or an event hook so that external plugins using the
1529 * permission system can inject specific information into the array
1530 *
1531 * @access public
1532 * @see serendipity_getPermissionNames()
1533 * @see serendipity_getDBPermissionNames()
1534 * @return  array   Returns the array with all information about all permission names
1535 */
1536function &serendipity_getAllPermissionNames() {
1537    global $serendipity;
1538
1539    $DBperms =& serendipity_getDBPermissionNames();
1540    $perms   = serendipity_getPermissionNames();
1541
1542    foreach($DBperms AS $perm) {
1543        if (!isset($perms[$perm['property']])) {
1544            $perms[$perm['property']] = array();
1545        }
1546    }
1547
1548    return $perms;
1549}
1550
1551/**
1552 * Checks if two users are members of the same group
1553 *
1554 * This function will retrieve all group memeberships of a
1555 * foreign user ($checkuser) and yourself ($myself). Then it
1556 * will check if there is any group membership that those
1557 * two users have in common.
1558 * It can be used for detecting if a different author should
1559 * be allowed to access your entries, because he's in the same
1560 * group, for example.
1561 *
1562 * @access public
1563 * @param   int     ID of the first author to check group memberships
1564 * @param   int     ID of the second author to check group memberships
1565 * @return  boolea  True if a membership intersects, false if not
1566 */
1567function serendipity_intersectGroup($checkuser = null, $myself = null) {
1568    global $serendipity;
1569
1570    if ($myself === null) {
1571        $myself = $serendipity['authorid'];
1572    }
1573
1574    $my_groups  = serendipity_getGroups($myself, true);
1575    $his_groups = serendipity_getGroups($checkuser, true);
1576
1577    foreach($his_groups AS $his_group) {
1578        if (in_array($his_group, $my_groups)) {
1579            return true;
1580        }
1581    }
1582
1583    return false;
1584}
1585
1586/**
1587 * Updates the configuration of permissions of a specific group
1588 *
1589 * This function ensures that a group can only be updated from users that have permissions to do so.
1590 * @access public
1591 * @param   int     The ID of the group to update
1592 * @param   array   The associative array of permission names
1593 * @param   array   The associative array of new values for the permissions. Needs the same associative keys like the $perms array.
1594 * @param   bool    Indicates if an all new privilege should be inserted (true) or if an existing privilege is going to be checked
1595 * @param   array   The associative array of plugin permission names
1596 * @param   array   The associative array of plugin permission hooks
1597 * @return true
1598 */
1599function serendipity_updateGroupConfig($groupid, &$perms, &$values, $isNewPriv = false, $forbidden_plugins = null, $forbidden_hooks = null) {
1600    global $serendipity;
1601
1602    if (!serendipity_checkPermission('adminUsersGroups')) {
1603        return false;
1604    }
1605
1606    if (!serendipity_checkPermission('adminUsersMaintainOthers')) {
1607        // Only groups should be accessible where a user has access rights.
1608        $my_groups = serendipity_getGroups($serendipity['authorid'], true);
1609        if (!in_array($groupid, $my_groups)) {
1610            return false;
1611        }
1612    }
1613
1614    $storage =& serendipity_fetchGroup($groupid);
1615
1616    serendipity_db_query("DELETE FROM {$serendipity['dbPrefix']}groupconfig WHERE id = " . (int)$groupid);
1617    foreach ($perms AS $perm => $userlevels) {
1618        if (substr($perm, 0, 2) == 'f_') {
1619            continue;
1620        }
1621
1622        if (isset($values[$perm]) && $values[$perm] == 'true') {
1623            $value = 'true';
1624        } elseif (isset($values[$perm]) && $values[$perm] === 'false') {
1625            $value = 'false';
1626        } elseif (isset($values[$perm])) {
1627            $value = $values[$perm];
1628        } else {
1629            $value = 'false';
1630        }
1631
1632        if ($isNewPriv == false && !serendipity_checkPermission($perm) && $perm != 'hiddenGroup') {
1633            if (!isset($storage[$perm])) {
1634                $value = 'false';
1635            } else {
1636                $value = $storage[$perm];
1637            }
1638        }
1639
1640        serendipity_db_query(
1641            sprintf("INSERT INTO {$serendipity['dbPrefix']}groupconfig (id, property, value) VALUES (%d, '%s', '%s')",
1642                (int)$groupid,
1643                serendipity_db_escape_string($perm),
1644                serendipity_db_escape_string($value)
1645            )
1646        );
1647    }
1648
1649    if (is_array($forbidden_plugins)) {
1650        foreach($forbidden_plugins AS $plugid) {
1651            serendipity_db_query(
1652                sprintf("INSERT INTO {$serendipity['dbPrefix']}groupconfig (id, property, value) VALUES (%d, '%s', 'true')",
1653                    (int)$groupid,
1654                    serendipity_db_escape_string('f_' . urldecode($plugid))
1655                )
1656            );
1657        }
1658    }
1659
1660    if (is_array($forbidden_hooks)) {
1661        foreach($forbidden_hooks AS $hook) {
1662            serendipity_db_query(
1663                sprintf("INSERT INTO {$serendipity['dbPrefix']}groupconfig (id, property, value) VALUES (%d, '%s', 'true')",
1664                    (int)$groupid,
1665                    serendipity_db_escape_string('f_' . urldecode($hook))
1666                )
1667            );
1668        }
1669    }
1670
1671    serendipity_db_query("UPDATE {$serendipity['dbPrefix']}groups SET name = '" . serendipity_db_escape_string($values['name']) . "' WHERE id = " . (int)$groupid);
1672
1673    if (is_array($values['members'])) {
1674        serendipity_db_query("DELETE FROM {$serendipity['dbPrefix']}authorgroups WHERE groupid = " . (int)$groupid);
1675        foreach($values['members'] AS $member) {
1676            serendipity_db_query(
1677                sprintf("INSERT INTO {$serendipity['dbPrefix']}authorgroups (groupid, authorid) VALUES (%d, %d)",
1678                    (int)$groupid,
1679                    (int)$member
1680                )
1681            );
1682        }
1683    }
1684
1685    return true;
1686}
1687
1688/**
1689 * Adds a default internal group (Editor, Chief Editor, Admin)
1690 *
1691 * @access public
1692 * @param   string  The name of the group to insert
1693 * @param   int     The userlevel that represents this group (0|1|255 for Editor/Chief/Admin).
1694 * @return true
1695 */
1696function serendipity_addDefaultGroup($name, $level) {
1697    global $serendipity;
1698
1699    static $perms = null;
1700    if ($perms === null) {
1701        $perms = serendipity_getPermissionNames();
1702    }
1703
1704    serendipity_db_query("INSERT INTO {$serendipity['dbPrefix']}groups (name) VALUES ('" . serendipity_db_escape_string($name) . "')");
1705    $gid = (int)serendipity_db_insert_id('groups', 'id');
1706    serendipity_db_query("INSERT INTO {$serendipity['dbPrefix']}groupconfig (id, property, value) VALUES ($gid, 'userlevel', '" . (int)$level . "')");
1707
1708    $authors = serendipity_db_query("SELECT * FROM {$serendipity['dbPrefix']}authors WHERE userlevel = " . (int)$level);
1709
1710    if (is_array($authors)) {
1711        foreach($authors AS $author) {
1712            serendipity_db_query("INSERT INTO {$serendipity['dbPrefix']}authorgroups (authorid, groupid) VALUES ('{$author['authorid']}', '$gid')");
1713        }
1714    }
1715
1716    foreach($perms AS $permName => $permArray) {
1717        if (in_array($level, $permArray)) {
1718            serendipity_db_query("INSERT INTO {$serendipity['dbPrefix']}groupconfig (id, property, value) VALUES ($gid, '" . serendipity_db_escape_string($permName) . "', 'true')");
1719        } else {
1720            serendipity_db_query("INSERT INTO {$serendipity['dbPrefix']}groupconfig (id, property, value) VALUES ($gid, '" . serendipity_db_escape_string($permName) . "', 'false')");
1721        }
1722    }
1723
1724    return true;
1725}
1726
1727/**
1728 * Allow access to a specific item (category or entry) for a specific usergroup
1729 *
1730 * ACL are Access Control Lists. They indicate which read/write permissions a
1731 * specific item has for specific usergroups.
1732 * An artifact in terms of Serendipity can be either a category or an entry, or
1733 * anything beyond that for future compatibility.
1734 * This function sets up the ACLs.
1735 *
1736 * @access public
1737 * @param   int     The ID of the artifact to set the access
1738 * @param   string  The type of an artifact (category|entry)
1739 * @param   string  The type of access to grant (read|write)
1740 * @param   array   The ID of the group to grant access to
1741 * @param   string  A variable option for an artifact
1742 * @return  boolean True if ACL was applied, false if not.
1743 */
1744function serendipity_ACLGrant($artifact_id, $artifact_type, $artifact_mode, $groups, $artifact_index = '') {
1745    global $serendipity;
1746
1747    if (empty($groups) || !is_array($groups)) {
1748        return false;
1749    }
1750
1751    // Delete all old existing relations.
1752    serendipity_db_query("DELETE FROM {$serendipity['dbPrefix']}access
1753                                WHERE artifact_id    = " . (int)$artifact_id . "
1754                                  AND artifact_type  = '" . serendipity_db_escape_string($artifact_type) . "'
1755                                  AND artifact_mode  = '" . serendipity_db_escape_string($artifact_mode) . "'
1756                                  AND artifact_index = '" . serendipity_db_escape_string($artifact_index) . "'");
1757
1758    $data = array(
1759        'artifact_id'    => (int)$artifact_id,
1760        'artifact_type'  => $artifact_type,
1761        'artifact_mode'  => $artifact_mode,
1762        'artifact_index' => $artifact_index
1763    );
1764
1765    if (count($data) < 1) {
1766        return true;
1767    }
1768
1769    foreach($groups AS $group) {
1770        $data['groupid'] = $group;
1771        serendipity_db_insert('access', $data);
1772    }
1773
1774    return true;
1775}
1776
1777/**
1778 * Checks if a specific item (category or entry) can be accessed by a specific usergroup
1779 *
1780 * ACL are Access Control Lists. They indicate which read/write permissions a
1781 * specific item has for specific usergroups.
1782 * An artifact in terms of Serendipity can be either a category or an entry, or
1783 * anything beyond that for future compatibility.
1784 * This function retrieves the ACLs.
1785 *
1786 * @access public
1787 * @param   int     The ID of the artifact to set the access
1788 * @param   string  The type of an artifact (category|entry)
1789 * @param   string  The type of access to check for (read|write)
1790 * @param   string  A variable option for an artifact
1791 * @return  array   Returns an array of all groups that are allowed for this kind of access. You can then check if you are the member of any of the groups returned here.
1792 */
1793function serendipity_ACLGet($artifact_id, $artifact_type, $artifact_mode, $artifact_index = '') {
1794    global $serendipity;
1795
1796    $sql = "SELECT groupid, artifact_index FROM {$serendipity['dbPrefix']}access
1797                    WHERE artifact_type  = '" . serendipity_db_escape_string($artifact_type) . "'
1798                      AND artifact_id    = '" . (int)$artifact_id . "'
1799                      AND artifact_mode  = '" . serendipity_db_escape_string($artifact_mode) . "'
1800                      AND artifact_index = '" . serendipity_db_escape_string($artifact_index) . "'";
1801    $rows =& serendipity_db_query($sql, false, 'assoc');
1802
1803    if (!is_array($rows)) {
1804        return false;
1805    }
1806
1807    $acl = array();
1808    foreach($rows AS $row) {
1809        $acl[$row['groupid']] = $row['artifact_index'];
1810    }
1811
1812    return $acl;
1813}
1814
1815/**
1816 * Checks if a specific item (category or entry) can be accessed by a specific Author
1817 *
1818 * ACL are Access Control Lists. They indicate which read/write permissions a
1819 * specific item has for specific usergroups.
1820 * An artifact in terms of Serendipity can be either a category or an entry, or
1821 * anything beyond that for future compatibility.
1822 * This function retrieves the ACLs for a specific user.
1823 *
1824 * @access public
1825 * @param   int     The ID of the author to check against.
1826 * @param   int     The ID of the artifact to set the access
1827 * @param   string  The type of an artifact ('category', more to come)
1828 * @param   string  The type of access to check for (read|write)
1829 * @return  boolean Returns true, if the author has access to this artifact. False if not.
1830 */
1831function serendipity_ACLCheck($authorid, $artifact_id, $artifact_type, $artifact_mode) {
1832    global $serendipity;
1833
1834    $artifact_sql = array();
1835
1836    // TODO: If more artifact_types are available, the JOIN needs to be edited so that the first AND portion is not required, and the join is fully made on that conditiion.
1837    switch($artifact_type) {
1838        default:
1839        case 'category':
1840            $artifact_sql['unique']= "atf.categoryid";
1841            $artifact_sql['cond']  = "atf.categoryid = " . (int)$artifact_id;
1842            $artifact_sql['where'] = "     ag.groupid = a.groupid
1843                                        OR a.groupid  = 0
1844                                        OR (a.artifact_type IS NULL AND (atf.authorid = " . (int)$authorid . " OR atf.authorid = 0 OR atf.authorid IS NULL))";
1845            $artifact_sql['table'] = 'category';
1846    }
1847
1848    $sql = "SELECT {$artifact_sql['unique']} AS result
1849              FROM {$serendipity['dbPrefix']}{$artifact_sql['table']} AS atf
1850   LEFT OUTER JOIN {$serendipity['dbPrefix']}authorgroups AS ag
1851                ON ag.authorid = ". (int)$authorid . "
1852   LEFT OUTER JOIN {$serendipity['dbPrefix']}access AS a
1853                ON (    a.artifact_type = '" . serendipity_db_escape_string($artifact_type) . "'
1854                    AND a.artifact_id   = " . (int)$artifact_id . "
1855                    AND a.artifact_mode = '" . serendipity_db_escape_string($artifact_mode) . "'
1856                   )
1857
1858             WHERE {$artifact_sql['cond']}
1859               AND ( {$artifact_sql['where']} )
1860          GROUP BY result";
1861
1862    $res =& serendipity_db_query($sql, true, 'assoc');
1863    if (is_array($res) && !empty($res['result'])) {
1864        return true;
1865    }
1866
1867    return false;
1868}
1869
1870/**
1871 * Prepares a SQL statement to be used in queries that should be ACL restricted.
1872 *
1873 * ACL are Access Control Lists. They indicate which read/write permissions a
1874 * specific item has for specific usergroups.
1875 * An artifact in terms of Serendipity can be either a category or an entry, or
1876 * anything beyond that for future compatibility.
1877 * This function evaluates and applies the SQL statements required.
1878 * It is currently only written for retrieving Category ACLs.
1879 * All of the SQL code that will be used in serendipity_fetchEntries will be stored
1880 * within the referenced $cond array.
1881 *
1882 * @access private
1883 * @param   array       Associative array that holds the SQL part array to be used in other functions like serendipity_fetchEntries()
1884 * @param   boolean     Some queries do not need to joins categories. When ACLs need to be applied, this column is required, so if $append_category is set to true it will perform this missing JOIN.
1885 * @param   string      The ACL type ('category', 'directory')
1886 * @param   string      ACL mode
1887 * @return  true        True if ACLs were applied, false if not.
1888 */
1889function serendipity_ACL_SQL(&$cond, $append_category = false, $type = 'category', $mode = 'read') {
1890    global $serendipity;
1891
1892    // A global configuration item controls whether the blog should apply ACLs or not!
1893    if (!isset($serendipity['enableACL']) || $serendipity['enableACL'] == true) {
1894
1895        // If the user is logged in, we retrieve his authorid for the upcoming checks
1896        if ($_SESSION['serendipityAuthedUser'] === true) {
1897            $read_id = (int)$serendipity['authorid'];
1898            $read_id_sql = 'acl_a.groupid OR acl_acc.groupid = 0';
1899        } else {
1900            // "0" as category property counts as "anonymous viewers"
1901            $read_id     = 0;
1902            $read_id_sql = 0;
1903        }
1904
1905        if ($append_category) {
1906            if ($append_category !== 'limited') {
1907                $cond['joins'] .= " LEFT JOIN {$serendipity['dbPrefix']}entrycat ec
1908                                           ON e.id = ec.entryid";
1909            }
1910
1911            $cond['joins'] .= " LEFT JOIN {$serendipity['dbPrefix']}category c
1912                                       ON ec.categoryid = c.categoryid";
1913        }
1914
1915        switch($type) {
1916            case 'directory':
1917                $sql_artifact_column = 'i.path IS NULL OR
1918                                        acl_acc.groupid IS NULL';
1919                $sql_artifact = 'AND acl_acc.artifact_index = i.path';
1920                break;
1921
1922            case 'category':
1923                $sql_artifact_column = 'c.categoryid IS NULL';
1924                $sql_artifact = 'AND acl_acc.artifact_id   = c.categoryid';
1925                break;
1926        }
1927
1928        $cond['joins'] .= " LEFT JOIN {$serendipity['dbPrefix']}authorgroups AS acl_a
1929                                   ON acl_a.authorid = " . $read_id . "
1930                            LEFT JOIN {$serendipity['dbPrefix']}access AS acl_acc
1931                                   ON (    acl_acc.artifact_mode = '" . $mode . "'
1932                                       AND acl_acc.artifact_type = '" . $type . "'
1933                                       " . $sql_artifact . "
1934                                      )";
1935
1936        if (empty($cond['and'])) {
1937            $cond['and'] .= ' WHERE ';
1938        } else {
1939            $cond['and'] .= ' AND ';
1940        }
1941
1942        // When in Admin-Mode, apply readership permissions.
1943        $cond['and'] .= "    (
1944                                 " . $sql_artifact_column . "
1945                                 OR ( acl_acc.groupid = " . $read_id_sql . ")
1946                                 OR ( acl_acc.artifact_id IS NULL
1947                                      " . (isset($serendipity['GET']['adminModule']) &&
1948                                           $serendipity['GET']['adminModule'] == 'entries' &&
1949                                           !serendipity_checkPermission('adminEntriesMaintainOthers')
1950                                        ? "AND (c.authorid IS NULL OR c.authorid = 0 OR c.authorid = " . $read_id . ")"
1951                                        : "") . "
1952                                    )
1953                               )";
1954        return true;
1955    }
1956
1957    return false;
1958}
1959
1960/**
1961 * Check for Cross-Site-Request-Forgery attacks because of missing HTTP Referer
1962 *
1963 * http://de.wikipedia.org/wiki/XSRF
1964 * This function checks the HTTP referer, and if it is part of the current Admin panel.
1965 *
1966 * @access public
1967 * @return  Returns true if XSRF was detected, false if not. The script should abort, if TRUE is returned.
1968 */
1969function serendipity_checkXSRF() {
1970    global $serendipity;
1971
1972    // If no module was requested, the user has just logged in and no action will be performed.
1973    if (empty($serendipity['GET']['adminModule'])) {
1974        return false;
1975    }
1976
1977    // The referrer was empty. Deny access.
1978    if (empty($_SERVER['HTTP_REFERER'])) {
1979        echo serendipity_reportXSRF(1, true, true);
1980        return false;
1981    }
1982
1983    // Parse the Referrer host. Abort if not parseable.
1984    $hostinfo = @parse_url($_SERVER['HTTP_REFERER']);
1985    if (!is_array($hostinfo)) {
1986        echo serendipity_reportXSRF(2, true, true);
1987        return true;
1988    }
1989
1990    // Get the server against we will perform the XSRF check.
1991    $server = '';
1992    if (empty($_SERVER['HTTP_HOST'])) {
1993        $myhost = @parse_url($serendipity['baseURL']);
1994        if (is_array($myhost)) {
1995            $server = $myhost['host'];
1996        }
1997    } else {
1998        $server = $_SERVER['HTTP_HOST'];
1999    }
2000
2001    // If the current server is different than the referred server, deny access.
2002    if ($hostinfo['host'] != $server) {
2003        echo serendipity_reportXSRF(3, true, true);
2004        return true;
2005    }
2006    return false;
2007}
2008
2009/**
2010 * Report a XSRF attempt to the Serendipity Interface
2011 *
2012 * http://de.wikipedia.org/wiki/XSRF
2013 *
2014 * LONG
2015 *
2016 * @access public
2017 * @see serendipity_checkXSRF()
2018 * @param   string      The type of XSRF check that got hit. Used for CSS formatting.
2019 * @param   boolean     If true, the XSRF error should be fatal
2020 * @param   boolean     If true, tell Serendipity to check the $serendipity['referrerXSRF'] config option to decide if an error should be reported or not.
2021 * @return  string      Returns the HTML error report
2022 */
2023function serendipity_reportXSRF($type = 0, $reset = true, $use_config = false) {
2024    global $serendipity;
2025
2026    // Set this in your serendipity_config_local.inc.php if you want HTTP Referrer blocking:
2027    // $serendipity['referrerXSRF'] = true;
2028
2029    $string = '<div class="msg_error XSRF_' . $type . '"><span class="icon-attention" aria-hidden="true"></span> ' . ERROR_XSRF . '</div>';
2030    if ($reset) {
2031        // Config key "referrerXSRF" can be set to enable blocking based on HTTP Referrer. Recommended for Paranoia.
2032        if (($use_config && isset($serendipity['referrerXSRF']) && $serendipity['referrerXSRF']) || $use_config === false) {
2033            $serendipity['GET']['adminModule'] = '';
2034        } else {
2035            // Paranoia not enabled. Do not report XSRF.
2036            $string = '';
2037        }
2038    }
2039    return $string;
2040}
2041
2042/**
2043 * Prevent XSRF attacks by checking for a form token
2044 *
2045 * http://de.wikipedia.org/wiki/XSRF
2046 *
2047 * This function checks, if a valid Form token was posted to the site.
2048 *
2049 * @access public
2050 * @see serendipity_setFormToken()
2051 * @return  boolean     Returns true, if XSRF attempt was found and the token was missing
2052 */
2053function serendipity_checkFormToken($output = true) {
2054    global $serendipity;
2055    $token = '';
2056    if (!empty($serendipity['POST']['token'])) {
2057        $token = $serendipity['POST']['token'];
2058    } elseif (!empty($serendipity['GET']['token'])) {
2059        $token = $serendipity['GET']['token'];
2060    }
2061
2062    if (empty($token)) {
2063        if ($output) echo serendipity_reportXSRF('token', false);
2064        return false;
2065    }
2066
2067    if ($token != md5(session_id()) &&
2068        $token != md5($serendipity['COOKIE']['old_session'])) {
2069        if ($output) echo serendipity_reportXSRF('token', false);
2070        return false;
2071    }
2072
2073    return true;
2074}
2075
2076/**
2077 * Prevent XSRF attacks by setting a form token within HTTP Forms
2078 *
2079 * http://de.wikipedia.org/wiki/XSRF
2080 *
2081 * By inserting a unique Form token that holds the session id, all requests
2082 * to serendipity HTTP forms can only be processed if the token is present.
2083 * This effectively makes XSRF attacks impossible. Only bundled with XSS
2084 * attacks it can be bypassed.
2085 *
2086 * 'form' type tokens can be embedded within the <form> script.
2087 * 'url' type token can be embedded within HTTP GET calls.
2088 *
2089 * @access public
2090 * @param   string      The type of token to return (form|url|plain)
2091 * @return  string      Returns the form token to be used in your functions
2092 */
2093function serendipity_setFormToken($type = 'form') {
2094    global $serendipity;
2095
2096    if ($type == 'form') {
2097        return '<input type="hidden" name="serendipity[token]" value="' . md5(session_id()) . '" />'."\n";
2098    } elseif ($type == 'url') {
2099        return 'serendipity[token]=' . md5(session_id());
2100    } else {
2101        return md5(session_id());
2102    }
2103}
2104
2105/**
2106 * Load available/configured options for a specific theme (through config.inc.php of a template directory)
2107 * into an array.
2108 *
2109 * @param   array   Referenced variable coming from the config.inc.php file, where the config values will be stored in
2110 * @param   boolean Use true boolean mode in array $template_config in the config.inc.php file
2111 * @return  array   Final return array with default values
2112 */
2113function &serendipity_loadThemeOptions(&$template_config, $okey = '', $bc_bool = false) {
2114    global $serendipity;
2115
2116    if (empty($okey)) {
2117        $okey = $serendipity['template'];
2118    }
2119
2120    $_template_vars =& serendipity_db_query("SELECT name, value FROM {$serendipity['dbPrefix']}options
2121                                             WHERE okey = 't_" . serendipity_db_escape_string($okey) . "'
2122                                                OR okey = 't_global'", false, 'assoc', false, 'name', 'value');
2123    if (!is_array($_template_vars)) {
2124        $template_vars = array();
2125    } else {
2126        $template_vars =& $_template_vars;
2127    }
2128
2129    foreach($template_config AS $key => $item) {
2130        if (!isset($template_vars[$item['var']])) {
2131            $template_vars[$item['var']] = $item['default'];
2132        }
2133    }
2134    if($bc_bool) {
2135        foreach($template_vars AS $k => $i) {
2136            if ($i == 'true' || $i == 'false') {
2137                $template_vars[$k] = serendipity_db_bool($i);
2138            }
2139        }
2140        //reset smarty compiled template ?
2141    }
2142    return $template_vars;
2143}
2144
2145/**
2146 * Load global available/configured options for a specific theme
2147 * into an array.
2148 *
2149 * @param   array   Referenced variable coming from the config.inc.php file, where the config values will be stored in
2150 * @param   array   Current template configuration
2151 * @return  array   Final return array with default values
2152 */
2153function serendipity_loadGlobalThemeOptions(&$template_config, &$template_loaded_config, $supported = array()) {
2154    global $serendipity;
2155
2156    if ($supported['navigation']) {
2157        $navlinks = array();
2158
2159        $conf_amount = array(
2160                'var'           => 'amount',
2161                'name'          => NAVLINK_AMOUNT,
2162                'type'          => 'string',
2163                'default'       => '5',
2164                'scope'         => 'global'
2165        );
2166
2167        // This always needs to be present, if not it could happen that the template options do have an older version of this variable
2168        $template_config[] = $conf_amount;
2169
2170        if (!isset($template_loaded_config['amount']) || empty($template_loaded_config['amount'])) {
2171            $template_loaded_config['amount'] = $conf_amount['default'];
2172        }
2173
2174        // Check if we are currently inside the admin interface.
2175        if ($serendipity['POST']['adminModule'] == 'templates' && $serendipity['POST']['adminAction'] == 'configure' && !empty($serendipity['POST']['template']['amount'])) {
2176            $template_loaded_config['amount'] = (int)$serendipity['POST']['template']['amount'];
2177        }
2178
2179        for ($i = 0; $i < $template_loaded_config['amount']; $i++) {
2180            $navlinks[] = array(
2181                'title' => $template_loaded_config['navlink' . $i . 'text'],
2182                'href'  => $template_loaded_config['navlink' . $i . 'url']
2183            );
2184
2185            $template_config[] = array(
2186                'var'           => 'navlink' . $i . 'text',
2187                'name'          => NAV_LINK_TEXT . ' #' . ($i+1),
2188                'type'          => 'string',
2189                'default'       => 'Link #' . ($i+1),
2190                'scope'         => 'global'
2191            );
2192            $template_config[] = array(
2193                'var'           => 'navlink' . $i . 'url',
2194                'name'          => NAV_LINK_URL . ' #' . ($i+1),
2195                'type'          => 'string',
2196                'default'       => '#',
2197                'scope'         => 'global'
2198            );
2199        }
2200
2201        $serendipity['smarty']->assignByRef('navlinks', $navlinks);
2202    }
2203
2204    // Forward thinking. ;-)
2205    serendipity_plugin_api::hook_event('backend_templates_globalthemeoptions', $template_config, $supported);
2206}
2207
2208
2209
2210/**
2211 * Check if a member of a group has permissions to execute a plugin
2212 *
2213 * @param string    Pluginname
2214 * @param int       ID of the group of which the members should be checked
2215 * @return boolean
2216 */
2217function serendipity_hasPluginPermissions($plugin, $groupid = null) {
2218    static $forbidden = null;
2219    global $serendipity;
2220
2221    if (empty($serendipity['authorid'])) {
2222        return true;
2223    }
2224
2225    if ($forbidden === null || ($groupid !== null && !isset($forbidden[$groupid]))) {
2226        $forbidden = array();
2227
2228        if ($groupid === null) {
2229            $groups = serendipity_checkPermission(null, null, 'all');
2230        } else {
2231            $groups = array($groupid => serendipity_fetchGroup($groupid));
2232        }
2233
2234        foreach($groups AS $idx => $group) {
2235            if ($idx == 'membership') {
2236                continue;
2237            }
2238            foreach($group AS $key => $val) {
2239                if (substr($key, 0, 2) == 'f_') {
2240                    $forbidden[$groupid][$key] = true;
2241                }
2242            }
2243        }
2244    }
2245
2246    if (isset($forbidden[$groupid]['f_' . $plugin])) {
2247        return false;
2248    } else {
2249        return true;
2250    }
2251}
2252
2253/**
2254 * Return the bcrypt hash of a value
2255 *
2256 * @param string    The string to hash
2257 * @return string   The hashed string
2258 */
2259function serendipity_hash($string) {
2260    return password_hash($string, PASSWORD_BCRYPT);
2261}
2262
2263/**
2264 * Return the SHA1 (with pre-hash) of a value
2265 *
2266 * @param string    The string to hash
2267 * @return string   The hashed string
2268 */
2269function serendipity_sha1_hash($string) {
2270    global $serendipity;
2271
2272    if (empty($serendipity['hashkey'])) {
2273        serendipity_set_config_var('hashkey', time(), 0);
2274    }
2275
2276    return sha1($serendipity['hashkey'] . $string);
2277}
2278
2279/**
2280 * Backwards-compatibility to recognize old-style md5 passwords to allow migration
2281 *
2282 * @param string The string to hash
2283 * @param string  Either SHA1 or MD5 hash, depending on value
2284 */
2285function serendipity_passwordhash($cleartext_password) {
2286    global $serendipity;
2287
2288    if ($_SESSION['serendipityHashType'] > 0) {
2289        return serendipity_hash($cleartext_password);
2290    } else {
2291        return md5($cleartext_password);
2292    }
2293}
2294
2295/* vim: set sts=4 ts=4 expandtab : */
2296