diff --git a/.filesgalleryplan/index.php b/.filesgalleryplan/index.php new file mode 100644 index 0000000..d4a1455 --- /dev/null +++ b/.filesgalleryplan/index.php @@ -0,0 +1,4376 @@ + '', + '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 + 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); + + // 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) + ?>
+ +