'', 'root_url_path' => null, 'root_lock' => 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_use_imagemagick' => false, 'image_resize_cache' => true, 'image_resize_cache_use_dir' => false, 'image_resize_dimensions' => 320, 'image_resize_dimensions_retina' => 480, 'image_resize_dimensions_allowed' => '', '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, 'clean_cache_interval' => 7, 'clean_cache_allow_manual' => false, 'image_cache_file' => 'cache.txt', 'image_cache_max_last_access_time' => 90, 'image_cache_validate_time' => true, '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', 'ffmpeg_path' => 'ffmpeg', 'imagemagick_path' => 'convert', 'imagemagick_prefer_imagick' => true, 'imagemagick_image_types' => 'heif, heic, tiff, tif, psd, dng', '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.15.3'; // Files Gallery version public static $config = []; // config array merged from _filesconfig.php, config.php and default config // TSV-PATCH (einzige Änderung an dieser Datei): absolut statt relativ. // Der Original-Pfad wird gegen das aktuelle Arbeitsverzeichnis aufgelöst. Weicht das ab // (z. B. unter `php -S` mit Router-Script), findet die App _filesconfig.php nicht und // startet mit ihrer Default-Config — also OHNE Login und mit falschen Pfaden. // Nach jedem Update neu setzen: `php bin/filesgallery-update.php` erledigt und prüft das. public static $localconfigpath = __DIR__ . '/_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); // set absolute storagepath, create storage dirs if required, and load, create or update storage config.php $this->storage(); // get server document root with normalized OS path self::$document_root = Path::realpath($_SERVER['DOCUMENT_ROOT']); // 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(); // assign root realpath after login user is resolved self::$root = Path::valid_root(self::get('root')); // error if root path does not exist if(!self::$root) U::error('Invalid root dir "' . self::get('root') . '"'); // 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; } // public shortcut function to get config option Config::get('option') public static function get($option){ return self::$config[$option]; } // public get config comma-delimited string option as array public static function get_array($option) { $str = self::$config[$option]; return !empty($str) && is_string($str) ? array_map('trim', explode(',', $str)) : []; } // 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; } // get usernames from user_dirs() public static function get_usernames(){ return array_map(function($path){ $arr = explode('/', $path); // get basename, better than basename() in case of multibyte chars return end($arr); // get basename, better than basename() in case of multibyte chars }, self::users_dir() ? glob(self::users_dir() . '/*', GLOB_ONLYDIR|GLOB_NOSORT) : []); } // 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 = [ 'root_lock', // should be global and pre-assiged in _filesconfig.php 'image_resize_use_imagemagick', // should be global 'image_resize_cache_use_dir', // should be global '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 'image_cache_file', // should be global 'image_cache_max_last_access_time', // should be global 'image_cache_validate_time', // should be global 'storage_path', // storage path is always global and must be defined in main config 'ffmpeg_path', // should be global 'imagemagick_path', // should be global 'index_cache', // should be global / not available 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 (self::get_usernames() as $username) { // case-insensitive username matches user dir, get user config from $dirname with case in tact (for $_SESSION['username']) if($lower_username === $this->lower($username)) return $this->get_user_config($username); } } // 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) ?>