0, 'path' => '/', 'domain' => $_SERVER['HTTP_HOST'] ?? '', 'secure' => $isSecure, 'httponly' => true, 'samesite' => 'Lax', ]); // Startet die PHP-Session mit den oben gesetzten Parametern. session_start(); $kcGalleryUserdataRoot = realpath(__DIR__ . '/../../../userdata'); $kcLockSessionKey = 'kc_fg_lock_root'; $kcLockedRootPath = ''; if ($kcGalleryUserdataRoot !== false) { if (isset($_REQUEST['kc_lock_path'])) { $requestedLockPath = trim((string) $_REQUEST['kc_lock_path'], '/'); if ($requestedLockPath !== '' && preg_match('#^knowledgecenter_update/posts/[a-f0-9]{24}$#', $requestedLockPath)) { $candidateFullPath = $kcGalleryUserdataRoot . '/' . $requestedLockPath; $candidatePath = realpath($candidateFullPath); if ($candidatePath === false) { $postsBasePath = $kcGalleryUserdataRoot . '/knowledgecenter_update/posts'; if (!is_dir($postsBasePath)) { @mkdir($postsBasePath, 0775, true); } if (!is_dir($candidateFullPath)) { @mkdir($candidateFullPath, 0775, true); } $candidatePath = realpath($candidateFullPath); } if ($candidatePath !== false && strpos($candidatePath, $kcGalleryUserdataRoot . DIRECTORY_SEPARATOR) === 0 && is_dir($candidatePath)) { $_SESSION[$kcLockSessionKey] = $candidatePath; } } elseif ($requestedLockPath === '') { unset($_SESSION[$kcLockSessionKey]); } } if (!empty($_SESSION[$kcLockSessionKey]) && is_string($_SESSION[$kcLockSessionKey])) { $sessionLockPath = realpath($_SESSION[$kcLockSessionKey]); if ($sessionLockPath !== false && strpos($sessionLockPath, $kcGalleryUserdataRoot . DIRECTORY_SEPARATOR) === 0 && is_dir($sessionLockPath)) { $kcLockedRootPath = $sessionLockPath; } else { unset($_SESSION[$kcLockSessionKey]); } } } if ($kcLockedRootPath !== '' && !empty($_SERVER['QUERY_STRING'])) { $queryParts = explode('&', $_SERVER['QUERY_STRING']); if (!empty($queryParts) && strpos($queryParts[0], '=') === false) { array_shift($queryParts); $_SERVER['QUERY_STRING'] = implode('&', $queryParts); } } $GLOBALS['kc_fg_locked_root'] = $kcLockedRootPath; // Prüft, ob ein Benutzer eingeloggt ist (login_id existiert und > 0). // Wenn nicht, wird sofort zur Startseite weitergeleitet. if (empty($_SESSION['login_id']) || $_SESSION['login_id'] == 0) { header('Location: /'); exit; } /* Files Gallery 0.12.4 www.files.gallery | www.files.gallery/docs/ | www.files.gallery/docs/license/ --- This PHP file is only 10% of the application, used only to connect with the file system. 90% of the codebase, including app logic, interface, design and layout is managed by the app Javascript and CSS files. --- class Config / load config with static methods to access config options class Login / check and manage logins class U / static utility functions class Path / static functions to convert and validate file paths class Json / JSON response functions class X3 / helper functions when running Files Gallery alongside X3 www.photo.gallery class Tests / outputs PHP, server and config diagnostics by url ?action=tests class FileResponse / outputs file, video preview image, resized image and proxies any file by PHP class ResizeImage / serves a resized image class Dirs / outputs menu json from dir structure class Dir / loads data array for a single dir class File / returns data array for a single file class Iptc / extract IPTC image data from images class Exif / extract Exif image data from images class Filemanager / functions that handle file operations on server class Zipper / create and extract zip files class Request / extract parameters for all actions class Document / creates the main Files Gallery document response */ // class Config / constructor and static methods to access config options class Config { // config defaults / https://www.files.gallery/docs/config/ // Only edit directly here if it is a temporary installation. Settings here will be lost when updating! // Instead, add options into external config file in your storage_path _files/config/config.php (generated on first run) private static $default = [ 'root' => '', 'root_url_path' => null, 'start_path' => false, 'username' => '', 'password' => '', 'load_images' => true, 'load_files_proxy_php' => false, 'load_images_max_filesize' => 1000000, 'image_resize_enabled' => true, 'image_resize_cache' => true, 'image_resize_dimensions' => 320, 'image_resize_dimensions_retina' => 480, 'image_resize_dimensions_allowed' => '', 'image_resize_types' => 'jpeg, png, gif, webp, bmp, avif', 'image_resize_quality' => 85, 'image_resize_function' => 'imagecopyresampled', 'image_resize_sharpen' => true, 'image_resize_memory_limit' => 256, 'image_resize_max_pixels' => 60000000, 'image_resize_min_ratio' => 1.5, 'image_resize_cache_direct' => false, 'folder_preview_image' => true, 'folder_preview_default' => '_filespreview.jpg', 'menu_enabled' => true, 'menu_max_depth' => 5, 'menu_sort' => 'name_asc', 'menu_cache_validate' => true, 'menu_load_all' => false, 'menu_recursive_symlinks' => true, 'layout' => 'rows', 'cache' => true, 'cache_key' => 0, 'storage_path' => '_files', 'files_include' => '', 'files_exclude' => '', 'dirs_include' => '', 'dirs_exclude' => '', 'allow_symlinks' => true, 'get_mime_type' => false, 'license_key' => '', 'download_dir' => 'browser', 'download_dir_cache' => 'dir', 'assets' => '', 'allow_all' => false, 'allow_upload' => false, 'allow_delete' => false, 'allow_rename' => false, 'allow_new_folder' => false, 'allow_new_file' => false, 'allow_duplicate' => false, 'allow_text_edit' => false, 'allow_zip' => false, 'allow_unzip' => false, 'allow_move' => false, 'allow_copy' => false, 'allow_download' => true, 'allow_mass_download' => false, 'allow_mass_copy_links' => false, 'allow_settings' => false, 'allow_check_updates' => false, 'allow_tests' => true, 'allow_tasks' => false, 'demo_mode' => false, 'upload_allowed_file_types' => '', 'upload_max_filesize' => 0, 'upload_exists' => 'increment', 'video_thumbs' => true, 'video_ffmpeg_path' => 'ffmpeg', 'pdf_thumbs' => true, 'imagemagick_path' => 'magick', 'use_google_docs_viewer' => false, 'lang_default' => 'en', 'lang_auto' => true, 'index_cache' => false, ]; // global application variables created on new Config() public static $version = '0.12.4'; // Files Gallery version public static $config = []; // config array merged from _filesconfig.php, config.php and default config public static $localconfigpath = '_filesconfig.php'; // optional config file in current dir, useful when overriding shared configs public static $localconfig = []; // config array from localconfigpath public static $storagepath; // absolute storage path for cache, config, plugins and more, normally _files dir public static $storageconfigpath; // absolute path to storage config, normally _files/config/config.php public static $storageconfig = []; // config array from storage path, normally _files/config/config.php public static $cachepath; // absolute cache path shortcut public static $__dir__; // absolute __DIR__ path with normalized OS path public static $__file__; // absolute __FILE__ path with normalized OS path public static $root; // absolute root path interpolated from config root option, normally current dir public static $document_root; // absolute server document root with normalized OS path public static $created = []; // checks what dirs and files get created by config on ?action=tests // config construct created static app vars and merge configs public function __construct() { // get absolute __DIR__ and __FILE__ paths with normalized OS paths self::$__dir__ = Path::realpath(__DIR__); self::$__file__ = Path::realpath(__FILE__); // load local config _filesconfig.php if exists self::$localconfig = $this->load(self::$localconfigpath); // create initial config array from default and localconfig self::$config = array_replace(self::$default, self::$localconfig); $this->apply_locked_root_config(); // set absolute storagepath, create storage dirs if required, and load, create or update storage config.php $this->storage(); $this->apply_locked_root_config(); // install.php - allow edit settings and create users from interface temporarily when file is named "install.php" // useful when installing Files Gallery, allows editing settings and creating users without having to modify config.php manually // remember to rename the file back to index.php once you have edited settings and/or created users. if(U::basename(__FILE__) === 'install.php') self::$config['allow_settings'] = true; // at this point we must check if login is required or user is already logged in, and then merge user config new Login(); $this->apply_locked_root_config(); // shortcut option `allow_all` allows all file actions (except settings, check_updates, tests, tasks) if(self::get('allow_all')) foreach (['upload', 'delete', 'rename', 'new_folder', 'new_file', 'duplicate', 'text_edit', 'zip', 'unzip', 'move', 'copy', 'download', 'mass_download', 'mass_copy_links'] as $k) self::$config['allow_'.$k] = true; // assign public real root path self::$root = Path::realpath(self::get('root')); // error if root path does not exist if(!self::$root) U::error('root dir ' . self::get('root') . ' does not exist'); // storagepath can't be the same as root dir, because storage_path is excluded if(self::$storagepath === self::$root) U::error('storage_path can\'t be the same as root'); // get server document root with normalized OS path self::$document_root = Path::realpath($_SERVER['DOCUMENT_ROOT']); } private function apply_locked_root_config() { if (!empty($GLOBALS['kc_fg_locked_root']) && is_string($GLOBALS['kc_fg_locked_root'])) { self::$config['root'] = $GLOBALS['kc_fg_locked_root']; self::$config['start_path'] = false; self::$config['menu_enabled'] = false; } } // public shortcut function to get config option Config::get('option') public static function get($option){ return self::$config[$option]; } // load a config file and trim values / returns empty array if file doesn't exist private function load($path) { if(empty($path) || !file_exists($path)) return []; $config = include $path; if(empty($config) || !is_array($config)) return []; return array_map(function($v){ return is_string($v) ? trim($v) : $v; }, $config); } // set storagepath from config, create dir if necessary private function storage(){ // ignore storagepath and disable cache settings if storage_path is specifically set to FALSE if(self::get('storage_path') === false) { foreach (['cache', 'image_resize_cache', 'folder_preview_image'] as $key) self::$config[$key] = false; return; } // shortcut to config storage_path $path = rtrim(self::get('storage_path'), '\/'); // invalid config storage_path can't be empty or non-string if(!$path || !is_string($path)) U::error('Invalid storage_path parameter'); // get request ?action if any, to determine if we attempt to make dirs and files on config construct $action = U::get('action'); // if ?action=tests, check what dirs and files will get created, for tests output if($action === 'tests') { foreach (['', '/config', '/config/config.php', '/cache/images', '/cache/folders', '/cache/menu'] as $key) { if(!file_exists($path . $key)) self::$created[] = $path . $key; } } // only make dirs and config if main document (no ?action, except action tests) $make = !$action || $action === 'tests'; // make storage path dir if it doesn't exist or return error if($make) U::mkdir($path); // store absolute storagepath self::$storagepath = Path::realpath($path); // error in case storagepath still doesn't seem to exist from realpath() if(!self::$storagepath) U::error('storage_path does not exist and can\'t be created'); // absolute cache path shortcut self::$cachepath = self::$storagepath . '/cache'; // assign storage config path (normally */_files/config/config.php), from where we load config and save options self::$storageconfigpath = self::$storagepath . '/config/config.php'; // load storage config (normally _files/config/config.php) or return empty array self::$storageconfig = $this->load(self::$storageconfigpath); // if storage config is not empty, update config by merging default, storageconfig and localconfig if(!empty(self::$storageconfig)) self::$config = array_replace(self::$default, self::$storageconfig, self::$localconfig); // only make storage dirs and config.php if main document or ?action=tests if(!$make) return; // create required storage dirs if they don't exist / error on fail foreach (['config', 'cache/images', 'cache/folders', 'cache/menu'] as $dir) U::mkdir(self::$storagepath . '/' . $dir); // create or update config file if older than index.php if(!file_exists(self::$storageconfigpath) || filemtime(self::$storageconfigpath) < filemtime(__FILE__)) self::save(); } // save to config.php in storagepath (normally _files/config/config.php) or create new config.php if file doesn't exist public static function save($options = []){ // merge array of parameters with current storageconfig, and intersect with default, to remove invalida/outdated options $save = array_intersect_key(array_replace(self::$storageconfig, $options), self::$default); // create exported array string with save values merged into default values, all commented out $export = preg_replace("/ '/", " //'", U::var_export(array_replace(self::$default, $save))); // loop save options and un-comment options where values differ from default options (for convenience, only store differences) foreach ($save as $key => $value) if($value !== self::$default[$key]) $export = str_replace("//'" . $key, "'" . $key, $export); // write formatted config array to config (normally _files/config/config.php) return @file_put_contents(self::$storageconfigpath, 'set_session_token(); // detect $_POST login attempt if($this->is_login_attempt()) { // on successful login, merge user config and login if($this->is_successful_login()) return $this->login(); // check if browser is already logged in by session } else if($this->is_logged_in()){ // ?logout=1 parameter to logout can only apply if user is already logged in if(U::get('logout')) { // we can return and serve request without login if default config does not require login // un-comment the below if you want to redirect to non-login version on logout, instead of showing the login form // if(!self::$has_public_login) return $this->clear_session(); // logout displays login form return $this->form(); } // merge user config and login return $this->login(); // if not logged in and default config does not require login (no username or password) } else if(!self::$has_public_login) { // ?login=1 displays login form when default config does not require login if(U::get('login')) { // remove $_SESSION['username'] just in case user was removed while session remains if(isset($_SESSION['username'])) unset($_SESSION['username']); // serve request without login if default config does not require login } else return; } // return error if request is an action (don't display login form) if($this->action_request()) return; // display form if not logged in or login failed attempt $this->form(); } // check if _files/users dir exists and return path public static function users_dir(){ return Config::$storagepath && file_exists(Config::$storagepath . '/users') ? Config::$storagepath . '/users' : false; } // assign CSRF security $_SESSION['token'] private function set_session_token(){ if(isset($_SESSION['token'])) return; // token already set $_SESSION['token'] = bin2hex(function_exists('random_bytes') ? random_bytes(32) : openssl_random_pseudo_bytes(32)); } // check if user is already logged in by session private function is_logged_in(){ // exit if session username or login hash is not set if(!isset($_SESSION['username']) || !isset($_SESSION['login'])) return false; // get user config from $_SESSION username $this->user = $this->get_user($_SESSION['username']); // logged in if user found login hash matches session login hash // may fail if user is deleted or username/password/IP/user-agent/app-location changes return $this->user && $this->equals($this->login_hash($this->user), $_SESSION['login']); } // detect login attempt private function is_login_attempt(){ // on javascript fetch() from non-login interface, we must populate $_POST from php://input if(U::get('action') === 'login' && empty($_POST)) $_POST = @json_decode(@trim(@file_get_contents('php://input')), true); // is login attempt if $_POST['fusername'] return !!U::post('fusername'); } // detect successful login attempt private function is_successful_login(){ // login attempt if fusername, fpassword and token in $_POST and 'token' exists in $_SESSION if(!U::post('fusername') || !U::post('fpassword') || !U::post('token') || !isset($_SESSION['token'])) return false; // make sure $_SESSION token matches $_POST token if(!$this->equals($_SESSION['token'], U::post('token'))) return false; // get user config from $_POST username $this->user = $this->get_user($_POST['fusername']); // exit if can't find user or password doesn't match if(!$this->user || !$this->passwords_match($this->user['password'], $_POST['fpassword'])) return false; // store username in session $_SESSION['username'] = $this->user['username']; // store login hash specific to user, must match on active sessions $_SESSION['login'] = $this->login_hash($this->user); // successfull login return true; } // successfully logged in by session or login attempt private function login(){ // list of excluded user config options because they should be global or have no function for user or could cause harm // you can add your own options here if you want to prevent some options from being changed per user $user_exclude = [ 'image_resize_dimensions', // should not change per user as it invalidates shared image cache 'image_resize_dimensions_retina', // should not change per user as it invalidates shared image cache 'image_resize_dimensions_allowed', // should not change per user as it invalidates shared image cache 'image_resize_quality', // should not change per user as it invalidates shared image cache 'image_resize_function', // should not change per user as it invalidates shared image cache 'image_resize_sharpen', // should not change per user as it invalidates shared image cache 'storage_path', // storage path is always global and must be defined in main config 'video_ffmpeg_path', // should be in global config 'imagemagick_path', // should be in global config 'index_cache', // should be global config / not avaialble for logged in users anyway ]; // we are hereby logged in self::$is_logged_in = true; // merge user config into config object Config::$config = array_replace(Config::$config, array_diff_key($this->user, array_flip($user_exclude))); } // clear login-specific session vars, essentially logging out the user private function clear_session(){ foreach (['username', 'login'] as $key) unset($_SESSION[$key]); } // get user config from login attempt or session private function get_user($username){ // trim username just in case $username = trim($username); // create lowercase username for case-insensitive comparison $lower_username = $this->lower($username); // user equals default config user / return username/password array to verify password or session login if($this->lower(Config::get('username')) === $lower_username) { self::$is_default_user = true; // is default config user return [ 'username' => Config::get('username'), 'password' => Config::get('password') ]; } // exit it _files/users dir doesn't exist if(!self::users_dir()) return false; // check if user config exists at _files/users/$username/config.php without making case-insensitive lookup // this should apply in most cases when username is input in identical case or from $_SESSION['username'] // Mac OS will find user case-insensitive, but that's fine as it doesn't then matter how $_SESSION['username'] is stored $user = $this->get_user_config($username); if($user) return $user; // loop user dirs and make case-insensitive username comparison foreach (glob(self::users_dir() . '/*', GLOB_ONLYDIR|GLOB_NOSORT) as $dir) { $arr = explode('/', $dir); // get basename, better than basename() in case of multibyte chars $dirname = end($arr); // get basename, better than basename() in case of multibyte chars // case-insensitive username matches user dir, get user config from $dirname with case in tact (for $_SESSION['username']) if($lower_username === $this->lower($dirname)) return $this->get_user_config($dirname); } } // get user config.php file for a specific user $dirname private function get_user_config($dirname){ $user = U::uinclude("users/$dirname/config.php"); // return user config array if(!$user) return; // exit if not found // error if the user array does not contain password *required if(empty($user['password'])) return $this->error('User does not have valid password'); // return user array merged with username, which is used for $_SESSION['login'] login_hash() return array_replace($user, ['username' => $dirname]); } // creates a login hash unique for username/password/IP/user-agent/app-location private function login_hash($user){ return md5($user['username'] . $user['password'] . $this->ip() . $this->server('HTTP_USER_AGENT') . __FILE__); } // compares strings with more secure hash_equals() function (PHP >= 5.6) private function equals($secret, $user){ return function_exists('hash_equals') ? hash_equals($secret, $user) : $secret === $user; } // match passwords using password_verify() if password is encrypted else use plain equality matching for non-encrypted passwords private function passwords_match($stored, $posted){ if(password_get_info($stored)['algoName'] === 'unknown') return $this->equals($stored, $posted); return password_verify($posted, $stored); } // get client IP for login hash matching private function ip(){ foreach(['HTTP_CLIENT_IP','HTTP_X_FORWARDED_FOR','HTTP_X_FORWARDED','HTTP_FORWARDED_FOR','HTTP_FORWARDED','REMOTE_ADDR'] as $key){ $ip = explode(',', $this->server($key))[0]; if($ip && filter_var($ip, FILTER_VALIDATE_IP)) return $ip; } return ''; // return empty string if nothing found } // get $_SERVER parameters helpers private function server($str){ return isset($_SERVER[$str]) ? $_SERVER[$str] : ''; } // lowercase username for case-insensitive username validation uses mb_strtolower() if function exists private function lower($str){ return function_exists('mb_strtolower') ? mb_strtolower($str) : strtolower($str); } // check if request is an action, in which case we return error instead of the form private function action_request(){ // exit if !action (or action is "tests", which requires login from the form) if(!U::get('action') || U::get('action') === 'tests') return false; // return json error if request is POST if($_SERVER['REQUEST_METHOD'] === 'POST') return Json::error('login'); // login error with login link U::error('Please login to continue', 401); } // login page / output form html and exit private function form() { // get form alert caused by logout, invalid session or incorrect login, before we destroy sessions vars $alert = $this->get_form_alert(); // destroy login-specific session vars on logout or if they are invalid / session_unset() $this->clear_session(); // get login form page header U::html_header('Login', 'page-login'); // login page html / check language and render form via javascript (blocks simple bots) ?>