<?php
# Copyright (c) 2003-2005, Jannis Hermanns (on behalf the Serendipity Developer Team)
# All rights reserved.  See LICENSE file for licensing details

if (IN_serendipity !== true) {
    die ("Don't hack!");
}


/**
 * Adds a new author account
 *
 * @access public
 * @param   string  New username
 * @param   string  New password
 * @param   string  The realname of the user
 * @param   string  The email address of the user
 * @param   int     The userlevel of a user
 * @return  int     The new user ID of the added author
 */
function serendipity_addAuthor($username, $password, $realname, $email, $userlevel=0, $hashtype=2) {
    global $serendipity;
    $password = serendipity_hash($password);
    $query = "INSERT INTO {$serendipity['dbPrefix']}authors (username, password, realname, email, userlevel, hashtype)
                        VALUES  ('" . serendipity_db_escape_string($username) . "',
                                 '" . serendipity_db_escape_String($password) . "',
                                 '" . serendipity_db_escape_String($realname) . "',
                                 '" . serendipity_db_escape_String($email) . "',
                                 '" . serendipity_db_escape_String($userlevel) . "',
                                 '" . serendipity_db_escape_String($hashtype) . "'
                                 )";
    serendipity_db_query($query);
    $cid = serendipity_db_insert_id('authors', 'authorid');

    $data = array(
        'authorid' => $cid,
        'username' => $username,
        'realname' => $realname,
        'email'    => $email
    );

    serendipity_insertPermalink($data, 'author');
    return $cid;
}

/**
 * Delete an author account
 *
 * (Note, this function does not delete entries by an author)
 *
 * @access public
 * @param   int     The author ID to delete
 * @return  boolean     True on success, false on error or unsufficient privileges
 */
function serendipity_deleteAuthor($authorid) {
    global $serendipity;

    if (!serendipity_checkPermission('adminUsersDelete')) {
        return false;
    }

    if (serendipity_db_query("DELETE FROM {$serendipity['dbPrefix']}authors WHERE authorid=" . (int)$authorid)) {
        serendipity_db_query("DELETE FROM {$serendipity['dbPrefix']}permalinks WHERE entry_id=" . (int)$authorid ." and type='author'");
    }
    return true;
}

/**
 * Removes a configuration value from the Serendipity Configuration
 *
 * Global config items have the authorid 0, author-specific configuration items have the corresponding authorid.
 *
 * @access public
 * @param   string      The name of the configuration value
 * @param   int         The ID of the owner of the config value (0: global)
 * @return null
 */
function serendipity_remove_config_var($name, $authorid = 0) {
    global $serendipity;
    serendipity_db_query("DELETE FROM {$serendipity['dbPrefix']}config where name='" . serendipity_db_escape_string($name) . "' AND authorid = " . (int)$authorid);
}

/**
 * Sets a configuration value for the Serendipity Configuration
 *
 * Global config items have the authorid 0, author-specific configuration items have the corresponding authorid.
 *
 * @access public
 * @param   string      The name of the configuration value
 * @param   string      The value of the configuration item
 * @param   int         The ID of the owner of the config value (0: global)
 */
function serendipity_set_config_var($name, $val, $authorid = 0) {
    global $serendipity;

    serendipity_db_query("DELETE FROM {$serendipity['dbPrefix']}config where name='" . serendipity_db_escape_string($name) . "' AND authorid = " . (int)$authorid);

    if ($name == 'password' || $name == 'check_password') {
        return;
    }

    $r = serendipity_db_insert('config', array('name' => $name, 'value' => $val, 'authorid' => $authorid));

    if ($authorid === 0 || $authorid === $serendipity['authorid']) {
        if ($val === 'false') {
            $serendipity[$name] = false;
        } else {
            $serendipity[$name] = $val;
        }
    }

    if (is_string($r)) {
        # if $r is a string, it is the error-message from the insert
        echo $r;
    }
}

/**
 * Retrieve a global configuration value for a specific item of the current Serendipity Configuration
 *
 * @access public
 * @param   string      The name of the configuration value
 * @param   string      The default value of a configuration item, if not found in the Database
 * @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.
 * @return  string      The configuration value content
 */
function serendipity_get_config_var($name, $defval = false, $empty = false) {
    global $serendipity;
    if (isset($serendipity[$name])) {
        if ($empty && gettype($serendipity[$name]) == 'string' && $serendipity[$name] === '') {
            return $defval;
        } else {
            return $serendipity[$name];
        }
    } else {
        return $defval;
    }
}

/**
 * Retrieve an author-specific configuration value for an item of the Serendipity Configuration stored in the DB
 *
 * Despite the serendipity_get_config_var() function, this will retrieve author-specific values straight from the Database.
 *
 * @access public
 * @param   string      The name of the configuration value
 * @param   int         The ID of the owner of the config value (0: global)
 * @param   string      The default value of a configuration option, if not set in the DB
 * @return  string      The configuration value content
 */
function serendipity_get_user_config_var($name, $authorid, $default = '') {
    global $serendipity;

    $author_sql = '';
    if (!empty($authorid)) {
        $author_sql = "authorid = " . (int)$authorid . " AND ";
    } elseif (isset($serendipity[$name])) {
        return $serendipity[$name];
    }

    $r = serendipity_db_query("SELECT value FROM {$serendipity['dbPrefix']}config WHERE $author_sql name = '" . $name . "' LIMIT 1", true);

    if (is_array($r)) {
        return $r[0];
    } else {
        return $default;
    }
}

/**
 * Retrieves an author-specific account value
 *
 * This retrieves specific account data from the user configuration, not from the serendipity configuration.
 *
 * @access public
 * @param   string      The name of the configuration value
 * @param   int         The ID of the author to fetch the configuration for
 * @param   string      The default value of a configuration option, if not set in the DB
 * @return  string      The configuration value content
 */
function serendipity_get_user_var($name, $authorid, $default) {
    global $serendipity;

    $r = serendipity_db_query("SELECT $name FROM {$serendipity['dbPrefix']}authors WHERE authorid = " . (int)$authorid, true);

    if (is_array($r)) {
        return $r[0];
    } else {
        return $default;
    }
}

/**
 * Updates data from the author-specific account
 *
 * This sets the personal account data of a serendipity user within the 'authors' DB table
 *
 * @access public
 * @param   string      The name of the configuration value
 * @param   string      The content of the configuration value
 * @param   int         The ID of the author to set the configuration for
 * @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.
 * @return null
 */
function serendipity_set_user_var($name, $val, $authorid, $copy_to_s9y = true) {
    global $serendipity;

    // When inserting a DB value, this array maps the new values to the corresponding s9y variables
    static $user_map_array = array(
        'username'  => 'serendipityUser',
        'email'     => 'serendipityEmail',
        'userlevel' => 'serendipityUserlevel'
    );

    // Special case for inserting a password
    switch($name) {
        case 'check_password':
            //Skip this field.  It doesn't need to be stored.
            return;
        case 'password':
            if (empty($val)) {
                return;
            }

            $val = serendipity_hash($val);
            $copy_to_s9y = false;
            break;

        case 'right_publish':
        case 'mail_comments':
        case 'mail_trackbacks':
        case 'second_factor':
            $val = (serendipity_db_bool($val) ? 1 : '0');
            break;
    }

    serendipity_db_query("UPDATE {$serendipity['dbPrefix']}authors SET $name = '" . serendipity_db_escape_string($val) . "' WHERE authorid = " . (int)$authorid);

    if ($copy_to_s9y) {
        if (isset($user_map_array[$name])) {
            $key = $user_map_array[$name];
        } else {
            $key = 'serendipity' . ucfirst($name);
        }

        $_SESSION[$key] = $serendipity[$key] = $val;
    }
}

/**
 * Gets the full filename and path of a template/style/theme file
 *
 * The returned full path is depending on the second parameter, where you can either fetch a HTTP path, or a realpath.
 * The file is searched in the current template, and if it is not found there, it is returned from the default template.
 *
 * @access public
 * @param   string      The filename to search for in the selected template
 * @param   string      The path selector that tells whether to return a HTTP or realpath
 * @param   bool        Enable to include frontend template fallback chaining (used for wysiwyg Editor custom config files, emoticons, etc)
 * @return  string      The full path+filename to the requested file
 */
function serendipity_getTemplateFile($file, $key = 'serendipityHTTPPath', $force_frontend_fallback = false) {
    global $serendipity;

    $directories = array();
    if ((! defined('IN_serendipity_admin')) || $force_frontend_fallback) {
        $directories[] = $serendipity['template'] . '/';  # In the frontend or when forced (=preview_iframe.tpl), use the frontend theme
    } else {
        $directories[] = $serendipity['template_backend'] . '/';    # Since 2.0 s9y can have a independent backend theme
    }
    $directories[] = ($serendipity['template_engine'] ?? null) . '/'; # themes can set an engine, which will be used if they do not have the file
    $directories[] = $serendipity['defaultTemplate'] .'/';  # the default theme is the last place we will look in, serving as pure fallback
    $directories = array_unique($directories); # save performance by not checking for file existence multiple times in the same directory

    foreach ($directories as $directory) {
        $templateFile = $serendipity['templatePath'] . $directory . $file;
        if (file_exists($serendipity['serendipityPath'] . $templateFile)) {
            return ($serendipity[$key] ?? '') . $templateFile;
        }

        if (file_exists($serendipity['serendipityPath'] . $templateFile . ".tpl")) {
            # catch *.tpl files serving as template, used by the backend for serendipity_editor.js.tpl
            return $serendipity['baseURL'] . 'index.php?/plugin/' . $file;
        }
    }

    return false;
}

/**
 * Loads all configuration values and imports them to the $serendipity array
 *
 * This function may be called twice - once for the global config and once for
 * user-specific config
 *
 * @access public
 * @param   int     The Authorid to fetch the configuration from (0: global)
 * @return  null
 */
function serendipity_load_configuration($author = null) {
    global $serendipity;
    static $config_loaded = array();

    if (isset($config_loaded[$author])) {
        return true;
    }

    if (!empty($author)) {
        // Replace default configuration directives with user-relevant data
        $rows = serendipity_db_query("SELECT name,value
                                        FROM {$serendipity['dbPrefix']}config
                                        WHERE authorid = '". (int)$author ."'");
    } else {
        // Only get default variables, user-independent (frontend)
        $rows = serendipity_db_query("SELECT name, value
                                        FROM {$serendipity['dbPrefix']}config
                                        WHERE authorid = 0");
    }

    if (is_array($rows)) {
        foreach ($rows as $row) {
            // Convert 'true' and 'false' into booleans
            $serendipity[$row['name']] = serendipity_get_bool($row['value']);
        }
    }
    $config_loaded[$author] = true;

    // Set baseURL to defaultBaseURL
    if ((empty($author) || empty($serendipity['baseURL'])) && isset($serendipity['defaultBaseURL'])) {
        $serendipity['baseURL'] = $serendipity['defaultBaseURL'];
    }

    // check language selection or fallback
    if (isset($serendipity['lang']) && !isset($serendipity['languages'][$serendipity['lang']])) {
        $serendipity['lang'] = $serendipity['autolang'];
    }
    
    if (empty($author)) {
        $serendipity['default_lang'] = $serendipity['lang'];
    }

}

/**
 * Perform logout functions (destroys session data)
 *
 * @access public
 * @return null
 */
function serendipity_logout() {
    global $serendipity;
    $_SESSION['serendipityAuthedUser'] = false;
    serendipity_session_destroy();
    serendipity_deleteCookie('author_username');
    serendipity_deleteCookie('author_autologintoken');
    serendipity_deleteCookie('author_token');
}

/**
 * Destroys a session, keeps important stuff intact.
 * @access public
 * @return null
 */
function serendipity_session_destroy() {
    $no_smarty = $_SESSION['no_smarty'] ?? null;
    @session_destroy();
    session_start();
    session_regenerate_id();

    $_SESSION['SERVER_GENERATED_SID'] = true;
    $_SESSION['no_smarty']            = $no_smarty;
}

/**
 * Perform login to Serendipity
 *
 * @access public
 * @param   boolean     If set to true, external plugins will be queried for getting a login
 * @return  boolean     Return true, if the user is logged in. False if not.
 */
function serendipity_login($use_external = true) {
    global $serendipity;

    if (serendipity_authenticate_author('', '', false, $use_external)) {
        #The session has this data already
        #we previously just checked the value of $_SESSION['serendipityAuthedUser'] but
        #we need the authorid still, so call serendipity_authenticate_author with blank
        #params
        return true;
    }

    // First try login via POST data. If true, the userinformation will be stored in a cookie (optionally)
    if (serendipity_authenticate_author($serendipity['POST']['user'] ?? null, $serendipity['POST']['pass'] ?? null, false, $use_external)) {
        if (empty($serendipity['POST']['auto'])) {
            serendipity_deleteCookie('author_information');
            return false;
        } else {
            serendipity_issueAutologin($serendipity['POST']['user']);
            return true;
        }
    // Now try login via COOKIE data
    } elseif (isset($serendipity['COOKIE']['author_username'])) {
        $user = $serendipity['COOKIE']['author_username'];
        $valid_logintoken = serendipity_checkAutologin($user);
        if ($valid_logintoken === true) {
            // if we do not tie down the session here 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.
            serendipity_setAuthorToken();
            serendipity_load_userdata($user);
            $_SESSION['serendipity2faSuccess'] = true;
            return true;
        } else {
            serendipity_deleteCookie('author_username');
            serendipity_deleteCookie('author_autologintoken');
            return false;
        }
    }

    $data = array('ext' => $use_external, 'mode' => 2, 'user' => $serendipity['POST']['user'] ?? null, 'pass' => $serendipity['POST']['pass'] ?? null);
    if ($use_external) serendipity_plugin_api::hook_event('backend_loginfail', $data);
}

/**
 * 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). 
 * @param String The username 
 */
function serendipity_issueAutologin($user) {
    global $serendipity;

    try {
        // TODO: Add https://github.com/paragonie/random_compat for PHP < 7.0
        $random_string = random_bytes(32);
    } catch (TypeError $e) {
        // Well, it's an integer, so this IS unexpected.
        die("An unexpected error has occurred"); 
    } catch (Error $e) {
        // This is also unexpected because 32 is a reasonable integer.
        die("An unexpected error has occurred");
    } catch (Exception $e) {
        // If you get this message, the CSPRNG failed hard.
        die("Could not generate a random string. Is our OS secure?");
    }

    $rnd = bin2hex($random_string);
    

    // Delete possible current cookie. Also delete any autologin keys that smell like 3-week-old, dead fish.
    $threeWeeksAgo = time() - 1814400;
    $okeyCast = serendipity_db_cast('okey', 'unsigned');
    serendipity_db_query("DELETE FROM {$serendipity['dbPrefix']}options 
                                WHERE name = 'autologin_" . serendipity_db_escape_string($user) . "'
                                   OR (name LIKE 'autologin_%' AND $okeyCast < {$threeWeeksAgo})");

    // Issue new autologin cookie
    serendipity_db_query("INSERT INTO {$serendipity['dbPrefix']}options (name, value, okey) VALUES ('autologin_" . serendipity_db_escape_string($user) . "', '" . $rnd  . "', '" . time() . "')");
    serendipity_setCookie('author_autologintoken', $rnd, true, false, true);
    serendipity_setCookie('author_username', $user);
}

/**
 * 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
 * @param String The username
 * @returns bool true if valid, false if not
 */
function serendipity_checkAutologin($user) {
    global $serendipity;

    $threeWeeksAgo = time() - 1814400;
    $okeyCast = serendipity_db_cast('okey', 'unsigned');

    // Fetch autologin data from DB
    $autologin_stored = serendipity_db_query("SELECT name, value, okey FROM {$serendipity['dbPrefix']}options
                                WHERE name = 'autologin_" . serendipity_db_escape_string($user) . "'
                                   AND $okeyCast > {$threeWeeksAgo} LIMIT 1",
                                   true, 'assoc');
    
    if (!is_array($autologin_stored)) {
        return false;
    }

    if ($serendipity['COOKIE']['author_autologintoken'] !== $autologin_stored['value']) {
        return false;
    }

    if ($autologin_stored['okey'] < (time()-86400)) {
        // Issued autologin cookie has been issued more than 1 day ago. Re-Issue new cookie, invalidate old one to prevent abuse
        if ($serendipity['expose_s9y']) serendipity_header('X-ReIssue-Cookie: +' . (time() - $autologin_stored['okey']) . 's');
        serendipity_issueAutologin($user);
    }

    return true;
}

/**
 * Set a session cookie which can identify a user across http/https boundaries
 */
function serendipity_setAuthorToken() {
    $hash = bin2hex(random_bytes(20));
    serendipity_setCookie('author_token', $hash);
    $_SESSION['author_token'] = $hash;
}

/**
 * Send a login code to the user's email adress, if the user has not currently already an active
 * login code.
 */
function serendipity_send2faCode() {
    global $serendipity;
    $storedSecondFactor = serendipity_getCacheItem($serendipity['serendipityUser'] . '_2faCode');
    if (! $storedSecondFactor) {
        $secondFactor = bin2hex(random_bytes(3));
        
        $subject = sprintf(SECOND_FACTOR_MAIL_TITLE, $serendipity['serendipityUser']);
        $message = sprintf(SECOND_FACTOR_MAIL, $serendipity['serendipityUser'], $secondFactor);
        if (serendipity_sendMail($serendipity['serendipityEmail'], $subject, $message, $serendipity['blogMail'])) {
            return serendipity_cacheItem($serendipity['serendipityUser'] . '_2faCode', $secondFactor, 60 * 15);
        } else {
            return false;
        }
    }
    return false;
}

function serendipity_validate2faCode() {
    global $serendipity;
    $storedSecondFactor = serendipity_getCacheItem($serendipity['serendipityUser'] . '_2faCode');
    if ($storedSecondFactor && $storedSecondFactor === $serendipity['POST']['2fa']) {
        $_SESSION['serendipity2faSuccess'] = true;
        serendipity_cacheItem($serendipity['serendipityUser'] . '_2faCode', false);
    }
}

/**
 * Perform user authentication routine
 *
 * If a user is already authenticated via session data, this bypasses some routines.
 * After a user has ben authenticated, several SESSION variables ar set.
 * If the authentication fails, the session is destroyed.
 *
 * @access public
 * @param   string      The username to check
 * @param   string      The password to check (may contain plaintext or MD5 hash)
 * @param   boolean     Indicates whether the input password is already in MD5 format (TRUE) or not (FALSE).
 * @param   boolean     Indicates whether to query external plugins for authentication
 * @return  boolean     True on success, False on error
 */
function serendipity_authenticate_author($username = '', $password = '', $is_hashed = false, $use_external = true) {
    global $serendipity;
    static $debug = false;
    static $debugc = 0;

    if ($debug) {
        $fp = fopen('login.log', 'a');
        flock($fp, LOCK_EX);
        $debugc++;
        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");
    }
    if (isset($_SESSION['serendipityUser']) && isset($_SESSION['serendipityPassword']) && isset($_SESSION['serendipityAuthedUser']) && $_SESSION['serendipityAuthedUser'] == true) {
        $username = $_SESSION['serendipityUser'];
        $password = $_SESSION['serendipityPassword'];
        // 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
        #$is_hashed = true;
        if ($debug) fwrite($fp, date('Y-m-d H:i') . ' - Recall from session: ' . $username . ':' . $password . "\n");
    }

    # We give each user only 5 tries a minute to login. This is to limit brute force attacks.
    $loginID = 'logintry' . $username . preg_replace(
             ['/\.\d*$/', '/[\da-f]*:[\da-f]*$/'],      # anonymize both IPv4 and IPv6
             ['.XXX', 'XXXX:XXXX'],
             $_SERVER['REMOTE_ADDR']
            );
    $login_try = (int) serendipity_getCacheItem($loginID);  # false == 0 when not set
    
    if ($login_try > 4) {
        // We are in block mode, user failed log in too often.
        serendipity_cacheItem($loginID, $login_try + 1, 60); // Renew the block for 60 seconds
        return false;
    }

    if ($debug) fwrite($fp, date('Y-m-d H:i') . ' - Login ext check' . "\n");
    $is_authenticated = false;
    if ($use_external) serendipity_plugin_api::hook_event('backend_login', $is_authenticated, NULL);
    if ($is_authenticated) return true;

    if ($debug) fwrite($fp, date('Y-m-d H:i') . ' - Login username check:' . $username . "\n");
    if (!empty($username) && is_string($username)) {
        if ($use_external) serendipity_plugin_api::hook_event('backend_auth', $is_hashed, array('username' => $username, 'password' => $password));

        $query = "SELECT DISTINCT
                    email, password, realname, authorid, userlevel, right_publish, hashtype
                  FROM
                    {$serendipity['dbPrefix']}authors
                  WHERE
                    username   = '" . serendipity_db_escape_string($username) . "'";
        if ($debug) fwrite($fp, date('Y-m-d H:i') . ' - Login check (' . serialize($is_hashed) . ', ' . $_SESSION['serendipityPassword'] . '):' . $query . "\n");

        $rows = serendipity_db_query($query, false, 'assoc');
        if (is_array($rows)) {
            $is_valid_user = false;
            foreach($rows AS $row) {
                if ($is_valid_user) break;

                if (empty($row['hashtype']) || $row['hashtype'] == 0) {
                    // Old MD5 hashing routine. Will convert user.
                    if (isset($serendipity['hashkey']) && (time() - $serendipity['hashkey']) >= 15768000) {
                        die('You can no longer login with an old-style MD5 hash to prevent MD5-Hostage abuse.
                             Please ask the Administrator to set you a new password.');
                    }

                    if ( ($is_hashed === false && (string)$row['password'] === (string)md5($password)) ||
                         ($is_hashed !== false && (string)$row['password'] === (string)$password) ) {

                        serendipity_db_query("UPDATE {$serendipity['dbPrefix']}authors
                                                 SET password = '" . ($is_hashed === false ? serendipity_hash($password) : $password) . "',
                                                     hashtype = 2
                                               WHERE authorid = '" . $row['authorid'] . "'");
                        if ($debug) fwrite($fp, date('Y-m-d H:i') . ' - Migrated user:' . $row['username'] . "\n");
                        $is_valid_user = true;
                    } else {
                        continue;
                    }
                } else {
                    if ($row['hashtype'] == 1) {
                        // Old sha1 hashing routine. Will convert user.
                        if ( ($is_hashed === false && (string)$row['password'] === (string)serendipity_sha1_hash($password)) ||
                             ($is_hashed !== false && (string)$row['password'] === (string)$password) ) {

                            serendipity_db_query("UPDATE {$serendipity['dbPrefix']}authors
                                                     SET password = '" . ($is_hashed === false ? serendipity_hash($password) : $password) . "',
                                                         hashtype = 2
                                                   WHERE authorid = '" . $row['authorid'] . "'");
                            if ($debug) fwrite($fp, date('Y-m-d H:i') . ' - Migrated user:' . $row['username'] . "\n");
                            $is_valid_user = true;
                        } else {
                            continue;
                        }
                    } else {
                        if ( ($is_hashed === false && password_verify((string)$password, $row['password'])) ||
                             ($is_hashed !== false && (string)$row['password'] === (string)$password) ) {

                            $is_valid_user = true;
                            if ($debug) fwrite($fp, date('Y-m-d H:i') . ' - Validated ' . $row['password'] . ' == ' . ($is_hashed === false ? 'unhash:' . serendipity_hash($password) : 'hash:' . $password) . "\n");
                        } else {
                            if ($debug) fwrite($fp, date('Y-m-d H:i') . ' - INValidated ' . $row['password'] . ' == ' . ($is_hashed === false ? 'unhash:' . serendipity_hash($password) : 'hash:' . $password) . "\n");
                            continue;
                        }
                    }
                }

                // This code is only reached if the password before is valid.
                if ($is_valid_user) {
                    if ($debug) fwrite($fp, date('Y-m-d H:i') . ' [sid:' . session_id() . '] - Success.' . "\n");
                    serendipity_setCookie('old_session', session_id(), false);
                    if (!$is_hashed) {
                        serendipity_setAuthorToken();
                        $_SESSION['serendipityPassword']    = $serendipity['serendipityPassword'] = $password;
                    }

                    serendipity_load_userdata($username);
                    if ($serendipity['POST']['2fa'] || $serendipity['POST']['user']) {
                        # serendipity_load_userdata sets serendipity2faSuccess to true for cookie logins,
                        # but here, on a POST login, it is always still false
                        $_SESSION['serendipity2faSuccess'] = false;
                    }
                    return true;

                    
                }
            }
        }

        // Only reached, when proper login did not yet return true.
        if ($debug) fwrite($fp, date('Y-m-d H:i') . ' - FAIL.' . "\n");
        // Raise the count for failed attempts, to potentially trigger the brute force protection
        // above.
        serendipity_cacheItem($loginID, $login_try + 1, 60);
        $_SESSION['serendipityAuthedUser'] = false;
        serendipity_session_destroy();
    }

    if ($debug) {
        fwrite($fp, date('Y-m-d H:i') . ' [sid:' . session_id() . '] - Uninit' . "\n");
        fclose($fp);
    }

    return false;
}

function serendipity_load_userdata($username) {
    global $serendipity;
    
    $query = "SELECT DISTINCT
                    email, password, realname, authorid, userlevel, right_publish, hashtype
                  FROM
                    {$serendipity['dbPrefix']}authors
                  WHERE
                    username   = '" . serendipity_db_escape_string($username) . "'";
                    
    $rows = serendipity_db_query($query, false, 'assoc');
    $row = $rows[0];
    
    $_SESSION['serendipityUser']         = $serendipity['serendipityUser']         = $username;
    $_SESSION['serendipityRealname']     = $serendipity['serendipityRealname']     = $row['realname'];
    $_SESSION['serendipityEmail']        = $serendipity['serendipityEmail']        = $row['email'];
    $_SESSION['serendipityAuthorid']     = $serendipity['authorid']                = $row['authorid'];
    $_SESSION['serendipityUserlevel']    = $serendipity['serendipityUserlevel']    = $row['userlevel'];
    $_SESSION['serendipityAuthedUser']   = $serendipity['serendipityAuthedUser']   = true;
    $_SESSION['serendipityRightPublish'] = $serendipity['serendipityRightPublish'] = $row['right_publish'];
    $_SESSION['serendipityHashType']     = $serendipity['serendipityHashType']     = $row['hashtype'];

    serendipity_load_configuration($serendipity['authorid']);
    return true;
}

/**
 * Check if a user is logged in
 *
 * @access public
 * @return boolean  TRUE when logged in, FALSE when not.
 */
function serendipity_userLoggedIn() {
    global $serendipity;
    if (IS_installed) {
		$secondFactorEnabled = serendipity_db_bool(serendipity_get_user_config_var('second_factor', $serendipity['authorid'], false));
        if ($secondFactorEnabled) {
            // Only check for serendipity2faSuccess if config is active
            serendipity_validate2faCode();
            if (($_SESSION['serendipityAuthedUser'] ?? false) === true && $_SESSION['serendipity2faSuccess']) {
                return true;
            }
        } else {
            if (($_SESSION['serendipityAuthedUser'] ?? false) === true) {
                return true;
            }
        }
	}
    return false;
}

/**
 * A clone of an ifsetor() function to set a variable conditional on if the target already exists
 *
 * 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
 *
 * @access public
 * @param   mixed   Source variable that should be set into the target variable (reference call!)
 * @param   mixed   Target variable, that should get the contents of the source variable (reference call!)
 * @return  boolean True, when $target was not yet set and has been altered. False when no changes where made.
 */
function serendipity_restoreVar(&$source, &$target) {
    global $serendipity;

    if (isset($source) && !isset($target)) {
        $target = $source;
        return true;
    }

    return false;
}

/**
 * Set a Cookie via HTTP calls, and update $_COOKIE plus $serendipity['COOKIE'] array.
 *
 * @access public
 * @param   string      The name of the cookie variable
 * @param   string      The contents of the cookie variable
 * @param   int         Cookie validity (unix timestamp)
 * @return null
 */
function serendipity_setCookie($name, $value, $securebyprot = true, $custom_timeout = false, $httpOnly = false, $samesite = 'Strict') {
    global $serendipity;

    if ($securebyprot) {
        $secure = (array_key_exists('HTTPS', $_SERVER) && strtolower($_SERVER['HTTPS']) == 'on') ? true : false;
    } else {
        $secure = false;
    }

    if ($custom_timeout === false) {
        $custom_timeout = time() + 60*60*24*30;
    }

    setcookie("serendipity[$name]", $value, ['expires' => $custom_timeout, 'path' => $serendipity['serendipityHTTPPath'], 'secure' => $secure, 'httponly' => $httpOnly, 'samesite' => $samesite]);
    $_COOKIE[$name] = $value;
    $serendipity['COOKIE'][$name] = $value;
}

/**
 * Echo Javascript code to set a cookie variable
 *
 * This function is useful if your HTTP headers were already sent, but you still want to set a cookie
 * Note that contents are echo'd, not return'd. Can be used by plugins.
 *
 * @access public
 * @param   string      The name of the cookie variable
 * @param   string      The contents of the cookie variable
 * @return  null
 */
function serendipity_JSsetCookie($name, $value) {
    $name  = serendipity_entities($name);
    $value = urlencode($value);

    echo '<script type="text/javascript">serendipity.SetCookie("' . $name . '", unescape("' . $value . '"))</script>' . "\n";
}


/**
 * Deletes an existing cookie value by resetting it with an expired time
 *
 * LONG
 *
 * @access public
 * @param   string      Name of the cookie to delete
 * @return
 */
function serendipity_deleteCookie($name) {
    global $serendipity;

    setcookie("serendipity[$name]", '', time()-4000, $serendipity['serendipityHTTPPath']);
    unset($_COOKIE[$name]);
    unset($serendipity['COOKIE'][$name]);
}

/**
 * Performs a check whether an iframe for the admin section shall be emitted
 *
 * The iframe is used for previewing an entry with the stylesheet of the frontend.
 * It fetches its data from the session input data.
 *
 * @access private
 * @return boolean  True, if iframe was requested, false if not.
 */
function serendipity_is_iframe() {
    global $serendipity;

    if ($serendipity['GET']['is_iframe'] ?? false && is_array($_SESSION['save_entry'])) {
        if (!is_object($serendipity['smarty'] ?? null)) {
            // We need smarty also in the iframe to load a template's config.inc.php and register possible event hooks.
            serendipity_smarty_init();
        }
        return true;
    }
    return false;
}

/**
 * Prints the content of the iframe.
 *
 * Called by serendipity_is_iframe, when preview is requested. Fetches data from session.
 * An iframe is used so that a single s9y page must not timeout on intensive operations,
 * and so that the frontend stylesheet can be embedded without screwing up the backend.
 *
 * @access private
 * @see serendipity_is_iframe()
 * @param   mixed   The entry array (comes from session variable)
 * @param   string  Indicates whether an entry is previewed or saved. Save performs XML-RPC calls.
 * @param   boolean Use smarty templating?
 * @return  boolean Indicates whether iframe data was printed
 */
function serendipity_iframe(&$entry, $mode = null) {
    global $serendipity;

    if (empty($mode) || !is_array($entry)) {
        return false;
    }


    $data = array();
    $data['is_preview'] =  true;
    $data['mode'] =  $mode;

    switch ($mode) {
        case 'save':
            ob_start();
            $res = serendipity_updertEntry($entry);
            $data['updertHooks'] = ob_get_contents();
            ob_end_clean();
            if (is_string($res)) {
                $data['res'] = $res;
            }
            if (!empty($serendipity['lastSavedEntry'])) {
                $data['lastSavedEntry'] = $serendipity['lastSavedEntry'];
            }
            $data['entrylink'] = serendipity_archiveURL($res, $entry['title'], 'serendipityHTTPPath', true, array('timestamp' => $entry['timestamp']));
            break;

        case 'preview':
            $data['preview'] = serendipity_printEntries(array($entry), ($entry['extended'] != '' ? 1 : 0), true);
            break;
    }
    return serendipity_smarty_show('preview_iframe.tpl', $data);
}

/**
 * Creates the necessary session data to be used by later iframe calls
 *
 * This function emits the actual <iframe> call.
 *
 * @access private
 * @see serendipity_is_iframe()
 * @param   string  Indicates whether an entry is previewed or saved. Save performs XML-RPC calls.
 * @param   mixed   The entry array (comes from HTTP POST request)
 * @return  boolean Indicates whether iframe data was stored
 */
function serendipity_iframe_create($mode, &$entry) {
    global $serendipity;

    if (!empty($serendipity['POST']['no_save'])) {
        return true;
    }

    if (!serendipity_checkFormToken()) {
        return false;
    }

    $_SESSION['save_entry']      = $entry;
    $_SESSION['save_entry_POST'] = $serendipity['POST'];

    $attr = '';
    switch($mode) {
        case 'save':
            $attr = ' height="100" ';
            break;

        case 'preview':
            $attr = ' height="300" ';
            break;
    }

    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">'
         . IFRAME_WARNING
         . '</iframe>';
}

/**
 * Pre-Checks certain server environments to indicate available options when installing Serendipity
 *
 * @access public
 * @param   string      The name of the configuration option that needs to be checked for environmental data.
 * @return  array       Returns the array of available options for the requested config option
 */
function serendipity_probeInstallation($item) {
    global $serendipity;
    $res = NULL;

    switch ( $item ) {
        case 'dbType' :
            $res =  array();
            if (extension_loaded('mysql')) {
                $res['mysql'] = 'MySQL';
            }
            if (extension_loaded('PDO') &&
                in_array('pgsql', PDO::getAvailableDrivers())) {
                $res['pdo-postgres'] = 'PDO::PostgreSQL';
            }

            if (extension_loaded('PDO') &&
                in_array('sqlite', PDO::getAvailableDrivers())) {
                $res['pdo-sqlite'] = 'PDO::SQLite';
                $has_pdo = true;
            } else {
                $has_pdo = false;
            }

            if (extension_loaded('pgsql')) {
                $res['postgres'] = 'PostgreSQL';
            }
            if (extension_loaded('mysqli')) {
                $res['mysqli'] = 'MySQLi';
            }
            if (extension_loaded('sqlite') && function_exists('sqlite_open')) {
                $res['sqlite'] = 'SQLite';
            }
            if (extension_loaded('SQLITE3') && function_exists('sqlite3_open')) {
                $res['sqlite3'] = 'SQLite3';
            }
            if (class_exists('SQLite3')) {
                if ($has_pdo) {
                    $res['sqlite3oo'] = 'SQLite3 (OO) (Preferably use PDO-SQlite!)';
                } else {
                    $res['sqlite3oo'] = 'SQLite3 (OO)';
                }
            }
            if (function_exists('sqlrcon_alloc')) {
                $res['sqlrelay'] = 'SQLRelay';
            }
            break;

        case 'rewrite' :
            $res = array();
            $res['none'] = 'Disable URL Rewriting';
            $res['errordocs'] = 'Use Apache errorhandling';
            if( !function_exists('apache_get_modules') || in_array('mod_rewrite', apache_get_modules()) ) {
                $res['rewrite'] = 'Use Apache mod_rewrite';
            }
            if( !function_exists('apache_get_modules') || in_array('mod_rewrite', apache_get_modules()) ) {
                $res['rewrite2'] = 'Use Apache mod_rewrite (for 1&amp;1 and problematic servers)';
            }

            break;
    }

    return $res;
}

/**
 * Sets a HTTP header
 *
 * @access public
 * @param   string      The HTTP header to set
 * @return null
 */
function serendipity_header($header) {
    if (!headers_sent()) {
        header($header);
    }
}

/**
 * Gets the currently selected language. Either from the browser, or the personal configuration, or the global configuration.
 *
 * This function also sets HTTP Headers and cookies to contain the language for follow-up requests
 * TODO:
 * This previously was handled inside a plugin with an event hook, but caching
 * the event plugins that early in sequence created trouble with plugins not
 * having loaded the right language.
 * Find a way to let plugins hook into that sequence :-)
 *
 * Rules of precedence:
 * active user selection:
 * 1. POST 'user_language' from plugin_multilingual sidebar dropdown
 * 2. GET 'user_language' from article footer with option 'force full lanugage switch'
 * 
 * prior user selection:
 * 3. $_SESSION 'serendipityLanguage' from the running browser session
 * 4. $_COOKIE 'serendipityLanguage' new session - user precedence
 * 
 * no user selection:
 * 5. new user - browser autodetect with serendipity_detectLang()
 * 6. blog language from serendipity_load_configuration() - prior value of $serendipity['lang']
 * 
 * or if a user is logged in
 * 6. user language from serendipity_load_configuration(user) -> handled with a later call to
 *    serendipity_getPostAuthSessionLanguage()
 * 8. default language
 * 
 * @access public
 * @return  string      Returns the name of the selected language.
 */
function serendipity_getSessionLanguage() {
    global $serendipity;

    // try POST
    if (isset($serendipity['POST']['user_language']) && !empty($serendipity['languages'][$serendipity['POST']['user_language']])) {
        $lang = $serendipity['POST']['user_language'];
        if ($serendipity['expose_s9y']) serendipity_header('X-Serendipity-InterfaceLangSource: POST');
        
    // try GET
    } elseif (isset($serendipity['GET']['user_language']) && !empty($serendipity['languages'][$serendipity['GET']['user_language']])) {
        $lang = $serendipity['GET']['user_language'];
        if ($serendipity['expose_s9y']) serendipity_header('X-Serendipity-InterfaceLangSource: GET');
    }
    
    // save user selection into COOKIE and SESSION
    if (isset($lang) && !headers_sent()) {
        serendipity_setCookie('serendipityLanguage', $lang, false);
        $serendipity['COOKIE']['serendipityLanguage'] = $lang;
    }

    if (isset($lang)) {
        $_SESSION['serendipityLanguage'] = $lang;
    }

    // no active user selection, fall back to saved prior selections
    if (!isset($lang)) {
        // try SESSION
        if (isset($_SESSION['serendipityLanguage']) && !empty($serendipity['languages'][$_SESSION['serendipityLanguage']])) {
            $lang = $_SESSION['serendipityLanguage'];
            if ($serendipity['expose_s9y']) serendipity_header('X-Serendipity-InterfaceLangSource: SESSION');
        
        // try COOKIE
        } elseif (isset($serendipity['COOKIE']['serendipityLanguage']) && !empty($serendipity['languages'][$serendipity['COOKIE']['serendipityLanguage']])) {
        $lang = $serendipity['COOKIE']['serendipityLanguage'];
            if ($serendipity['expose_s9y']) serendipity_header('X-Serendipity-InterfaceLangSource: COOKIE');
        
        // try Browser language negotiation
    } elseif (serendipity_db_bool($serendipity['lang_content_negotiation'])) {
            $lang = serendipity_detectLang();  // returns negotiated language
            if (!empty($lang) && $serendipity['expose_s9y']) serendipity_header('X-Serendipity-InterfaceLangSource: Content-Negotiation');
    }

        // cache the result
        if (isset($lang) && $lang) {
            $serendipity['detected_lang'] = $lang;
        }
    
        // try configuration: blog default language
        if (empty($lang) && $serendipity['lang']) {
                    $lang = $serendipity['lang'];
            if ($serendipity['expose_s9y']) serendipity_header('X-Serendipity-InterfaceLangSource: Configuration');
        } 
        
        // if everthing failed, fall back to autolang which is still 'en' because detectLang failed too
        if (empty($lang)) {
                    $lang = $serendipity['autolang'];
                }
    } else {
        // cache the result
        if ($lang) $serendipity['detected_lang'] = $lang;
    }

    // save the result into SESSION
    if ($lang) $_SESSION['serendipityLanguage'] = $lang; 
            if ($serendipity['expose_s9y']) serendipity_header('X-Serendipity-InterfaceLang: ' . $lang);
    return $lang;
}

/**
 * Gets the selected language from personal configuration if needed
 * if getSessionLanguage detected a language by user selection or browser negotiation, it has priority
 *
 * This function also sets HTTP Headers and cookies to contain the language for follow-up requests
 *
 * @access public
 * @return  string      Returns the name of the selected language.
 */
function serendipity_getPostAuthSessionLanguage() {
    global $serendipity;

    // cached result from getSessionLanguage()
    if (!empty($serendipity['detected_lang']) && !defined('IN_serendipity_admin')) return $serendipity['detected_lang'];

    // if that returned the fallback language, try user default language or fallback
    if ($_SESSION['serendipityAuthedUser']) {
        if ($serendipity['expose_s9y']) serendipity_header('X-Serendipity-InterfaceLangSource: Database');
        $lang = $serendipity['lang']; //$serendipity['lang'] is overwritten by load_configuration(user) and points to the user default language
    }

    // save the result into SESSION
    $_SESSION['serendipityLanguage'] = $lang;

    if ($serendipity['expose_s9y']) serendipity_header('X-Serendipity-InterfaceLang: ' . $lang);

    return $lang;
}

/**
 * Retrieves an array of applying permissions to an author
 *
 * The privileges of each group an author is a member of are aggreated
 * and stored in a larger array. So both memberships and all aplying
 * privileges are returned.
 *
 * @access public
 * @param   int     The ID of the author to fetch permissions/group memberships for
 * @return  array   Multi-dimensional associative array which holds a 'membership' and permission name data
 */
function &serendipity_getPermissions($authorid) {
    global $serendipity;

        // Get group information
        $groups = serendipity_db_query("SELECT ag.groupid, g.name, gc.property, gc.value
                                          FROM {$serendipity['dbPrefix']}authorgroups AS ag
                               LEFT OUTER JOIN {$serendipity['dbPrefix']}groups AS g
                                            ON ag.groupid = g.id
                               LEFT OUTER JOIN {$serendipity['dbPrefix']}groupconfig AS gc
                                            ON gc.id = g.id
                                         WHERE ag.authorid = " . (int)$authorid);
        $perm = array('membership' => array());
        if (is_array($groups)) {
            foreach($groups AS $group) {
                $perm['membership'][$group['groupid']]       = $group['groupid'];
                $perm[$group['groupid']][$group['property']] = $group['value'];
            }
        }
        return $perm;
}

/**
 * Returns the list of available internal Serendipity permission field names
 *
 * This function also mapps which function was available to which userleves in older
 * Serendipity versions. Thus if an author does not have a certain privilege he should
 * have because of his userlevel, this can be reverse-mapped.
 *
 * @access public
 * @return  array   Multi-dimensional associative array which the list of all permission items plus their userlevel associations
 */
function serendipity_getPermissionNames() {
    return array(
        'personalConfiguration'
            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF, USERLEVEL_EDITOR),
        'personalConfigurationUserlevel'
            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF),
        'personalConfigurationNoCreate'
            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF),
        'personalConfigurationRightPublish'
            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF),
        'siteConfiguration'
            => array(USERLEVEL_ADMIN),
        'blogConfiguration'
            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF),

        'adminEntries'
            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF, USERLEVEL_EDITOR),
        'adminEntriesMaintainOthers'
            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF),

        'adminImport'
            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF),

        'adminCategories'
            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF, USERLEVEL_EDITOR),
        'adminCategoriesMaintainOthers'
            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF),
        'adminCategoriesDelete'
            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF),

        'adminUsers'
            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF),
        'adminUsersDelete'
            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF),
        'adminUsersEditUserlevel'
            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF),
        'adminUsersMaintainSame'
            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF),
        'adminUsersMaintainOthers'
            => array(USERLEVEL_ADMIN),
        'adminUsersCreateNew'
            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF),
        'adminUsersGroups'
            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF),

        'adminPlugins'
            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF),
        'adminPluginsMaintainOthers'
            => array(USERLEVEL_ADMIN),

        'adminImages'
            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF, USERLEVEL_EDITOR),
        'adminImagesDirectories'
            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF),
        'adminImagesAdd'
            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF, USERLEVEL_EDITOR),
        'adminImagesDelete'
            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF, USERLEVEL_EDITOR),
        'adminImagesMaintainOthers'
            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF),
        'adminImagesViewOthers'
            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF, USERLEVEL_EDITOR),
        'adminImagesView'
            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF, USERLEVEL_EDITOR),
        'adminImagesSync'
            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF),

        'adminComments'
            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF),

        'adminTemplates'
            => array(USERLEVEL_ADMIN, USERLEVEL_CHIEF),

        'hiddenGroup'
            => array(-1)
    );
}

/**
 * Checks if a permission is granted to a specific author
 *
 * This function caches all permission chacks in static function variables to not
 * fetch all permissions time and again.
 * The permission checks are performed against the values of each group. If a privilege
 * is set in one of the groups the author is a user of, the function returns true.
 * If a privilege is not set, the userlevel of an author is checked to act for backwards-compatibility.
 *
 * @access public
 * @see serendipity_getPermissionNames()
 * @param   string      The name of the permission to check
 * @param   int         The authorid for which the permission check should be performed
 * @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.
 * @return  mixed       Either returns true if a permission check is performed, or return an array of group memberships. Depends on the $returnMyGroups variable.
 */
function serendipity_checkPermission($permName, $authorid = null, $returnMyGroups = false) {
    global $serendipity;

    // Define old serendipity permissions
    static $permissions = null;
    static $group = null;

    if (IS_installed !== true) {
        return true;
    }

    if ($permissions === null) {
        $permissions = serendipity_getPermissionNames();
    }

    if ($group === null) {
        $group = array();
    }

    if ($authorid === null) {
        $authorid = $serendipity['authorid'] ?? null;
    }

    if (!isset($group[$authorid])) {
        $group[$authorid] = serendipity_getPermissions($authorid);
    }

    if ($returnMyGroups) {
        if ($returnMyGroups === 'all') {
            return $group[$authorid];
        } else {
            return $group[$authorid]['membership'];
        }
    }

    if ($authorid == ($serendipity['authorid'] ?? null) && (isset($serendipity['no_create']) && $serendipity['no_create'])) {
        // This no_create user privilege overrides other permissions.
        return false;
    }

    $return = true;

    foreach($group[$authorid] AS $item) {
        if (!isset($item[$permName])) {
            continue;
        }

        if ($item[$permName] === 'true') {
            return true;
        } else {
            $return = false;
        }
    }

    // If the function did not yet return it means there's a check for a permission which is not defined anywhere.
    // Let's use a backwards compatible way.
    if ($return && isset($permissions[$permName]) && in_array($serendipity['serendipityUserlevel'] ?? null, $permissions[$permName])) {
        return true;
    }

    return false;
}

/**
 * Update author group membership(s)
 *
 * @access public
 * @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.
 * @param   int         The ID of the author to update
 * @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.
 * @return
 */
function serendipity_updateGroups($groups, $authorid, $apply_acl = true) {
    global $serendipity;

    if ($apply_acl && !serendipity_checkPermission('adminUsersMaintainOthers')) {
        return false;
    }

    serendipity_db_query("DELETE FROM {$serendipity['dbPrefix']}authorgroups WHERE authorid = " . (int)$authorid);

    foreach($groups AS $group) {
        serendipity_db_query("INSERT INTO {$serendipity['dbPrefix']}authorgroups (authorid, groupid) VALUES (" . (int)$authorid . ", " . (int)$group . ")");
    }
    return true;
}

/**
 * Returns all authorgroups that are available
 *
 * If a groupname is prefixed with "USERLEVEL_" then the constant of that name is used to
 * return the name of the group. This allows inserting the special groups Chief Editor, Editor
 * and admin and still being able to use multilingual names for these groups.
 *
 * @access public
 * @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.
 * @return  array   An associative array of group names.
 */
function &serendipity_getAllGroups($apply_ACL_user = false) {
    global $serendipity;

    if ($apply_ACL_user) {
        $groups = serendipity_db_query("SELECT g.id   AS confkey,
                                                g.name AS confvalue,
                                                g.id   AS id,
                                                g.name AS name
                                           FROM {$serendipity['dbPrefix']}authorgroups AS ag
                                LEFT OUTER JOIN {$serendipity['dbPrefix']}groups AS g
                                             ON g.id = ag.groupid
                                          WHERE ag.authorid = " . (int)$apply_ACL_user . "
                                       ORDER BY g.name", false, 'assoc');
    } else {
        $groups = serendipity_db_query("SELECT g.id   AS confkey,
                                                g.name AS confvalue,
                                                g.id   AS id,
                                                g.name AS name
                                          FROM {$serendipity['dbPrefix']}groups AS g
                                      ORDER BY  g.name", false, 'assoc');
    }
    if (is_array($groups)) {
        foreach ($groups as $k => $v) {
            if ('USERLEVEL_' == substr($v['confvalue'], 0, 10)) {
                $groups[$k]['confvalue'] = $groups[$k]['name'] = constant($v['confvalue']);
            }
        }
    }
    return $groups;
}

/**
 * Fetch the permissions of a certain group
 *
 * @access public
 * @param   int     The ID of the group that the permissions are fetched for
 * @return  array   The associative array of permissions of a group.
 */
function &serendipity_fetchGroup($groupid) {
    global $serendipity;

    $conf = array();
    $groups = serendipity_db_query("SELECT g.id        AS confkey,
                                            g.name      AS confvalue,
                                            g.id        AS id,
                                            g.name      AS name,

                                            gc.property AS property,
                                            gc.value    AS value
                                      FROM {$serendipity['dbPrefix']}groups AS g
                           LEFT OUTER JOIN {$serendipity['dbPrefix']}groupconfig AS gc
                                        ON g.id = gc.id
                                     WHERE g.id = " . (int)$groupid, false, 'assoc');

    if (is_array($groups)) {
        foreach($groups AS $group) {
            $conf[$group['property']] = $group['value'];
        }
    }

    // The following are unique
    $conf['name']      = $groups[0]['name'];
    $conf['id']        = $groups[0]['id'];
    $conf['confkey']   = $groups[0]['confkey'];
    $conf['confvalue'] = $groups[0]['confvalue'];

    return $conf;
}

/**
 * Gets all groups a user is a member of
 *
 * @access public
 * @param   int         The authorid to fetch groups for
 * @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).
 * @return
 */
function &serendipity_getGroups($authorid, $sequence = false) {
    global $serendipity;

    $_groups = serendipity_db_query("SELECT g.id  AS confkey,
                                            g.name AS confvalue,
                                            g.id   AS id,
                                            g.name AS name
                                      FROM {$serendipity['dbPrefix']}authorgroups AS ag
                           LEFT OUTER JOIN {$serendipity['dbPrefix']}groups AS g
                                        ON g.id = ag.groupid
                                     WHERE ag.authorid = " . (int)$authorid, false, 'assoc');
    if (!is_array($_groups)) {
        $groups = array();
    } else {
        $groups = $_groups;
    }

    if ($sequence) {
        $rgroups  = array();
        foreach($groups AS $grouprow) {
            $rgroups[] = $grouprow['confkey'];
        }
    } else {
        $rgroups = $groups;
    }

    return $rgroups;
}

/**
 * Gets all author IDs of a specific group
 *
 * @access public
 * @param   int     The ID of the group to fetch the authors of
 * @return  array   The associative array of author IDs and names
 */
function &serendipity_getGroupUsers($groupid) {
    global $serendipity;

    $groups = serendipity_db_query("SELECT g.name     AS name,
                                            a.realname AS author,
                                            a.authorid AS id
                                      FROM {$serendipity['dbPrefix']}authorgroups AS ag
                           LEFT OUTER JOIN {$serendipity['dbPrefix']}groups AS g
                                        ON g.id = ag.groupid
                           LEFT OUTER JOIN {$serendipity['dbPrefix']}authors AS a
                                        ON ag.authorid = a.authorid
                                     WHERE ag.groupid = " . (int)$groupid, false, 'assoc');
    return $groups;
}

/**
 * Deletes a specific author group by ID
 *
 * @access public
 * @param   int     The group ID to delete
 * @return  boolean Return true if group could be deleted, false if unsufficient privileges.
 */
function serendipity_deleteGroup($groupid) {
    global $serendipity;

    if (!serendipity_checkPermission('adminUsersGroups')) {
        return false;
    }

    if (!serendipity_checkPermission('adminUsersMaintainOthers')) {
        // Only groups should be accessible where a user has access rights.
        $my_groups = serendipity_getGroups($serendipity['authorid'], true);
        if (!in_array($groupid, $my_groups)) {
            return false;
        }
    }

    serendipity_db_query("DELETE FROM {$serendipity['dbPrefix']}groups       WHERE id = " . (int)$groupid);
    serendipity_db_query("DELETE FROM {$serendipity['dbPrefix']}authorgroups WHERE groupid = " . (int)$groupid);

    return true;
}

/**
 * Creates a new author group
 *
 * @access public
 * @param   string      The name of the new group
 * @return  int         The id of the created group
 */
function serendipity_addGroup($name) {
    global $serendipity;

    serendipity_db_query("INSERT INTO {$serendipity['dbPrefix']}groups (name) VALUES ('" . serendipity_db_escape_string($name) . "')");
    $gid = serendipity_db_insert_id('groups', 'id');

    return $gid;
}

/**
 * Returns a list of all existing permission names.
 *
 * Additional plugins might insert specific properties into the groupconfig database to
 * handle their own privileges. This call returns an array of all available permission names
 * so that it can be intersected with the list of internal permission names (serendipity_getPermissionNames())
 * and the be distincted.
 *
 * @access public
 * @see serendipity_getPermissionNames()
 * @return  array   associative array of all available permission names
 */
function &serendipity_getDBPermissionNames() {
    global $serendipity;

    $config = serendipity_db_query("SELECT property FROM {$serendipity['dbPrefix']}groupconfig GROUP BY property ORDER BY property", false, 'assoc');

    return $config;
}

/**
 * Gets the list of all Permissions and merges the two arrays
 *
 * The first call will fetch all existing permission names of the database. Then it will
 * fetch the list of defined internal permission names, which has an array with extra information
 * (like userlevel). This array will be merged with those permission names only found in the
 * database. The returned array will then hold as much information about permission names as is
 * available.
 * TODO Might need further pushing and/or an event hook so that external plugins using the
 * permission system can inject specific information into the array
 *
 * @access public
 * @see serendipity_getPermissionNames()
 * @see serendipity_getDBPermissionNames()
 * @return  array   Returns the array with all information about all permission names
 */
function &serendipity_getAllPermissionNames() {
    global $serendipity;

    $DBperms =& serendipity_getDBPermissionNames();
    $perms   = serendipity_getPermissionNames();

    foreach($DBperms AS $perm) {
        if (!isset($perms[$perm['property']])) {
            $perms[$perm['property']] = array();
        }
    }

    return $perms;
}

/**
 * Checks if two users are members of the same group
 *
 * This function will retrieve all group memeberships of a
 * foreign user ($checkuser) and yourself ($myself). Then it
 * will check if there is any group membership that those
 * two users have in common.
 * It can be used for detecting if a different author should
 * be allowed to access your entries, because he's in the same
 * group, for example.
 *
 * @access public
 * @param   int     ID of the first author to check group memberships
 * @param   int     ID of the second author to check group memberships
 * @return  boolea  True if a membership intersects, false if not
 */
function serendipity_intersectGroup($checkuser = null, $myself = null) {
    global $serendipity;

    if ($myself === null) {
        $myself = $serendipity['authorid'];
    }

    $my_groups  = serendipity_getGroups($myself, true);
    $his_groups = serendipity_getGroups($checkuser, true);

    foreach($his_groups AS $his_group) {
        if (in_array($his_group, $my_groups)) {
            return true;
        }
    }

    return false;
}

/**
 * Updates the configuration of permissions of a specific group
 *
 * This function ensures that a group can only be updated from users that have permissions to do so.
 * @access public
 * @param   int     The ID of the group to update
 * @param   array   The associative array of permission names
 * @param   array   The associative array of new values for the permissions. Needs the same associative keys like the $perms array.
 * @param   bool    Indicates if an all new privilege should be inserted (true) or if an existing privilege is going to be checked
 * @param   array   The associative array of plugin permission names
 * @param   array   The associative array of plugin permission hooks
 * @return true
 */
function serendipity_updateGroupConfig($groupid, &$perms, &$values, $isNewPriv = false, $forbidden_plugins = null, $forbidden_hooks = null) {
    global $serendipity;

    if (!serendipity_checkPermission('adminUsersGroups')) {
        return false;
    }

    if (!serendipity_checkPermission('adminUsersMaintainOthers')) {
        // Only groups should be accessible where a user has access rights.
        $my_groups = serendipity_getGroups($serendipity['authorid'], true);
        if (!in_array($groupid, $my_groups)) {
            return false;
        }
    }

    $storage =& serendipity_fetchGroup($groupid);
    
    serendipity_db_query("DELETE FROM {$serendipity['dbPrefix']}groupconfig WHERE id = " . (int)$groupid);
    foreach ($perms AS $perm => $userlevels) {
        if (substr($perm, 0, 2) == 'f_') {
            continue;
        }

        if (isset($values[$perm]) && $values[$perm] == 'true') {
            $value = 'true';
        } elseif (isset($values[$perm]) && $values[$perm] === 'false') {
            $value = 'false';
        } elseif (isset($values[$perm])) {
            $value = $values[$perm];
        } else {
            $value = 'false';
        }

        if ($isNewPriv == false && !serendipity_checkPermission($perm) && $perm != 'hiddenGroup') {
            if (!isset($storage[$perm])) {
                $value = 'false';
            } else {
                $value = $storage[$perm];
            }
        }

        serendipity_db_query(
            sprintf("INSERT INTO {$serendipity['dbPrefix']}groupconfig (id, property, value) VALUES (%d, '%s', '%s')",
                (int)$groupid,
                serendipity_db_escape_string($perm),
                serendipity_db_escape_string($value)
            )
        );
    }

    if (is_array($forbidden_plugins)) {
        foreach($forbidden_plugins AS $plugid) {
            serendipity_db_query(
                sprintf("INSERT INTO {$serendipity['dbPrefix']}groupconfig (id, property, value) VALUES (%d, '%s', 'true')",
                    (int)$groupid,
                    serendipity_db_escape_string('f_' . urldecode($plugid))
                )
            );
        }
    }

    if (is_array($forbidden_hooks)) {
        foreach($forbidden_hooks AS $hook) {
            serendipity_db_query(
                sprintf("INSERT INTO {$serendipity['dbPrefix']}groupconfig (id, property, value) VALUES (%d, '%s', 'true')",
                    (int)$groupid,
                    serendipity_db_escape_string('f_' . urldecode($hook))
                )
            );
        }
    }

    serendipity_db_query("UPDATE {$serendipity['dbPrefix']}groups SET name = '" . serendipity_db_escape_string($values['name']) . "' WHERE id = " . (int)$groupid);

    if (is_array($values['members'] ?? null)) {
        serendipity_db_query("DELETE FROM {$serendipity['dbPrefix']}authorgroups WHERE groupid = " . (int)$groupid);
        foreach($values['members'] AS $member) {
            serendipity_db_query(
                sprintf("INSERT INTO {$serendipity['dbPrefix']}authorgroups (groupid, authorid) VALUES (%d, %d)",
                    (int)$groupid,
                    (int)$member
                )
            );
        }
    }

    return true;
}

/**
 * Adds a default internal group (Editor, Chief Editor, Admin)
 *
 * @access public
 * @param   string  The name of the group to insert
 * @param   int     The userlevel that represents this group (0|1|255 for Editor/Chief/Admin).
 * @return true
 */
function serendipity_addDefaultGroup($name, $level) {
    global $serendipity;

    static $perms = null;
    if ($perms === null) {
        $perms = serendipity_getPermissionNames();
    }

    serendipity_db_query("INSERT INTO {$serendipity['dbPrefix']}groups (name) VALUES ('" . serendipity_db_escape_string($name) . "')");
    $gid = (int)serendipity_db_insert_id('groups', 'id');
    serendipity_db_query("INSERT INTO {$serendipity['dbPrefix']}groupconfig (id, property, value) VALUES ($gid, 'userlevel', '" . (int)$level . "')");

    $authors = serendipity_db_query("SELECT * FROM {$serendipity['dbPrefix']}authors WHERE userlevel = " . (int)$level);

    if (is_array($authors)) {
        foreach($authors AS $author) {
            serendipity_db_query("INSERT INTO {$serendipity['dbPrefix']}authorgroups (authorid, groupid) VALUES ('{$author['authorid']}', '$gid')");
        }
    }

    foreach($perms AS $permName => $permArray) {
        if (in_array($level, $permArray)) {
            serendipity_db_query("INSERT INTO {$serendipity['dbPrefix']}groupconfig (id, property, value) VALUES ($gid, '" . serendipity_db_escape_string($permName) . "', 'true')");
        } else {
            serendipity_db_query("INSERT INTO {$serendipity['dbPrefix']}groupconfig (id, property, value) VALUES ($gid, '" . serendipity_db_escape_string($permName) . "', 'false')");
        }
    }

    return true;
}

/**
 * Allow access to a specific item (category or entry) for a specific usergroup
 *
 * ACL are Access Control Lists. They indicate which read/write permissions a
 * specific item has for specific usergroups.
 * An artifact in terms of Serendipity can be either a category or an entry, or
 * anything beyond that for future compatibility.
 * This function sets up the ACLs.
 *
 * @access public
 * @param   int     The ID of the artifact to set the access
 * @param   string  The type of an artifact (category|entry)
 * @param   string  The type of access to grant (read|write)
 * @param   array   The ID of the group to grant access to
 * @param   string  A variable option for an artifact
 * @return  boolean True if ACL was applied, false if not.
 */
function serendipity_ACLGrant($artifact_id, $artifact_type, $artifact_mode, $groups, $artifact_index = '') {
    global $serendipity;

    if (empty($groups) || !is_array($groups)) {
        return false;
    }

    // Delete all old existing relations.
    serendipity_db_query("DELETE FROM {$serendipity['dbPrefix']}access
                                WHERE artifact_id    = " . (int)$artifact_id . "
                                  AND artifact_type  = '" . serendipity_db_escape_string($artifact_type) . "'
                                  AND artifact_mode  = '" . serendipity_db_escape_string($artifact_mode) . "'
                                  AND artifact_index = '" . serendipity_db_escape_string($artifact_index) . "'");

    $data = array(
        'artifact_id'    => (int)$artifact_id,
        'artifact_type'  => $artifact_type,
        'artifact_mode'  => $artifact_mode,
        'artifact_index' => $artifact_index
    );

    if (count($data) < 1) {
        return true;
    }

    foreach($groups AS $group) {
        $data['groupid'] = $group;
        serendipity_db_insert('access', $data);
    }

    return true;
}

/**
 * Checks if a specific item (category or entry) can be accessed by a specific usergroup
 *
 * ACL are Access Control Lists. They indicate which read/write permissions a
 * specific item has for specific usergroups.
 * An artifact in terms of Serendipity can be either a category or an entry, or
 * anything beyond that for future compatibility.
 * This function retrieves the ACLs.
 *
 * @access public
 * @param   int     The ID of the artifact to set the access
 * @param   string  The type of an artifact (category|entry)
 * @param   string  The type of access to check for (read|write)
 * @param   string  A variable option for an artifact
 * @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.
 */
function serendipity_ACLGet($artifact_id, $artifact_type, $artifact_mode, $artifact_index = '') {
    global $serendipity;

    $sql = "SELECT groupid, artifact_index FROM {$serendipity['dbPrefix']}access
                    WHERE artifact_type  = '" . serendipity_db_escape_string($artifact_type) . "'
                      AND artifact_id    = '" . (int)$artifact_id . "'
                      AND artifact_mode  = '" . serendipity_db_escape_string($artifact_mode) . "'
                      AND artifact_index = '" . serendipity_db_escape_string($artifact_index) . "'";
    $rows = serendipity_db_query($sql, false, 'assoc');

    if (!is_array($rows)) {
        return false;
    }

    $acl = array();
    foreach($rows AS $row) {
        $acl[$row['groupid']] = $row['artifact_index'];
    }

    return $acl;
}

/**
 * Checks if a specific item (category or entry) can be accessed by a specific Author
 *
 * ACL are Access Control Lists. They indicate which read/write permissions a
 * specific item has for specific usergroups.
 * An artifact in terms of Serendipity can be either a category or an entry, or
 * anything beyond that for future compatibility.
 * This function retrieves the ACLs for a specific user.
 *
 * @access public
 * @param   int     The ID of the author to check against.
 * @param   int     The ID of the artifact to set the access
 * @param   string  The type of an artifact ('category', more to come)
 * @param   string  The type of access to check for (read|write)
 * @return  boolean Returns true, if the author has access to this artifact. False if not.
 */
function serendipity_ACLCheck($authorid, $artifact_id, $artifact_type, $artifact_mode) {
    global $serendipity;

    $artifact_sql = array();

    // 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.
    switch($artifact_type) {
        default:
        case 'category':
            $artifact_sql['unique']= "atf.categoryid";
            $artifact_sql['cond']  = "atf.categoryid = " . (int)$artifact_id;
            $artifact_sql['where'] = "     ag.groupid = a.groupid
                                        OR a.groupid  = 0
                                        OR (a.artifact_type IS NULL AND (atf.authorid = " . (int)$authorid . " OR atf.authorid = 0 OR atf.authorid IS NULL))";
            $artifact_sql['table'] = 'category';
    }

    $sql = "SELECT {$artifact_sql['unique']} AS result
              FROM {$serendipity['dbPrefix']}{$artifact_sql['table']} AS atf
   LEFT OUTER JOIN {$serendipity['dbPrefix']}authorgroups AS ag
                ON ag.authorid = ". (int)$authorid . "
   LEFT OUTER JOIN {$serendipity['dbPrefix']}access AS a
                ON (    a.artifact_type = '" . serendipity_db_escape_string($artifact_type) . "'
                    AND a.artifact_id   = " . (int)$artifact_id . "
                    AND a.artifact_mode = '" . serendipity_db_escape_string($artifact_mode) . "'
                   )

             WHERE {$artifact_sql['cond']}
               AND ( {$artifact_sql['where']} )
          GROUP BY result";

    $res = serendipity_db_query($sql, true, 'assoc');
    if (is_array($res) && !empty($res['result'])) {
        return true;
    }

    return false;
}

/**
 * Prepares a SQL statement to be used in queries that should be ACL restricted.
 *
 * ACL are Access Control Lists. They indicate which read/write permissions a
 * specific item has for specific usergroups.
 * An artifact in terms of Serendipity can be either a category or an entry, or
 * anything beyond that for future compatibility.
 * This function evaluates and applies the SQL statements required.
 * It is currently only written for retrieving Category ACLs.
 * All of the SQL code that will be used in serendipity_fetchEntries will be stored
 * within the referenced $cond array.
 *
 * @access private
 * @param   array       Associative array that holds the SQL part array to be used in other functions like serendipity_fetchEntries()
 * @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.
 * @param   string      The ACL type ('category', 'directory')
 * @param   string      ACL mode
 * @return  true        True if ACLs were applied, false if not.
 */
function serendipity_ACL_SQL(&$cond, $append_category = false, $type = 'category', $mode = 'read') {
    global $serendipity;

    // A global configuration item controls whether the blog should apply ACLs or not!
    if (!isset($serendipity['enableACL']) || $serendipity['enableACL'] == true) {

        // If the user is logged in, we retrieve his authorid for the upcoming checks
        if ($_SESSION['serendipityAuthedUser'] === true) {
            $read_id = (int)$serendipity['authorid'];
            $read_id_sql = 'acl_a.groupid OR acl_acc.groupid = 0';
        } else {
            // "0" as category property counts as "anonymous viewers"
            $read_id     = 0;
            $read_id_sql = 0;
        }

        if (! array_key_exists('joins', $cond) ) {
            $cond['joins'] = '';
        }

        if ($append_category) {
            if ($append_category !== 'limited') {
                $cond['joins'] .= " LEFT JOIN {$serendipity['dbPrefix']}entrycat ec
                                           ON e.id = ec.entryid";
            }

            $cond['joins'] .= " LEFT JOIN {$serendipity['dbPrefix']}category c
                                       ON ec.categoryid = c.categoryid";
        }

        switch($type) {
            case 'directory':
                $sql_artifact_column = 'i.path IS NULL OR
                                        acl_acc.groupid IS NULL';
                $sql_artifact = 'AND acl_acc.artifact_index = i.path';
                break;

            case 'category':
                $sql_artifact_column = 'c.categoryid IS NULL';
                $sql_artifact = 'AND acl_acc.artifact_id   = c.categoryid';
                break;
        }

        $cond['joins'] .= " LEFT JOIN {$serendipity['dbPrefix']}authorgroups AS acl_a
                                   ON acl_a.authorid = " . $read_id . "
                            LEFT JOIN {$serendipity['dbPrefix']}access AS acl_acc
                                   ON (    acl_acc.artifact_mode = '" . $mode . "'
                                       AND acl_acc.artifact_type = '" . $type . "'
                                       " . $sql_artifact . "
                                      )";

        if (empty($cond['and'])) {
            $cond['and'] = ' WHERE ';
        } else {
            $cond['and'] .= ' AND ';
        }

        // When in Admin-Mode, apply readership permissions.
        $cond['and'] .= "    (
                                 " . $sql_artifact_column . "
                                 OR ( acl_acc.groupid = " . $read_id_sql . ")
                                 OR ( acl_acc.artifact_id IS NULL
                                      " . (isset($serendipity['GET']['adminModule']) &&
                                           $serendipity['GET']['adminModule'] == 'entries' &&
                                           !serendipity_checkPermission('adminEntriesMaintainOthers')
                                        ? "AND (c.authorid IS NULL OR c.authorid = 0 OR c.authorid = " . $read_id . ")"
                                        : "") . "
                                    )
                               )";
        return true;
    }

    return false;
}

/**
 * Check for Cross-Site-Request-Forgery via the Sec-Fetch-Site header, with a fallback to ORIGIN.
 *
 * http://de.wikipedia.org/wiki/XSRF
 * This function checks the HTTP Sec-Fetch-Site header, and if it is part of the current Admin panel.
 *
 * @access public
 * @return  Returns true if XSRF was detected, false if not. The script should abort if TRUE is returned.
 */
function serendipity_checkXSRF() {
    global $serendipity;

    
    if (array_key_exists('HTTP_SEC_FETCH_SITE', $_SERVER)) {
        // The Sec-Fetch-Site header was set (=a modern browser is used), but reported context was
        // not 'same-origin'
        if ($_SERVER['HTTP_SEC_FETCH_SITE'] != 'same-origin' && $_SERVER['HTTP_SEC_FETCH_SITE'] != 'none') {
            echo serendipity_reportXSRF(1, true, true);
            return true;
        }
    } else {
        // The Sec-Fetch-Site header is missing, so maybe an old browser is used

        if ($_SERVER['REQUEST_METHOD'] != 'GET') {
            // We ignore GET requests as those are not supposed to be state changing, and all
            // of this is very unlikely to happen, given the broad adoption of Sec_Fetch_Site. Most
            // users GET requests would also be protected by the Sec_Fetch_Site check.
            
            // But we got not a GET. Now we need to protect this call. so we will try to use the
            // ORIGIN header for that. It predates SEC_FETCH_SITE and is common since 2019.
            $origin = filter_var($_SERVER['HTTP_ORIGIN'], FILTER_VALIDATE_URL); // HTTP_ORIGIN can also be null
            if ((! $origin) || (parse_url($origin, PHP_URL_HOST) != parse_url($serendipity['baseURL'], PHP_URL_HOST))) {
                // We got no origin or the origin does not match our configured baseURL, so we block:
                echo serendipity_reportXSRF(2, true, true);
                return true;
            }
        }
    }
    
    return false;
}

/**
 * Report a XSRF attempt to the Serendipity Interface
 *
 * http://de.wikipedia.org/wiki/XSRF
 *
 * LONG
 *
 * @access public
 * @see serendipity_checkXSRF()
 * @param   string      The type of XSRF check that got hit. Used for CSS formatting.
 * @param   boolean     unused
 * @param   boolean     unused
 * @return  string      Returns the HTML error report
 */
function serendipity_reportXSRF($type = 0, $reset = true, $use_config = false) {
    global $serendipity;

    $string = '<div class="msg_error XSRF_' . $type . '"><span class="icon-attention" aria-hidden="true"></span> ' . ERROR_XSRF . '</div>';
    return $string;
}

/**
 * Prevent XSRF attacks, calls serendipity_checkXSRF for HTTP header check. Used a token check
 * before and is kept for backwards compatibility, as it is used in plugins.
 *
 * @access public
 * @see serendipity_setFormToken()
 * @return  boolean     Returns true, if XSRF attempt was found and the token was missing
 */
function serendipity_checkFormToken($output = true) {
    // We have to invert the returned bool here, as serendipity_checkFormToken returned false when
    // the token mismatched, but serendipity_checkXSRF uses true to signal an XSRF attempt
    return ! serendipity_checkXSRF();
}

/**
 * Does nothing. Prevented XSRF attacks by setting a form token within HTTP Forms. This is now done
 * via a browser header check in serendipity_checkXSRF
 * 
 * @access public
 * @param   string      unused
 * @return  string      Returns an empty string
 */
function serendipity_setFormToken($type = 'form') {
    return '';
}

/**
 * Load available/configured options for a specific theme (through config.inc.php of a template directory)
 * into an array.
 *
 * @param   array   Referenced variable coming from the config.inc.php file, where the config values will be stored in
 * @param   boolean Use true boolean mode in array $template_config in the config.inc.php file
 * @return  array   Final return array with default values
 */
function &serendipity_loadThemeOptions(&$template_config, $okey = '', $bc_bool = false) {
    global $serendipity;

    if (empty($okey)) {
        $okey = $serendipity['template'];
    }

    $_template_vars = serendipity_db_query("SELECT name, value FROM {$serendipity['dbPrefix']}options
                                             WHERE okey = 't_" . serendipity_db_escape_string($okey) . "'
                                                OR okey = 't_global'", false, 'assoc', false, 'name', 'value');
    if (!is_array($_template_vars)) {
        $template_vars = array();
    } else {
        $template_vars =& $_template_vars;
    }

    foreach($template_config AS $key => $item) {
        if (isset($item['var']) && !isset($template_vars[$item['var']])) {
            $template_vars[$item['var']] = $item['default'] ?? null;
        }
    }
    if($bc_bool) {
        foreach($template_vars AS $k => $i) {
            if ($i == 'true' || $i == 'false') {
                $template_vars[$k] = serendipity_db_bool($i);
            }
        }
        //reset smarty compiled template ?
    }
    return $template_vars;
}

/**
 * Load global available/configured options for a specific theme
 * into an array.
 *
 * @param   array   Referenced variable coming from the config.inc.php file, where the config values will be stored in
 * @param   array   Current template configuration
 * @return  array   Final return array with default values
 */
function serendipity_loadGlobalThemeOptions(&$template_config, &$template_loaded_config, $supported = array()) {
    global $serendipity;

    if ($supported['navigation']) {
        $navlinks = array();

        $conf_amount = array(
                'var'           => 'amount',
                'name'          => NAVLINK_AMOUNT,
                'type'          => 'string',
                'default'       => '5',
                'scope'         => 'global'
        );

        // This always needs to be present, if not it could happen that the template options do have an older version of this variable
        $template_config[] = $conf_amount;

        if (!isset($template_loaded_config['amount']) || empty($template_loaded_config['amount'])) {
            $template_loaded_config['amount'] = $conf_amount['default'];
        }

        // Check if we are currently inside the admin interface.
        if ($serendipity['POST']['adminModule'] ?? '' == 'templates' && $serendipity['POST']['adminAction'] == 'configure' && !empty($serendipity['POST']['template']['amount'])) {
            $template_loaded_config['amount'] = (int)($serendipity['POST']['template']['amount'] ?? 0);
        }

        for ($i = 0; $i < $template_loaded_config['amount']; $i++) {
            $navlinks[] = array(
                'title' => $template_loaded_config['navlink' . $i . 'text'] ?? null,
                'href'  => $template_loaded_config['navlink' . $i . 'url'] ?? null
            );

            $template_config[] = array(
                'var'           => 'navlink' . $i . 'text',
                'name'          => NAV_LINK_TEXT . ' #' . ($i+1),
                'type'          => 'string',
                'default'       => 'Link #' . ($i+1),
                'scope'         => 'global'
            );
            $template_config[] = array(
                'var'           => 'navlink' . $i . 'url',
                'name'          => NAV_LINK_URL . ' #' . ($i+1),
                'type'          => 'string',
                'default'       => '#',
                'scope'         => 'global'
            );
        }

        $serendipity['smarty']->assignByRef('navlinks', $navlinks);
    }

    // Forward thinking. ;-)
    serendipity_plugin_api::hook_event('backend_templates_globalthemeoptions', $template_config, $supported);
}



/**
 * Check if a member of a group has permissions to execute a plugin
 *
 * @param string    Pluginname
 * @param int       ID of the group of which the members should be checked
 * @return boolean
 */
function serendipity_hasPluginPermissions($plugin, $groupid = null) {
    static $forbidden = null;
    global $serendipity;

    if (empty($serendipity['authorid'])) {
        return true;
    }

    if ($forbidden === null || ($groupid !== null && !isset($forbidden[$groupid]))) {
        $forbidden = array();

        if ($groupid === null) {
            $groups = serendipity_checkPermission(null, null, 'all');
        } else {
            $groups = array($groupid => serendipity_fetchGroup($groupid));
        }

        foreach($groups AS $idx => $group) {
            if ($idx == 'membership') {
                continue;
            }
            foreach($group AS $key => $val) {
                if (substr($key, 0, 2) == 'f_') {
                    $forbidden[$groupid][$key] = true;
                }
            }
        }
    }

    if (isset($forbidden[$groupid]['f_' . $plugin])) {
        return false;
    } else {
        return true;
    }
}

/**
 * Return the bcrypt hash of a value
 *
 * @param string    The string to hash
 * @return string   The hashed string
 */
function serendipity_hash($string) {
    return password_hash($string, PASSWORD_BCRYPT);
}

/**
 * Return the SHA1 (with pre-hash) of a value
 *
 * @param string    The string to hash
 * @return string   The hashed string
 */
function serendipity_sha1_hash($string) {
    global $serendipity;

    if (empty($serendipity['hashkey'])) {
        serendipity_set_config_var('hashkey', time(), 0);
    }

    return sha1($serendipity['hashkey'] . $string);
}

/**
 * Backwards-compatibility to recognize old-style md5 passwords to allow migration
 *
 * @param string The string to hash
 * @param string  Either SHA1 or MD5 hash, depending on value
 */
function serendipity_passwordhash($cleartext_password) {
    global $serendipity;

    if ($_SESSION['serendipityHashType'] > 0) {
        return serendipity_hash($cleartext_password);
    } else {
        return md5($cleartext_password);
    }
}

/* vim: set sts=4 ts=4 expandtab : */
