├── Dockerfile
├── README.md
├── README.zh-CN.md
├── img
├── DockerOperation.png
├── admin.png
├── default.png
├── login.png
└── web.png
└── src
├── fileadmin.php
├── files.js
└── index.php
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM php:5.6.37-apache
2 | MAINTAINER cjy0526 "1332794849@qq.com"
3 |
4 | ENV MAXSIZE=5000M USER=fileadmin PASSWORD=fileadmin
5 |
6 | ADD src /var/www/html
7 |
8 | # change php.ini
9 | RUN sed -i "s/upload_max_filesize = 2M/upload_max_filesize = ${MAXSIZE}/" $PHP_INI_DIR/php.ini-production \
10 | && sed -i "s/post_max_size = 8M/post_max_size = ${MAXSIZE}/" $PHP_INI_DIR/php.ini-production \
11 | && mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
12 |
13 | # install gd requirement && install extensions
14 | RUN apt-get update && apt-get install -y libgd-dev libzip-dev zip \
15 | && docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ \
16 | && docker-php-ext-configure zip --with-libzip \
17 | && docker-php-ext-install gd mbstring fileinfo exif zip
18 |
19 |
20 |
21 | # set username and password for admin.php which can manage files
22 | RUN cd /var/www/html/ && mkdir content && chmod -R 777 content \
23 | && sed -i "22s/''/'$USER'/" fileadmin.php \
24 | && sed -i "23s/''/'$PASSWORD'/" fileadmin.php
25 |
26 |
27 | EXPOSE 80
28 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Files
2 |
3 | The project provides a docker image for Files App(files.photo.gallery).
4 |
5 | 
6 |
7 |
8 | English | 中文
9 |
10 |
11 |
12 | ### Introduction
13 |
14 | For the purpose of learning,I replace the `files.js` with cracked version which just remove authorization code.
15 |
16 | So you can run this image directly to display your files.
17 |
18 | Please support the original if you could.
19 |
20 |
21 |
22 | ### usage
23 |
24 | you can just run:
25 |
26 | ```
27 | docker run -d --name files -v /root/content:/var/www/html/content -p 3000:80 cjy0526/files:v1
28 | ```
29 |
30 | 
31 |
32 | 
33 |
34 |
35 |
36 |
37 |
38 | If you want to build image yourself,please clone the project and run:
39 |
40 | ```
41 | docker build -t files:v1 .
42 |
43 | docker run -d --name files -v /root/content:/var/www/html/content -p 3000:80 files:v1
44 | ```
45 |
46 |
47 |
48 | ### 0.3.1 update
49 |
50 | Files 0.3.1 can upload/delete/rename/new file and folder and download files as zip
51 |
52 | In our docker container , you can login fileadmin.php to manage files. Default username:fileadmin , password: fileadmin , max upload file size: 5000M.
53 |
54 | 
55 |
56 | 
57 |
58 | 
59 |
60 |
61 |
62 | ```
63 | docker run -d --name files -v /root/content:/var/www/html/content -p 3000:80 cjy0526/files:v2
64 | ```
65 |
66 |
67 |
68 | If you want to change username or password or max upload file size, just clone this project and edit Dockerfile then build image yourself.
69 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/README.zh-CN.md:
--------------------------------------------------------------------------------
1 | # Files
2 |
3 | 这个项目提供了Files App(files.photo.gallery) 一款简洁漂亮的目录程序的docker镜像
4 |
5 | 
6 |
7 |
8 | English | 中文
9 |
10 |
11 | ### 介绍
12 |
13 | 出于学习研究的目的,我将`files.js`替换成了去除授权代码的破解版。
14 |
15 | 因此你可以直接运行这个镜像用于展示自己的文件。
16 |
17 | 如有能力,请支持正版。
18 |
19 |
20 |
21 | ### 使用
22 |
23 | 你可以直接拉取运行已经上传到dockerhub上的镜像:
24 |
25 | ```
26 | docker pull cjy0526/files:v1
27 | docker run -d --name files -v /root/content:/var/www/html/content -p 3000:80 cjy0526/files:v1
28 | ```
29 |
30 | 
31 |
32 | 
33 |
34 |
35 |
36 |
37 |
38 | 如果想自行编译这个镜像的话,请把这个项目克隆到本地并运行:
39 |
40 | ```
41 | docker build -t files:v1 .
42 | docker run -d --name files -v /root/content:/var/www/html/content -p 3000:80 files:v1
43 | ```
44 |
45 |
46 |
47 | ### 0.3.1版本更新
48 |
49 | Files 0.3.1 更新了文件的上传、删除、重命名、新增 和 文件夹的新增、重命名 以及 以zip打包下载所有的文件
50 |
51 | 在这个容器中,可以通过访问`/fileadmin.php`登录后台管理文件。默认用户名:fileadmin,默认密码:fileadmin,最大文件上传容量:5000M
52 |
53 | 
54 |
55 | 
56 |
57 | 
58 |
59 |
60 |
61 | ```
62 | docker run -d --name files -v /root/content:/var/www/html/content -p 3000:80 cjy0526/files:v2
63 | ```
64 |
65 |
66 |
67 | 如果你想更改默认的用户名、密码、最大文件上传容量,克隆这个项目编辑Dockerfile并且自行编译镜像
--------------------------------------------------------------------------------
/img/DockerOperation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/caijynb/files/ce7af61a26889ec0e6fb5fbd0c05854bf059ba7c/img/DockerOperation.png
--------------------------------------------------------------------------------
/img/admin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/caijynb/files/ce7af61a26889ec0e6fb5fbd0c05854bf059ba7c/img/admin.png
--------------------------------------------------------------------------------
/img/default.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/caijynb/files/ce7af61a26889ec0e6fb5fbd0c05854bf059ba7c/img/default.png
--------------------------------------------------------------------------------
/img/login.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/caijynb/files/ce7af61a26889ec0e6fb5fbd0c05854bf059ba7c/img/login.png
--------------------------------------------------------------------------------
/img/web.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/caijynb/files/ce7af61a26889ec0e6fb5fbd0c05854bf059ba7c/img/web.png
--------------------------------------------------------------------------------
/src/fileadmin.php:
--------------------------------------------------------------------------------
1 | 'content/', // root path relative to script / empty is same as files app location
19 | 'start_path' => false, // start path relative to script. If empty, root is start path
20 |
21 | // login
22 | 'username' => '',
23 | 'password' => '', // Add password directly or use https://files.photo.gallery/tools/hash/ to encrypt the password (encrypted password is more secure, as it prevents your password from being exposed directly in a file).
24 |
25 | // images
26 | 'load_images' => true,
27 | 'load_files_proxy_php' => false,
28 | 'load_images_max_filesize' => 50000000, // maximum file size (bytes) for un-resized images loaded into list
29 | 'load_svg_max_filesize' => 100000, // 100k
30 | 'image_resize_enabled' => true, // resize images for the interface
31 | 'image_resize_cache' => true, // todo: remove this option and just use 'cache?
32 | 'image_resize_dimensions' => 320,
33 | 'image_resize_dimensions_retina' => 480,
34 | 'image_resize_dimensions_allowed' => '', // comma-separated list of allowed resize dimensions
35 | 'image_resize_types' => 'jpeg, png, gif, webp, bmp', // image types to resize / jpeg, png, gif, webp, bmp
36 | 'image_resize_quality' => 85,
37 | 'image_resize_function' => 'imagecopyresampled', // imagecopyresampled / imagecopyresized
38 | 'image_resize_sharpen' => true,
39 | 'image_resize_memory_limit' => 128, // 128 MB is suffient to resize images around 6000 px / 0 = ignore memory
40 | 'image_resize_max_pixels' => 30000000, // 30 MP equivalent to an image 6000 x 5000 / 0 = no limit
41 | 'image_resize_min_ratio' => 1.5, // min size diff original vs resize. Only resizes if ratio > min ratio
42 | 'image_resize_cache_direct' => false, // if enabled and delete cache, must increase cache_key
43 | 'folder_preview_image' => true, // enable folder preview images / might be slow as it requires searching each dir for images
44 | 'folder_preview_default' => '_filespreview.jpg', // use this image as folder preview if exists in dir
45 |
46 | // menu
47 | 'menu_enabled' => true,
48 | 'menu_show' => true,
49 | 'menu_max_depth' => 5,
50 | 'menu_sort' => 'name_asc', // name_asc, name_desc, date_asc, date_desc
51 | 'menu_cache_validate' => true,
52 | 'menu_load_all' => false,
53 | 'menu_recursive_symlinks' => true, // List sub-directories of symlinks in the main menu. May cause loops and duplicates
54 |
55 | // files layout
56 | 'layout' => 'rows', // list, imagelist, blocks, grid, rows, columns
57 | 'sort' => 'name_asc', // name, date, filesize, kind / asc, desc
58 | 'sort_dirs_first' => true, // sort dirs on top
59 |
60 | // cache
61 | 'cache' => true,
62 | 'cache_key' => 0,
63 | 'storage_path' => '_files',
64 |
65 | // exclude files directories regex
66 | 'files_exclude' => '', // '/\.(pdf|jpe?g)$/i'
67 | 'dirs_exclude' => '', //'/\/Convert|\/football|\/node_modules(\/|$)/i',
68 | 'allow_symlinks' => true, // allow symlinks
69 |
70 | // various
71 | 'history' => true,
72 | 'breadcrumbs' => true,
73 | 'transitions' => true,
74 | 'click' => 'popup', // popup, modal, download, window, menu / default item click
75 | 'click_window' => '', // 'pdf, html, php, zip' / list of file extensions to open directly on click
76 | 'click_window_popup' => true, // Popup instead of new tab. Useful for viewing PDF, HTML and text type documents / desktop only
77 | 'code_max_load' => 100000, // max filesize of text files to load and preview
78 | 'topbar_sticky' => 'scroll', // true, false, 'scroll'
79 | 'check_updates' => false, // show notification in topbar with option to update when new version is available
80 | 'allow_tasks' => false,
81 | 'get_mime_type' => false, // get file mime type from server (slow) instead of from extension (fast)
82 | 'context_menu' => true, // disable context-menu button and right-click menu
83 | 'prevent_right_click' => false, // blocks browser right-click menu on sensitive items (images, list items, menu)
84 | 'license_key' => '',
85 | 'filter_live' => true, // live search filtering on keyboard input / does not apply for mobile devices
86 | 'filter_props' => 'name, filetype, mime, features, title', // file properties to filter / name, filetype, mime, features, title, headline, description, creator, credit, copyright, keywords, city, sub-location, province-state'
87 | 'download_dir' => 'zip', // download all files in folder / 'zip' / 'files' / '' false (disabled)
88 | 'download_dir_cache' => 'dir', // enable caching of created zip dirs / 'dir' / 'storage' / '' false (disabled)
89 |
90 | // filemanager options
91 | 'allow_upload' => true, // allow uploader
92 | 'allow_delete' => true, // allow deleting files and folders
93 | 'allow_rename' => true, // allow renaming files and folders
94 | 'allow_new_folder' => true, // allow make new directory
95 | 'allow_new_file' => true, // allow make new empty file
96 | 'allow_duplicate' => true, // allow duplicate files
97 | 'allow_text_edit' => true, // allow editing text-based files in modal
98 | 'demo_mode' => false, // block all filemanager operations but allow them to show in interface / used in Files app demo
99 |
100 | // uploader options
101 | 'upload_allowed_file_types' => '', // comma-separated list of allowed upload file types / empty = allow any / 'jpeg, jpg, image/*'
102 | 'upload_max_filesize' => 0, // [bytes] / 0 = unlimited (but limited by server PHP upload_max_filesize)
103 | 'upload_note' => '', // include a small text note at bottom of uploader / 'Max file size %upload_max_filesize%'
104 | 'upload_exists' => 'increment', // 'increment' / 'overwrite' / 'fail'
105 |
106 | // popup options
107 | 'popup_video' => true, // opens videos in the popup (instead of modal)
108 | 'popup_transition' => 'glide', // none, slide, glide, fade, zoom, pop, elastic
109 | 'popup_transition_play' => 'inherit', // transition in play mode
110 | 'popup_interval' => 5000, // interval ms between slides in play mode
111 | 'popup_caption' => true, // enable popup caption
112 | 'popup_caption_hide' => true, // autohide popup caption after a few seconds without user input
113 | 'popup_caption_style' => 'block', // block, box, gradient, topbar, none
114 | 'popup_caption_align' => 'center-left', // left, center-left, center, right
115 |
116 | // video
117 | 'video_thumbs' => true, // allow video thumbnails / requires FFmpeg and PHP exec() function enabled.
118 | 'video_ffmpeg_path' => 'ffmpeg', // path to ffmpeg command, normally 'ffmpeg' http://ffmpeg.org/
119 | 'video_autoplay' => true, // video autoplay on click
120 |
121 | // language
122 | 'lang_default' => 'en', // default language if browser lang is not supported/detected or lang_auto is disabled
123 | 'lang_auto' => true, // automatically load language based on detected browser language
124 | 'lang_menu' => false, // display dropdown menu to select language
125 | );
126 |
127 | // config (will popuplate)
128 | public static $config = array();
129 |
130 | // app vars
131 | static $__dir__ = __DIR__;
132 | static $__file__ = __FILE__;
133 | static $assets;
134 | static $prod = true;
135 | static $version = '0.3.1';
136 | static $root;
137 | static $doc_root;
138 | static $has_login = false;
139 | static $storage_path;
140 | static $storage_is_within_doc_root = false;
141 | static $storage_config_realpath;
142 | static $storage_config;
143 | static $cache_path;
144 | static $image_resize_cache_direct;
145 | static $image_resize_dimensions_retina = false;
146 | static $dirs_hash = false;
147 | static $local_config_file = '_filesconfig.php';
148 | static $username = false;
149 | static $password = false;
150 | static $x3_path = false;
151 |
152 | // get config
153 | private function get_config($path) {
154 | if(empty($path) || !file_exists($path)) return array();
155 | $config = include $path;
156 | return empty($config) || !is_array($config) ? array() : array_map(function($v){
157 | return is_string($v) ? trim($v) : $v;
158 | }, $config);
159 | }
160 |
161 | // files check system and config [diagnostics]
162 | private function files_check($local_config, $storage_path, $storage_config, $user_config, $user_valid){
163 |
164 | // BASIC DIAGNOSTICS
165 | echo 'Files App check system and config.Files App ' . config::$version . '
' . (isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] . '
' : '') . 'PHP ' . phpversion() . '
' . (isset($_SERVER['SERVER_SOFTWARE']) ? $_SERVER['SERVER_SOFTWARE'] : '') . '
* The following tests are only to help diagnose feature-specific issues.
';
166 | // output helper
167 | function prop($name, $success = 'neutral', $val = false){
168 | return '
'. $name . ($val ? ': ' . $val . '' : '') . '
';
169 | }
170 | // filesystem
171 | echo prop('storage_path exists', file_exists(config::$config['storage_path']));
172 | if(file_exists(config::$config['storage_path'])) echo prop('storage_path is_writeable', is_writable(config::$config['storage_path']));
173 | echo prop('root is_writeable', is_writable(config::$config['root']));
174 | // extension_loaded
175 | if(function_exists('extension_loaded')) foreach (['gd', 'exif'] as $name) echo prop($name, extension_loaded($name));
176 | // zip
177 | echo prop('ZipArchive', class_exists('ZipArchive'));
178 | // function_exsists
179 | foreach (['mime_content_type', 'finfo_file', 'iptcparse', 'exif_imagetype', 'session_start', 'ini_get', 'exec'] as $name) echo prop($name . '()', function_exists($name));
180 | // ffmpeg
181 | if(function_exists('exec')) echo prop('ffmpeg', !!exec('type -P ' . config::$config['video_ffmpeg_path']));
182 | // ini_get
183 | if(function_exists('ini_get')) foreach (['memory_limit', 'file_uploads', 'upload_max_filesize', 'post_max_size', 'max_file_uploads'] as $name) echo prop($name, 'neutral', @ini_get($name));
184 |
185 | // CONFIG OUTPUT
186 | echo '
Config
';
187 | // invalid and duplicate arrays
188 | $user_invalid = array_diff_key($user_config, self::$default);
189 | $user_duplicate = array_intersect_assoc($user_valid, self::$default);
190 |
191 | // items
192 | $items = array(
193 | ['arr' => $local_config, 'comment' => "// LOCAL CONFIG\n// " . self::$local_config_file],
194 | ['arr' => $storage_config, 'comment' => "// STORAGE CONFIG\n// " . rtrim($storage_path ?: '', '\/') . '/config/config.php'],
195 | ['arr' => $user_invalid, 'comment' => "// INVALID PARAMS\n// The following custom parameters will be ignored as they are not valid:", 'var' => '$invalid', 'hide' => empty($user_invalid)],
196 | ['arr' => $user_duplicate, 'comment' => "// DUPLICATE DEFAULT PARAMS\n// The following custom parameters will have no effect as they are identical to defaults:", 'var' => '$duplicate', 'hide' => empty($user_duplicate)],
197 | ['arr' => $user_valid, 'comment' => "// USER CONFIG\n// User config parameters.", 'var' => '$user', 'hide' => (empty($local_config) || empty($storage_config)) && empty($user_invalid)],
198 | ['arr' => self::$config, 'comment' => "// CONFIG\n// User parameters merged with default parameters.", 'var' => '$config'],
199 | ['arr' => self::$default, 'comment' => "// DEFAULT CONFIG\n// Default config parameters.", 'var' => '$default'],
200 | //['arr' => array_diff_key(get_class_vars('config'), array_flip(['default', 'config'])), 'comment' => "// STATIC VARS\n// Static app vars.", 'var' => '$static']
201 | );
202 |
203 | // loop
204 | $output = ' $props) {
206 | $is_empty = empty($props['arr']);
207 | if(isset($props['hide']) && $props['hide']) continue;
208 | foreach (['username', 'password', 'license_key', 'allow_tasks', '__dir__', '__file__'] as $prop) if(isset($props['arr'][$prop]) && !empty($props['arr'][$prop]) && is_string($props['arr'][$prop])) $props['arr'][$prop] = '***';
209 | $export = $is_empty ? 'array ()' : var_export($props['arr'], true);
210 | $comment = preg_replace('/\n/', " [" . count($props['arr']) . "]\n", $props['comment'], 1);
211 | $var = isset($props['var']) ? $props['var'] . ' = ' : 'return ';
212 | $output .= PHP_EOL . $comment . PHP_EOL . $var . $export . ';' . PHP_EOL;
213 | }
214 | highlight_string($output . PHP_EOL . ';?>');
215 | echo '';
216 | exit;
217 | }
218 |
219 | // save config
220 | public static function save_config($config = array()){
221 | $save_config = array_intersect_key(array_replace(self::$storage_config, $config), self::$default);
222 | $export = preg_replace("/ '/", " //'", var_export(array_replace(self::$default, $save_config), true));
223 | foreach ($save_config as $key => $value) if($value !== self::$default[$key]) $export = str_replace("//'" . $key, "'" . $key, $export);
224 | return @file_put_contents(config::$storage_config_realpath, 'storage_path must be a unique dir.');
241 | self::$storage_config_realpath = $storage_realpath ? $storage_realpath . '/config/config.php' : false;
242 | self::$storage_config = self::get_config(self::$storage_config_realpath);
243 |
244 | // config
245 | $user_config = array_replace(self::$storage_config, $local_config);
246 | $user_valid = array_intersect_key($user_config, self::$default);
247 | self::$config = array_replace(self::$default, $user_valid);
248 |
249 | // files check with ?check=true
250 | if(get('check')) self::files_check($local_config, $storage_path, self::$storage_config, $user_config, $user_valid);
251 | // if(get('phpinfo')) { phpinfo(); exit; } // check system phpinfo with ?phpinfo=true / disabled for security / un-comment if you want to use
252 |
253 | // CDN assets
254 | self::$assets = self::$prod ? 'https://cdn.jsdelivr.net/npm/files.photo.gallery@' . self::$version . '/' : '';
255 |
256 | // root
257 | self::$root = real_path(self::$config['root']);
258 | if($is_doc && !self::$root) error('root dir "' . self::$config['root'] . '" does not exist.');
259 |
260 | // doc root
261 | self::$doc_root = real_path($_SERVER['DOCUMENT_ROOT']);
262 |
263 | // login credentials
264 | self::$username = self::$config['username'];
265 | self::$password = self::$config['password'];
266 |
267 | // X3 compatibility / x3 login / images from X3 resize cache / invalidate X3 cache on filemanager op / X3 license
268 | $x3_path = dirname(self::$root);
269 | self::$x3_path = file_exists($x3_path . '/app/x3.inc.php') ? $x3_path : false;
270 | if(self::$x3_path && self::$username === 'x3'){
271 | $x3_config = file_exists($x3_path . '/config/config.user.json') ? json_decode(file_get_contents($x3_path . '/config/config.user.json'), true) : false;
272 | self::$username = isset($x3_config['back']['panel']['username']) ? $x3_config['back']['panel']['username'] : 'admin';
273 | self::$password = isset($x3_config['back']['panel']['password']) ? $x3_config['back']['panel']['password'] : 'admin';
274 | if(self::$username === 'admin' && self::$password === 'admin' && isset($x3_config['back']['panel']['use_db'])) error('Sorry, Files app is not compatible with X3 panel database login. Assign "username" and "password" separately in _files/config/config.php.', 403);
275 | }
276 |
277 | // has_login
278 | self::$has_login = self::$username || self::$password ? true : false;
279 |
280 | // $image_cache
281 | $image_cache = self::$config['image_resize_enabled'] && self::$config['image_resize_cache'] && self::$config['load_images'] ? true : false;
282 |
283 | // cache enabled
284 | if($image_cache || self::$config['cache']){
285 |
286 | // create storage_path
287 | if(empty($storage_realpath)){
288 | $storage_path = is_string($storage_path) ? rtrim($storage_path, '\/') : false;
289 | if(empty($storage_path)) error('Invalid storage_path parameter.');
290 | mkdir_or_error($storage_path);
291 | $storage_realpath = real_path($storage_path);
292 | if(empty($storage_realpath)) error("storage_path $storage_path does not exist and can't be created.");
293 | self::$storage_config_realpath = $storage_realpath . '/config/config.php'; // update since it wasn't assigned
294 | }
295 | self::$storage_path = $storage_realpath;
296 |
297 | // storage path is within doc root
298 | if(is_within_docroot(self::$storage_path)) self::$storage_is_within_doc_root = true;
299 |
300 | // cache_path real path
301 | self::$cache_path = self::$storage_path . '/cache';
302 |
303 | // create storage dirs
304 | if($is_doc){
305 | $create_dirs = [$storage_realpath . '/config'];
306 | if($image_cache) $create_dirs[] = self::$cache_path . '/images';
307 | if(self::$config['cache']) array_push($create_dirs, self::$cache_path . '/folders', self::$cache_path . '/menu');
308 | foreach($create_dirs as $create_dir) mkdir_or_error($create_dir);
309 | }
310 |
311 | // create/update config file, with default parameters commented out.
312 | if($is_doc && self::$storage_config_realpath && (!file_exists(self::$storage_config_realpath) || filemtime(self::$storage_config_realpath) < filemtime(__FILE__))) self::save_config();
313 |
314 | // image resize cache direct
315 | if(self::$config['image_resize_cache_direct'] && !self::$has_login && self::$config['load_images'] && self::$config['image_resize_cache'] && self::$config['image_resize_enabled'] && self::$storage_is_within_doc_root) self::$image_resize_cache_direct = true;
316 | }
317 |
318 | // image_resize_dimensions_retina
319 | if(self::$config['image_resize_dimensions_retina'] && self::$config['image_resize_dimensions_retina'] > self::$config['image_resize_dimensions']) self::$image_resize_dimensions_retina = self::$config['image_resize_dimensions_retina'];
320 |
321 | // dirs hash
322 | self::$dirs_hash = substr(md5(self::$doc_root . self::$__dir__ . self::$root . self::$version . self::$config['cache_key'] . self::$image_resize_cache_direct . self::$config['files_exclude'] . self::$config['dirs_exclude']), 0, 6);
323 |
324 | // login
325 | if(self::$has_login) check_login($is_doc);
326 | }
327 | };
328 |
329 | // login page
330 | function login_page($is_login_attempt, $sidx, $is_logout, $client_hash){
331 | ?>
332 |
333 |
334 |
335 |
336 |
337 |
338 | Login
339 |
340 |
341 |
342 |
343 |
365 |
366 | = 5.5 && !password_needs_rehash(config::$password, PASSWORD_DEFAULT) ? password_verify(trim($_POST['fpassword']), config::$password) : (trim($_POST['fpassword']) == config::$password)) &&
404 | $_POST['client_hash'] === $client_hash &&
405 | $_POST['sidx'] === $sidx
406 | ){
407 | $_SESSION['login'] = $login_hash;
408 |
409 | // display login page and exit
410 | } else {
411 | login_page($is_login_attempt, $sidx, $is_logout, $client_hash);
412 | }
413 |
414 | // not logged in (images or post API requests), don't show form.
415 | } else if(post('action')){
416 | json_error('login');
417 |
418 | } else {
419 | error('You are not logged in.', 401);
420 | }
421 | }
422 | }
423 |
424 | //
425 | function mkdir_or_error($path){
426 | if(!file_exists($path) && !mkdir($path, 0777, true)) error('Failed to create ' . $path, 500);
427 | }
428 | function real_path($path){
429 | $real_path = realpath($path);
430 | return $real_path ? str_replace('\\', '/', $real_path) : false;
431 | }
432 | function root_relative($dir){
433 | return ltrim(substr($dir, strlen(config::$root)), '\/');
434 | }
435 | function root_absolute($dir){
436 | return config::$root . ($dir ? '/' . $dir : '');
437 | }
438 | function is_within_path($path, $root){
439 | return strpos($path . '/', $root . '/') === 0;
440 | }
441 | function is_within_root($path){
442 | return is_within_path($path, config::$root);
443 | }
444 | function is_within_docroot($path){
445 | return is_within_path($path, config::$doc_root);
446 | }
447 | function get_folders_cache_path($name){
448 | return config::$cache_path . '/folders/' . $name . '.json';
449 | }
450 | function get_json_cache_url($name){
451 | $file = get_folders_cache_path($name);
452 | return file_exists($file) ? get_url_path($file) : false;
453 | }
454 | function get_dir_cache_path($dir, $mtime = false){
455 | if(!config::$config['cache'] || !$dir) return;
456 | return get_folders_cache_path(get_dir_cache_hash($dir, $mtime));
457 | }
458 | function get_dir_cache_hash($dir, $mtime = false){
459 | return config::$dirs_hash . '.' . substr(md5($dir), 0, 6) . '.' . ($mtime ?: filemtime($dir));
460 | //
461 | }
462 | function header_memory_time(){
463 | return (isset($_SERVER['REQUEST_TIME_FLOAT']) ? round(microtime(true) - $_SERVER['REQUEST_TIME_FLOAT'], 3) . 's, ' : '') . round(memory_get_peak_usage() / 1048576, 1) . 'M';
464 | }
465 |
466 | // read file
467 | // todo: add files-date header
468 | function read_file($path, $mime = false, $msg = false, $props = false, $cache_headers = false, $clone = false){
469 | if(!$path || !file_exists($path)) return false;
470 | $cloned = $clone && @copy($path, $clone) ? true : false;
471 | //if($mime == 'image/svg') $mime .= '+xml';
472 | header('content-type: ' . ($mime ?: 'image/jpeg'));
473 | header('content-length: ' . filesize($path));
474 | header('content-disposition: filename="' . basename($path) . '"');
475 | if($msg) header('files-msg: ' . $msg . ($cloned ? ' [cloned to ' . basename($clone) . ']' : '') . ' [' . ($props ? $props . ', ' : '') . header_memory_time() . ']');
476 | if($cache_headers) set_cache_headers();
477 | if(!is_readable($path) || readfile($path) === false) error('Failed to read file ' . $path . '.', 400);
478 | exit;
479 | }
480 |
481 | // get mime
482 | function get_mime($path){
483 | if(function_exists('mime_content_type')){
484 | return mime_content_type($path);
485 | } else {
486 | return function_exists('finfo_file') ? finfo_file(finfo_open(FILEINFO_MIME_TYPE), $path) : false;
487 | }
488 | }
489 |
490 | // set cache headers
491 | function set_cache_headers(){
492 | $seconds = 31536000; // 1 year;
493 | header('expires: ' . gmdate('D, d M Y H:i:s', time() + $seconds) . ' GMT');
494 | header("cache-control: public, max-age=$seconds, s-maxage=$seconds, immutable");
495 | header('pragma: cache');
496 | // header("Last-Modified:" . gmdate('D, d M Y H:i:s', time() - $seconds) . ' GMT');
497 | // etag?
498 | }
499 |
500 | // get image cache path
501 | function get_image_cache_path($path, $image_resize_dimensions, $filesize, $filemtime){
502 | return config::$cache_path . '/images/' . substr(md5($path), 0, 6) . '.' . $filesize . '.' . $filemtime . '.' . $image_resize_dimensions . '.jpg';
503 | }
504 |
505 | // is exclude
506 | function is_exclude($path = false, $is_dir = true, $symlinked = false){
507 |
508 | // early exit
509 | if(!$path || $path === config::$root) return;
510 |
511 | // exclude all paths that start with /_files* (reserved for any files and folders to be ignored and hidden from Files app)
512 | if(strpos($path, '/_files') !== false) return true;
513 |
514 | // exclude files PHP application
515 | if($path === config::$__file__) return true;
516 |
517 | // symlinks not allowed
518 | if($symlinked && !config::$config['allow_symlinks']) return true;
519 |
520 | // exclude storage path
521 | if(config::$storage_path && is_within_path($path, config::$storage_path)) return true;
522 |
523 | // dirs_exclude: check root relative dir path
524 | if(config::$config['dirs_exclude']) {
525 | $dirname = $is_dir ? $path : dirname($path);
526 | if($dirname !== config::$root && preg_match(config::$config['dirs_exclude'], substr($dirname, strlen(config::$root)))) return true;
527 | }
528 |
529 | // files_exclude: check vs basename
530 | if(!$is_dir){
531 | $basename = basename($path);
532 | if($basename === config::$local_config_file) return true;
533 | if(config::$config['files_exclude'] && preg_match(config::$config['files_exclude'], $basename)) return true;
534 | }
535 | }
536 |
537 | // valid root path
538 | function valid_root_path($path, $is_dir = false){
539 |
540 | // invalid
541 | if($path === false) return;
542 | if(!$is_dir && empty($path)) return; // path cannot be empty if file
543 | if($path && substr($path, -1) == '/') return; // path should never be root absolute or end with /
544 |
545 | // absolute path may differ if path contains symlink
546 | $root_absolute = root_absolute($path);
547 | $real_path = real_path($root_absolute);
548 |
549 | // file does not exist
550 | if(!$real_path) return;
551 |
552 | // security checks if path contains symlink
553 | if($root_absolute !== $real_path) {
554 | if(strpos(($is_dir ? $path : dirname($path)), ':') !== false) return; // dir may not contain ':'
555 | if(strpos($path, '..') !== false) return; // path may not contain '..'
556 | if(is_exclude($root_absolute, $is_dir, true)) return;
557 | }
558 |
559 | // nope
560 | if(!is_readable($real_path)) return; // not readable
561 | if($is_dir && !is_dir($real_path)) return; // dir check
562 | if(!$is_dir && !is_file($real_path)) return; // file check
563 | if(is_exclude($real_path, $is_dir)) return; // exclude path
564 |
565 | // return root_absolute
566 | return $root_absolute;
567 | }
568 |
569 | // image create from
570 | function image_create_from($path, $type){
571 | if(!$path || !$type) return;
572 | if($type === IMAGETYPE_JPEG){
573 | return imagecreatefromjpeg($path);
574 | } else if ($type === IMAGETYPE_PNG) {
575 | return imagecreatefrompng($path);
576 | } else if ($type === IMAGETYPE_GIF) {
577 | return imagecreatefromgif($path);
578 | } else if ($type === 18/*IMAGETYPE_WEBP*/) {
579 | if(version_compare(PHP_VERSION, '5.4.0') >= 0) return imagecreatefromwebp($path);
580 | } else if ($type === IMAGETYPE_BMP) {
581 | if(version_compare(PHP_VERSION, '7.2.0') >= 0) return imagecreatefrombmp($path);
582 | }
583 | }
584 |
585 | // get file (proxy or resize image)
586 | function get_file($path, $resize = false){
587 |
588 | // validate
589 | if(!$path) error('Invalid file request.', 404);
590 | $path = real_path($path); // in case of symlink path
591 | $mime = get_mime($path); // may return false if server does not support mime_content_type() or finfo_file()
592 |
593 | // video thumbnail (FFmpeg)
594 | if($resize == 'video') {
595 |
596 | // requirements with diagnostics / only check $mime if $mime detected
597 | if($mime && strtok($mime, '/') !== 'video') error('' . basename($path) . ' (' . $mime . ') is not a video.', 415);
598 | foreach (array('video_thumbs', 'load_images', 'image_resize_cache', 'video_ffmpeg_path') as $key) if(empty(config::$config[$key])) error($key . ' option disabled.', 400);
599 | if(!function_exists('exec')) error('PHP exec() function is disabled on this server.', 400);
600 | if(empty(exec('type -P ' . config::$config['video_ffmpeg_path']))) error('Can\'t find FFmpeg in location "' . config::$config['video_ffmpeg_path'] . '".', 400);
601 |
602 | // get cache path
603 | $cache = get_image_cache_path($path, 480, filesize($path), filemtime($path));
604 |
605 | // check for cached video thumbnail / $path, $mime, $msg, $props, $cache_headers
606 | if($cache) read_file($cache, null, 'Video thumb served from cache', null, true);
607 |
608 | // ffmpeg command
609 | $cmd = escapeshellarg(config::$config['video_ffmpeg_path']) . ' -i ' . escapeshellarg($path) . ' -deinterlace -an -ss 1 -t 1 -vf "thumbnail,scale=480:320:force_original_aspect_ratio=increase,crop=480:320" -r 1 -y -f mjpeg ' . $cache . ' 2>&1';
610 |
611 | // try to execute command
612 | exec($cmd, $output, $result_code);
613 |
614 | // fail if result_code is anything else than 0
615 | if($result_code) error("Error generating thumbnail for video (\$result_code $result_code)", 400);
616 |
617 | // output created video thumbnail
618 | read_file($cache, null, 'Video thumb created', null, true);
619 |
620 | // resize image
621 | } else if($resize){
622 | if($mime && strtok($mime, '/') !== 'image') error('' . basename($path) . ' (' . $mime . ') is not an image.', 415);
623 | foreach (['load_images', 'image_resize_enabled'] as $key) if(!config::$config[$key]) error('[' .$key . '] disabled.', 400);
624 | $resize_dimensions = intval($resize);
625 | if(!$resize_dimensions) error("Invalid resize parameter $resize.", 400);
626 | $allowed = config::$config['image_resize_dimensions_allowed'] ?: [];
627 | if(!in_array($resize_dimensions, array_merge([config::$config['image_resize_dimensions'], config::$config['image_resize_dimensions_retina']], array_map('intval', is_array($allowed) ? $allowed : explode(',', $allowed))))) error("Resize parameter $resize_dimensions is not allowed.", 400);
628 | resize_image($path, $resize_dimensions);
629 |
630 | // proxy file
631 | } else {
632 |
633 | // disable if !proxy and path is within document root (file should never be proxied)
634 | if(!config::$config['load_files_proxy_php'] && is_within_docroot($path)) error('File cannot be proxied.', 400);
635 |
636 | // read file / $mime or 'application/octet-stream'
637 | read_file($path, ($mime ?: 'application/octet-stream'), $msg = 'File ' . basename($path) . ' proxied.', false, true);
638 | }
639 | }
640 |
641 | // sharpen resized image
642 | function sharpen_image($image){
643 | $matrix = array(
644 | array(-1, -1, -1),
645 | array(-1, 20, -1),
646 | array(-1, -1, -1),
647 | );
648 | $divisor = array_sum(array_map('array_sum', $matrix));
649 | $offset = 0;
650 | imageconvolution($image, $matrix, $divisor, $offset);
651 | }
652 |
653 | // exif orientation
654 | // https://github.com/gumlet/php-image-resize/blob/master/lib/ImageResize.php
655 | function exif_orientation($orientation, &$image){
656 | if(empty($orientation) || !is_numeric($orientation) || $orientation < 3 || $orientation > 8) return;
657 | $image = imagerotate($image, array(6 => 270, 5 => 270, 3 => 180, 4 => 180, 8 => 90, 7 => 90)[$orientation], null);
658 | if(in_array($orientation, array(5, 4, 7)) && function_exists('imageflip')) imageflip($image, IMG_FLIP_HORIZONTAL);
659 | return true;
660 | }
661 |
662 | // resize image
663 | function resize_image($path, $resize_dimensions, $clone = false){
664 |
665 | // file size
666 | $file_size = filesize($path);
667 |
668 | // header props
669 | $header_props = 'w:' . $resize_dimensions . ', q:' . config::$config['image_resize_quality'] . ', ' . config::$config['image_resize_function'] . ', cache:' . (config::$config['image_resize_cache'] ? '1' : '0');
670 |
671 | // cache
672 | $cache = config::$config['image_resize_cache'] ? get_image_cache_path($path, $resize_dimensions, $file_size, filemtime($path)) : NULL;
673 | if($cache) read_file($cache, null, 'Resized image served from cache', $header_props, true, $clone);
674 |
675 | // imagesize
676 | $info = getimagesize($path);
677 | if(empty($info) || !is_array($info)) error('Invalid image / failed getimagesize().', 500);
678 | $resize_ratio = max($info[0], $info[1]) / $resize_dimensions;
679 |
680 | // image_resize_max_pixels early exit
681 | if(config::$config['image_resize_max_pixels'] && $info[0] * $info[1] > config::$config['image_resize_max_pixels']) error('Image resolution ' . $info[0] . ' x ' . $info[1] . ' (' . ($info[0] * $info[1]) . ' px) exceeds image_resize_max_pixels (' . config::$config['image_resize_max_pixels'] . ' px).', 400);
682 |
683 | // header props
684 | $header_props .= ', ' . $info['mime'] . ', ' . $info[0] . 'x' . $info[1] . ', ratio:' . round($resize_ratio, 2);
685 |
686 | // check if image type is in image_resize_types / jpeg, png, gif, webp, bmp
687 | $is_resize_type = in_array(image_type_to_extension($info[2], false), array_map(function($key){
688 | $type = trim(strtolower($key));
689 | return $type === 'jpg' ? 'jpeg' : $type;
690 | }, explode(',', config::$config['image_resize_types'])));
691 |
692 | // serve original if !$is_resize_type || resize ratio < image_resize_min_ratio
693 | if((!$is_resize_type || $resize_ratio < max(config::$config['image_resize_min_ratio'], 1)) && !read_file($path, $info['mime'], 'Original image served', $header_props, true, $clone)) error('File does not exist.', 404);
694 |
695 | // Calculate new image dimensions.
696 | $resize_width = round($info[0] / $resize_ratio);
697 | $resize_height = round($info[1] / $resize_ratio);
698 |
699 | // memory
700 | $memory_limit = config::$config['image_resize_memory_limit'] && function_exists('ini_get') ? (int) @ini_get('memory_limit') : false;
701 | if($memory_limit && $memory_limit > -1){
702 | // $memory_required = ceil(($info[0] * $info[1] * 4 + $resize_width * $resize_height * 4) / 1048576);
703 | $memory_required = round(($info[0] * $info[1] * (isset($info['bits']) ? $info['bits'] / 8 : 1) * (isset($info['channels']) ? $info['channels'] : 3) * 1.33 + $resize_width * $resize_height * 4) / 1048576, 1);
704 | $new_memory_limit = function_exists('ini_set') ? max($memory_limit, config::$config['image_resize_memory_limit']) : $memory_limit;
705 | if($memory_required > $new_memory_limit) error('Resizing this image requires at least ' . $memory_required . 'M. Your current PHP memory_limit is ' . $new_memory_limit .'M.', 400);
706 | if($memory_limit < $new_memory_limit && @ini_set('memory_limit', $new_memory_limit . 'M')) $header_props .= ', ' . $memory_limit . 'M => ' . $new_memory_limit . 'M (min ' . $memory_required . 'M)';
707 | }
708 |
709 | // new dimensions headers
710 | $header_props .= ', ' . $resize_width . 'x' . $resize_height;
711 |
712 | // create new $image
713 | $image = image_create_from($path, $info[2]);
714 | if(!$image) error('Failed to create image resource.', 500);
715 |
716 | // Create final image with new dimensions.
717 | $new_image = imagecreatetruecolor($resize_width, $resize_height);
718 | if(!call_user_func(config::$config['image_resize_function'], $new_image, $image, 0, 0, 0, 0, $resize_width, $resize_height, $info[0], $info[1])) error('Failed to resize image.', 500);
719 |
720 | // destroy original $image resource
721 | imagedestroy($image);
722 |
723 | // exif orientation
724 | $exif = function_exists('exif_read_data') ? @exif_read_data($path) : false;
725 | if(!empty($exif) && is_array($exif) && isset($exif['Orientation']) && exif_orientation($exif['Orientation'], $new_image)) $header_props .= ', orientated from EXIF:' . $exif['Orientation'];
726 |
727 | // sharpen resized image
728 | if(config::$config['image_resize_sharpen']) sharpen_image($new_image);
729 |
730 | // save to cache
731 | if($cache){
732 | if(!imagejpeg($new_image, $cache, config::$config['image_resize_quality'])) error('imagejpeg() failed to create and cache resized image.', 500);
733 |
734 | // clone cache (used for folder previews)
735 | if($clone) @copy($cache, $clone);
736 |
737 | // cache disabled / direct output
738 | } else {
739 | set_cache_headers();
740 | header('content-type: image/jpeg');
741 | header('files-msg: Resized image served [' . $header_props . ', ' . header_memory_time() . ']');
742 | if(!imagejpeg($new_image, null, config::$config['image_resize_quality'])) error('imagejpeg() failed to create and output resized image.', 500);
743 | }
744 |
745 | // destroy image
746 | imagedestroy($new_image);
747 |
748 | // cache readfile
749 | if($cache && !read_file($cache, null, 'Resized image cached and served', $header_props, true, $clone)) error('Cache file does not exist.', 404);
750 |
751 | //
752 | exit;
753 | // https://github.com/maxim/smart_resize_image/blob/master/smart_resize_image.function.php
754 | // https://github.com/gavmck/resize/blob/master/php/lib/resize-class.php
755 | // https://github.com/gumlet/php-image-resize/blob/master/lib/ImageResize.php
756 | // https://www.bitrepository.com/resize-an-image-keeping-its-aspect-ratio-using-php-and-gd.html
757 | }
758 |
759 | function get_url_path($dir){
760 | if(!is_within_docroot($dir)) return false;
761 |
762 | // if in __dir__ path, __dir__ relative
763 | if(is_within_path($dir, config::$__dir__)) return $dir === config::$__dir__ ? '.' : substr($dir, strlen(config::$__dir__) + 1);
764 |
765 | // doc root, doc root relative
766 | return $dir === config::$doc_root ? '/' : substr($dir, strlen(config::$doc_root));
767 | }
768 |
769 | // get dir
770 | function get_dir($path, $files = false, $json_url = false){
771 |
772 | // realpath
773 | $realpath = $path ? real_path($path) : false;
774 | if(!$realpath) return; // no real path for any reason
775 | $symlinked = $realpath !== $path; // path is symlinked at some point
776 |
777 | // exclude
778 | if(is_exclude($path, true, $symlinked)) return; // exclude
779 | if($symlinked && is_exclude($realpath, true, $symlinked)) return; // exclude check again symlink realpath
780 |
781 | // vars
782 | $filemtime = filemtime($realpath);
783 | $url_path = get_url_path($realpath) ?: ($symlinked ? get_url_path($path) : false);
784 | $is_readable = is_readable($realpath);
785 |
786 | // array
787 | $arr = array(
788 | 'basename' => basename($realpath) ?: basename($path) ?: '',
789 | 'fileperms' => substr(sprintf('%o', fileperms($realpath)), -4),
790 | 'filetype' => 'dir',
791 | 'is_readable' => $is_readable,
792 | 'is_writeable' => is_writeable($realpath),
793 | 'is_link' => $symlinked ? is_link($path) : false,
794 | 'is_dir' => true,
795 | 'mime' => 'directory',
796 | 'mtime' => $filemtime,
797 | 'path' => root_relative($path)
798 | );
799 |
800 | // url path
801 | if($url_path) $arr['url_path'] = $url_path;
802 |
803 | // get_files() || config::menu_load_all
804 | if($files && $is_readable) {
805 |
806 | // files array
807 | $arr['files'] = get_files_data($path, $url_path, $arr['dirsize'], $arr['files_count'], $arr['images_count'], $arr['preview']);
808 |
809 | // download_dir cache direct access to zip / better caching and no need to access PHP / only works when download_dir_cache === 'dir'
810 | /*if($url_path && config::$config['download_dir'] === 'zip' && config::$config['download_dir_cache'] === 'dir') {
811 | $zip = $realpath . '/_files.zip';
812 | if(file_exists($zip) && filemtime($zip) >= $filemtime) $arr['zip'] = get_url_path($zip);
813 | }*/
814 | }
815 |
816 | // json cache path
817 | if($json_url && config::$storage_is_within_doc_root && !config::$has_login && config::$config['cache']){
818 | $json_cache = get_json_cache_url(get_dir_cache_hash($realpath, $filemtime));
819 | if($json_cache) $arr['json_cache'] = $json_cache;
820 | }
821 |
822 | //
823 | return $arr;
824 | }
825 |
826 | // get menu sort
827 | function get_menu_sort($dirs){
828 | if(strpos(config::$config['menu_sort'], 'date') === 0){
829 | usort($dirs, function($a, $b) {
830 | return filemtime($a) - filemtime($b);
831 | });
832 | } else {
833 | natcasesort($dirs);
834 | }
835 | return substr(config::$config['menu_sort'], -4) === 'desc' ? array_reverse($dirs) : $dirs;
836 | }
837 |
838 | // recursive directory scan
839 | function get_dirs($path = false, &$arr = array(), $depth = 0) {
840 |
841 | // get this dir (ignore root, unless load all ... root already loaded into page)
842 | if($depth || config::$config['menu_load_all']) {
843 | $data = get_dir($path, config::$config['menu_load_all'], !config::$config['menu_load_all']);
844 | if(!$data) return $arr;
845 |
846 | //
847 | $arr[] = $data;
848 |
849 | // max depth
850 | if(config::$config['menu_max_depth'] && $depth >= config::$config['menu_max_depth']) return $arr;
851 |
852 | // don't recursive if symlink
853 | if($data['is_link'] && !config::$config['menu_recursive_symlinks']) return $arr;
854 | }
855 |
856 | // get dirs from files array if $data['files'] or glob subdirs
857 | $subdirs = isset($data['files']) ? array_filter(array_map(function($file){
858 | return $file['filetype'] === 'dir' ? root_absolute($file['path']) : false;
859 | }, $data['files'])) : glob($path . '/*', GLOB_NOSORT|GLOB_ONLYDIR);
860 |
861 | // sort and loop subdirs
862 | if(!empty($subdirs)) foreach(get_menu_sort($subdirs) as $subdir) get_dirs($subdir, $arr, $depth + 1);
863 |
864 | // return
865 | return $arr;
866 | }
867 |
868 | // encode to UTF-8 when required
869 | function safe_iptc_tag($val){
870 | $val = @substr($val, 0, 1000);
871 | return @mb_detect_encoding($val, 'UTF-8', true) ? $val : @utf8_encode($val);
872 | }
873 |
874 | // get IPTC
875 | function get_iptc($image_info){
876 | if(!$image_info || !isset($image_info['APP13']) || !function_exists('iptcparse')) return;
877 | $app13 = @iptcparse($image_info['APP13']);
878 | if(empty($app13)) return;
879 | $iptc = array();
880 |
881 | // loop title, headline, description, creator, credit, copyright, keywords, city, sub-location and province-state
882 | foreach (['title'=>'005', 'headline'=>'105', 'description'=>'120', 'creator'=>'080', 'credit'=>'110', 'copyright'=>'116', 'keywords'=>'025', 'city'=>'090', 'sub-location'=>'092', 'province-state'=>'095'] as $name => $code) {
883 | if(isset($app13['2#' . $code][0]) && !empty($app13['2#' . $code][0])) $iptc[$name] = $name === 'keywords' ? $app13['2#' . $code] : safe_iptc_tag($app13['2#' . $code][0]);
884 | }
885 |
886 | // return IPTC
887 | return $iptc;
888 | }
889 |
890 | // get exif
891 | function get_exif($path){
892 | if(!function_exists('exif_read_data')) return;
893 | $exif_data = @exif_read_data($path, 'ANY_TAG', 0); // @exif_read_data($path);
894 | if(empty($exif_data) || !is_array($exif_data)) return;
895 | $exif = array();
896 | foreach (array('DateTime', 'DateTimeOriginal', 'ExposureTime', 'FNumber', 'FocalLength', 'Make', 'Model', 'Orientation', 'ISOSpeedRatings', 'Software') as $name) {
897 | if(isset($exif_data[$name])) $exif[$name] = trim($exif_data[$name]);
898 | }
899 | if(isset($exif['DateTime'])) $exif['DateTime'] = @strtotime($exif['DateTime']);
900 | if(isset($exif['DateTimeOriginal'])) $exif['DateTimeOriginal'] = @strtotime($exif['DateTimeOriginal']);
901 |
902 | /*LensInfo 24-70mm f/?
903 | Lens EF24-70mm f/2.8L USM
904 | LensID 230*/
905 |
906 | // ApertureFNumber (f_stop)
907 | if(isset($exif_data['COMPUTED']['ApertureFNumber'])) $exif['ApertureFNumber'] = $exif_data['COMPUTED']['ApertureFNumber'];
908 |
909 | // flash
910 | if(isset($exif_data['Flash'])) $exif['Flash'] = ($exif_data['Flash'] & 1) != 0;
911 |
912 | // GPS
913 | $gps = get_image_location($exif_data);
914 | if(!empty($gps)) $exif['gps'] = $gps;
915 |
916 | // return
917 | return $exif;
918 | }
919 |
920 | // exif GPS / get_image_location
921 | function get_image_location($exif) {
922 | $arr = array();
923 | foreach (array('GPSLatitude', 'GPSLongitude') as $key) {
924 | if(!isset($exif[$key]) || !isset($exif[$key.'Ref'])) return false;
925 | $coordinate = $exif[$key];
926 | if(is_string($coordinate)) $coordinate = array_map('trim', explode(',', $coordinate));
927 | for ($i = 0; $i < 3; $i++) {
928 | $part = explode('/', $coordinate[$i]);
929 | if (count($part) == 1) {
930 | $coordinate[$i] = $part[0];
931 | } else if (count($part) == 2) {
932 | if($part[1] == 0) return false; // can't be 0 / invalid GPS
933 | $coordinate[$i] = floatval($part[0])/floatval($part[1]);
934 | } else {
935 | $coordinate[$i] = 0;
936 | }
937 | }
938 | list($degrees, $minutes, $seconds) = $coordinate;
939 | $sign = ($exif[$key.'Ref'] == 'W' || $exif[$key.'Ref'] == 'S') ? -1 : 1;
940 | $arr[] = $sign * ($degrees + $minutes/60 + $seconds/3600);
941 | }
942 | return $arr;
943 | }
944 |
945 | /*function get_image_location($exif){
946 | $arr = array('GPSLatitudeRef', 'GPSLatitude', 'GPSLongitudeRef', 'GPSLongitude');
947 | foreach ($arr as $val) {
948 | if(!isset($exif[$val])) return false;
949 | }
950 |
951 | $GPSLatitudeRef = $exif[$arr[0]];
952 | $GPSLatitude = $exif[$arr[1]];
953 | $GPSLongitudeRef= $exif[$arr[2]];
954 | $GPSLongitude = $exif[$arr[3]];
955 |
956 | $lat_degrees = count($GPSLatitude) > 0 ? gps2Num($GPSLatitude[0]) : 0;
957 | $lat_minutes = count($GPSLatitude) > 1 ? gps2Num($GPSLatitude[1]) : 0;
958 | $lat_seconds = count($GPSLatitude) > 2 ? gps2Num($GPSLatitude[2]) : 0;
959 |
960 | $lon_degrees = count($GPSLongitude) > 0 ? gps2Num($GPSLongitude[0]) : 0;
961 | $lon_minutes = count($GPSLongitude) > 1 ? gps2Num($GPSLongitude[1]) : 0;
962 | $lon_seconds = count($GPSLongitude) > 2 ? gps2Num($GPSLongitude[2]) : 0;
963 |
964 | $lat_direction = ($GPSLatitudeRef == 'W' or $GPSLatitudeRef == 'S') ? -1 : 1;
965 | $lon_direction = ($GPSLongitudeRef == 'W' or $GPSLongitudeRef == 'S') ? -1 : 1;
966 |
967 | $latitude = $lat_direction * ($lat_degrees + ($lat_minutes / 60) + ($lat_seconds / (60*60)));
968 | $longitude = $lon_direction * ($lon_degrees + ($lon_minutes / 60) + ($lon_seconds / (60*60)));
969 |
970 | return array($latitude, $longitude);
971 | }
972 |
973 | function gps2Num($coordPart){
974 | $parts = explode('/', $coordPart);
975 | if(count($parts) <= 0) return 0;
976 | if(count($parts) == 1) return $parts[0];
977 | if($parts[1] == 0) return 0;
978 | return floatval($parts[0]) / floatval($parts[1]);
979 | }*/
980 |
981 | //
982 | function get_files_data($dir, $url_path = false, &$dirsize = 0, &$files_count = 0, &$images_count = 0, &$preview = false){
983 |
984 | // scandir
985 | $filenames = scandir($dir, SCANDIR_SORT_NONE);
986 | if(empty($filenames)) return array();
987 | $items = array();
988 |
989 | // look for folder_preview_default (might be excluded in loop)
990 | if(config::$config['folder_preview_default'] && in_array(config::$config['folder_preview_default'], $filenames)) $preview = config::$config['folder_preview_default'];
991 |
992 | // loop filenames
993 | foreach($filenames as $filename) {
994 |
995 | //
996 | if($filename === '.' || $filename === '..') continue;
997 | $path = $dir . '/' . $filename;
998 |
999 | // paths
1000 | $realpath = real_path($path); // differs from $path only if is symlinked
1001 | if(!$realpath) continue; // no real path for any reason, for example symlink dead
1002 | $symlinked = $realpath !== $path; // path is symlinked at some point
1003 |
1004 | // filetype
1005 | $filetype = filetype($realpath);
1006 | $is_dir = $filetype === 'dir' ? true : false;
1007 |
1008 | // exclude
1009 | if(is_exclude($path, $is_dir, $symlinked)) continue; // exclude
1010 | if($symlinked && is_exclude($realpath, $is_dir, $symlinked)) continue; // exclude check again symlink realpath
1011 |
1012 | // vars
1013 | if(!$is_dir) $files_count ++; // files count
1014 | $is_link = $symlinked ? is_link($path) : false; // symlink
1015 | $basename = $is_link ? (basename($realpath) ?: $filename) : $filename;
1016 | $filemtime = filemtime($realpath);
1017 | $is_readable = is_readable($realpath);
1018 | $filesize = $is_dir ? false : filesize($realpath);
1019 | if($filesize) $dirsize += $filesize;
1020 |
1021 | // url_path / symlink
1022 | $item_url_path = $symlinked ? get_url_path($realpath) : false; // url_path from realpath if symlinked
1023 | if(!$item_url_path && $url_path) $item_url_path = $url_path . ($url_path === '/' ? '' : '/') . ($is_link ? basename($path) : $basename);
1024 |
1025 | // root path // path relative to config::$root
1026 | if(!$symlinked || is_within_root($realpath)){
1027 | $root_path = root_relative($realpath);
1028 |
1029 | // path is symlinked and !is_within_root(), get path-relative
1030 | } else {
1031 |
1032 | // root path to symlink
1033 | $root_path = root_relative($path);
1034 |
1035 | // check for symlink loop
1036 | if($is_link && $is_dir && $path && $root_path) {
1037 | $basename_path = basename($root_path);
1038 | if($basename_path && preg_match('/(\/|^)' . $basename_path. '\//', $root_path)){
1039 | $loop_path = '';
1040 | $segments = explode('/', $root_path);
1041 | array_pop($segments);
1042 | foreach ($segments as $segment) {
1043 | $loop_path .= ($loop_path ? '/' : '') . $segment;
1044 | if($segment !== $basename_path) continue;
1045 | $loop_abs_path = root_absolute($loop_path);
1046 | if(!is_link($loop_abs_path) || $realpath !== real_path($loop_abs_path)) continue;
1047 | $root_path = $loop_path;
1048 | $item_url_path = get_url_path($loop_abs_path) ?: $item_url_path; // new symlink is within doc_root
1049 | break;
1050 | }
1051 | }
1052 | }
1053 | }
1054 |
1055 | // add properties
1056 | $item = array(
1057 | 'basename' => $basename,
1058 | 'fileperms' => substr(sprintf('%o', fileperms($realpath)), -4),
1059 | 'filetype' => $filetype,
1060 | 'filesize' => $filesize,
1061 | 'is_readable' => $is_readable,
1062 | 'is_writeable' => is_writeable($realpath),
1063 | 'is_link' => $is_link,
1064 | 'is_dir' => $is_dir,
1065 | 'mtime' => $filemtime,
1066 | 'path' => $root_path
1067 | );
1068 |
1069 | // optional props
1070 | //$ext = !$is_dir ? pathinfo($realpath, PATHINFO_EXTENSION) : false;
1071 | $ext = !$is_dir ? substr(strrchr($realpath, '.'), 1) : false;
1072 | if($ext) {
1073 | $ext = strtolower($ext);
1074 | $item['ext'] = $ext;
1075 | }
1076 | $mime = $is_dir ? 'directory' : ($is_readable && (!$ext || $ext === 'ts' || config::$config['get_mime_type']) ? get_mime($realpath) : false);
1077 | if($mime) $item['mime'] = $mime;
1078 | if($item_url_path) $item['url_path'] = $item_url_path;
1079 |
1080 | // image / check from mime, fallback to extension
1081 | $is_image = $is_dir ? false : ($mime ? (strtok($mime, '/') === 'image' && !strpos($mime, 'svg')) : in_array($ext, array('gif','jpg','jpeg','jpc','jp2','jpx','jb2','png','swf','psd','bmp','tiff','tif','wbmp','xbm','ico','webp')));
1082 | if($is_image){
1083 |
1084 | // imagesize
1085 | $imagesize = $is_readable ? @getimagesize($realpath, $info) : false;
1086 |
1087 | // image count and icon
1088 | $images_count ++;
1089 | $item['icon'] = 'image';
1090 |
1091 | // is imagesize
1092 | if(!empty($imagesize) && is_array($imagesize)){
1093 |
1094 | // set folder_preview
1095 | if(!$preview && in_array($ext, array('gif','jpg','jpeg','png'))) $preview = $basename;
1096 |
1097 | // start image array
1098 | $image = array();
1099 | foreach (array(0 => 'width', 1 => 'height', 2 => 'type', 'bits' => 'bits', 'channels' => 'channels', 'mime' => 'mime') as $key => $name) if(isset($imagesize[$key])) $image[$name] = $imagesize[$key];
1100 |
1101 | // mime from image
1102 | if(!$mime && isset($image['mime'])) $item['mime'] = $image['mime'];
1103 |
1104 | // IPTC
1105 | $iptc = $info ? get_iptc($info) : false;
1106 | if(!empty($iptc)) $image['iptc'] = $iptc;
1107 |
1108 | // EXIF
1109 | $exif = get_exif($realpath);
1110 | if(!empty($exif)) {
1111 | $image['exif'] = $exif;
1112 | if(isset($exif['DateTimeOriginal'])) $item['DateTimeOriginal'] = $exif['DateTimeOriginal'];
1113 | // invert width/height if exif orientation
1114 | if(isset($exif['Orientation']) && $exif['Orientation'] > 4 && $exif['Orientation'] < 9){
1115 | $image['width'] = $imagesize[1];
1116 | $image['height'] = $imagesize[0];
1117 | }
1118 | }
1119 |
1120 | // image resize cache direct
1121 | if(config::$image_resize_cache_direct){
1122 | $resize1 = get_image_cache_path($realpath, config::$config['image_resize_dimensions'], $filesize, $filemtime);
1123 | if(file_exists($resize1)) $image['resize' . config::$config['image_resize_dimensions']] = get_url_path($resize1);
1124 | $retina = config::$image_resize_dimensions_retina;
1125 | if($retina){
1126 | $resize2 = get_image_cache_path($realpath, $retina, $filesize, $filemtime);
1127 | if(file_exists($resize2)) $image['resize' . $retina] = get_url_path($resize2);
1128 | }
1129 | }
1130 |
1131 | // add image to item
1132 | $item['image'] = $image;
1133 |
1134 | // get real mime if getimagesize fails. Could be non-image disguised as image extension
1135 | } else if($is_readable && !$mime){
1136 | $mime = get_mime($realpath);
1137 | if($mime) {
1138 | $item['mime'] = $mime;
1139 | if(strtok($mime, '/') !== 'image'){ // unset images_count and icon because is not image after all
1140 | $images_count --;
1141 | unset($item['icon']);
1142 | }
1143 | }
1144 | }
1145 | }
1146 |
1147 | // add to items with basename as key
1148 | $items[$basename] = $item;
1149 | }
1150 |
1151 | // Sort dirs on top and natural case sort / need to do in JS anyway
1152 | uasort($items, function($a, $b){
1153 | if(!config::$config['sort_dirs_first'] || $a['is_dir'] === $b['is_dir']) return strnatcasecmp($a['basename'], $b['basename']);
1154 | return $b['is_dir'] ? 1 : -1;
1155 | });
1156 |
1157 | //
1158 | return $items;
1159 | }
1160 |
1161 | // get files
1162 | function get_files($dir){
1163 |
1164 | // invalid $dir
1165 | if(!$dir) json_error('Invalid directory');
1166 |
1167 | // cache
1168 | $cache = get_dir_cache_path(real_path($dir));
1169 |
1170 | // read cache or get dir and cache
1171 | if(!read_file($cache, 'application/json', 'files json served from cache')) {
1172 | json_cache(get_dir($dir, true), 'files json created' . ($cache ? ' and cached' : ''), $cache);
1173 | }
1174 | }
1175 |
1176 | /* start here */
1177 | function post($param){
1178 | return isset($_POST[$param]) && !empty($_POST[$param]) ? $_POST[$param] : false;
1179 | }
1180 | function get($param){
1181 | return isset($_GET[$param]) && !empty($_GET[$param]) ? $_GET[$param] : false;
1182 | }
1183 | function json_cache($arr = array(), $msg = false, $cache = true){
1184 | $json = empty($arr) ? '{}' : json_encode($arr, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_PARTIAL_OUTPUT_ON_ERROR);
1185 | if(empty($json)) json_error(json_last_error() ? json_last_error_msg() : 'json_encode() error');
1186 | if($cache) @file_put_contents($cache, $json);
1187 | if($msg) header('files-msg: ' . $msg . ' [' . header_memory_time() . ']');
1188 | header('content-type: application/json');
1189 | echo $json;
1190 | }
1191 | function json_error($error = 'Error'){
1192 | json_exit(array('error' => $error));
1193 | }
1194 | function json_success($success = 'Success'){
1195 | json_exit(array('success' => $success));
1196 | }
1197 | function json_toggle($success, $error){
1198 | json_exit(array_filter(array('success' => $success, 'error' => empty($success) ? $error : 0)));
1199 | }
1200 | function json_exit($arr = array()){
1201 | header('content-type: application/json');
1202 | exit(json_encode($arr));
1203 | }
1204 | function error($msg, $code = false){
1205 | // 400 Bad Request, 403 Forbidden, 401 Unauthorized, 404 Not Found, 500 Internal Server Error
1206 | if($code) http_response_code($code);
1207 | header('content-type: text/html');
1208 | header('Expires: ' . gmdate('D, d M Y H:i:s') . ' GMT');
1209 | header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0, s-maxage=0');
1210 | header('Cache-Control: post-check=0, pre-check=0', false);
1211 | header('Pragma: no-cache');
1212 | exit('Error
' . $msg);
1213 | }
1214 |
1215 | // get valid menu cache
1216 | function get_valid_menu_cache($cache){
1217 | if(!$cache || !file_exists($cache)) return;
1218 | $json = @file_get_contents($cache);
1219 | if(empty($json)) return;
1220 | if(!config::$config['menu_cache_validate']) return $json;
1221 | $arr = @json_decode($json, true);
1222 | if(empty($arr)) return;
1223 | foreach ($arr as $key => $val) {
1224 | $path = $val['path'];
1225 | if(strpos($path, '/') !== false && $val['mtime'] !== @filemtime(root_absolute($path))) return; // skip shallow 1st level dirs, and compare filemtime
1226 | }
1227 | return $json;
1228 | }
1229 |
1230 | // get root dirs
1231 | function get_root_dirs(){
1232 | $root_dirs = glob(config::$root . '/*', GLOB_ONLYDIR|GLOB_NOSORT);
1233 | if(empty($root_dirs)) return array();
1234 | return array_filter($root_dirs, function($dir){
1235 | return !is_exclude($dir, true, is_link($dir));
1236 | });
1237 | }
1238 |
1239 | // get menu cache hash
1240 | function get_menu_cache_hash($root_dirs){
1241 | $mtime_count = filemtime(config::$root);
1242 | foreach ($root_dirs as $root_dir) $mtime_count += filemtime($root_dir);
1243 | return substr(md5(config::$doc_root . config::$__dir__ . config::$root), 0, 6) . '.' . substr(md5(config::$version . config::$config['cache_key'] . config::$config['menu_max_depth'] . config::$config['menu_load_all'] . (config::$config['menu_load_all'] ? config::$config['files_exclude'] . config::$image_resize_cache_direct : '') . config::$has_login . config::$config['dirs_exclude'] . config::$config['menu_sort']), 0, 6) . '.' . $mtime_count;
1244 | }
1245 |
1246 | // get dirs
1247 | function dirs(){
1248 |
1249 | // get menu_cache_hash
1250 | if(config::$config['cache']){
1251 | $menu_cache_hash = post('menu_cache_hash'); // get menu cache hash
1252 | $menu_cache_arr = $menu_cache_hash ? explode('.', $menu_cache_hash) : false;
1253 | if(!$menu_cache_arr ||
1254 | count($menu_cache_arr) !== 3 ||
1255 | strlen($menu_cache_arr[0]) !== 6 ||
1256 | strlen($menu_cache_arr[1]) !== 6 ||
1257 | !is_numeric($menu_cache_arr[2])
1258 | ) json_error('Invalid menu cache hash'); // early exit
1259 | }
1260 | $cache = config::$config['cache'] ? config::$cache_path . '/menu/' . $menu_cache_hash . '.json' : false; // get cache path
1261 | $json = $cache ? get_valid_menu_cache($cache) : false; // get valid json menu cache
1262 |
1263 | // $json is valid from menu cache file
1264 | if($json){
1265 | header('content-type: application/json');
1266 | header('files-msg: valid menu cache hash [' . $menu_cache_hash . ']' . (!config::$config['menu_cache_validate'] ? '[deep validation disabled]' : '') . '[' . header_memory_time() . ']');
1267 | echo (post('localstorage') ? '{"localstorage":"1"}' : $json);
1268 |
1269 | // reload dirs
1270 | } else {
1271 | json_cache(get_dirs(config::$root), 'dirs reloaded' . ($cache ? ' and cached.' : ' [cache disabled]'), $cache);
1272 | }
1273 | }
1274 |
1275 | // include file html, php, css, js
1276 | function get_include($file){
1277 | if(!config::$storage_path) return;
1278 | $path = config::$storage_path . '/' . $file;
1279 | if(!file_exists($path)) return;
1280 | $ext = pathinfo($path, PATHINFO_EXTENSION);
1281 | if(in_array($ext, ['html', 'php'])) return include $path;
1282 | if(!config::$storage_is_within_doc_root) return;
1283 | $src = get_url_path($path) . '?' . filemtime($path);
1284 | if($ext === 'js') echo '';
1285 | if($ext === 'css') echo '';
1286 | }
1287 |
1288 | // POST
1289 | if(post('action')){
1290 |
1291 | // post action
1292 | $action = post('action');
1293 |
1294 | //
1295 | new config();
1296 |
1297 | // filemanager actions [beta]
1298 | if($action === 'fm') {
1299 |
1300 | // validate task
1301 | $task = post('task');
1302 | if(empty($task) || !isset(config::$config['allow_' . $task]) || !config::$config['allow_' . $task]) json_error('invalid task');
1303 | // demo_mode
1304 | if(config::$config['demo_mode']) json_error('Action not allowed in demo mode');
1305 | // license required for file manager action
1306 | //if(!config::$config['license_key']) json_error('License required!');
1307 |
1308 | // valid path / path must be inside assigned root
1309 | $is_dir = post('is_dir');
1310 | $path = valid_root_path(post('path'), $is_dir);
1311 | if(empty($path)) json_error('invalid path ' . post('path'));
1312 | $path = real_path($path); // in case of symlink path
1313 |
1314 | // name_is_allowed / trim name, fail if empty or dodgy characters, mkfile, mkdir, rename, duplicate
1315 | function name_is_allowed($name){
1316 | $name = $name ? trim($name) : false; // trim
1317 | // block empty / <>:"'/\|?*# chars / .. / endswith .
1318 | if(empty($name) || preg_match('/[<>:"\'\/\\\|?*#]|\.\.|\.$/', $name)) json_error('invalid name ' . $name);
1319 | return $name; // return valid trimmed name
1320 | }
1321 |
1322 | // filemanager json_toggle
1323 | function fm_json_toggle($success, $error){
1324 | fm_json_exit($success, array_filter(array('success' => $success, 'error' => empty($success) ? $error : 0)));
1325 | }
1326 | // filemanager json_exit / includes feature to invalidate X3 cache if $x3_path
1327 | function fm_json_exit($success, $arr){
1328 | if($success && config::$x3_path) touch(config::$x3_path . '/app/x3.inc.php');
1329 | json_exit($arr);
1330 | }
1331 |
1332 | // UPLOAD
1333 | if($task === 'upload'){
1334 | // upload path must be dir
1335 | if(!$is_dir) json_error('invalid dir ' . post('path'));
1336 | // upload path must be writeable
1337 | if(!is_writable($path)) json_error('upload dir ' . post('path') . ' is not writeable');
1338 | // get $_FILES['file']
1339 | $file = isset($_FILES) && isset($_FILES['file']) && is_array($_FILES['file']) ? $_FILES['file'] : false;
1340 | // invalid $_FILES['file']
1341 | if(empty($file) || !isset($file['error']) || is_array($file['error'])) json_error('invalid $_FILES[]');
1342 | // PHP meaningful file upload errors / https://www.php.net/manual/en/features.file-upload.errors.php
1343 | if($file['error'] !== 0) {
1344 | $upload_errors = array(
1345 | 1 => 'Uploaded file exceeds upload_max_filesize directive in php.ini',
1346 | 2 => 'Uploaded file exceeds MAX_FILE_SIZE directive specified in the HTML form',
1347 | 3 => 'The uploaded file was only partially uploaded',
1348 | 4 => 'No file was uploaded',
1349 | 6 => 'Missing a temporary folder',
1350 | 7 => 'Failed to write file to disk.',
1351 | 8 => 'A PHP extension stopped the file upload.'
1352 | );
1353 | json_error(isset($upload_errors[$file['error']]) ? $upload_errors[$file['error']] : 'unknown error');
1354 | }
1355 | // invalid $file['size']
1356 | if(!isset($file['size']) || empty($file['size'])) json_error('invalid file size');
1357 | // $file['size'] must not exceed $config['upload_max_filesize']
1358 | if(config::$config['upload_max_filesize'] && $file['size'] > config::$config['upload_max_filesize']) json_error('File size [' . $file['size'] . '] exceeds upload_max_filesize option [' . config::$config['upload_max_filesize'] . ']');
1359 | // filename
1360 | $filename = $file['name'];
1361 | // security: slashes are never ever allowed in filenames / always basenamed() but just in case
1362 | if(strpos($filename, '/') !== false || strpos($filename, '\\') !== false) json_error('Illegal \slash/ in filename ' . $filename);
1363 | // allow only valid file types from config::$config['upload_allowed_file_types'] / 'image/*, .pdf, .mp4'
1364 | $allowed_file_types = !empty(config::$config['upload_allowed_file_types']) ? array_filter(array_map('trim', explode(',', config::$config['upload_allowed_file_types']))) : false;
1365 | if(!empty($allowed_file_types)){
1366 | $mime = get_mime($file['tmp_name']) ?: $file['type']; // mime from PHP or upload[type]
1367 | $ext = strrchr(strtolower($filename), '.');
1368 | $is_valid = false;
1369 | // check if extension match || wildcard match mime type image/*
1370 | foreach ($allowed_file_types as $allowed_file_type) if($ext === ('.'.ltrim($allowed_file_type, '.')) || fnmatch($allowed_file_type, $mime)) {
1371 | $is_valid = true;
1372 | break;
1373 | }
1374 | if(!$is_valid) json_error('invalid file type ' . $filename);
1375 | // extra security: check if image is image
1376 | if(function_exists('exif_imagetype') && in_array($ext, ['.gif', '.jpeg', '.jpg', '.png', '.swf', '.psd', '.bmp', '.tif', '.tiff', 'webp']) && !@exif_imagetype($file['tmp_name'])) json_error('invalid image type ' . $filename);
1377 | }
1378 |
1379 | // file naming if !overwrite and file exists
1380 | if(config::$config['upload_exists'] !== 'overwrite' && file_exists("$path/$filename")){
1381 |
1382 | // fail if !increment / 'upload_exists' => 'fail' || false || '' empty
1383 | if(config::$config['upload_exists'] !== 'increment') json_error("$filename already exists");
1384 |
1385 | // increment filename / 'upload_exists' => 'increment'
1386 | $name = pathinfo($filename, PATHINFO_FILENAME);
1387 | $ext = pathinfo($filename, PATHINFO_EXTENSION);
1388 | $inc = 1;
1389 | while(file_exists($path . '/' . $name . '-' . $inc . '.' . $ext)) $inc ++;
1390 | $filename = $name . '-' . $inc . '.' . $ext;
1391 | }
1392 |
1393 | // all is well! attempt to move_uploaded_file()
1394 | if(@move_uploaded_file($file['tmp_name'], "$path/$filename")) fm_json_exit(true, array(
1395 | 'success' => true,
1396 | 'filename' => $filename, // return filename in case it was incremented or renamed
1397 | 'url' => get_url_path("$path/$filename") // for usage with showLinkToFileUploadResult
1398 | ));
1399 |
1400 | // error if failed to move uploaded file
1401 | json_error('failed to move_uploaded_file()');
1402 |
1403 | // DELETE
1404 | } else if($task === 'delete'){
1405 |
1406 | // dir recursive
1407 | if($is_dir){
1408 |
1409 | // success/fail count
1410 | $success = 0;
1411 | $fail = 0;
1412 |
1413 | // recursive rmdir
1414 | function rrmdir($dir, &$success, &$fail) {
1415 | //global $success, $fail;
1416 | if(!is_readable($dir)) return $fail ++;
1417 | $files = array_diff(scandir($dir), array('.','..'));
1418 | if(!empty($files)) foreach ($files as $file) {
1419 | is_dir("$dir/$file") ? rrmdir("$dir/$file", $success, $fail) : (@unlink("$dir/$file") ? $success++ : $fail++);
1420 | }
1421 | @rmdir($dir) ? $success ++ : $fail ++;
1422 | }
1423 |
1424 | // recursive rmdir start
1425 | rrmdir($path, $success, $fail);
1426 |
1427 | // response with partial success/fail count or error if there is !$success
1428 | fm_json_exit($success, array_filter(array('success' => $success, 'fail' => $fail, 'error' => (empty($success) ? 'Failed to delete dir' : 0))));
1429 |
1430 | // single file
1431 | } else {
1432 | fm_json_toggle(@unlink($path), 'PHP unlink() failed');
1433 | }
1434 |
1435 | // new_folder || new_file
1436 | } else if($task === 'new_folder' || $task === 'new_file'){
1437 | if(!$is_dir) json_error('invalid dir ' . post('path')); // parent path must be dir
1438 | if(!is_writable($path)) json_error(post('path') . ' is not writeable.'); // dir must be writeable
1439 | $name = name_is_allowed(post('name')); // trim and check valid
1440 | $file_path = $path . '/' . $name;
1441 | if(file_exists($file_path)) json_error($name . ' already exists');
1442 | fm_json_toggle($task === 'new_folder' ? @mkdir($file_path) : @touch($file_path), $task . ' failed');
1443 |
1444 | // rename $path (file or dir)
1445 | } else if($task === 'rename'){
1446 | if(!is_writable($path)) json_error(post('path') . ' is not writeable.'); // path must be writeable
1447 | $name = name_is_allowed(post('name')); // trim and check valid
1448 | $new_path = dirname($path) . '/' . $name;
1449 | if(file_exists($new_path)) json_error("$name already exists."); // new name exists
1450 | // security: prevent renaming 'file.html' to 'file.php' / file must already be *.php when renaming
1451 | if(!$is_dir && stripos($path, '.php') === false && stripos($name, '.php') !== false) json_error('cannot rename files to .php');
1452 | fm_json_toggle(@rename($path, $new_path), 'PHP rename() failed');
1453 |
1454 | // duplicate file
1455 | } else if($task === 'duplicate'){
1456 | if($is_dir) json_error('Can\'t duplicate dir');
1457 | $parent_dir = dirname($path);
1458 | if(!is_writable($parent_dir)) json_error(basename($parent_dir) . ' is not writeable.'); // dir must be writeable
1459 | $name = name_is_allowed(post('name')); // trim and check valid
1460 | $copy_path = $parent_dir . '/' . $name;
1461 | if(file_exists($copy_path)) json_error($name . ' already exists.');
1462 | fm_json_toggle(@copy($path, $copy_path), 'PHP copy() failed');
1463 |
1464 | // text / code edit
1465 | } else if($task === 'text_edit'){
1466 | if($is_dir) json_error('Can\'t write text to directory');
1467 | if(!is_writeable($path) || !is_file($path)) json_error('File is not writeable');
1468 | $success = isset($_POST['text']) && @file_put_contents($path, $_POST['text']) !== false ? 1 : 0; // text could be '' (empty)
1469 | if($success) @touch(dirname($path)); // invalidate any cache by updating parent dir mtime
1470 | fm_json_toggle($success, 'PHP file_put_contents() failed');
1471 | }
1472 |
1473 | // dirs
1474 | } else if($action === 'dirs'){
1475 | dirs(post('localstorage'));
1476 |
1477 | // files
1478 | } else if($action === 'files'){
1479 | if(!isset($_POST['dir'])) json_error('Missing dir parameter');
1480 | get_files(valid_root_path($_POST['dir'], true));
1481 |
1482 | // file read
1483 | } else if($action === 'file'){
1484 |
1485 | // valid path
1486 | $file = valid_root_path(post('file'));
1487 | if(!$file) error('Invalid file path');
1488 |
1489 | // read text file
1490 | header('content-type:text/plain;charset=utf-8');
1491 | if(@readfile(real_path($file)) === false) error('failed to read file ' . post('file'), 500);
1492 |
1493 | // check login
1494 | } else if($action === 'check_login'){
1495 | json_success(true);
1496 |
1497 | // check updates
1498 | } else if($action === 'check_updates'){
1499 | $json = @json_decode(@file_get_contents('https://data.jsdelivr.com/v1/package/npm/files.photo.gallery'), true);
1500 | $latest = !empty($json) && isset($json['versions'][0]) && version_compare($json['versions'][0], config::$version) > 0 ? $json['versions'][0] : false;
1501 | json_exit(array(
1502 | 'success' => $latest,
1503 | 'writeable' => $latest && is_writable(__FILE__) // only check writeable if $latest
1504 | ));
1505 |
1506 | // do update
1507 | } else if($action === 'do_update'){
1508 | $version = post('version');
1509 | if(!$version || version_compare($version, config::$version) <= 0 || !is_writable(__FILE__)) json_error(); // requirements
1510 | $get = @file_get_contents('https://cdn.jsdelivr.net/npm/files.photo.gallery@' . $version . '/index.php');
1511 | if(empty($get) || strpos($get, ' @file_put_contents(__FILE__, $get)));
1513 |
1514 | // store license
1515 | } else if($action === 'license'){
1516 | $key = post('key') ? trim(post('key')) : false;
1517 | json_exit(array(
1518 | 'success' => $key && config::$storage_config_realpath && config::save_config(array('license_key' => $key)),
1519 | 'md5' => $key ? md5($key) : false
1520 | ));
1521 |
1522 | // invalid action
1523 | } else {
1524 | json_error('invalid action: ' . $action);
1525 | }
1526 |
1527 | // GET
1528 | } else /*if($_SERVER['REQUEST_METHOD'] === 'GET')*/{
1529 |
1530 | // download_dir_zip / download files in directory as zip file
1531 | if(get('download_dir_zip')) {
1532 | new config();
1533 |
1534 | // check download_dir enabled
1535 | if(config::$config['download_dir'] !== 'zip') error('download_dir Zip disabled.', 403);
1536 |
1537 | // valid dir
1538 | $dir = valid_root_path(get('download_dir_zip'), true);
1539 | if(!$dir) error('Invalid download path ' . get('download_dir_zip') . '', 404);
1540 | $dir = real_path($dir); // in case of symlink path
1541 |
1542 | // create zip cache directly in dir (recommended, so that dir can be renamed while zip cache remains)
1543 | if(!config::$storage_path || config::$config['download_dir_cache'] === 'dir') {
1544 | if(!is_writable($dir)) error('Dir ' . basename($dir) . ' is not writeable.', 500);
1545 | $zip_file_name = '_files.zip';
1546 | $zip_file = $dir . '/' . $zip_file_name;
1547 |
1548 | // create zip file in storage _files/zip/$dirname.$md5.zip /
1549 | } else {
1550 | mkdir_or_error(config::$storage_path . '/zip');
1551 | $zip_file_name = basename($dir) . '.' . substr(md5($dir), 0, 6) . '.zip';
1552 | $zip_file = config::$storage_path . '/zip/' . $zip_file_name;
1553 | }
1554 |
1555 | // cached / download_dir_cache && file_exists() && zip is not older than dir time
1556 | $cached = !empty(config::$config['download_dir_cache']) && file_exists($zip_file) && filemtime($zip_file) >= filemtime($dir);
1557 |
1558 | // create zip if !cached
1559 | if(!$cached){
1560 |
1561 | // use shell zip command instead / probably faster and more robust than PHP / if use, comment out PHP ZipArchive method starting below
1562 | // exec('zip ' . $zip_file . ' ' . $dir . '/*.* -j -x _files*', $out, $res);
1563 |
1564 | // check that ZipArchive class exists
1565 | if(!class_exists('ZipArchive')) error('Missing PHP ZipArchive class.', 500);
1566 |
1567 | // glob files / must be readable / is_file / !symlink / !is_exclude
1568 | $files = array_filter(glob($dir. '/*', GLOB_NOSORT), function($file){
1569 | return is_readable($file) && is_file($file) && !is_link($file) && !is_exclude($file, false);
1570 | });
1571 |
1572 | // !no files available to zip
1573 | if(empty($files)) error('No files to zip!', 400);
1574 |
1575 | // new ZipArchive
1576 | $zip = new ZipArchive();
1577 |
1578 | // create new $zip_file
1579 | if($zip->open($zip_file, ZipArchive::CREATE | ZIPARCHIVE::OVERWRITE) !== true) error('Failed to create ZIP file ' . $zip_file_name . '.', 500);
1580 |
1581 | // add files to zip / flatten with basename()
1582 | foreach($files as $file) $zip->addFile($file, basename($file));
1583 |
1584 | // no files added (for some reason)
1585 | if(!$zip->numFiles) error('Could not add any files to ' . $zip_file_name . '.', 500);
1586 |
1587 | // close zip
1588 | $zip->close();
1589 |
1590 | // make sure created zip file exists / just in case
1591 | if(!file_exists($zip_file)) error('Zip file ' . $zip_file_name . ' does not exist.', 500);
1592 | }
1593 |
1594 | // redirect instead of readfile() / might be useful if readfile() fails and/or for caching and performance
1595 | /*$zip_url = get_url_path($zip_file);
1596 | if($zip_url){
1597 | header('Location:' . $zip_url . '?' . filemtime($dir), true, 302);
1598 | exit;
1599 | }*/
1600 |
1601 | // output headers
1602 | if(config::$has_login) {
1603 | header('cache-control: must-revalidate, post-check=0, pre-check=0');
1604 | header('cache-control: public');
1605 | header('expires: 0');
1606 | header('pragma: public');
1607 | } else {
1608 | set_cache_headers();
1609 | }
1610 | header('content-description: File Transfer');
1611 | header('content-disposition: attachment; filename="' . addslashes(basename($dir)) . '.zip"');
1612 | $content_length = filesize($zip_file);
1613 | header('content-length: ' . $content_length);
1614 | header('content-transfer-encoding: binary');
1615 | header('content-type: application/zip');
1616 | header('files-msg: [' . $zip_file_name . '][' . ($cached ? 'cached' : 'created') . ']');
1617 |
1618 | // ignore user abort so we can delete file also on download cancel
1619 | if(empty(config::$config['download_dir_cache'])) @ignore_user_abort(true);
1620 |
1621 | // clear output buffer for large files
1622 | while (ob_get_level()) ob_end_clean();
1623 |
1624 | // output zip readfile()
1625 | if(!readfile($zip_file)) error('Failed to readfile(' . $zip_file_name . ').', 500);
1626 |
1627 | // delete temp zip file if cache disable
1628 | if(empty(config::$config['download_dir_cache'])) @unlink($zip_file);
1629 |
1630 |
1631 | // folder preview image
1632 | } else if(get('preview')){
1633 | new config();
1634 |
1635 | // allow only if only if folder_preview_image + load_images + image_resize_enabled
1636 | foreach (['folder_preview_image', 'load_images', 'image_resize_enabled'] as $key) if(!config::$config[$key]) error('[' .$key . '] disabled.', 400);
1637 |
1638 | // get real path and validate
1639 | $path = valid_root_path(get('preview'), true); // make sure is valid dir
1640 | if(!$path) error('Invalid directory.', 404);
1641 |
1642 |
1643 | // 1. first check for default '_filespreview.jpg' inside dir
1644 | $default = config::$config['folder_preview_default'] ? $path . '/' . config::$config['folder_preview_default'] : false;
1645 | if($default && file_exists($default)) {
1646 | header('files-preview: folder_preview_default found [' . config::$config['folder_preview_default'] . ']');
1647 | resize_image($default, config::$config['image_resize_dimensions']);
1648 | }
1649 |
1650 |
1651 | // 2. check preview cache
1652 | $cache = config::$cache_path . '/images/preview.' . substr(md5($path), 0, 6) . '.jpg';
1653 |
1654 | // cache file exists
1655 | if(file_exists($cache)) {
1656 |
1657 | // make sure cache file is valid (must be newer than dir updated time)
1658 | if(filemtime($cache) >= filemtime($path)) read_file($cache, null, 'preview image served from cache', null, true);
1659 |
1660 | // delete expired cache file if is older than dir updated time [silent]
1661 | @unlink($cache);
1662 | }
1663 |
1664 |
1665 | // 3. glob images / GLOB_BRACE may fail on some non GNU systems, like Solaris.
1666 | $images = @glob($path . '/*.{jpg,JPG,jpeg,JPEG,png,PNG,gif,GIF}', GLOB_NOSORT|GLOB_BRACE);
1667 |
1668 | // loop images to locate first match that is not excluded
1669 | if(!empty($images)) foreach ($images as $image) {
1670 | if(!is_exclude($image, false)) {
1671 | header('files-preview: glob() found [' . basename($image) . ']');
1672 | resize_image($image, config::$config['image_resize_dimensions'], $cache); // + clone into $cache
1673 | break; exit; // just in case
1674 | }
1675 | }
1676 |
1677 |
1678 | // 4. nothing found (no images in dir)
1679 | // create empty 1px in $cache, and output (so next check knows dir is empty or has no images, unless updated)
1680 | if(imagejpeg(imagecreate(1, 1), $cache)) read_file($cache, 'image/jpeg', '1px placeholder image created and cached', null, true);
1681 |
1682 |
1683 | // file/image
1684 | } else if(isset($_GET['file'])){
1685 | new config();
1686 | get_file(valid_root_path(get('file')), get('resize'));
1687 |
1688 | // download
1689 | } else if(isset($_GET['download'])){
1690 | new config();
1691 |
1692 | // valid download
1693 | $download = valid_root_path(get('download'));
1694 | if(!$download) error('Invalid download path ' . get('download') . '', 404);
1695 | $download = real_path($download); // in case of symlink path
1696 |
1697 | // required for some browsers
1698 | if(@ini_get('zlib.output_compression')) @ini_set('zlib.output_compression', 'Off');
1699 |
1700 | // headers
1701 | header('Content-Description: File Transfer');
1702 | header('Content-Type: application/octet-stream');
1703 | header('Content-Disposition: attachment; filename="' . basename($download) . '"');
1704 | header('Content-Transfer-Encoding: binary');
1705 | header('Expires: 0');
1706 | header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
1707 | header('Pragma: public');
1708 | header('Content-Length: ' . filesize($download));
1709 | while (ob_get_level()) ob_end_clean();
1710 | readfile($download);
1711 |
1712 | // tasks plugin
1713 | } else if(get('task')){
1714 |
1715 | // new config with tests
1716 | new config(true);
1717 |
1718 | // get plugin
1719 | $tasks_path = config::$storage_path . '/plugins/tasks.php';
1720 | if(!file_exists($tasks_path)) error("Tasks plugin does not exist at $tasks_path", 404);
1721 | include $tasks_path;
1722 | exit;
1723 |
1724 | // main document
1725 | } else {
1726 |
1727 | // new config, with tests
1728 | new config(true);
1729 |
1730 | // validate exclude regex
1731 | if(config::$config['files_exclude'] && @preg_match(config::$config['files_exclude'], '') === false) error('Invalid files_exclude regex ' . config::$config['files_exclude'] . '');
1732 | if(config::$config['dirs_exclude'] && @preg_match(config::$config['dirs_exclude'], '') === false) error('Invalid dirs_exclude regex ' . config::$config['dirs_exclude'] . '');
1733 |
1734 | // start path
1735 | $start_path = config::$config['start_path'];
1736 | if($start_path){
1737 | $real_start_path = real_path($start_path);
1738 | if(!$real_start_path) error('start_path ' . $start_path . ' does not exist.');
1739 | if(!is_within_root($real_start_path)) error('start_path ' . $start_path . ' is not within root dir ' . config::$config['root']);
1740 | $start_path = root_relative($real_start_path);
1741 | }
1742 |
1743 | // root dirs (if menu)
1744 | $root_dirs = config::$config['menu_enabled'] || config::$config['breadcrumbs'] ? get_root_dirs() : false;
1745 | $menu_enabled = config::$config['menu_enabled'] && !empty($root_dirs) ? true : false;
1746 | $breadcrumbs = config::$config['breadcrumbs'] && !empty($root_dirs) ? true : false;
1747 |
1748 | // get menu cache hash
1749 | $menu_cache_hash = false;
1750 | $menu_cache_file = false;
1751 | if($menu_enabled){
1752 | $menu_cache_hash = get_menu_cache_hash($root_dirs);
1753 | // menu cache file (if cache, !menu_cache_validate, exists and is within doc root)
1754 | if(config::$storage_is_within_doc_root && config::$config['cache'] && !config::$config['menu_cache_validate']) {
1755 | $menu_cache_path = config::$cache_path . '/menu/' . $menu_cache_hash . '.json';
1756 | $menu_cache_file = file_exists($menu_cache_path) ? get_url_path($menu_cache_path) : false;
1757 | if($menu_cache_file) $menu_cache_file .= '?' . filemtime($menu_cache_path);
1758 | }
1759 | }
1760 |
1761 | // init path
1762 | $query = config::$config['history'] && isset($_SERVER['QUERY_STRING']) && !empty($_SERVER['QUERY_STRING']) ? explode('&', $_SERVER['QUERY_STRING']) : false;
1763 | $query_path = $query && strpos($query[0], '=') === false ? rtrim(rawurldecode($query[0]), '/') : false;
1764 | $query_path_valid = $query_path ? valid_root_path($query_path, true) : false;
1765 | $init_path = $query_path ?: $start_path ?: '';
1766 |
1767 | // init dirs, with files if cache
1768 | function get_dir_init($dir){
1769 | $cache = get_dir_cache_path(real_path($dir));
1770 | if(file_exists($cache)) return json_decode(file_get_contents($cache), true);
1771 | return get_dir($dir);
1772 | }
1773 |
1774 | // get dirs for root and start path
1775 | $dirs = array('' => get_dir_init(config::$root));
1776 | if($query_path){
1777 | if($query_path_valid) $dirs[$query_path] = get_dir_init($query_path_valid);
1778 | } else if($start_path){
1779 | $dirs[$start_path] = get_dir_init($real_start_path);
1780 | }
1781 |
1782 | // resize image types
1783 | $resize_image_types = array('jpeg', 'jpg', 'png', 'gif');
1784 | if(version_compare(PHP_VERSION, '5.4.0') >= 0) {
1785 | $resize_image_types[] = 'webp';
1786 | if(version_compare(PHP_VERSION, '7.2.0') >= 0) $resize_image_types[] = 'bmp';
1787 | }
1788 |
1789 | // image resize memory limit
1790 | $image_resize_memory_limit = config::$config['image_resize_enabled'] && config::$config['image_resize_memory_limit'] && function_exists('ini_get') ? (int) @ini_get('memory_limit') : 0;
1791 | if($image_resize_memory_limit && function_exists('ini_set')) $image_resize_memory_limit = max($image_resize_memory_limit, config::$config['image_resize_memory_limit']);
1792 |
1793 | // wtc
1794 | $wtc = config::$config[base64_decode('bGljZW5zZV9rZXk')];
1795 |
1796 | // look for custom language files _files/lang/*.json
1797 | function lang_custom() {
1798 | $dir = config::$storage_path ? config::$storage_path . '/lang' : false;
1799 | $files = $dir && file_exists($dir) ? glob($dir . '/*.json') : false;
1800 | if(empty($files)) return false;
1801 | $langs = array();
1802 | foreach ($files as $path) {
1803 | $json = @file_get_contents($path);
1804 | $data = !empty($json) ? @json_decode($json, true) : false;
1805 | if(!empty($data)) $langs[strtok(basename($path), '.')] = $data;
1806 | }
1807 | return !empty($langs) ? $langs : false;
1808 | }
1809 |
1810 | // exclude some user settings from frontend
1811 | $exclude = array_diff_key(config::$config, array_flip(array('root', 'start_path', 'image_resize_cache', 'image_resize_quality', 'image_resize_function', 'image_resize_cache_direct', 'menu_sort', 'menu_load_all', 'cache_key', 'storage_path', 'files_exclude', 'dirs_exclude', 'username', 'password', 'breadcrumbs', 'allow_tasks', 'allow_symlinks', 'menu_recursive_symlinks', 'image_resize_sharpen', 'get_mime_type', 'license_key', 'video_thumbs', 'video_ffmpeg_path', 'folder_preview_default', 'image_resize_dimensions_allowed', 'download_dir_cache')));
1812 |
1813 | // json config
1814 | $json_config = array_replace($exclude, array(
1815 | 'breadcrumbs' => $breadcrumbs,
1816 | 'script' => basename(__FILE__),
1817 | 'menu_enabled' => $menu_enabled,
1818 | 'menu_cache_hash' => $menu_cache_hash,
1819 | 'menu_cache_file' => $menu_cache_file,
1820 | 'query_path' => $query_path,
1821 | 'query_path_valid' => $query_path_valid ? true : false,
1822 | 'init_path' => $init_path,
1823 | 'dirs' => $dirs,
1824 | 'dirs_hash' => config::$dirs_hash,
1825 | 'resize_image_types' => $resize_image_types,
1826 | 'image_cache_hash' => config::$config['load_images'] ? substr(md5(config::$doc_root . config::$root . config::$config['image_resize_function'] . config::$config['image_resize_quality']), 0, 6) : false,
1827 | 'image_resize_dimensions_retina' => config::$image_resize_dimensions_retina,
1828 | 'location_hash' => md5(config::$root),
1829 | 'has_login' => config::$has_login,
1830 | 'version' => config::$version,
1831 | 'index_html' => intval(get('index_html')),
1832 | 'server_exif' => function_exists('exif_read_data'),
1833 | 'image_resize_memory_limit' => $image_resize_memory_limit,
1834 | 'qrx' => $wtc && is_string($wtc) ? substr(md5($wtc), 0, strlen($wtc)) : false,
1835 | 'video_thumbs_enabled' => config::$config['video_thumbs'] && config::$config['video_ffmpeg_path'] && config::$config['load_images'] && config::$config['image_resize_cache'] && @function_exists('exec') && @exec('type -P ' . config::$config['video_ffmpeg_path']),
1836 | 'lang_custom' => lang_custom(),
1837 | 'x3_path' => config::$x3_path ? get_url_path(config::$x3_path) : false
1838 | ));
1839 |
1840 | function php_directive_value_to_bytes($directive) {
1841 | $val = function_exists('ini_get') ? @ini_get($directive) : false;
1842 | if (empty($val) || !is_string($val)) return 0;
1843 | preg_match('/^(?\d+)(?