diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 3b75c58..be9f59a 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,8 +1,8 @@ # These are supported funding model platforms -github: [SergiX44] +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username -ko_fi: # +ko_fi: sergix44 tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel -custom: http://bit.ly/XBackBonePaypal +custom: http://bit.ly/XBackBoneDonate diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 6155cdb..ede08fa 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -10,8 +10,7 @@ assignees: '' **System Info** + PHP Version: + XBackBone Version: -+ Webserver: [Apache/Nginx/...] -+ Database backend: [SQLite/Mysql/...] ++ Webserver: **Describe the bug** A clear and concise description of what the bug is. @@ -28,6 +27,3 @@ A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. - -**Logs** -XBackBone and/or webserver logs. diff --git a/.github/xbackbone.png b/.github/xbackbone.png deleted file mode 100644 index 5f56b18..0000000 Binary files a/.github/xbackbone.png and /dev/null differ diff --git a/CHANGELOG.md b/CHANGELOG.md index a172e67..ce891eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,50 +1,3 @@ -## v.3.1 (WIP) -+ Added tagging system (add, delete, search of tagged files). -+ Added basic media auto-tagging on upload. -+ Added registration system. -+ Added password recovery system. -+ Added ability to export all media of an account. -+ Added ability to choose between default and raw url on copy. -+ Added hide by default option. -+ Added user disk quota. -+ Added reCAPTCHA login protection. -+ Added bulk delete. -+ Added account clean function. -+ Added user disk quota system. -+ Added notification option on account create. -+ Added LDAP authentication. -+ Fixed bug html files raws are rendered in a browser. -+ The theme is now re-applied after every system update. -+ Updated system settings page. -+ Updated translations. -+ Improved grid layout. -+ Fixes and improvements. - -## v.3.0.2 -+ Fixed error with migrate command. -+ Updated translations. - -## v.3.0.1 -+ Fixed error with older mysql versions. -+ Fixed config is compiled with the di container. -+ Small installer update. - -## v.3.0 -+ Upgraded from Slim3 to Slim 4. -+ Added web upload. -+ Added ability to add custom HTML in \ tag. -+ Added ability to show a preview of PDF files. -+ Added remember me functionality. -+ Added delete button on the preview page if the user is logged in. -+ New project icon (by [@SerenaItalia](https://www.deviantart.com/serenaitalia)). -+ Raw URL now accept file extensions. -+ The linux script can be used on headless systems. -+ Improved installer. -+ Improved thumbnail generation. -+ Replaced videojs player with Plyr. -+ Implemented SameSite XSS protection. -+ Small fixes and improvements. - ## v.2.6.6 + Ability to choose between releases and prereleases with the web updater. + Updated translations. diff --git a/Gruntfile.js b/Gruntfile.js index ea0939e..34b802f 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -85,9 +85,9 @@ module.exports = function (grunt) { }, { expand: true, - cwd: 'node_modules/plyr/dist', - src: ['plyr.min.js', 'plyr.css'], - dest: 'static/plyr' + cwd: 'node_modules/video.js/dist', + src: ['video.min.js', 'video-js.min.css'], + dest: 'static/videojs' }, { expand: true, @@ -95,30 +95,6 @@ module.exports = function (grunt) { src: ['styles/**/*', 'highlight.pack.min.js'], dest: 'static/highlightjs' }, - { - expand: true, - cwd: 'node_modules/dropzone/dist/min', - src: ['dropzone.min.css', 'dropzone.min.js'], - dest: 'static/dropzone' - }, - { - expand: true, - cwd: 'node_modules/bootstrap4-toggle/css', - src: ['bootstrap4-toggle.min.css'], - dest: 'static/bootstrap/css' - }, - { - expand: true, - cwd: 'node_modules/bootstrap4-toggle/js', - src: ['bootstrap4-toggle.min.js'], - dest: 'static/bootstrap/js' - }, - { - expand: true, - cwd: 'src/images', - src: ['**/*'], - dest: 'static/images' - }, {expand: true, cwd: 'node_modules/jquery/dist', src: ['jquery.min.js'], dest: 'static/jquery'} ], }, diff --git a/README.md b/README.md index 94e1401..b40e2f9 100644 --- a/README.md +++ b/README.md @@ -1,48 +1,168 @@ -# [![Weblate](https://hosted.weblate.org/widgets/xbackbone/-/xbackbone/svg-badge.svg)](https://hosted.weblate.org/engage/xbackbone/?utm_source=widget) [![Donations](https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=6RXF8ZGCZBL68&item_name=Support+the+XBackBone+Development¤cy_code=EUR&source=url) - -

- -

- +# XBackBone 📤 ![Weblate](https://hosted.weblate.org/widgets/xbackbone/-/xbackbone/svg-badge.svg) [![Build Status](https://travis-ci.org/SergiX44/XBackBone.svg?branch=master)](https://travis-ci.org/SergiX44/XBackBone) [![Donations](https://i.imgur.com/bAqVIw8.png?2)](http://bit.ly/XBackBoneDonate) XBackBone is a simple, self-hosted, lightweight PHP file manager that support the instant sharing tool ShareX and *NIX systems. It supports uploading and displaying images, GIF, video, code, formatted text, and file downloading and uploading. Also have a web UI with multi user management, past uploads history and search support. - -## Documentation -All the installations, configuration, and usage instructions are available in the GitHub Pages: - -[XBackBone Documentation](https://sergix44.github.io/XBackBone/) - -## Main Features +## Features + Supports every upload type from ShareX. -+ Config generator for ShareX. + Low memory footprint. + Multiple backends support: Local storage, AWS S3, Google Cloud, Dropbox, FTP(s). -+ Web file upload. + Code uploads syntax highlighting. + Video and audio uploads webplayer. -+ PDF viewer. + Files preview page. + Bootswatch themes support. -+ Responsive theme for mobile use. ++ Responsive theme. + Multi language support. -+ User management, multi user features, roles and disk quota. ++ User management, multi user features and roles. + Public and private uploads. ++ Web UI for each user. + Logging system. ++ Auto config generator for ShareX. + Share to Telegram. -+ Linux supported via a per-user custom generated script (server and desktop). ++ Linux supported via a per-user custom generated script. + Direct downloads using curl or wget commands. + Direct images links support on Discord, Telegram, Facebook, etc. -+ System updates without FTP or CLI. -+ Easy web installer. -+ LDAP authentication. -+ Registration system. -+ Automatic uploads tagging system. -+ Tag uploads with custom tags for categorization. -+ ... and more. + +## How to Install +#### Prerequisites +XBackBone require PHP >= `7.1`, with installed the required extensions: ++ `php-sqlite3` for SQLite. ++ `php-mysql` for MariaDB/MySQL. ++ `php-gd` image manipualtion library. ++ `php-json` json file support. ++ `php-intl` internationalization functions. + +### Web installation ++ **[release, stable]** Download latest release from GitHub: [Latest Release](https://github.com/SergiX44/XBackBone/releases/latest) ++ Extract the release zip to your document root. ++ Navigate to the webspace root (ex. `http://example.com/xbackbone`, this should auto redirect your browser to the install page `http://example.com/xbackbone/install/`) ++ Follow the instructions. + +### Manual installation ++ **[release, stable]** Download latest release from GitHub: [Latest Release](https://github.com/SergiX44/XBackBone/releases/latest) ++ Extract the release zip to your document root. ++ Copy and edit the config file: +```sh +cp config.example.php config.php && nano config.php +``` +By default, XBackBone will use Sqlite3 as DB engine, and a `storage` dir in the main directory. You can leave these settings unchanged for a simple personal installation. +You must set the `base_url`, or remove it for get dynamically the url from request (not recommended). + +```php +return [ + 'base_url' => 'https://example.com', // no trailing slash + 'storage' => [ + 'driver' => 'local', + 'path' => 'storage', + ], + 'db' => [ + 'connection' => 'sqlite', // current support for sqlite and mysql + 'dsn' => 'resources/database/xbackbone.db', + 'username' => null, // username and password not needed for sqlite + 'password' => null, + ] +]; +``` ++ Finally, run the migrate script to setup the database + +```sh +php bin/migrate --install +``` ++ Delete the `/install` directory. ++ Now just login with `admin/admin`, **be sure to change these credentials after your first login**. + +## How to update +Self-update (since v2.5) **[BETA]**: ++ Navigate to the system page as admin. ++ Click the check for update button, and finally the upgrade button. ++ Wait until the browser redirect to the install page. ++ Click the update button. ++ Done. + + +Manual update: ++ Download and extract the release zip to your document root, overwriting any file. ++ Navigate to the `/install` path (es: `http://example.com/` -> `http://example.com/install/`) ++ Click the update button. ++ Done. + +#### Docker deployment ++ [Docker container](https://hub.docker.com/r/pe46dro/xbackbone-docker) + +## Translations +You can help translating the project on [Weblate](https://hosted.weblate.org/projects/xbackbone/xbackbone/). + + +Stato traduzione + + +## Changing themes +XBackBone supports all [bootswatch.com](https://bootswatch.com/) themes. + +From the web UI: ++ Navigate to the web interface as admin -> System Menu -> Choose a theme from the dropdown. + +From the CLI: ++ Run the command `php bin/theme` to see the available themes. ++ Use the same command with the argument name (`php bin/theme `) to choose a theme. ++ If you want to revert back to the original bootstrap theme, run the command `php bin/theme default`. + +*Clear the browser cache once you have applied.* + +### Change app install name +Add to the `config.php` file an array element like this: +```php +return array( + 'app_name' => 'This line will overwrite "XBackBone"', + ... +); +``` +## ShareX Configuration +Once you are logged in, just go in your profile settings and download the ShareX config file for your account. + +## Linux Support +Since ShareX does not support Linux, XBackBone can generate a script that allows you to share an item from any tool: ++ Login into your account ++ Navigate to your profile and download the Linux script for your account. ++ Place the script where you want (ex. in your user home: `/home/`). ++ Add execution permissions (`chmod +x xbackbone_uploader_XXX.sh`) ++ Run the script for the first time to create the desktop entry: `./xbackbone_uploader_XXX.sh -desktop-entry`. + +Now, to upload a media, just use the right click on the file > "Open with ..." > search XBackBone Uploader (XXX) in the app list. +You can use this feature in combination with tools like [Flameshot](https://github.com/lupoDharkael/flameshot), just use the "Open with ..." button once you have done the screenshot. + +The script requires `xclip`, `curl`, and `notify-send`. + +*Note: XXX is the username of your XBackBone account.* + +## Web server configuration notes +If you do not use Apache, or the Apache `.htaccess` is not enabled, set your web server so that the `static/` folder is the only one accessible from the outside, otherwise even private uploads and logs will be accessible! + +You can find an example configuration `nginx.conf` in the project repository. + +## Maintenance Mode +Maintenance mode is automatically enabled during an upgrade using the upgrade manager. You can activate it manually by adding in the configuration file this: + +```php +return array( + ... + 'maintenance' => true, +); +``` + +## Animated Demo +![img](https://i.imgur.com/iV8Rirn.gif) ## License This software is licensed under the GNU Affero General Public License v3.0, available in this repository. As a "copyright notice" it is sufficient to keep the small footer at the bottom of the page, also to help other people to learn about this project! +## Built with ++ Slim 3, since `v2.0` (https://www.slimframework.com/) and some great PHP packages (Flysystem, Intervention Image, Twig, etc) ++ FlightPHP, up to `v1.x` (http://flightphp.com/) ++ Bootstrap 4 (https://getbootstrap.com/) ++ Font Awesome 5 (http://fontawesome.com) ++ ClipboardJS (https://clipboardjs.com/) ++ HighlightJS (https://highlightjs.org/) ++ JQuery (https://jquery.com/) ++ video.js (https://videojs.com/) diff --git a/app/Controllers/AdminController.php b/app/Controllers/AdminController.php index 6eca5d0..de84918 100644 --- a/app/Controllers/AdminController.php +++ b/app/Controllers/AdminController.php @@ -2,102 +2,92 @@ namespace App\Controllers; -use App\Database\Migrator; + use League\Flysystem\FileNotFoundException; -use Psr\Http\Message\ResponseInterface as Response; -use Psr\Http\Message\ServerRequestInterface as Request; +use Slim\Http\Request; +use Slim\Http\Response; class AdminController extends Controller { - /** - * @param Request $request - * @param Response $response - * - * @return Response - * @throws \Twig\Error\LoaderError - * @throws \Twig\Error\RuntimeError - * @throws \Twig\Error\SyntaxError - * - */ - public function system(Request $request, Response $response): Response - { - return view()->render($response, 'dashboard/system.twig', [ - 'usersCount' => $usersCount = $this->database->query('SELECT COUNT(*) AS `count` FROM `users`')->fetch()->count, - 'mediasCount' => $mediasCount = $this->database->query('SELECT COUNT(*) AS `count` FROM `uploads`')->fetch()->count, - 'orphanFilesCount' => $orphanFilesCount = $this->database->query('SELECT COUNT(*) AS `count` FROM `uploads` WHERE `user_id` IS NULL')->fetch()->count, - 'totalSize' => humanFileSize($totalSize = $this->database->query('SELECT SUM(`current_disk_quota`) AS `sum` FROM `users`')->fetch()->sum ?? 0), - 'post_max_size' => ini_get('post_max_size'), - 'upload_max_filesize' => ini_get('upload_max_filesize'), - 'installed_lang' => $this->lang->getList(), - 'forced_lang' => $request->getAttribute('forced_lang'), - 'php_version' => phpversion(), - 'max_memory' => ini_get('memory_limit'), - 'register_enabled' => $this->getSetting('register_enabled', 'off'), - 'hide_by_default' => $this->getSetting('hide_by_default', 'off'), - 'copy_url_behavior' => $this->getSetting('copy_url_behavior', 'off'), - 'quota_enabled' => $this->getSetting('quota_enabled', 'off'), - 'default_user_quota' => humanFileSize($this->getSetting('default_user_quota', stringToBytes('1G')), 0, true), - 'recaptcha_enabled' => $this->getSetting('recaptcha_enabled', 'off'), - 'recaptcha_site_key' => $this->getSetting('recaptcha_site_key'), - 'recaptcha_secret_key' => $this->getSetting('recaptcha_secret_key'), - ]); - } - /** - * @param Response $response - * - * @return Response - */ - public function deleteOrphanFiles(Response $response): Response - { - $orphans = $this->database->query('SELECT * FROM `uploads` WHERE `user_id` IS NULL')->fetchAll(); + /** + * @param Request $request + * @param Response $response + * @return Response + * @throws FileNotFoundException + */ + public function system(Request $request, Response $response): Response + { + $usersCount = $this->database->query('SELECT COUNT(*) AS `count` FROM `users`')->fetch()->count; + $mediasCount = $this->database->query('SELECT COUNT(*) AS `count` FROM `uploads`')->fetch()->count; + $orphanFilesCount = $this->database->query('SELECT COUNT(*) AS `count` FROM `uploads` WHERE `user_id` IS NULL')->fetch()->count; - $filesystem = $this->storage; - $deleted = 0; + $medias = $this->database->query('SELECT `uploads`.`storage_path` FROM `uploads`')->fetchAll(); - foreach ($orphans as $orphan) { - try { - $filesystem->delete($orphan->storage_path); - $deleted++; - } catch (FileNotFoundException $e) { - } - } + $totalSize = 0; - $this->database->query('DELETE FROM `uploads` WHERE `user_id` IS NULL'); + $filesystem = $this->storage; + foreach ($medias as $media) { + $totalSize += $filesystem->getSize($media->storage_path); + } - $this->session->alert(lang('deleted_orphans', [$deleted])); + return $this->view->render($response, 'dashboard/system.twig', [ + 'usersCount' => $usersCount, + 'mediasCount' => $mediasCount, + 'orphanFilesCount' => $orphanFilesCount, + 'totalSize' => humanFileSize($totalSize), + 'post_max_size' => ini_get('post_max_size'), + 'upload_max_filesize' => ini_get('upload_max_filesize'), + 'installed_lang' => $this->lang->getList(), + ]); + } - return redirect($response, route('system')); - } + /** + * @param Request $request + * @param Response $response + * @return Response + */ + public function deleteOrphanFiles(Request $request, Response $response): Response + { + $orphans = $this->database->query('SELECT * FROM `uploads` WHERE `user_id` IS NULL')->fetchAll(); - /** - * @param Response $response - * - * @return Response - */ - public function getThemes(Response $response): Response - { - $apiJson = json_decode(file_get_contents('https://bootswatch.com/api/4.json')); + $filesystem = $this->storage; + $deleted = 0; - $out = []; + foreach ($orphans as $orphan) { + try { + $filesystem->delete($orphan->storage_path); + $deleted++; + } catch (FileNotFoundException $e) { + } + } - $out['Default - Bootstrap 4 default theme'] = 'https://bootswatch.com/_vendor/bootstrap/dist/css/bootstrap.min.css'; - foreach ($apiJson->themes as $theme) { - $out["{$theme->name} - {$theme->description}"] = $theme->cssMin; - } + $this->database->query('DELETE FROM `uploads` WHERE `user_id` IS NULL'); - return json($response, $out); - } + $this->session->alert(lang('deleted_orphans', [$deleted])); - /** - * @param Response $response - * @return Response - */ - public function recalculateUserQuota(Response $response): Response - { - $migrator = new Migrator($this->database, null); - $migrator->reSyncQuotas($this->storage); - $this->session->alert(lang('quota_recalculated')); - return redirect($response, route('system')); - } -} + return redirect($response, 'system'); + } + + /** + * @param Request $request + * @param Response $response + * @return Response + */ + public function applyLang(Request $request, Response $response): Response + { + $config = require BASE_DIR . 'config.php'; + + if ($request->getParam('lang') !== 'auto') { + $config['lang'] = $request->getParam('lang'); + } else { + unset($config['lang']); + } + + file_put_contents(BASE_DIR . 'config.php', 'session->alert(lang('lang_set', [$request->getParam('lang')])); + + return redirect($response, 'system'); + } +} \ No newline at end of file diff --git a/app/Controllers/Auth/LoginController.php b/app/Controllers/Auth/LoginController.php deleted file mode 100644 index 03b9003..0000000 --- a/app/Controllers/Auth/LoginController.php +++ /dev/null @@ -1,180 +0,0 @@ -session->get('logged', false)) { - return redirect($response, route('home')); - } - - return view()->render($response, 'auth/login.twig', [ - 'register_enabled' => $this->getSetting('register_enabled', 'off'), - 'recaptcha_site_key' => $this->getSetting('recaptcha_enabled') === 'on' ? $this->getSetting('recaptcha_site_key') : null, - ]); - } - - /** - * @param Request $request - * @param Response $response - * - * @return Response - * @throws \Exception - * - */ - public function login(Request $request, Response $response): Response - { - if ($this->getSetting('recaptcha_enabled') === 'on') { - $recaptcha = json_decode(file_get_contents('https://www.google.com/recaptcha/api/siteverify?secret='.$this->getSetting('recaptcha_secret_key').'&response='.param($request, 'recaptcha_token'))); - - if ($recaptcha->success && $recaptcha->score < 0.5) { - $this->session->alert(lang('recaptcha_failed'), 'danger'); - return redirect($response, route('login')); - } - } - - $username = param($request, 'username'); - $user = $this->database->query('SELECT `id`, `email`, `username`, `password`,`is_admin`, `active`, `current_disk_quota`, `max_disk_quota`, `ldap`, `copy_raw` FROM `users` WHERE `username` = ? OR `email` = ? LIMIT 1', [$username, $username])->fetch(); - - if ($this->config['ldap']['enabled'] && ($user->ldap ?? true)) { - $user = $this->ldapLogin($request, $username, param($request, 'password'), $user); - } - - $validator = ValidationChecker::make() - ->rules([ - 'login' => $user && password_verify(param($request, 'password'), $user->password), - 'maintenance' => !isset($this->config['maintenance']) || !$this->config['maintenance'] || $user->is_admin ?? false, - 'user_active' => $user->active ?? false, - ]) - ->onFail(function ($rule) { - $alerts = [ - 'login' => lang('bad_login'), - 'maintenance' => lang('maintenance_in_progress'), - 'user_active' => lang('account_disabled'), - ]; - - $this->session->alert($alerts[$rule], $rule === 'maintenance' ? 'info' : 'danger'); - }); - if ($validator->fails()) { - return redirect($response, route('login')); - } - - $this->session->set('logged', true); - $this->session->set('user_id', $user->id); - $this->session->set('username', $user->username); - $this->session->set('admin', $user->is_admin); - $this->session->set('copy_raw', $user->copy_raw); - $this->setSessionQuotaInfo($user->current_disk_quota, $user->max_disk_quota); - - $this->session->alert(lang('welcome', [$user->username]), 'info'); - $this->logger->info("User $user->username logged in."); - - if (param($request, 'remember') === 'on') { - $this->refreshRememberCookie($user->id); - } - - if ($this->session->has('redirectTo')) { - return redirect($response, $this->session->get('redirectTo')); - } - - return redirect($response, route('home')); - } - - /** - * @param Request $request - * @param Response $response - * - * @return Response - */ - public function logout(Request $request, Response $response): Response - { - $this->session->clear(); - $this->session->set('logged', false); - $this->session->alert(lang('goodbye'), 'warning'); - - if (!empty($request->getCookieParams()['remember'])) { - setcookie('remember', null); - } - - return redirect($response, route('login.show')); - } - - /** - * @param Request $request - * @param string $username - * @param string $password - * @param $dbUser - * @return bool - * @throws \Slim\Exception\HttpNotFoundException - * @throws \Slim\Exception\HttpUnauthorizedException - */ - protected function ldapLogin(Request $request, string $username, string $password, $dbUser) - { - $server = $this->ldapConnect(); - if (!$server) { - $this->session->alert(lang('ldap_cant_connect'), 'warning'); - return $dbUser; - } - if (!@ldap_bind($server, $this->getLdapRdn($username), $password)) { - if ($dbUser && !$dbUser->ldap) { - return $dbUser; - } - return null; - } - if (!$dbUser) { - $email = $username; - if (!filter_var($username, FILTER_VALIDATE_EMAIL)) { - $search = ldap_search($server, $this->config['ldap']['base_domain'], 'uid='.addslashes($username), ['mail']); - $entry = ldap_first_entry($server, $search); - $email = @ldap_get_values($server, $entry, 'mail')[0] ?? platform_mail($username.rand(0, 100)); // if the mail is not set, generate a placeholder - } - /** @var UserQuery $userQuery */ - $userQuery = make(UserQuery::class); - $userQuery->create($email, $username, $password, 0, 1, (int) $this->getSetting('default_user_quota', -1), null, 1); - return $userQuery->get($request, $this->database->getPdo()->lastInsertId()); - } - - if (!password_verify($password, $dbUser->password)) { - $userQuery = make(UserQuery::class); - $userQuery->update($dbUser->id, $dbUser->email, $username, $password, $dbUser->is_admin, $dbUser->active, $dbUser->max_disk_quota, $dbUser->ldap); - return $userQuery->get($request, $dbUser->id); - } - - return $dbUser; - } - - /** - * @param string $username - * @return string - */ - private function getLdapRdn(string $username) - { - $bindString = 'uid='.addslashes($username); - if ($this->config['ldap']['user_domain'] !== null) { - $bindString .= ','.$this->config['ldap']['user_domain']; - } - - if ($this->config['ldap']['base_domain'] !== null) { - $bindString .= ','.$this->config['ldap']['base_domain']; - } - - return $bindString; - } -} diff --git a/app/Controllers/Auth/PasswordRecoveryController.php b/app/Controllers/Auth/PasswordRecoveryController.php deleted file mode 100644 index 25edbc1..0000000 --- a/app/Controllers/Auth/PasswordRecoveryController.php +++ /dev/null @@ -1,135 +0,0 @@ -render($response, 'auth/recover_mail.twig'); - } - - - /** - * @param Request $request - * @param Response $response - * @return Response - * @throws \Exception - */ - public function recoverMail(Request $request, Response $response): Response - { - if ($this->session->get('logged', false)) { - return redirect($response, route('home')); - } - - $user = $this->database->query('SELECT `id`, `username` FROM `users` WHERE `email` = ? AND NOT `ldap` LIMIT 1', param($request, 'email'))->fetch(); - - if (!isset($user->id)) { - $this->session->alert(lang('recover_email_sent'), 'success'); - return redirect($response, route('recover')); - } - - $resetToken = bin2hex(random_bytes(16)); - - $this->database->query('UPDATE `users` SET `reset_token`=? WHERE `id` = ?', [ - $resetToken, - $user->id, - ]); - - Mail::make() - ->from(platform_mail(), $this->config['app_name']) - ->to(param($request, 'email')) - ->subject(lang('mail.recover_password', [$this->config['app_name']])) - ->message(lang('mail.recover_text', [ - $user->username, - route('recover.password', ['resetToken' => $resetToken]), - ])) - ->send(); - - $this->session->alert(lang('recover_email_sent'), 'success'); - return redirect($response, route('recover')); - } - - /** - * @param Request $request - * @param Response $response - * @param string $resetToken - * @return Response - * @throws \Twig\Error\LoaderError - * @throws \Twig\Error\RuntimeError - * @throws \Twig\Error\SyntaxError - * @throws HttpNotFoundException - */ - public function recoverPasswordForm(Request $request, Response $response, string $resetToken): Response - { - $user = $this->database->query('SELECT `id` FROM `users` WHERE `reset_token` = ? LIMIT 1', $resetToken)->fetch(); - - if (!$user) { - throw new HttpNotFoundException($request); - } - - return view()->render($response, 'auth/recover_password.twig', [ - 'reset_token' => $resetToken - ]); - } - - /** - * @param Request $request - * @param Response $response - * @param string $resetToken - * @return Response - * @throws HttpNotFoundException - */ - public function recoverPassword(Request $request, Response $response, string $resetToken): Response - { - $user = $this->database->query('SELECT `id` FROM `users` WHERE `reset_token` = ? LIMIT 1', $resetToken)->fetch(); - - if (!$user) { - throw new HttpNotFoundException($request); - } - - $validator = ValidationChecker::make() - ->rules([ - 'password.required' => !empty(param($request, 'password')), - 'password.match' => param($request, 'password') === param($request, 'password_repeat'), - ]) - ->onFail(function ($rule) { - $alerts = [ - 'password.required' => lang('password_required'), - 'password.match' => lang('password_match'), - ]; - - $this->session->alert($alerts[$rule], 'danger'); - }); - - if ($validator->fails()) { - return redirect($response, route('recover.password', ['resetToken' => $resetToken])); - } - - $this->database->query('UPDATE `users` SET `password`=?, `reset_token`=? WHERE `id` = ?', [ - password_hash(param($request, 'password'), PASSWORD_DEFAULT), - null, - $user->id, - ]); - - $this->session->alert(lang('password_restored'), 'success'); - return redirect($response, route('login.show')); - } -} diff --git a/app/Controllers/Auth/RegisterController.php b/app/Controllers/Auth/RegisterController.php deleted file mode 100644 index 8f0d417..0000000 --- a/app/Controllers/Auth/RegisterController.php +++ /dev/null @@ -1,129 +0,0 @@ -session->get('logged', false)) { - return redirect($response, route('home')); - } - - if ($this->getSetting('register_enabled', 'off') === 'off') { - throw new HttpNotFoundException($request); - } - - return view()->render($response, 'auth/register.twig', [ - 'recaptcha_site_key' => $this->getSetting('recaptcha_enabled') === 'on' ? $this->getSetting('recaptcha_site_key') : null, - ]); - } - - /** - * @param Request $request - * @param Response $response - * @return Response - * @throws HttpNotFoundException - * @throws \Exception - */ - public function register(Request $request, Response $response): Response - { - if ($this->session->get('logged', false)) { - return redirect($response, route('home')); - } - - if ($this->getSetting('register_enabled', 'off') === 'off') { - throw new HttpNotFoundException($request); - } - - if ($this->getSetting('recaptcha_enabled') === 'on') { - $recaptcha = json_decode(file_get_contents('https://www.google.com/recaptcha/api/siteverify?secret='.$this->getSetting('recaptcha_secret_key').'&response='.param($request, 'recaptcha_token'))); - - if ($recaptcha->success && $recaptcha->score < 0.5) { - $this->session->alert(lang('recaptcha_failed'), 'danger'); - return redirect($response, route('register.show')); - } - } - - $validator = $this->getUserCreateValidator($request); - - if ($validator->fails()) { - return redirect($response, route('register.show')); - } - - $activateToken = bin2hex(random_bytes(16)); - - make(UserQuery::class)->create( - param($request, 'email'), - param($request, 'username'), - param($request, 'password'), - 0, - 0, - (int) $this->getSetting('default_user_quota', -1), - $activateToken - ); - - Mail::make() - ->from(platform_mail(), $this->config['app_name']) - ->to(param($request, 'email')) - ->subject(lang('mail.activate_account', [$this->config['app_name']])) - ->message(lang('mail.activate_text', [ - param($request, 'username'), - $this->config['app_name'], - $this->config['base_url'], - route('activate', ['activateToken' => $activateToken]), - ])) - ->send(); - - $this->session->alert(lang('register_success', [param($request, 'username')]), 'success'); - $this->logger->info('New user registered.', [array_diff_key($request->getParsedBody(), array_flip(['password']))]); - - return redirect($response, route('login.show')); - } - - /** - * @param Response $response - * @param string $activateToken - * @return Response - */ - public function activateUser(Response $response, string $activateToken): Response - { - if ($this->session->get('logged', false)) { - return redirect($response, route('home')); - } - - $userId = $this->database->query('SELECT `id` FROM `users` WHERE `activate_token` = ? LIMIT 1', $activateToken)->fetch()->id ?? null; - - if ($userId === null) { - $this->session->alert(lang('account_not_found'), 'warning'); - return redirect($response, route('login.show')); - } - - $this->database->query('UPDATE `users` SET `activate_token`=?, `active`=? WHERE `id` = ?', [ - null, - 1, - $userId, - ]); - - $this->session->alert(lang('account_activated'), 'success'); - return redirect($response, route('login.show')); - } -} diff --git a/app/Controllers/ClientController.php b/app/Controllers/ClientController.php deleted file mode 100644 index 0cec422..0000000 --- a/app/Controllers/ClientController.php +++ /dev/null @@ -1,75 +0,0 @@ -get($request, $id, true); - - if ($user->token === null || $user->token === '') { - $this->session->alert(lang('no_upload_token'), 'danger'); - - return redirect($response, $request->getHeaderLine('Referer')); - } - - $json = [ - 'DestinationType' => 'ImageUploader, TextUploader, FileUploader', - 'RequestURL' => route('upload'), - 'FileFormName' => 'upload', - 'Arguments' => [ - 'file' => '$filename$', - 'text' => '$input$', - 'token' => $user->token, - ], - 'URL' => '$json:url$', - 'ThumbnailURL' => '$json:url$/raw', - 'DeletionURL' => '$json:url$/delete/'.$user->token, - ]; - - return json($response, $json, 200, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) - ->withHeader('Content-Disposition', 'attachment;filename="'.$user->username.'-ShareX.sxcu"'); - } - - /** - * @param Request $request - * @param Response $response - * @param int $id - * - * @return Response - * @throws \Twig\Error\LoaderError - * @throws \Twig\Error\RuntimeError - * @throws \Twig\Error\SyntaxError - */ - public function getBashScript(Request $request, Response $response, int $id): Response - { - $user = make(UserQuery::class)->get($request, $id, true); - - if ($user->token === null || $user->token === '') { - $this->session->alert(lang('no_upload_token'), 'danger'); - - return redirect($response, $request->getHeaderLine('Referer')); - } - - return view()->render($response->withHeader('Content-Disposition', 'attachment;filename="xbackbone_uploader_'.$user->username.'.sh"'), - 'scripts/xbackbone_uploader.sh.twig', - [ - 'username' => $user->username, - 'upload_url' => route('upload'), - 'token' => $user->token, - ] - ); - } -} diff --git a/app/Controllers/Controller.php b/app/Controllers/Controller.php index 7005965..25da84e 100644 --- a/app/Controllers/Controller.php +++ b/app/Controllers/Controller.php @@ -3,189 +3,65 @@ namespace App\Controllers; use App\Database\DB; -use App\Database\Queries\UserQuery; use App\Web\Lang; use App\Web\Session; -use App\Web\ValidationChecker; -use App\Web\View; -use DI\Container; -use DI\DependencyException; -use DI\NotFoundException; -use Exception; +use League\Flysystem\FileNotFoundException; use League\Flysystem\Filesystem; use Monolog\Logger; -use Psr\Http\Message\ServerRequestInterface as Request; +use Slim\Container; /** - * @property Session session - * @property View view - * @property DB database + * @property Session|null session + * @property mixed|null view + * @property DB|null database * @property Logger|null logger * @property Filesystem|null storage * @property Lang lang - * @property array config + * @property array settings */ abstract class Controller { - /** @var Container */ - protected $container; - public function __construct(Container $container) - { - $this->container = $container; - } + /** @var Container */ + protected $container; - /** - * @param $name - * - * @return mixed|null - * @throws NotFoundException - * - * @throws DependencyException - */ - public function __get($name) - { - if ($this->container->has($name)) { - return $this->container->get($name); - } + public function __construct(Container $container) + { + $this->container = $container; + } - return null; - } + /** + * @param $name + * @return mixed|null + * @throws \Interop\Container\Exception\ContainerException + */ + public function __get($name) + { + if ($this->container->has($name)) { + return $this->container->get($name); + } + return null; + } - /** - * @param $key - * @param null $default - * @return object - */ - protected function getSetting($key, $default = null) - { - return $this->database->query('SELECT `value` FROM `settings` WHERE `key` = '.$this->database->getPdo()->quote($key))->fetch()->value ?? $default; - } + /** + * @param $id + * @return int + */ + protected function getUsedSpaceByUser($id): int + { + $medias = $this->database->query('SELECT `uploads`.`storage_path` FROM `uploads` WHERE `user_id` = ?', $id); - /** - * @param $current - * @param $max - */ - protected function setSessionQuotaInfo($current, $max) - { - $this->session->set('current_disk_quota', humanFileSize($current)); - if ($this->getSetting('quota_enabled', 'off') === 'on') { - if ($max < 0) { - $this->session->set('max_disk_quota', '∞'); - $this->session->set('percent_disk_quota', null); - } else { - $this->session->set('max_disk_quota', humanFileSize($max)); - $this->session->set('percent_disk_quota', round(($current * 100) / $max)); - } - } else { - $this->session->set('max_disk_quota', null); - $this->session->set('percent_disk_quota', null); - } - } + $totalSize = 0; - /** - * @param Request $request - * @param $userId - * @param $fileSize - * @param bool $dec - * @return bool - */ - protected function updateUserQuota(Request $request, $userId, $fileSize, $dec = false) - { - $user = make(UserQuery::class)->get($request, $userId); + $filesystem = $this->storage; + foreach ($medias as $media) { + try { + $totalSize += $filesystem->getSize($media->storage_path); + } catch (FileNotFoundException $e) { + $this->logger->error('Error calculating file size', ['exception' => $e]); + } + } - if ($dec) { - $tot = max($user->current_disk_quota - $fileSize, 0); - } else { - $tot = $user->current_disk_quota + $fileSize; - - if ($this->getSetting('quota_enabled') === 'on' && $user->max_disk_quota > 0 && $user->max_disk_quota < $tot) { - return false; - } - } - - $this->database->query('UPDATE `users` SET `current_disk_quota`=? WHERE `id` = ?', [ - $tot, - $user->id, - ]); - - return true; - } - - /** - * @param $userId - * @throws Exception - */ - protected function refreshRememberCookie($userId) - { - $selector = bin2hex(random_bytes(8)); - $token = bin2hex(random_bytes(32)); - $expire = time() + 604800; // a week - - $this->database->query('UPDATE `users` SET `remember_selector`=?, `remember_token`=?, `remember_expire`=? WHERE `id`=?', [ - $selector, - password_hash($token, PASSWORD_DEFAULT), - date('Y-m-d\TH:i:s', $expire), - $userId, - ]); - - // Workaround for php <= 7.3 - if (PHP_VERSION_ID < 70300) { - setcookie('remember', "{$selector}:{$token}", $expire, '; SameSite=Lax', '', false, true); - } else { - setcookie('remember', "{$selector}:{$token}", [ - 'expires' => $expire, - 'httponly' => true, - 'samesite' => 'Lax', - ]); - } - } - - /** - * @param Request $request - * @return ValidationChecker - */ - public function getUserCreateValidator(Request $request) - { - return ValidationChecker::make() - ->rules([ - 'email.required' => filter_var(param($request, 'email'), FILTER_VALIDATE_EMAIL) !== false, - 'username.required' => !empty(param($request, 'username')), - 'password.required' => !empty(param($request, 'password')), - 'email.unique' => $this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `email` = ?', param($request, 'email'))->fetch()->count == 0, - 'username.unique' => $this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `username` = ?', param($request, 'username'))->fetch()->count == 0, - ]) - ->onFail(function ($rule) { - $alerts = [ - 'email.required' => lang('email_required'), - 'username.required' => lang('username_required'), - 'password.required' => lang('password_required'), - 'email.unique' => lang('email_taken'), - 'username.unique' => lang('username_taken'), - ]; - - $this->session->alert($alerts[$rule], 'danger'); - }); - } - - /** - * @return bool|false|resource - */ - public function ldapConnect() - { - if (!extension_loaded('ldap')) { - $this->logger->error('The LDAP extension is not loaded.'); - return false; - } - - $server = ldap_connect($this->config['ldap']['host'], $this->config['ldap']['port']); - - if ($server) { - ldap_set_option($server, LDAP_OPT_PROTOCOL_VERSION, 3); - ldap_set_option($server, LDAP_OPT_REFERRALS, 0); - ldap_set_option($server, LDAP_OPT_NETWORK_TIMEOUT, 10); - } - - return $server; - } -} + return $totalSize; + } +} \ No newline at end of file diff --git a/app/Controllers/DashboardController.php b/app/Controllers/DashboardController.php index c668573..266dbe1 100644 --- a/app/Controllers/DashboardController.php +++ b/app/Controllers/DashboardController.php @@ -3,91 +3,78 @@ namespace App\Controllers; use App\Database\Queries\MediaQuery; -use App\Database\Queries\TagQuery; -use Psr\Http\Message\ResponseInterface as Response; -use Psr\Http\Message\ServerRequestInterface as Request; +use Slim\Http\Request; +use Slim\Http\Response; class DashboardController extends Controller { - /** - * @param Request $request - * @param Response $response - * - * @return Response - */ - public function redirects(Request $request, Response $response): Response - { - if (param($request, 'afterInstall') !== null && !is_dir(BASE_DIR.'install')) { - $this->session->alert(lang('installed'), 'success'); - } - return redirect($response, route('home')); - } + /** + * @param Request $request + * @param Response $response + * @return Response + */ + public function redirects(Request $request, Response $response): Response + { + if ($request->getParam('afterInstall') !== null && !is_dir(BASE_DIR . 'install')) { + $this->session->alert(lang('installed'), 'success'); + } - /** - * @param Request $request - * @param Response $response - * @param int|null $page - * - * @return Response - * @throws \Twig\Error\RuntimeError - * @throws \Twig\Error\SyntaxError - * - * @throws \Twig\Error\LoaderError - */ - public function home(Request $request, Response $response, int $page = 0): Response - { - $page = max(0, --$page); + return redirect($response, 'home'); + } - switch (param($request, 'sort', 'time')) { - case 'size': - $order = MediaQuery::ORDER_SIZE; - break; - case 'name': - $order = MediaQuery::ORDER_NAME; - break; - default: - case 'time': - $order = MediaQuery::ORDER_TIME; - break; - } + /** + * @param Request $request + * @param Response $response + * @param $args + * @return Response + */ + public function home(Request $request, Response $response, $args): Response + { + $page = isset($args['page']) ? (int)$args['page'] : 0; + $page = max(0, --$page); - /** @var MediaQuery $query */ - $query = make(MediaQuery::class, ['isAdmin' => (bool) $this->session->get('admin', false)]) - ->orderBy($order, param($request, 'order', 'DESC')) - ->withUserId($this->session->get('user_id')) - ->search(param($request, 'search', null)) - ->filterByTag(param($request, 'tag')) - ->run($page); + $query = new MediaQuery($this->database, $this->session->get('admin', false), $this->storage); - $tags = make(TagQuery::class, [ - 'isAdmin' => (bool) $this->session->get('admin', false), - 'userId' => $this->session->get('user_id') - ])->all(); + switch ($request->getParam('sort', 'time')) { + case 'size': + $order = MediaQuery::ORDER_SIZE; + break; + case 'name': + $order = MediaQuery::ORDER_NAME; + break; + default: + case 'time': + $order = MediaQuery::ORDER_TIME; + break; + } - return view()->render( - $response, - ($this->session->get('admin', false) && $this->session->get('gallery_view', true)) ? 'dashboard/list.twig' : 'dashboard/grid.twig', - [ - 'medias' => $query->getMedia(), - 'next' => $page < floor($query->getPages()), - 'previous' => $page >= 1, - 'current_page' => ++$page, - 'copy_raw' => $this->session->get('copy_raw', false), - 'tags' => $tags, - ] - ); - } + $query->orderBy($order, $request->getParam('order', 'DESC')) + ->withUserId($this->session->get('user_id')) + ->search($request->getParam('search', null)) + ->run($page); - /** - * @param Response $response - * - * @return Response - */ - public function switchView(Response $response): Response - { - $this->session->set('gallery_view', !$this->session->get('gallery_view', true)); + return $this->view->render( + $response, + ($this->session->get('admin', false) && $this->session->get('gallery_view', true)) ? 'dashboard/admin.twig' : 'dashboard/home.twig', + [ + 'medias' => $query->getMedia(), + 'next' => $page < floor($query->getPages()), + 'previous' => $page >= 1, + 'current_page' => ++$page, + ] + ); + } - return redirect($response, route('home')); - } -} + /** + * @param Request $request + * @param Response $response + * @param $args + * @return Response + */ + public function switchView(Request $request, Response $response, $args): Response + { + $this->session->set('gallery_view', !$this->session->get('gallery_view', true)); + return redirect($response, 'home'); + } +} \ No newline at end of file diff --git a/app/Controllers/ExportController.php b/app/Controllers/ExportController.php deleted file mode 100644 index 84093db..0000000 --- a/app/Controllers/ExportController.php +++ /dev/null @@ -1,49 +0,0 @@ -get($request, $id, true); - - $medias = $this->database->query('SELECT `uploads`.`filename`, `uploads`.`storage_path` FROM `uploads` WHERE `user_id` = ?', $user->id); - - $this->logger->info("User $user->id, $user->name, exporting data..."); - - set_time_limit(0); - ob_end_clean(); - - $options = new Archive(); - $options->setSendHttpHeaders(true); - - $zip = new ZipStream($user->username.'-'.time().'-export.zip', $options); - - $filesystem = $this->storage; - foreach ($medias as $media) { - try { - $zip->addFileFromStream($media->filename, $filesystem->readStream($media->storage_path)); - } catch (FileNotFoundException $e) { - $this->logger->error('Cannot export file', ['exception' => $e]); - } - } - $zip->finish(); - exit(0); - } -} diff --git a/app/Controllers/LoginController.php b/app/Controllers/LoginController.php new file mode 100644 index 0000000..3a7bcf1 --- /dev/null +++ b/app/Controllers/LoginController.php @@ -0,0 +1,79 @@ +session->get('logged', false)) { + return redirect($response, 'home'); + } + return $this->view->render($response, 'auth/login.twig'); + } + + /** + * @param Request $request + * @param Response $response + * @return Response + */ + public function login(Request $request, Response $response): Response + { + + $result = $this->database->query('SELECT `id`, `email`, `username`, `password`,`is_admin`, `active` FROM `users` WHERE `username` = ? OR `email` = ? LIMIT 1', [$request->getParam('username'), $request->getParam('username')])->fetch(); + + if (!$result || !password_verify($request->getParam('password'), $result->password)) { + $this->session->alert(lang('bad_login'), 'danger'); + return redirect($response, 'login'); + } + + if (isset($this->settings['maintenance']) && $this->settings['maintenance'] && !$result->is_admin) { + $this->session->alert(lang('maintenance_in_progress'), 'info'); + return redirect($response, 'login'); + } + + if (!$result->active) { + $this->session->alert(lang('account_disabled'), 'danger'); + return redirect($response, 'login'); + } + + $this->session->set('logged', true); + $this->session->set('user_id', $result->id); + $this->session->set('username', $result->username); + $this->session->set('admin', $result->is_admin); + $this->session->set('used_space', humanFileSize($this->getUsedSpaceByUser($result->id))); + + $this->session->alert(lang('welcome', [$result->username]), 'info'); + $this->logger->info("User $result->username logged in."); + + if ($this->session->has('redirectTo')) { + return $response->withRedirect($this->session->get('redirectTo')); + } + + return redirect($response, 'home'); + } + + /** + * @param Request $request + * @param Response $response + * @return Response + */ + public function logout(Request $request, Response $response): Response + { + $this->session->clear(); + $this->session->set('logged', false); + $this->session->alert(lang('goodbye'), 'warning'); + return redirect($response, 'login.show'); + } + +} \ No newline at end of file diff --git a/app/Controllers/MediaController.php b/app/Controllers/MediaController.php deleted file mode 100644 index 84f94e6..0000000 --- a/app/Controllers/MediaController.php +++ /dev/null @@ -1,418 +0,0 @@ -getMedia($userCode, $mediaCode); - - if (!$media || (!$media->published && $this->session->get('user_id') !== $media->user_id && !$this->session->get('admin', false))) { - throw new HttpNotFoundException($request); - } - - $filesystem = $this->storage; - - if (isBot($request->getHeaderLine('User-Agent'))) { - return $this->streamMedia($request, $response, $filesystem, $media); - } - - try { - $media->mimetype = $filesystem->getMimetype($media->storage_path); - $size = $filesystem->getSize($media->storage_path); - - $type = explode('/', $media->mimetype)[0]; - if ($type === 'image' && !isDisplayableImage($media->mimetype)) { - $type = 'application'; - $media->mimetype = 'application/octet-stream'; - } - if ($type === 'text') { - if ($size <= (200 * 1024)) { // less than 200 KB - $media->text = $filesystem->read($media->storage_path); - } else { - $type = 'application'; - $media->mimetype = 'application/octet-stream'; - } - } - $media->size = humanFileSize($size); - } catch (FileNotFoundException $e) { - throw new HttpNotFoundException($request); - } - - return view()->render($response, 'upload/public.twig', [ - 'delete_token' => $token, - 'media' => $media, - 'type' => $type, - 'url' => urlFor("/{$userCode}/{$mediaCode}"), - 'copy_url_behavior' => $this->getSetting('copy_url_behavior', 'off'), - ]); - } - - /** - * @param Request $request - * @param Response $response - * @param int $id - * - * @return Response - * @throws HttpNotFoundException - * - * @throws FileNotFoundException - */ - public function getRawById(Request $request, Response $response, int $id): Response - { - $media = $this->database->query('SELECT * FROM `uploads` WHERE `id` = ? LIMIT 1', $id)->fetch(); - - if (!$media) { - throw new HttpNotFoundException($request); - } - - return $this->streamMedia($request, $response, $this->storage, $media); - } - - /** - * @param Request $request - * @param Response $response - * @param string $userCode - * @param string $mediaCode - * @param string|null $ext - * - * @return Response - * @throws HttpBadRequestException - * @throws HttpNotFoundException - * - * @throws FileNotFoundException - */ - public function getRaw(Request $request, Response $response, string $userCode, string $mediaCode, ?string $ext = null): Response - { - $media = $this->getMedia($userCode, $mediaCode); - - if (!$media || !$media->published && $this->session->get('user_id') !== $media->user_id && !$this->session->get('admin', false)) { - throw new HttpNotFoundException($request); - } - - if ($ext !== null && pathinfo($media->filename, PATHINFO_EXTENSION) !== $ext) { - throw new HttpBadRequestException($request); - } - - // If contains html, return it as text/plain - if (strpos($this->storage->getMimetype($media->storage_path), 'text/htm') !== false) { - $response = $this->streamMedia($request, $response, $this->storage, $media); - return $response->withHeader('Content-Type', 'text/plain'); - } - - return $this->streamMedia($request, $response, $this->storage, $media); - } - - /** - * @param Request $request - * @param Response $response - * @param string $userCode - * @param string $mediaCode - * - * @return Response - * @throws HttpNotFoundException - * - * @throws FileNotFoundException - */ - public function download(Request $request, Response $response, string $userCode, string $mediaCode): Response - { - $media = $this->getMedia($userCode, $mediaCode); - - if (!$media || !$media->published && $this->session->get('user_id') !== $media->user_id && !$this->session->get('admin', false)) { - throw new HttpNotFoundException($request); - } - - return $this->streamMedia($request, $response, $this->storage, $media, 'attachment'); - } - - /** - * @param Request $request - * @param Response $response - * @param int $id - * - * @return Response - * @throws HttpNotFoundException - * - */ - public function togglePublish(Request $request, Response $response, int $id): Response - { - if ($this->session->get('admin')) { - $media = $this->database->query('SELECT * FROM `uploads` WHERE `id` = ? LIMIT 1', $id)->fetch(); - } else { - $media = $this->database->query('SELECT * FROM `uploads` WHERE `id` = ? AND `user_id` = ? LIMIT 1', [$id, $this->session->get('user_id')])->fetch(); - } - - if (!$media) { - throw new HttpNotFoundException($request); - } - - $this->database->query('UPDATE `uploads` SET `published`=? WHERE `id`=?', [$media->published ? 0 : 1, $media->id]); - - return $response; - } - - /** - * @param Request $request - * @param Response $response - * @param int $id - * - * @return Response - * @throws HttpNotFoundException - * @throws HttpUnauthorizedException - */ - public function delete(Request $request, Response $response, int $id): Response - { - $media = $this->database->query('SELECT * FROM `uploads` WHERE `id` = ? LIMIT 1', $id)->fetch(); - - if (!$media) { - throw new HttpNotFoundException($request); - } - - if ($this->session->get('admin', false) || $media->user_id === $this->session->get('user_id')) { - $this->deleteMedia($request, $media->storage_path, $id, $media->user_id); - - $this->logger->info('User '.$this->session->get('username').' deleted a media.', [$id]); - - if ($media->user_id === $this->session->get('user_id')) { - $user = make(UserQuery::class)->get($request, $media->user_id, true); - $this->setSessionQuotaInfo($user->current_disk_quota, $user->max_disk_quota); - } - } else { - throw new HttpUnauthorizedException($request); - } - - if ($request->getMethod() === 'GET') { - return redirect($response, route('home')); - } - - return $response; - } - - /** - * @param Request $request - * @param Response $response - * @param string $userCode - * @param string $mediaCode - * @param string $token - * - * @return Response - * @throws HttpUnauthorizedException - * - * @throws HttpNotFoundException - */ - public function deleteByToken(Request $request, Response $response, string $userCode, string $mediaCode, string $token): Response - { - $media = $this->getMedia($userCode, $mediaCode); - - if (!$media) { - throw new HttpNotFoundException($request); - } - - $user = $this->database->query('SELECT `id`, `active` FROM `users` WHERE `token` = ? LIMIT 1', $token)->fetch(); - - if (!$user) { - $this->session->alert(lang('token_not_found'), 'danger'); - - return redirect($response, $request->getHeaderLine('Referer')); - } - - if (!$user->active) { - $this->session->alert(lang('account_disabled'), 'danger'); - - return redirect($response, $request->getHeaderLine('Referer')); - } - - if ($this->session->get('admin', false) || $user->id === $media->user_id) { - $this->deleteMedia($request, $media->storage_path, $media->mediaId, $user->id); - $this->logger->info('User '.$user->username.' deleted a media via token.', [$media->mediaId]); - } else { - throw new HttpUnauthorizedException($request); - } - - return redirect($response, route('home')); - } - - /** - * @param Request $request - * @param string $storagePath - * @param int $id - * - * @param int $userId - * @return void - * @throws HttpNotFoundException - */ - protected function deleteMedia(Request $request, string $storagePath, int $id, int $userId) - { - try { - $size = $this->storage->getSize($storagePath); - $this->storage->delete($storagePath); - $this->updateUserQuota($request, $userId, $size, true); - } catch (FileNotFoundException $e) { - throw new HttpNotFoundException($request); - } finally { - $this->database->query('DELETE FROM `uploads` WHERE `id` = ?', $id); - } - } - - /** - * @param $userCode - * @param $mediaCode - * - * @return mixed - */ - protected function getMedia($userCode, $mediaCode) - { - $mediaCode = pathinfo($mediaCode)['filename']; - - return $this->database->query('SELECT `uploads`.*, `users`.*, `users`.`id` AS `userId`, `uploads`.`id` AS `mediaId` FROM `uploads` INNER JOIN `users` ON `uploads`.`user_id` = `users`.`id` WHERE `user_code` = ? AND `uploads`.`code` = ? LIMIT 1', [ - $userCode, - $mediaCode, - ])->fetch(); - } - - /** - * @param Request $request - * @param Response $response - * @param Filesystem $storage - * @param $media - * @param string $disposition - * - * @return Response - * @throws FileNotFoundException - * - */ - protected function streamMedia(Request $request, Response $response, Filesystem $storage, $media, string $disposition = 'inline'): Response - { - set_time_limit(0); - $mime = $storage->getMimetype($media->storage_path); - - if (param($request, 'width') !== null && explode('/', $mime)[0] === 'image') { - return $this->makeThumbnail($storage, $media, param($request, 'width'), param($request, 'height'), $disposition); - } else { - $stream = new Stream($storage->readStream($media->storage_path)); - - if (!in_array(explode('/', $mime)[0], ['image', 'video', 'audio']) || $disposition === 'attachment') { - return $response->withHeader('Content-Type', $mime) - ->withHeader('Content-Disposition', $disposition.'; filename="'.$media->filename.'"') - ->withHeader('Content-Length', $stream->getSize()) - ->withBody($stream); - } - - if (isset($request->getServerParams()['HTTP_RANGE'])) { - return $this->handlePartialRequest($response, $stream, $request->getServerParams()['HTTP_RANGE'], $disposition, $media, $mime); - } - - return $response->withHeader('Content-Type', $mime) - ->withHeader('Content-Length', $stream->getSize()) - ->withHeader('Accept-Ranges', 'bytes') - ->withBody($stream); - } - } - - /** - * @param Filesystem $storage - * @param $media - * @param null $width - * @param null $height - * @param string $disposition - * - * @return Response - * @throws FileNotFoundException - * - */ - protected function makeThumbnail(Filesystem $storage, $media, $width = null, $height = null, string $disposition = 'inline') - { - return Image::make($storage->readStream($media->storage_path)) - ->resize($width, $height, function (Constraint $constraint) { - $constraint->aspectRatio(); - }) - ->resizeCanvas($width, $height, 'center') - ->psrResponse('png') - ->withHeader('Content-Disposition', $disposition.';filename="scaled-'.pathinfo($media->filename, PATHINFO_FILENAME).'.png"'); - } - - /** - * @param Response $response - * @param Stream $stream - * @param string $range - * @param string $disposition - * @param $media - * @param $mime - * - * @return Response - */ - protected function handlePartialRequest(Response $response, Stream $stream, string $range, string $disposition, $media, $mime) - { - $end = $stream->getSize() - 1; - [, $range] = explode('=', $range, 2); - - if (strpos($range, ',') !== false) { - return $response->withHeader('Content-Type', $mime) - ->withHeader('Content-Disposition', $disposition.'; filename="'.$media->filename.'"') - ->withHeader('Content-Length', $stream->getSize()) - ->withHeader('Accept-Ranges', 'bytes') - ->withHeader('Content-Range', "0,{$stream->getSize()}") - ->withStatus(416) - ->withBody($stream); - } - - if ($range === '-') { - $start = $stream->getSize() - (int) substr($range, 1); - } else { - $range = explode('-', $range); - $start = (int) $range[0]; - $end = (isset($range[1]) && is_numeric($range[1])) ? (int) $range[1] : $stream->getSize(); - } - - $end = ($end > $stream->getSize() - 1) ? $stream->getSize() - 1 : $end; - $stream->seek($start); - - header("Content-Type: $mime"); - header('Content-Length: '.($end - $start + 1)); - header('Accept-Ranges: bytes'); - header("Content-Range: bytes $start-$end/{$stream->getSize()}"); - - http_response_code(206); - ob_end_clean(); - - $buffer = 16348; - $readed = $start; - while ($readed < $end) { - if ($readed + $buffer > $end) { - $buffer = $end - $readed + 1; - } - echo $stream->read($buffer); - $readed += $buffer; - } - - exit(0); - } -} diff --git a/app/Controllers/ProfileController.php b/app/Controllers/ProfileController.php deleted file mode 100644 index ead427f..0000000 --- a/app/Controllers/ProfileController.php +++ /dev/null @@ -1,84 +0,0 @@ -get($request, $this->session->get('user_id'), true); - - return view()->render($response, 'user/edit.twig', [ - 'profile' => true, - 'user' => $user, - ]); - } - - /** - * @param Request $request - * @param Response $response - * @param int $id - * - * @return Response - */ - public function profileEdit(Request $request, Response $response, int $id): Response - { - $user = make(UserQuery::class)->get($request, $id, true); - - $validator = ValidationChecker::make() - ->rules([ - 'email.required' => filter_var(param($request, 'email'), FILTER_VALIDATE_EMAIL), - 'email.unique' => $this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `email` = ? AND `email` <> ?', [param($request, 'email'), $user->email])->fetch()->count == 0, - ]) - ->onFail(function ($rule) { - $alerts = [ - 'email.required' => lang('email_required'), - 'email.unique' => lang('email_taken'), - ]; - - $this->session->alert($alerts[$rule], 'danger'); - }); - - if ($validator->fails()) { - return redirect($response, route('profile')); - } - - if (param($request, 'password') !== null && !empty(param($request, 'password'))) { - $this->database->query('UPDATE `users` SET `email`=?, `password`=?, `hide_uploads`=?, `copy_raw`=? WHERE `id` = ?', [ - param($request, 'email'), - password_hash(param($request, 'password'), PASSWORD_DEFAULT), - param($request, 'hide_uploads') !== null ? 1 : 0, - param($request, 'copy_raw') !== null ? 1 : 0, - $user->id, - ]); - } else { - $this->database->query('UPDATE `users` SET `email`=?, `hide_uploads`=?, `copy_raw`=? WHERE `id` = ?', [ - param($request, 'email'), - param($request, 'hide_uploads') !== null ? 1 : 0, - param($request, 'copy_raw') !== null ? 1 : 0, - $user->id, - ]); - } - - $this->session->set('copy_raw', param($request, 'copy_raw') !== null ? 1 : 0); - $this->session->alert(lang('profile_updated'), 'success'); - $this->logger->info('User '.$this->session->get('username')." updated profile of $user->id."); - - return redirect($response, route('profile')); - } -} diff --git a/app/Controllers/SettingController.php b/app/Controllers/SettingController.php deleted file mode 100644 index 16fded8..0000000 --- a/app/Controllers/SettingController.php +++ /dev/null @@ -1,99 +0,0 @@ -session->alert(lang('invalid_quota', 'danger')); - return redirect($response, route('system')); - } - - if (param($request, 'recaptcha_enabled', 'off') === 'on' && (empty(param($request, 'recaptcha_site_key')) || empty(param($request, 'recaptcha_secret_key')))) { - $this->session->alert(lang('recaptcha_keys_required', 'danger')); - return redirect($response, route('system')); - } - - // registrations - $this->updateSetting('register_enabled', param($request, 'register_enabled', 'off')); - - // quota - $this->updateSetting('quota_enabled', param($request, 'quota_enabled', 'off')); - $this->updateSetting('default_user_quota', stringToBytes(param($request, 'default_user_quota', '1G'))); - $user = make(UserQuery::class)->get($request, $this->session->get('user_id')); - $this->setSessionQuotaInfo($user->current_disk_quota, $user->max_disk_quota); - - $this->updateSetting('custom_head', param($request, 'custom_head')); - $this->updateSetting('recaptcha_enabled', param($request, 'recaptcha_enabled', 'off')); - $this->updateSetting('recaptcha_site_key', param($request, 'recaptcha_site_key')); - $this->updateSetting('recaptcha_secret_key', param($request, 'recaptcha_secret_key')); - - $this->applyTheme($request); - $this->applyLang($request); - - $this->logger->info("User $user->username updated the system settings."); - $this->session->alert(lang('settings_saved')); - - return redirect($response, route('system')); - } - - /** - * @param Request $request - */ - public function applyLang(Request $request) - { - if (param($request, 'lang') !== 'auto') { - $this->updateSetting('lang', param($request, 'lang')); - } else { - $this->database->query('DELETE FROM `settings` WHERE `key` = \'lang\''); - } - } - - - /** - * @param Request $request - */ - public function applyTheme(Request $request) - { - if (param($request, 'css') !== null) { - if (!is_writable(BASE_DIR.'static/bootstrap/css/bootstrap.min.css')) { - $this->session->alert(lang('cannot_write_file'), 'danger'); - } else { - file_put_contents(BASE_DIR.'static/bootstrap/css/bootstrap.min.css', file_get_contents(param($request, 'css'))); - } - - // if is default, remove setting - if (param($request, 'css') !== 'https://bootswatch.com/_vendor/bootstrap/dist/css/bootstrap.min.css') { - $this->updateSetting('css', param($request, 'css')); - } else { - $this->database->query('DELETE FROM `settings` WHERE `key` = \'css\''); - } - } - } - - /** - * @param $key - * @param null $value - */ - private function updateSetting($key, $value = null) - { - if (!$this->database->query('SELECT `value` FROM `settings` WHERE `key` = '.$this->database->getPdo()->quote($key))->fetch()) { - $this->database->query('INSERT INTO `settings`(`key`, `value`) VALUES ('.$this->database->getPdo()->quote($key).', ?)', $value); - } else { - $this->database->query('UPDATE `settings` SET `value`=? WHERE `key` = '.$this->database->getPdo()->quote($key), $value); - } - } -} diff --git a/app/Controllers/TagController.php b/app/Controllers/TagController.php deleted file mode 100644 index a95c1bd..0000000 --- a/app/Controllers/TagController.php +++ /dev/null @@ -1,78 +0,0 @@ -validateTag($request); - - if ($validator->fails()) { - throw new HttpBadRequestException($request); - } - - [$id, $limit] = make(TagQuery::class)->addTag(param($request, 'tag'), param($request, 'mediaId')); - - $this->logger->info("Tag added $id."); - - return json($response, [ - 'limitReached' => $limit, - 'tagId' => $id, - 'href' => queryParams(['tag' => $id]), - ]); - } - - /** - * @param Request $request - * @param Response $response - * @return Response - * @throws HttpBadRequestException - * @throws HttpNotFoundException - */ - public function removeTag(Request $request, Response $response): Response - { - $validator = $this->validateTag($request)->removeRule('tag.notEmpty'); - - if ($validator->fails()) { - throw new HttpBadRequestException($request); - } - - $result = make(TagQuery::class)->removeTag(param($request, 'tagId'), param($request, 'mediaId')); - - if (!$result) { - throw new HttpNotFoundException($request); - } - - $this->logger->info("Tag removed ".param($request, 'tagId').', from media '.param($request, 'mediaId')); - - return $response; - } - - protected function validateTag(Request $request) - { - return ValidationChecker::make() - ->rules([ - 'tag.notEmpty' => !empty(param($request, 'tag')), - 'mediaId.notEmpty' => !empty(param($request, 'mediaId')), - 'media.exists' => $this->database->query('SELECT COUNT(*) AS `count` FROM `uploads` WHERE `id` = ?', param($request, 'mediaId'))->fetch()->count > 0, - 'sameUserOrAdmin' => $this->session->get('admin', false) || $this->database->query('SELECT * FROM `uploads` WHERE `id` = ? LIMIT 1', param($request, 'mediaId'))->fetch()->user_id === $this->session->get('user_id'), - ]); - } -} diff --git a/app/Controllers/ThemeController.php b/app/Controllers/ThemeController.php new file mode 100644 index 0000000..d7c5f6a --- /dev/null +++ b/app/Controllers/ThemeController.php @@ -0,0 +1,41 @@ +themes as $theme) { + $out["{$theme->name} - {$theme->description}"] = $theme->cssMin; + } + + return $response->withJson($out); + } + + + public function applyTheme(Request $request, Response $response): Response + { + if (!is_writable(BASE_DIR . 'static/bootstrap/css/bootstrap.min.css')) { + $this->session->alert(lang('cannot_write_file'), 'danger'); + return redirect($response, 'system'); + } + + file_put_contents(BASE_DIR . 'static/bootstrap/css/bootstrap.min.css', file_get_contents($request->getParam('css'))); + return redirect($response, 'system'); + } + +} \ No newline at end of file diff --git a/app/Controllers/UpgradeController.php b/app/Controllers/UpgradeController.php index 6e070aa..22a3e64 100644 --- a/app/Controllers/UpgradeController.php +++ b/app/Controllers/UpgradeController.php @@ -2,9 +2,9 @@ namespace App\Controllers; -use Psr\Http\Message\ResponseInterface as Response; -use Psr\Http\Message\ServerRequestInterface as Request; -use RuntimeException; + +use Slim\Http\Request; +use Slim\Http\Response; use ZipArchive; class UpgradeController extends Controller @@ -12,46 +12,40 @@ class UpgradeController extends Controller const GITHUB_SOURCE_API = 'https://api.github.com/repos/SergiX44/XBackBone/releases'; /** - * @param Response $response - * + * @param Request $request + * @param Response $response * @return Response */ - public function upgrade(Response $response): Response + public function upgrade(Request $request, Response $response): Response { if (!is_writable(BASE_DIR)) { $this->session->alert(lang('path_not_writable', BASE_DIR), 'warning'); - - return redirect($response, route('system')); + return redirect($response, 'system'); } try { $json = $this->getApiJson(); - } catch (RuntimeException $e) { + } catch (\RuntimeException $e) { $this->session->alert($e->getMessage(), 'danger'); - - return redirect($response, route('system')); + return redirect($response, 'system'); } if (version_compare($json[0]->tag_name, PLATFORM_VERSION, '<=')) { $this->session->alert(lang('already_latest_version'), 'warning'); - - return redirect($response, route('system')); + return redirect($response, 'system'); } $tmpFile = sys_get_temp_dir().DIRECTORY_SEPARATOR.'xbackbone_update.zip'; if (file_put_contents($tmpFile, file_get_contents($json[0]->assets[0]->browser_download_url)) === false) { $this->session->alert(lang('cannot_retrieve_file'), 'danger'); - - return redirect($response, route('system')); - } + return redirect($response, 'system'); + }; if (filesize($tmpFile) !== $json[0]->assets[0]->size) { $this->session->alert(lang('file_size_no_match'), 'danger'); - - return redirect($response, route('system')); + return redirect($response, 'system'); } - $this->logger->info('System update started.'); $config = require BASE_DIR.'config.php'; $config['maintenance'] = true; @@ -70,6 +64,7 @@ class UpgradeController extends Controller removeDirectory(BASE_DIR.'vendor/'); + $updateZip = new ZipArchive(); $updateZip->open($tmpFile); @@ -90,26 +85,23 @@ class UpgradeController extends Controller $updateZip->close(); unlink($tmpFile); - $this->logger->info('System update completed.'); - - return redirect($response, urlFor('/install')); + return redirect($response, '/install'); } /** - * @param Request $request - * @param Response $response - * + * @param Request $request + * @param Response $response * @return Response */ public function checkForUpdates(Request $request, Response $response): Response { $jsonResponse = [ - 'status' => null, + 'status' => null, 'message' => null, 'upgrade' => false, ]; - $acceptPrerelease = param($request, 'prerelease', 'false') === 'true'; + $acceptPrerelease = $request->getParam('prerelease', 'false') === 'true'; try { $json = $this->getApiJson(); @@ -128,12 +120,11 @@ class UpgradeController extends Controller break; } } - } catch (RuntimeException $e) { + } catch (\RuntimeException $e) { $jsonResponse['status'] = 'ERROR'; $jsonResponse['message'] = $e->getMessage(); } - - return json($response, $jsonResponse); + return $response->withJson($jsonResponse); } protected function getApiJson() @@ -151,9 +142,10 @@ class UpgradeController extends Controller $data = @file_get_contents(self::GITHUB_SOURCE_API, false, stream_context_create($opts)); if ($data === false) { - throw new RuntimeException('Cannot contact the Github API. Try again.'); + throw new \RuntimeException('Cannot contact the Github API. Try again.'); } return json_decode($data); } -} + +} \ No newline at end of file diff --git a/app/Controllers/UploadController.php b/app/Controllers/UploadController.php index 1c75fef..eb71a2d 100644 --- a/app/Controllers/UploadController.php +++ b/app/Controllers/UploadController.php @@ -2,231 +2,411 @@ namespace App\Controllers; -use App\Database\Queries\TagQuery; -use App\Database\Queries\UserQuery; -use App\Exceptions\ValidationException; -use Exception; -use Psr\Http\Message\ResponseInterface as Response; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\UploadedFileInterface; +use App\Exceptions\UnauthorizedException; +use Intervention\Image\ImageManagerStatic as Image; +use League\Flysystem\FileExistsException; +use League\Flysystem\FileNotFoundException; +use League\Flysystem\Filesystem; +use Slim\Exception\NotFoundException; +use Slim\Http\Request; +use Slim\Http\Response; +use Slim\Http\Stream; class UploadController extends Controller { - private $json = [ - 'message' => null, - 'version' => PLATFORM_VERSION, - ]; - /** - * @param Response $response - * - * @return Response - * @throws \Twig\Error\LoaderError - * @throws \Twig\Error\RuntimeError - * @throws \Twig\Error\SyntaxError - */ - public function uploadWebPage(Response $response): Response - { - $maxFileSize = min(stringToBytes(ini_get('post_max_size')), stringToBytes(ini_get('upload_max_filesize'))); + /** + * @param Request $request + * @param Response $response + * @return Response + * @throws FileExistsException + */ + public function upload(Request $request, Response $response): Response + { - return view()->render($response, 'upload/web.twig', [ - 'max_file_size' => humanFileSize($maxFileSize), - ]); - } + $json = [ + 'message' => null, + 'version' => PLATFORM_VERSION, + ]; - /** - * @param Request $request - * @param Response $response - * @return Response - * @throws Exception - */ - public function uploadWeb(Request $request, Response $response): Response - { - try { - $file = $this->validateFile($request, $response); + if ($this->settings['maintenance']) { + $json['message'] = 'Endpoint under maintenance.'; + return $response->withJson($json, 503); + } - $user = make(UserQuery::class)->get($request, $this->session->get('user_id')); + if ($request->getServerParam('CONTENT_LENGTH') > stringToBytes(ini_get('post_max_size'))) { + $json['message'] = 'File too large (post_max_size too low?).'; + return $response->withJson($json, 400); + } - $this->validateUser($request, $response, $file, $user); - } catch (ValidationException $e) { - return $e->response(); - } + if (isset($request->getUploadedFiles()['upload']) && $request->getUploadedFiles()['upload']->getError() === UPLOAD_ERR_INI_SIZE) { + $json['message'] = 'File too large (upload_max_filesize too low?).'; + return $response->withJson($json, 400); + } - if (!$this->updateUserQuota($request, $user->id, $file->getSize())) { - $this->json['message'] = 'User disk quota exceeded.'; + if ($request->getParam('token') === null) { + $json['message'] = 'Token not specified.'; + return $response->withJson($json, 400); + } - return json($response, $this->json, 507); - } + $user = $this->database->query('SELECT * FROM `users` WHERE `token` = ? LIMIT 1', $request->getParam('token'))->fetch(); - try { - $response = $this->saveMedia($response, $file, $user); - $this->setSessionQuotaInfo($user->current_disk_quota + $file->getSize(), $user->max_disk_quota); - } catch (Exception $e) { - $this->updateUserQuota($request, $user->id, $file->getSize(), true); - throw $e; - } + if (!$user) { + $json['message'] = 'Token specified not found.'; + return $response->withJson($json, 404); + } - return $response; - } + if (!$user->active) { + $json['message'] = 'Account disabled.'; + return $response->withJson($json, 401); + } - /** - * @param Request $request - * @param Response $response - * - * @return Response - * @throws Exception - */ - public function uploadEndpoint(Request $request, Response $response): Response - { - if ($this->config['maintenance']) { - $this->json['message'] = 'Endpoint under maintenance.'; + do { + $code = humanRandomString(); + } while ($this->database->query('SELECT COUNT(*) AS `count` FROM `uploads` WHERE `code` = ?', $code)->fetch()->count > 0); - return json($response, $this->json, 503); - } + /** @var \Psr\Http\Message\UploadedFileInterface $file */ + $file = $request->getUploadedFiles()['upload']; - try { - $file = $this->validateFile($request, $response); + $fileInfo = pathinfo($file->getClientFilename()); + $storagePath = "$user->user_code/$code.$fileInfo[extension]"; - if (param($request, 'token') === null) { - $this->json['message'] = 'Token not specified.'; + $this->storage->writeStream($storagePath, $file->getStream()->detach()); - return json($response, $this->json, 400); - } + $this->database->query('INSERT INTO `uploads`(`user_id`, `code`, `filename`, `storage_path`) VALUES (?, ?, ?, ?)', [ + $user->id, + $code, + $file->getClientFilename(), + $storagePath, + ]); - $user = $this->database->query('SELECT * FROM `users` WHERE `token` = ? LIMIT 1', param($request, 'token'))->fetch(); + $json['message'] = 'OK.'; + $json['url'] = urlFor("/$user->user_code/$code.$fileInfo[extension]"); - $this->validateUser($request, $response, $file, $user); - } catch (ValidationException $e) { - return $e->response(); - } + $this->logger->info("User $user->username uploaded new media.", [$this->database->raw()->lastInsertId()]); - if (!$this->updateUserQuota($request, $user->id, $file->getSize())) { - $this->json['message'] = 'User disk quota exceeded.'; + return $response->withJson($json, 201); + } - return json($response, $this->json, 507); - } + /** + * @param Request $request + * @param Response $response + * @param $args + * @return Response + * @throws FileNotFoundException + * @throws NotFoundException + */ + public function show(Request $request, Response $response, $args): Response + { + $media = $this->getMedia($args['userCode'], $args['mediaCode']); - try { - $response = $this->saveMedia($response, $file, $user); - } catch (Exception $e) { - $this->updateUserQuota($request, $user->id, $file->getSize(), true); - throw $e; - } - return $response; - } + if (!$media || (!$media->published && $this->session->get('user_id') !== $media->user_id && !$this->session->get('admin', false))) { + throw new NotFoundException($request, $response); + } - /** - * @param Request $request - * @param Response $response - * @return UploadedFileInterface - * @throws ValidationException - */ - protected function validateFile(Request $request, Response $response) - { - if ($request->getServerParams()['CONTENT_LENGTH'] > stringToBytes(ini_get('post_max_size'))) { - $this->json['message'] = 'File too large (post_max_size too low?).'; + $filesystem = $this->storage; - throw new ValidationException(json($response, $this->json, 400)); - } + if (isBot($request->getHeaderLine('User-Agent'))) { + return $this->streamMedia($request, $response, $filesystem, $media); + } else { + try { + $media->mimetype = $filesystem->getMimetype($media->storage_path); + $size = $filesystem->getSize($media->storage_path); - $file = array_values($request->getUploadedFiles()); - /** @var UploadedFileInterface|null $file */ - $file = $file[0] ?? null; + $type = explode('/', $media->mimetype)[0]; + if ($type === 'image' && !isDisplayableImage($media->mimetype)) { + $type = 'application'; + $media->mimetype = 'application/octet-stream'; + } + if ($type === 'text') { + if ($size <= (200 * 1024)) { // less than 200 KB + $media->text = $filesystem->read($media->storage_path); + } else { + $type = 'application'; + $media->mimetype = 'application/octet-stream'; + } + } + $media->size = humanFileSize($size); - if ($file === null) { - $this->json['message'] = 'Request without file attached.'; + } catch (FileNotFoundException $e) { + throw new NotFoundException($request, $response); + } - throw new ValidationException(json($response, $this->json, 400)); - } + return $this->view->render($response, 'upload/public.twig', [ + 'delete_token' => isset($args['token']) ? $args['token'] : null, + 'media' => $media, + 'type' => $type, + 'extension' => pathinfo($media->filename, PATHINFO_EXTENSION), + ]); + } + } - if ($file->getError() === UPLOAD_ERR_INI_SIZE) { - $this->json['message'] = 'File too large (upload_max_filesize too low?).'; + /** + * @param Request $request + * @param Response $response + * @param $args + * @return Response + * @throws NotFoundException + * @throws UnauthorizedException + */ + public function deleteByToken(Request $request, Response $response, $args): Response + { + $media = $this->getMedia($args['userCode'], $args['mediaCode']); - throw new ValidationException(json($response, $this->json, 400)); - } + if (!$media) { + throw new NotFoundException($request, $response); + } - return $file; - } + $user = $this->database->query('SELECT `id`, `active` FROM `users` WHERE `token` = ? LIMIT 1', $args['token'])->fetch(); - /** - * @param Request $request - * @param Response $response - * @param UploadedFileInterface $file - * @param $user - * @return void - * @throws ValidationException - */ - protected function validateUser(Request $request, Response $response, UploadedFileInterface $file, $user) - { - if (!$user) { - $this->json['message'] = 'Token specified not found.'; + if (!$user) { + $this->session->alert(lang('token_not_found'), 'danger'); + return $response->withRedirect($request->getHeaderLine('HTTP_REFERER')); + } - throw new ValidationException(json($response, $this->json, 404)); - } + if (!$user->active) { + $this->session->alert(lang('account_disabled'), 'danger'); + return $response->withRedirect($request->getHeaderLine('HTTP_REFERER')); + } - if (!$user->active) { - $this->json['message'] = 'Account disabled.'; + if ($this->session->get('admin', false) || $user->id === $media->user_id) { - throw new ValidationException(json($response, $this->json, 401)); - } - } + try { + $this->storage->delete($media->storage_path); + } catch (FileNotFoundException $e) { + throw new NotFoundException($request, $response); + } finally { + $this->database->query('DELETE FROM `uploads` WHERE `id` = ?', $media->mediaId); + $this->logger->info('User ' . $user->username . ' deleted a media via token.', [$media->mediaId]); + } + } else { + throw new UnauthorizedException(); + } + + return redirect($response, 'home'); + } + + /** + * @param Request $request + * @param Response $response + * @param $args + * @return Response + * @throws NotFoundException + * @throws FileNotFoundException + */ + public function getRawById(Request $request, Response $response, $args): Response + { + + $media = $this->database->query('SELECT * FROM `uploads` WHERE `id` = ? LIMIT 1', $args['id'])->fetch(); + + if (!$media) { + throw new NotFoundException($request, $response); + } + + return $this->streamMedia($request, $response, $this->storage, $media); + } + + /** + * @param Request $request + * @param Response $response + * @param $args + * @return Response + * @throws NotFoundException + * @throws FileNotFoundException + */ + public function showRaw(Request $request, Response $response, $args): Response + { + $media = $this->getMedia($args['userCode'], $args['mediaCode']); + + if (!$media || !$media->published && $this->session->get('user_id') !== $media->user_id && !$this->session->get('admin', false)) { + throw new NotFoundException($request, $response); + } + return $this->streamMedia($request, $response, $this->storage, $media); + } - /** - * @param Response $response - * @param UploadedFileInterface $file - * @param $user - * @return Response - * @throws \League\Flysystem\FileExistsException - * @throws \League\Flysystem\FileNotFoundException - */ - protected function saveMedia(Response $response, UploadedFileInterface $file, $user) - { - do { - $code = humanRandomString(); - } while ($this->database->query('SELECT COUNT(*) AS `count` FROM `uploads` WHERE `code` = ?', $code)->fetch()->count > 0); + /** + * @param Request $request + * @param Response $response + * @param $args + * @return Response + * @throws NotFoundException + * @throws FileNotFoundException + */ + public function download(Request $request, Response $response, $args): Response + { + $media = $this->getMedia($args['userCode'], $args['mediaCode']); - $fileInfo = pathinfo($file->getClientFilename()); - $storagePath = "$user->user_code/$code.$fileInfo[extension]"; + if (!$media || !$media->published && $this->session->get('user_id') !== $media->user_id && !$this->session->get('admin', false)) { + throw new NotFoundException($request, $response); + } + return $this->streamMedia($request, $response, $this->storage, $media, 'attachment'); + } - $this->storage->writeStream($storagePath, $file->getStream()->detach()); + /** + * @param Request $request + * @param Response $response + * @param $args + * @return Response + * @throws NotFoundException + */ + public function togglePublish(Request $request, Response $response, $args): Response + { + if ($this->session->get('admin')) { + $media = $this->database->query('SELECT * FROM `uploads` WHERE `id` = ? LIMIT 1', $args['id'])->fetch(); + } else { + $media = $this->database->query('SELECT * FROM `uploads` WHERE `id` = ? AND `user_id` = ? LIMIT 1', [$args['id'], $this->session->get('user_id')])->fetch(); + } - $this->database->query('INSERT INTO `uploads`(`user_id`, `code`, `filename`, `storage_path`, `published`) VALUES (?, ?, ?, ?, ?)', [ - $user->id, - $code, - $file->getClientFilename(), - $storagePath, - $user->hide_uploads == '1' ? 0 : 1, - ]); - $mediaId = $this->database->getPdo()->lastInsertId(); + if (!$media) { + throw new NotFoundException($request, $response); + } - $this->autoTag($mediaId, $storagePath); + $this->database->query('UPDATE `uploads` SET `published`=? WHERE `id`=?', [$media->published ? 0 : 1, $media->id]); - $this->json['message'] = 'OK'; - $this->json['url'] = urlFor("/{$user->user_code}/{$code}.{$fileInfo['extension']}"); + return $response->withStatus(200); + } - $this->logger->info("User $user->username uploaded new media.", [$mediaId]); + /** + * @param Request $request + * @param Response $response + * @param $args + * @return Response + * @throws NotFoundException + * @throws UnauthorizedException + */ + public function delete(Request $request, Response $response, $args): Response + { + $media = $this->database->query('SELECT * FROM `uploads` WHERE `id` = ? LIMIT 1', $args['id'])->fetch(); - return json($response, $this->json, 201); - } + if (!$media) { + throw new NotFoundException($request, $response); + } - /** - * @param $mediaId - * @param $storagePath - * @throws \League\Flysystem\FileNotFoundException - */ - protected function autoTag($mediaId, $storagePath) - { - $mime = $this->storage->getMimetype($storagePath); + if ($this->session->get('admin', false) || $media->user_id === $this->session->get('user_id')) { - [$type, $subtype] = explode('/', $mime); + try { + $this->storage->delete($media->storage_path); + } catch (FileNotFoundException $e) { + throw new NotFoundException($request, $response); + } finally { + $this->database->query('DELETE FROM `uploads` WHERE `id` = ?', $args['id']); + $this->logger->info('User ' . $this->session->get('username') . ' deleted a media.', [$args['id']]); + $this->session->set('used_space', humanFileSize($this->getUsedSpaceByUser($this->session->get('user_id')))); + } + } else { + throw new UnauthorizedException(); + } - /** @var TagQuery $query */ - $query = make(TagQuery::class); - $query->addTag($type, $mediaId); + return $response->withStatus(200); + } - if ($type === 'application') { - $query->addTag($subtype, $mediaId); - } - } -} + /** + * @param $userCode + * @param $mediaCode + * @return mixed + */ + protected function getMedia($userCode, $mediaCode) + { + $mediaCode = pathinfo($mediaCode)['filename']; + + $media = $this->database->query('SELECT `uploads`.*, `users`.*, `users`.`id` AS `userId`, `uploads`.`id` AS `mediaId` FROM `uploads` INNER JOIN `users` ON `uploads`.`user_id` = `users`.`id` WHERE `user_code` = ? AND `uploads`.`code` = ? LIMIT 1', [ + $userCode, + $mediaCode, + ])->fetch(); + + return $media; + } + + /** + * @param Request $request + * @param Response $response + * @param Filesystem $storage + * @param $media + * @param string $disposition + * @return Response + * @throws FileNotFoundException + */ + protected function streamMedia(Request $request, Response $response, Filesystem $storage, $media, string $disposition = 'inline'): Response + { + set_time_limit(0); + $mime = $storage->getMimetype($media->storage_path); + + if ($request->getParam('width') !== null && explode('/', $mime)[0] === 'image') { + + $image = Image::make($storage->readStream($media->storage_path)) + ->resizeCanvas( + $request->getParam('width'), + $request->getParam('height'), + 'center') + ->encode('png'); + + return $response + ->withHeader('Content-Type', 'image/png') + ->withHeader('Content-Disposition', $disposition . ';filename="scaled-' . pathinfo($media->filename)['filename'] . '.png"') + ->write($image); + } else { + $stream = new Stream($storage->readStream($media->storage_path)); + + if (!in_array(explode('/', $mime)[0], ['image', 'video', 'audio']) || $disposition === 'attachment') { + return $response->withHeader('Content-Type', $mime) + ->withHeader('Content-Disposition', $disposition . '; filename="' . $media->filename . '"') + ->withHeader('Content-Length', $stream->getSize()) + ->withBody($stream); + } + + $end = $stream->getSize() - 1; + + if ($request->getServerParam('HTTP_RANGE') !== null) { + list(, $range) = explode('=', $request->getServerParam('HTTP_RANGE'), 2); + + if (strpos($range, ',') !== false) { + return $response->withHeader('Content-Type', $mime) + ->withHeader('Content-Disposition', $disposition . '; filename="' . $media->filename . '"') + ->withHeader('Content-Length', $stream->getSize()) + ->withHeader('Accept-Ranges', 'bytes') + ->withHeader('Content-Range', "0,{$stream->getSize()}") + ->withStatus(416) + ->withBody($stream); + } + + if ($range === '-') { + $start = $stream->getSize() - (int)substr($range, 1); + } else { + $range = explode('-', $range); + $start = (int)$range[0]; + $end = (isset($range[1]) && is_numeric($range[1])) ? (int)$range[1] : $stream->getSize(); + } + + $end = ($end > $stream->getSize() - 1) ? $stream->getSize() - 1 : $end; + $stream->seek($start); + + header("Content-Type: $mime"); + header('Content-Length: ' . ($end - $start + 1)); + header('Accept-Ranges: bytes'); + header("Content-Range: bytes $start-$end/{$stream->getSize()}"); + + http_response_code(206); + ob_end_clean(); + + $buffer = 16348; + $readed = $start; + while ($readed < $end) { + if ($readed + $buffer > $end) { + $buffer = $end - $readed + 1; + } + echo $stream->read($buffer); + $readed += $buffer; + } + + exit(0); + } + + return $response->withHeader('Content-Type', $mime) + ->withHeader('Content-Length', $stream->getSize()) + ->withHeader('Accept-Ranges', 'bytes') + ->withStatus(200) + ->withBody($stream); + } + } +} \ No newline at end of file diff --git a/app/Controllers/UserController.php b/app/Controllers/UserController.php index ef1d198..f6ebf45 100644 --- a/app/Controllers/UserController.php +++ b/app/Controllers/UserController.php @@ -2,321 +2,420 @@ namespace App\Controllers; -use App\Database\Queries\UserQuery; -use App\Web\Mail; -use App\Web\ValidationChecker; -use League\Flysystem\FileNotFoundException; -use Psr\Http\Message\ResponseInterface as Response; -use Psr\Http\Message\ServerRequestInterface as Request; + +use App\Exceptions\UnauthorizedException; +use Slim\Exception\NotFoundException; +use Slim\Http\Request; +use Slim\Http\Response; class UserController extends Controller { - const PER_PAGE = 15; + const PER_PAGE = 15; - /** - * @param Response $response - * @param int|null $page - * - * @return Response - * @throws \Twig\Error\RuntimeError - * @throws \Twig\Error\SyntaxError - * - * @throws \Twig\Error\LoaderError - */ - public function index(Response $response, int $page = 0): Response - { - $page = max(0, --$page); + /** + * @param Request $request + * @param Response $response + * @param $args + * @return Response + */ + public function index(Request $request, Response $response, $args): Response + { + $page = isset($args['page']) ? (int)$args['page'] : 0; + $page = max(0, --$page); - $users = $this->database->query('SELECT * FROM `users` LIMIT ? OFFSET ?', [self::PER_PAGE, $page * self::PER_PAGE])->fetchAll(); + $users = $this->database->query('SELECT * FROM `users` LIMIT ? OFFSET ?', [self::PER_PAGE, $page * self::PER_PAGE])->fetchAll(); - $pages = $this->database->query('SELECT COUNT(*) AS `count` FROM `users`')->fetch()->count / self::PER_PAGE; + $pages = $this->database->query('SELECT COUNT(*) AS `count` FROM `users`')->fetch()->count / self::PER_PAGE; - return view()->render($response, - 'user/index.twig', - [ - 'users' => $users, - 'next' => $page < floor($pages), - 'previous' => $page >= 1, - 'current_page' => ++$page, - 'quota_enabled' => $this->getSetting('quota_enabled'), - ] - ); - } + return $this->view->render($response, + 'user/index.twig', + [ + 'users' => $users, + 'next' => $page < floor($pages), + 'previous' => $page >= 1, + 'current_page' => ++$page, + ] + ); + } - /** - * @param Response $response - * - * @return Response - * @throws \Twig\Error\RuntimeError - * @throws \Twig\Error\SyntaxError - * - * @throws \Twig\Error\LoaderError - */ - public function create(Response $response): Response - { - return view()->render($response, 'user/create.twig', [ - 'default_user_quota' => humanFileSize($this->getSetting('default_user_quota'), 0, true), - 'quota_enabled' => $this->getSetting('quota_enabled', 'off'), - ]); - } + /** + * @param Request $request + * @param Response $response + * @return Response + */ + public function create(Request $request, Response $response): Response + { + return $this->view->render($response, 'user/create.twig'); + } - /** - * @param Request $request - * @param Response $response - * - * @return Response - * @throws \Exception - */ - public function store(Request $request, Response $response): Response - { - $validator = $this->getUserCreateValidator($request); - $hasPassword = $validator->removeRule('password.required'); + /** + * @param Request $request + * @param Response $response + * @return Response + */ + public function store(Request $request, Response $response): Response + { + if ($request->getParam('email') === null) { + $this->session->alert(lang('email_required'), 'danger'); + return redirect($response, 'user.create'); + } - if ($validator->fails()) { - return redirect($response, route('user.create')); - } + if ($this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `email` = ?', $request->getParam('email'))->fetch()->count > 0) { + $this->session->alert(lang('email_taken'), 'danger'); + return redirect($response, 'user.create'); + } - $maxUserQuota = -1; - if ($this->getSetting('quota_enabled') === 'on') { - $maxUserQuotaStr = param($request, 'max_user_quota', humanFileSize($this->getSetting('default_user_quota', -1), 0, true)); - if (!preg_match('/([0-9]+[K|M|G|T])|(\-1)/i', $maxUserQuotaStr)) { - $this->session->alert(lang('invalid_quota', 'danger')); - return redirect($response, route('user.create')); - } + if ($request->getParam('username') === null) { + $this->session->alert(lang('username_required'), 'danger'); + return redirect($response, 'user.create'); + } - if ($maxUserQuotaStr !== '-1') { - $maxUserQuota = stringToBytes($maxUserQuotaStr); - } - } + if ($request->getParam('password') === null) { + $this->session->alert(lang('password_required'), 'danger'); + return redirect($response, 'user.create'); + } - make(UserQuery::class)->create( - param($request, 'email'), - param($request, 'username'), - param($request, 'password'), - param($request, 'is_admin') !== null ? 1 : 0, - param($request, 'is_active') !== null ? 1 : 0, - $maxUserQuota, - false, - param($request, 'hide_uploads') !== null ? 1 : 0, - param($request, 'copy_raw') !== null ? 1 : 0 - ); + if ($this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `username` = ?', $request->getParam('username'))->fetch()->count > 0) { + $this->session->alert(lang('username_taken'), 'danger'); + return redirect($response, 'user.create'); + } - if (param($request, 'send_notification') !== null) { - $this->sendCreateNotification($hasPassword, $request); - } + do { + $userCode = humanRandomString(5); + } while ($this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `user_code` = ?', $userCode)->fetch()->count > 0); - $this->session->alert(lang('user_created', [param($request, 'username')]), 'success'); - $this->logger->info('User '.$this->session->get('username').' created a new user.', [array_diff_key($request->getParsedBody(), array_flip(['password']))]); + $token = $this->generateNewToken(); - return redirect($response, route('user.index')); - } + $this->database->query('INSERT INTO `users`(`email`, `username`, `password`, `is_admin`, `active`, `user_code`, `token`) VALUES (?, ?, ?, ?, ?, ?, ?)', [ + $request->getParam('email'), + $request->getParam('username'), + password_hash($request->getParam('password'), PASSWORD_DEFAULT), + $request->getParam('is_admin') !== null ? 1 : 0, + $request->getParam('is_active') !== null ? 1 : 0, + $userCode, + $token, + ]); - /** - * @param Request $request - * @param Response $response - * @param int $id - * - * @return Response - * @throws \Twig\Error\LoaderError - * @throws \Twig\Error\RuntimeError - * @throws \Twig\Error\SyntaxError - */ - public function edit(Request $request, Response $response, int $id): Response - { - $user = make(UserQuery::class)->get($request, $id); + $this->session->alert(lang('user_created', [$request->getParam('username')]), 'success'); + $this->logger->info('User ' . $this->session->get('username') . ' created a new user.', [array_diff_key($request->getParams(), array_flip(['password']))]); - return view()->render($response, 'user/edit.twig', [ - 'profile' => false, - 'user' => $user, - 'quota_enabled' => $this->getSetting('quota_enabled', 'off'), - 'max_disk_quota' => $user->max_disk_quota > 0 ? humanFileSize($user->max_disk_quota, 0, true) : -1, - ]); - } + return redirect($response, 'user.index'); + } - /** - * @param Request $request - * @param Response $response - * @param int $id - * - * @return Response - */ - public function update(Request $request, Response $response, int $id): Response - { - $user = make(UserQuery::class)->get($request, $id); + /** + * @param Request $request + * @param Response $response + * @param $args + * @return Response + * @throws NotFoundException + */ + public function edit(Request $request, Response $response, $args): Response + { + $user = $this->database->query('SELECT * FROM `users` WHERE `id` = ? LIMIT 1', $args['id'])->fetch(); - $validator = ValidationChecker::make() - ->rules([ - 'email.required' => filter_var(param($request, 'email'), FILTER_VALIDATE_EMAIL), - 'username.required' => !empty(param($request, 'username')), - 'email.unique' => $this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `email` = ? AND `email` <> ?', [param($request, 'email'), $user->email])->fetch()->count == 0, - 'username.unique' => $this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `username` = ? AND `username` <> ?', [param($request, 'username'), $user->username])->fetch()->count == 0, - 'demote' => !($user->id === $this->session->get('user_id') && param($request, 'is_admin') === null), - ]) - ->onFail(function ($rule) { - $alerts = [ - 'email.required' => lang('email_required'), - 'username.required' => lang('username_required'), - 'email.unique' => lang('email_taken'), - 'username.unique' => lang('username_taken'), - 'demote' => lang('cannot_demote'), - ]; + if (!$user) { + throw new NotFoundException($request, $response); + } - $this->session->alert($alerts[$rule], 'danger'); - }); + return $this->view->render($response, 'user/edit.twig', [ + 'profile' => false, + 'user' => $user, + ]); + } - if ($validator->fails()) { - return redirect($response, route('user.edit', ['id' => $id])); - } + /** + * @param Request $request + * @param Response $response + * @param $args + * @return Response + * @throws NotFoundException + */ + public function update(Request $request, Response $response, $args): Response + { + $user = $this->database->query('SELECT * FROM `users` WHERE `id` = ? LIMIT 1', $args['id'])->fetch(); - $user->max_disk_quota = -1; - if ($this->getSetting('quota_enabled') === 'on') { - $maxUserQuota = param($request, 'max_user_quota', humanFileSize($this->getSetting('default_user_quota'), 0, true)); - if (!preg_match('/([0-9]+[K|M|G|T])|(\-1)/i', $maxUserQuota)) { - $this->session->alert(lang('invalid_quota', 'danger')); - return redirect($response, route('user.create')); - } + if (!$user) { + throw new NotFoundException($request, $response); + } - if ($maxUserQuota !== '-1') { - $user->max_disk_quota = stringToBytes($maxUserQuota); - } - } + if ($request->getParam('email') === null) { + $this->session->alert(lang('email_required'), 'danger'); + return redirect($response, 'user.edit', ['id' => $args['id']]); + } - make(UserQuery::class)->update( - $user->id, - param($request, 'email'), - param($request, 'username'), - param($request, 'password'), - param($request, 'is_admin') !== null ? 1 : 0, - param($request, 'is_active') !== null ? 1 : 0, - $user->max_disk_quota, - param($request, 'ldap') !== null ? 1 : 0, - param($request, 'hide_uploads') !== null ? 1 : 0, - param($request, 'copy_raw') !== null ? 1 : 0 - ); + if ($this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `email` = ? AND `email` <> ?', [$request->getParam('email'), $user->email])->fetch()->count > 0) { + $this->session->alert(lang('email_taken'), 'danger'); + return redirect($response, 'user.edit', ['id' => $args['id']]); + } - if ($user->id === $this->session->get('user_id')) { - $this->setSessionQuotaInfo($user->current_disk_quota, $user->max_disk_quota); - } + if ($request->getParam('username') === null) { + $this->session->alert(lang('username_required'), 'danger'); + return redirect($response, 'user.edit', ['id' => $args['id']]); + } - $this->session->alert(lang('user_updated', [param($request, 'username')]), 'success'); - $this->logger->info('User '.$this->session->get('username')." updated $user->id.", [ - array_diff_key((array) $user, array_flip(['password'])), - array_diff_key($request->getParsedBody(), array_flip(['password'])), - ]); + if ($this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `username` = ? AND `username` <> ?', [$request->getParam('username'), $user->username])->fetch()->count > 0) { + $this->session->alert(lang('username_taken'), 'danger'); + return redirect($response, 'user.edit', ['id' => $args['id']]); + } - return redirect($response, route('user.index')); - } + if ($user->id === $this->session->get('user_id') && $request->getParam('is_admin') === null) { + $this->session->alert(lang('cannot_demote'), 'danger'); + return redirect($response, 'user.edit', ['id' => $args['id']]); + } - /** - * @param Request $request - * @param Response $response - * @param int $id - * - * @return Response - */ - public function delete(Request $request, Response $response, int $id): Response - { - $user = make(UserQuery::class)->get($request, $id); + if ($request->getParam('password') !== null && !empty($request->getParam('password'))) { + $this->database->query('UPDATE `users` SET `email`=?, `username`=?, `password`=?, `is_admin`=?, `active`=? WHERE `id` = ?', [ + $request->getParam('email'), + $request->getParam('username'), + password_hash($request->getParam('password'), PASSWORD_DEFAULT), + $request->getParam('is_admin') !== null ? 1 : 0, + $request->getParam('is_active') !== null ? 1 : 0, + $user->id, + ]); + } else { + $this->database->query('UPDATE `users` SET `email`=?, `username`=?, `is_admin`=?, `active`=? WHERE `id` = ?', [ + $request->getParam('email'), + $request->getParam('username'), + $request->getParam('is_admin') !== null ? 1 : 0, + $request->getParam('is_active') !== null ? 1 : 0, + $user->id, + ]); + } - if ($user->id === $this->session->get('user_id')) { - $this->session->alert(lang('cannot_delete'), 'danger'); + $this->session->alert(lang('user_updated', [$request->getParam('username')]), 'success'); + $this->logger->info('User ' . $this->session->get('username') . " updated $user->id.", [ + array_diff_key((array)$user, array_flip(['password'])), + array_diff_key($request->getParams(), array_flip(['password'])), + ]); - return redirect($response, route('user.index')); - } + return redirect($response, 'user.index'); - $this->database->query('DELETE FROM `users` WHERE `id` = ?', $user->id); + } - $this->session->alert(lang('user_deleted'), 'success'); - $this->logger->info('User '.$this->session->get('username')." deleted $user->id."); + /** + * @param Request $request + * @param Response $response + * @param $args + * @return Response + * @throws NotFoundException + */ + public function delete(Request $request, Response $response, $args): Response + { + $user = $this->database->query('SELECT * FROM `users` WHERE `id` = ? LIMIT 1', $args['id'])->fetch(); - return redirect($response, route('user.index')); - } + if (!$user) { + throw new NotFoundException($request, $response); + } - /** - * @param Request $request - * @param Response $response - * @param int $id - * @return Response - */ - public function clearUserMedia(Request $request, Response $response, int $id): Response - { - $user = make(UserQuery::class)->get($request, $id, true); + if ($user->id === $this->session->get('user_id')) { + $this->session->alert(lang('cannot_delete'), 'danger'); + return redirect($response, 'user.index'); + } - $medias = $this->database->query('SELECT * FROM `uploads` WHERE `user_id` = ?', $user->id); + $this->database->query('DELETE FROM `users` WHERE `id` = ?', $user->id); - foreach ($medias as $media) { - try { - $this->storage->delete($media->storage_path); - } catch (FileNotFoundException $e) { - } - } + $this->session->alert(lang('user_deleted'), 'success'); + $this->logger->info('User ' . $this->session->get('username') . " deleted $user->id."); - $this->database->query('DELETE FROM `uploads` WHERE `user_id` = ?', $user->id); - $this->database->query('UPDATE `users` SET `current_disk_quota`=? WHERE `id` = ?', [ - 0, - $user->id, - ]); + return redirect($response, 'user.index'); + } - $this->session->alert(lang('account_media_deleted'), 'success'); - return redirect($response, route('user.edit', ['id' => $id])); - } + /** + * @param Request $request + * @param Response $response + * @return Response + * @throws NotFoundException + * @throws UnauthorizedException + */ + public function profile(Request $request, Response $response): Response + { + $user = $this->database->query('SELECT * FROM `users` WHERE `id` = ? LIMIT 1', $this->session->get('user_id'))->fetch(); - /** - * @param Request $request - * @param Response $response - * @param int $id - * - * @return Response - */ - public function refreshToken(Request $request, Response $response, int $id): Response - { - $query = make(UserQuery::class); - $user = $query->get($request, $id, true); + if (!$user) { + throw new NotFoundException($request, $response); + } - $this->logger->info('User '.$this->session->get('username')." refreshed token of user $user->id."); + if ($user->id !== $this->session->get('user_id') && !$this->session->get('admin', false)) { + throw new UnauthorizedException(); + } - $response->getBody()->write($query->refreshToken($user->id)); + return $this->view->render($response, 'user/edit.twig', [ + 'profile' => true, + 'user' => $user, + ]); + } - return $response; - } + /** + * @param Request $request + * @param Response $response + * @param $args + * @return Response + * @throws NotFoundException + * @throws UnauthorizedException + */ + public function profileEdit(Request $request, Response $response, $args): Response + { + $user = $this->database->query('SELECT * FROM `users` WHERE `id` = ? LIMIT 1', $args['id'])->fetch(); - /** - * @param $hasPassword - * @param $request - * @throws \Exception - */ - private function sendCreateNotification($hasPassword, $request) - { - if ($hasPassword) { - $message = lang('mail.new_account_text_with_pw', [ - param($request, 'username'), - $this->config['app_name'], - $this->config['base_url'], - param($request, 'username'), - param($request, 'password'), - route('login.show'), - ]); - } else { - $resetToken = bin2hex(random_bytes(16)); + if (!$user) { + throw new NotFoundException($request, $response); + } - $this->database->query('UPDATE `users` SET `reset_token`=? WHERE `id` = ?', [ - $resetToken, - $this->database->getPdo()->lastInsertId(), - ]); + if ($user->id !== $this->session->get('user_id') && !$this->session->get('admin', false)) { + throw new UnauthorizedException(); + } - $message = lang('mail.new_account_text_with_reset', [ - param($request, 'username'), - $this->config['app_name'], - $this->config['base_url'], - route('recover.password', ['resetToken' => $resetToken]), - ]); - } + if ($request->getParam('email') === null) { + $this->session->alert(lang('email_required'), 'danger'); + return redirect($response, 'profile'); + } - Mail::make() - ->from(platform_mail(), $this->config['app_name']) - ->to(param($request, 'email')) - ->subject(lang('mail.new_account', [$this->config['app_name']])) - ->message($message) - ->send(); - } -} + if ($this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `email` = ? AND `email` <> ?', [$request->getParam('email'), $user->email])->fetch()->count > 0) { + $this->session->alert(lang('email_taken'), 'danger'); + return redirect($response, 'profile'); + } + + if ($request->getParam('password') !== null && !empty($request->getParam('password'))) { + $this->database->query('UPDATE `users` SET `email`=?, `password`=? WHERE `id` = ?', [ + $request->getParam('email'), + password_hash($request->getParam('password'), PASSWORD_DEFAULT), + $user->id, + ]); + } else { + $this->database->query('UPDATE `users` SET `email`=? WHERE `id` = ?', [ + $request->getParam('email'), + $user->id, + ]); + } + + $this->session->alert(lang('profile_updated'), 'success'); + $this->logger->info('User ' . $this->session->get('username') . " updated profile of $user->id."); + + return redirect($response, 'profile'); + } + + /** + * @param Request $request + * @param Response $response + * @param $args + * @return Response + * @throws NotFoundException + * @throws UnauthorizedException + */ + public function refreshToken(Request $request, Response $response, $args): Response + { + $user = $this->database->query('SELECT * FROM `users` WHERE `id` = ? LIMIT 1', $args['id'])->fetch(); + + if (!$user) { + throw new NotFoundException($request, $response); + } + + if ($user->id !== $this->session->get('user_id') && !$this->session->get('admin', false)) { + throw new UnauthorizedException(); + } + + $token = $this->generateNewToken(); + + $this->database->query('UPDATE `users` SET `token`=? WHERE `id` = ?', [ + $token, + $user->id, + ]); + + $this->logger->info('User ' . $this->session->get('username') . " refreshed token of user $user->id."); + + $response->getBody()->write($token); + + return $response; + } + + /** + * @param Request $request + * @param Response $response + * @param $args + * @return Response + * @throws NotFoundException + * @throws UnauthorizedException + */ + public function getShareXconfigFile(Request $request, Response $response, $args): Response + { + $user = $this->database->query('SELECT * FROM `users` WHERE `id` = ? LIMIT 1', $args['id'])->fetch(); + + if (!$user) { + throw new NotFoundException($request, $response); + } + + if ($user->id !== $this->session->get('user_id') && !$this->session->get('admin', false)) { + throw new UnauthorizedException(); + } + + if ($user->token === null || $user->token === '') { + $this->session->alert('You don\'t have a personal upload token. (Click the update token button and try again)', 'danger'); + return $response->withRedirect($request->getHeaderLine('HTTP_REFERER')); + } + + $json = [ + 'DestinationType' => 'ImageUploader, TextUploader, FileUploader', + 'RequestURL' => route('upload'), + 'FileFormName' => 'upload', + 'Arguments' => [ + 'file' => '$filename$', + 'text' => '$input$', + 'token' => $user->token, + ], + 'URL' => '$json:url$', + 'ThumbnailURL' => '$json:url$/raw', + 'DeletionURL' => '$json:url$/delete/' . $user->token, + ]; + + return $response + ->withHeader('Content-Disposition', 'attachment;filename="' . $user->username . '-ShareX.sxcu"') + ->withJson($json, 200, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); + } + + /** + * @param Request $request + * @param Response $response + * @param $args + * @return Response + * @throws NotFoundException + * @throws UnauthorizedException + */ + public function getUploaderScriptFile(Request $request, Response $response, $args): Response + { + $user = $this->database->query('SELECT * FROM `users` WHERE `id` = ? LIMIT 1', $args['id'])->fetch(); + + if (!$user) { + throw new NotFoundException($request, $response); + } + + if ($user->id !== $this->session->get('user_id') && !$this->session->get('admin', false)) { + throw new UnauthorizedException(); + } + + if ($user->token === null || $user->token === '') { + $this->session->alert('You don\'t have a personal upload token. (Click the update token button and try again)', 'danger'); + return $response->withRedirect($request->getHeaderLine('HTTP_REFERER')); + } + + return $this->view->render($response->withHeader('Content-Disposition', 'attachment;filename="xbackbone_uploader_' . $user->username . '.sh"'), + 'scripts/xbackbone_uploader.sh.twig', + [ + 'username' => $user->username, + 'upload_url' => route('upload'), + 'token' => $user->token, + ] + ); + } + + /** + * @return string + */ + protected function generateNewToken(): string + { + do { + $token = 'token_' . md5(uniqid('', true)); + } while ($this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `token` = ?', $token)->fetch()->count > 0); + + return $token; + } +} \ No newline at end of file diff --git a/app/Database/DB.php b/app/Database/DB.php index 396e7e4..a95c2a4 100644 --- a/app/Database/DB.php +++ b/app/Database/DB.php @@ -2,64 +2,128 @@ namespace App\Database; + use PDO; class DB { - /** @var DB */ - protected static $instance; - /** @var PDO */ - protected $pdo; + /** @var DB */ + protected static $instance; - /** @var string */ - protected $currentDriver; + /** @var string */ + private static $password; - public function __construct(string $dsn, string $username = null, string $password = null) - { - $this->pdo = new PDO($dsn, $username, $password); - $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - $this->pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ); + /** @var string */ + private static $username; - $this->currentDriver = $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME); - if ($this->currentDriver === 'sqlite') { - $this->pdo->exec('PRAGMA foreign_keys = ON'); - } - } + /** @var PDO */ + protected $pdo; - public function query(string $query, $parameters = []) - { - if (!is_array($parameters)) { - $parameters = [$parameters]; - } - $query = $this->pdo->prepare($query); + /** @var string */ + protected static $dsn = 'sqlite:database.db'; - foreach ($parameters as $index => $parameter) { - $query->bindValue($index + 1, $parameter, is_int($parameter) ? PDO::PARAM_INT : PDO::PARAM_STR); - } + /** @var string */ + protected $currentDriver; - $query->execute(); + public function __construct(string $dsn, string $username = null, string $password = null) + { + self::setDsn($dsn, $username, $password); - return $query; - } + $this->pdo = new PDO($dsn, $username, $password); + $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $this->pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ); - /** - * Get the PDO instance. - * - * @return PDO - */ - public function getPdo(): PDO - { - return $this->pdo; - } + $this->currentDriver = $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME); + if ($this->currentDriver === 'sqlite') { + $this->pdo->exec('PRAGMA foreign_keys = ON'); + } + } - /** - * Get the current PDO driver. - * - * @return string - */ - public function getCurrentDriver(): string - { - return $this->currentDriver; - } -} + public function query(string $query, $parameters = []) + { + if (!is_array($parameters)) { + $parameters = [$parameters]; + } + $query = $this->pdo->prepare($query); + + foreach ($parameters as $index => $parameter) { + $query->bindValue($index + 1, $parameter, is_int($parameter) ? PDO::PARAM_INT : PDO::PARAM_STR); + } + + $query->execute(); + return $query; + } + + /** + * Get the PDO instance + * @return PDO + */ + public function getPdo(): PDO + { + return $this->pdo; + } + + /** + * Get the current PDO driver + * @return string + */ + public function getCurrentDriver(): string + { + return $this->currentDriver; + } + + public static function getInstance(): DB + { + if (self::$instance === null) { + self::$instance = new self(self::$dsn, self::$username, self::$password); + } + + return self::$instance; + } + + /** + * Perform a query + * @param string $query + * @param array $parameters + * @return bool|\PDOStatement|string + */ + public static function doQuery(string $query, $parameters = []) + { + return self::getInstance()->query($query, $parameters); + } + + /** + * Static method to get the current driver name + * @return string + */ + public static function driver(): string + { + + return self::getInstance()->getCurrentDriver(); + } + + /** + * Get directly the PDO instance + * @return PDO + */ + public static function raw(): PDO + { + + return self::getInstance()->getPdo(); + } + + /** + * Set the PDO connection string + * @param string $dsn + * @param string|null $username + * @param string|null $password + */ + public static function setDsn(string $dsn, string $username = null, string $password = null) + { + self::$dsn = $dsn; + self::$username = $username; + self::$password = $password; + } + +} \ No newline at end of file diff --git a/app/Database/Migrator.php b/app/Database/Migrator.php deleted file mode 100644 index 0ba89d4..0000000 --- a/app/Database/Migrator.php +++ /dev/null @@ -1,125 +0,0 @@ -db = $db; - $this->schemaPath = $schemaPath; - $this->firstMigrate = $firstMigrate; - } - - public function migrate() - { - try { - $this->db->query('SELECT 1 FROM `migrations` LIMIT 1'); - } catch (PDOException $exception) { - $this->firstMigrate = true; - } - - if ($this->firstMigrate) { - $this->db->getPdo()->exec(file_get_contents($this->schemaPath.DIRECTORY_SEPARATOR.'migrations.sql')); - } - - $files = glob($this->schemaPath.'/'.$this->db->getCurrentDriver().'/*.sql'); - - $names = array_map(function ($path) { - return basename($path); - }, $files); - - $in = str_repeat('?, ', count($names) - 1).'?'; - - $inMigrationsTable = $this->db->query("SELECT * FROM `migrations` WHERE `name` IN ($in)", $names)->fetchAll(); - - foreach ($files as $file) { - $continue = false; - $exists = false; - - foreach ($inMigrationsTable as $migration) { - if (basename($file) === $migration->name && $migration->migrated) { - $continue = true; - break; - } else { - if (basename($file) === $migration->name && !$migration->migrated) { - $exists = true; - break; - } - } - } - if ($continue) { - continue; - } - - $sql = file_get_contents($file); - - try { - $this->db->getPdo()->exec($sql); - if (!$exists) { - $this->db->query('INSERT INTO `migrations` VALUES (?,?)', [basename($file), 1]); - } else { - $this->db->query('UPDATE `migrations` SET `migrated`=? WHERE `name`=?', [1, basename($file)]); - } - } catch (PDOException $exception) { - if (!$exists) { - $this->db->query('INSERT INTO `migrations` VALUES (?,?)', [basename($file), 0]); - } - - throw $exception; - } - } - } - - /** - * @param Filesystem $filesystem - */ - public function reSyncQuotas(Filesystem $filesystem) - { - $uploads = $this->db->query('SELECT `id`,`user_id`, `storage_path` FROM `uploads`')->fetchAll(); - - $usersQuotas = []; - - foreach ($uploads as $upload) { - if (!array_key_exists($upload->user_id, $usersQuotas)) { - $usersQuotas[$upload->user_id] = 0; - } - try { - $usersQuotas[$upload->user_id] += $filesystem->getSize($upload->storage_path); - } catch (FileNotFoundException $e) { - $this->db->query('DELETE FROM `uploads` WHERE `id` = ?', $upload->id); - } - } - - foreach ($usersQuotas as $userId => $quota) { - $this->db->query('UPDATE `users` SET `current_disk_quota`=? WHERE `id` = ?', [ - $quota, - $userId, - ]); - } - } -} diff --git a/app/Database/Queries/MediaQuery.php b/app/Database/Queries/MediaQuery.php index 2721d09..444da5f 100644 --- a/app/Database/Queries/MediaQuery.php +++ b/app/Database/Queries/MediaQuery.php @@ -2,355 +2,230 @@ namespace App\Database\Queries; + use App\Database\DB; use League\Flysystem\FileNotFoundException; use League\Flysystem\Filesystem; -use League\Flysystem\Plugin\ListWith; +use League\Flysystem\Plugin\ListFiles; class MediaQuery { - const PER_PAGE = 21; - const PER_PAGE_ADMIN = 27; + const PER_PAGE = 21; + const PER_PAGE_ADMIN = 27; - const ORDER_TIME = 0; - const ORDER_NAME = 1; - const ORDER_SIZE = 2; - - /** @var DB */ - protected $db; - - /** @var bool */ - protected $isAdmin; - - protected $userId; - - /** @var int */ - protected $orderBy; - - /** @var string */ - protected $orderMode; - - /** @var string */ - protected $text; - - /** @var Filesystem */ - protected $storage; - - private $pages; - private $media; - - /** - * @var int - */ - private $tagId; - - /** - * MediaQuery constructor. - * - * @param DB $db - * @param bool $isAdmin - * @param Filesystem $storage - */ - public function __construct(DB $db, Filesystem $storage, bool $isAdmin) - { - $this->db = $db; - $this->isAdmin = $isAdmin; - $this->storage = $storage; - } - - /** - * @param DB $db - * @param bool $isAdmin - * @param Filesystem $storage - * @return MediaQuery - */ - public static function make(DB $db, Filesystem $storage, bool $isAdmin) - { - return new self($db, $storage, $isAdmin); - } - - /** - * @param $id - * - * @return $this - */ - public function withUserId($id) - { - $this->userId = $id; - - return $this; - } - - /** - * @param string|null $type - * @param string $mode - * - * @return $this - */ - public function orderBy(string $type = null, $mode = 'ASC') - { - $this->orderBy = ($type === null) ? self::ORDER_TIME : $type; - $this->orderMode = (strtoupper($mode) === 'ASC') ? 'ASC' : 'DESC'; - - return $this; - } - - /** - * @param string $text - * - * @return $this - */ - public function search(?string $text) - { - $this->text = $text; - - return $this; - } - - public function filterByTag($tagId) - { - if ($tagId !== null) { - $this->tagId = (int) $tagId; - } - - return $this; - } + const ORDER_TIME = 0; + const ORDER_NAME = 1; + const ORDER_SIZE = 2; - public function run(int $page) - { - if ($this->orderBy == self::ORDER_SIZE) { - $this->runWithFileSort($page); - } else { - $this->runWithDbSort($page); - } + /** @var DB */ + protected $db; - return $this; - } + /** @var bool */ + protected $isAdmin; - public function runWithDbSort(int $page) - { - $params = []; - if ($this->isAdmin) { - [$queryMedia, $queryPages] = $this->buildAdminQueries(); - $constPage = self::PER_PAGE_ADMIN; - } else { - [$queryMedia, $queryPages] = $this->buildUserQueries(); - $params[] = $this->userId; - $constPage = self::PER_PAGE; - } + protected $userId; - if ($this->text !== null) { - $params[] = '%'.htmlentities($this->text).'%'; - } + /** @var int */ + protected $orderBy; - $queryMedia .= $this->buildOrderBy().' LIMIT ? OFFSET ?'; + /** @var string */ + protected $orderMode; - $this->media = $this->db->query($queryMedia, array_merge($params, [$constPage, $page * $constPage]))->fetchAll(); - $this->pages = $this->db->query($queryPages, $params)->fetch()->count / $constPage; + /** @var string */ + protected $text; - $tags = $this->getTags(array_column($this->media, 'id')); + /** @var Filesystem */ + protected $storage; - foreach ($this->media as $media) { - try { - $media->size = humanFileSize($this->storage->getSize($media->storage_path)); - $media->mimetype = $this->storage->getMimetype($media->storage_path); - } catch (FileNotFoundException $e) { - $media->size = null; - $media->mimetype = null; - } - $media->extension = pathinfo($media->filename, PATHINFO_EXTENSION); - if (array_key_exists($media->id, $tags)) { - $media->tags = $tags[$media->id]; - } else { - $media->tags = []; - } - } + private $pages; + private $media; - return $this; - } + /** + * MediaQuery constructor. + * @param DB $db + * @param bool $isAdmin + * @param Filesystem $storage + */ + public function __construct(DB $db, bool $isAdmin, Filesystem $storage) + { + $this->db = $db; + $this->isAdmin = $isAdmin; + $this->storage = $storage; + } - public function runWithFileSort(int $page) - { - $this->storage->addPlugin(new ListWith()); + /** + * @param $id + * @return $this + */ + public function withUserId($id) + { + $this->userId = $id; + return $this; + } - if ($this->isAdmin) { - $files = $this->storage->listWith(['size', 'mimetype'], '/', true); - $offset = $page * self::PER_PAGE_ADMIN; - $limit = self::PER_PAGE_ADMIN; - } else { - $userCode = $this->db->query('SELECT `user_code` FROM `users` WHERE `id` = ?', $this->userId)->fetch()->user_code; - $files = $this->storage->listWith(['size', 'mimetype'], $userCode); - $offset = $page * self::PER_PAGE; - $limit = self::PER_PAGE; - } + /** + * @param string|null $type + * @param string $mode + * @return $this + */ + public function orderBy(string $type = null, $mode = 'ASC') + { + $this->orderBy = ($type === null) ? self::ORDER_TIME : $type; + $this->orderMode = (strtoupper($mode) === 'ASC') ? 'ASC' : 'DESC'; + return $this; + } - $files = array_filter($files, function ($file) { - return $file['type'] !== 'dir'; - }); + /** + * @param string $text + * @return $this + */ + public function search(?string $text) + { + $this->text = $text; + return $this; + } - array_multisort(array_column($files, 'size'), $this->buildOrderBy(), SORT_NUMERIC, $files); + /** + * @param int $page + */ + public function run(int $page) + { + if ($this->orderBy == self::ORDER_SIZE) { + $this->runWithOrderBySize($page); + return; + } - $params = []; - if ($this->text !== null) { - if ($this->isAdmin) { - [$queryMedia,] = $this->buildAdminQueries(); - } else { - [$queryMedia,] = $this->buildUserQueries(); - $params[] = $this->userId; - } + $queryPages = 'SELECT COUNT(*) AS `count` FROM `uploads`'; - $params[] = '%'.htmlentities($this->text).'%'; - $paths = array_column($files, 'path'); - } else { - if ($this->tagId !== null) { - $paths = array_column($files, 'path'); - $ids = $this->getMediaIdsByTagId($this->tagId); - $queryMedia = 'SELECT `uploads`.*, `users`.`user_code`, `users`.`username` FROM `uploads` LEFT JOIN `users` ON `uploads`.`user_id` = `users`.`id` WHERE `uploads`.`storage_path` IN ("'.implode('","', $paths).'") AND `uploads`.`id` IN ('.implode(',', $ids).')'; - } else { - $files = array_slice($files, $offset, $limit, true); - $paths = array_column($files, 'path'); - $queryMedia = 'SELECT `uploads`.*, `users`.`user_code`, `users`.`username` FROM `uploads` LEFT JOIN `users` ON `uploads`.`user_id` = `users`.`id` WHERE `uploads`.`storage_path` IN ("'.implode('","', $paths).'")'; - } - } + if ($this->isAdmin) { + $queryMedia = 'SELECT `uploads`.*, `users`.`user_code`, `users`.`username` FROM `uploads` LEFT JOIN `users` ON `uploads`.`user_id` = `users`.`id` %s LIMIT ? OFFSET ?'; + } else { + $queryMedia = 'SELECT `uploads`.*,`users`.`user_code`, `users`.`username` FROM `uploads` INNER JOIN `users` ON `uploads`.`user_id` = `users`.`id` WHERE `user_id` = ? %s LIMIT ? OFFSET ?'; + $queryPages .= ' WHERE `user_id` = ?'; + } - $medias = $this->db->query($queryMedia, $params)->fetchAll(); + $orderAndSearch = ''; + $params = []; - $paths = array_flip($paths); - foreach ($medias as $media) { - $paths[$media->storage_path] = $media; - } + if ($this->text !== null) { + $orderAndSearch = $this->isAdmin ? 'WHERE `uploads`.`filename` LIKE ? ' : 'AND `uploads`.`filename` LIKE ? '; + $queryPages .= $this->isAdmin ? ' WHERE `filename` LIKE ?' : ' AND `filename` LIKE ?'; + $params[] = '%' . htmlentities($this->text) . '%'; + } - $tags = $this->getTags(array_column($medias, 'id')); + switch ($this->orderBy) { + case self::ORDER_NAME: + $orderAndSearch .= 'ORDER BY `filename` ' . $this->orderMode; + break; + default: + case self::ORDER_TIME: + $orderAndSearch .= 'ORDER BY `timestamp` ' . $this->orderMode; + break; + } - $this->media = []; - foreach ($files as $file) { - $media = $paths[$file['path']]; - if (is_object($media)) { - $media->size = humanFileSize($file['size']); - $media->extension = $file['extension']; - $media->mimetype = $file['mimetype']; - $this->media[] = $media; - if (array_key_exists($media->id, $tags)) { - $media->tags = $tags[$media->id]; - } else { - $media->tags = []; - } - } - } + $queryMedia = sprintf($queryMedia, $orderAndSearch); - $this->pages = count($this->media) / $limit; + if ($this->isAdmin) { + $this->media = $this->db->query($queryMedia, array_merge($params, [self::PER_PAGE_ADMIN, $page * self::PER_PAGE_ADMIN]))->fetchAll(); + $this->pages = $this->db->query($queryPages, $params)->fetch()->count / self::PER_PAGE_ADMIN; + } else { + $this->media = $this->db->query($queryMedia, array_merge([$this->userId], $params, [self::PER_PAGE, $page * self::PER_PAGE]))->fetchAll(); + $this->pages = $this->db->query($queryPages, array_merge([$this->userId], $params))->fetch()->count / self::PER_PAGE; + } - if ($this->text !== null || $this->tagId !== null) { - $this->media = array_slice($this->media, $offset, $limit, true); - } + foreach ($this->media as $media) { + try { + $media->size = humanFileSize($this->storage->getSize($media->storage_path)); + $media->mimetype = $this->storage->getMimetype($media->storage_path); + } catch (FileNotFoundException $e) { + $media->size = null; + $media->mimetype = null; + } + $media->extension = pathinfo($media->filename, PATHINFO_EXTENSION); + } - return $this; - } + } - protected function buildAdminQueries() - { - $queryPages = 'SELECT COUNT(*) AS `count` FROM `uploads`'; - $queryMedia = 'SELECT `uploads`.*, `users`.`user_code`, `users`.`username` FROM `uploads` LEFT JOIN `users` ON `uploads`.`user_id` = `users`.`id`'; + /** + * @param int $page + */ + private function runWithOrderBySize(int $page) + { + $this->storage->addPlugin(new ListFiles()); - if ($this->text !== null || $this->tagId !== null) { - $queryMedia .= ' WHERE'; - $queryPages .= ' WHERE'; - } + if ($this->isAdmin) { + $files = $this->storage->listFiles('/', true); + $this->pages = count($files) / self::PER_PAGE_ADMIN; - if ($this->text !== null) { - $queryMedia .= ' `uploads`.`filename` LIKE ?'; - $queryPages .= ' `filename` LIKE ?'; - } + $offset = $page * self::PER_PAGE_ADMIN; + $limit = self::PER_PAGE_ADMIN; + } else { + $userCode = $this->db->query('SELECT `user_code` FROM `users` WHERE `id` = ?', [$this->userId])->fetch()->user_code; + $files = $this->storage->listFiles($userCode); + $this->pages = count($files) / self::PER_PAGE; - if ($this->tagId !== null) { - if ($this->text !== null) { - $queryMedia .= ' AND'; - $queryPages .= ' AND'; - } + $offset = $page * self::PER_PAGE; + $limit = self::PER_PAGE; + } - $ids = $this->getMediaIdsByTagId($this->tagId); - $queryMedia .= ' `uploads`.`id` IN ('.implode(',', $ids).')'; - $queryPages .= ' `uploads`.`id` IN ('.implode(',', $ids).')'; - } + array_multisort(array_column($files, 'size'), ($this->orderMode === 'ASC') ? SORT_ASC : SORT_DESC, SORT_NUMERIC, $files); - return [$queryMedia, $queryPages]; - } + if ($this->text !== null) { + if ($this->isAdmin) { + $medias = $this->db->query('SELECT `uploads`.*, `users`.`user_code`, `users`.`username` FROM `uploads` LEFT JOIN `users` ON `uploads`.`user_id` = `users`.`id` WHERE `uploads`.`filename` LIKE ? ', ['%' . htmlentities($this->text) . '%'])->fetchAll(); + } else { + $medias = $this->db->query('SELECT `uploads`.*, `users`.`user_code`, `users`.`username` FROM `uploads` LEFT JOIN `users` ON `uploads`.`user_id` = `users`.`id` WHERE `user_id` = ? AND `uploads`.`filename` LIKE ? ', [$this->userId, '%' . htmlentities($this->text) . '%'])->fetchAll(); + } - protected function buildUserQueries() - { - $queryPages = 'SELECT COUNT(*) AS `count` FROM `uploads` WHERE `user_id` = ?'; - $queryMedia = 'SELECT `uploads`.*,`users`.`user_code`, `users`.`username` FROM `uploads` INNER JOIN `users` ON `uploads`.`user_id` = `users`.`id` WHERE `user_id` = ?'; + $paths = array_column($files, 'path'); + } else { + $files = array_slice($files, $offset, $limit); + $paths = array_column($files, 'path'); - if ($this->text !== null) { - $queryMedia .= ' AND `uploads`.`filename` LIKE ? '; - $queryPages .= ' AND `filename` LIKE ?'; - } + $medias = $this->db->query('SELECT `uploads`.*, `users`.`user_code`, `users`.`username` FROM `uploads` LEFT JOIN `users` ON `uploads`.`user_id` = `users`.`id` WHERE `uploads`.`storage_path` IN ("' . implode('","', $paths) . '")')->fetchAll(); + } - if ($this->tagId !== null) { - $ids = $this->getMediaIdsByTagId($this->tagId); - $queryMedia .= ' AND `uploads`.`id` IN ('.implode(',', $ids).')'; - $queryPages .= ' AND `uploads`.`id` IN ('.implode(',', $ids).')'; - } + $paths = array_flip($paths); + foreach ($medias as $media) { + $paths[$media->storage_path] = $media; + } - return [$queryMedia, $queryPages]; - } + $this->media = []; + foreach ($files as $file) { + $media = $paths[$file['path']]; + if (!is_object($media)) { + continue; + } + $media->size = humanFileSize($file['size']); + try { + $media->mimetype = $this->storage->getMimetype($file['path']); + } catch (FileNotFoundException $e) { + $media->mimetype = null; + } + $media->extension = $file['extension']; + $this->media[] = $media; + } - protected function buildOrderBy() - { - switch ($this->orderBy) { - case self::ORDER_NAME: - return ' ORDER BY `filename` '.$this->orderMode; - case self::ORDER_TIME: - return ' ORDER BY `timestamp` '.$this->orderMode; - case self::ORDER_SIZE: - return ($this->orderMode === 'ASC') ? SORT_ASC : SORT_DESC; - default: - return ''; - } - } + if ($this->text !== null) { + $this->media = array_slice($this->media, $offset, $limit); + } + } - /** - * @param array $mediaIds - * @return array - */ - protected function getTags(array $mediaIds) - { - $allTags = $this->db->query('SELECT `uploads_tags`.`upload_id`,`tags`.`id`, `tags`.`name` FROM `uploads_tags` INNER JOIN `tags` ON `uploads_tags`.`tag_id` = `tags`.`id` WHERE `uploads_tags`.`upload_id` IN ("'.implode('","', $mediaIds).'") ORDER BY `tags`.`timestamp`')->fetchAll(); - $tags = []; - foreach ($allTags as $tag) { - $tags[$tag->upload_id][$tag->id] = $tag->name; - } - return $tags; - } + /** + * @return mixed + */ + public function getMedia() + { + return $this->media; - /** - * @param $tagId - * @return array - */ - protected function getMediaIdsByTagId($tagId) - { - $mediaIds = $this->db->query('SELECT `upload_id` FROM `uploads_tags` WHERE `tag_id` = ?', $tagId)->fetchAll(); - $ids = []; - foreach ($mediaIds as $pivot) { - $ids[] = $pivot->upload_id; - } - return $ids; - } + } - /** - * @return mixed - */ - public function getMedia() - { - return $this->media; - } - - /** - * @return mixed - */ - public function getPages() - { - return $this->pages; - } -} + /** + * @return mixed + */ + public function getPages() + { + return $this->pages; + } +} \ No newline at end of file diff --git a/app/Database/Queries/TagQuery.php b/app/Database/Queries/TagQuery.php deleted file mode 100644 index 8a99d03..0000000 --- a/app/Database/Queries/TagQuery.php +++ /dev/null @@ -1,105 +0,0 @@ -db = $db; - $this->isAdmin = $isAdmin; - $this->userId = $userId; - } - - /** - * @return array - */ - public function all() - { - if ($this->isAdmin) { - return $this->db->query('SELECT * FROM `tags` ORDER BY `name`')->fetchAll(); - } - - return $this->db->query('SELECT `tags`.* FROM `tags` INNER JOIN `uploads_tags` ON `tags`.`id` = `uploads_tags`.`tag_id` INNER JOIN `uploads` ON `uploads`.`id` = `uploads_tags`.`upload_id` WHERE `uploads`.`user_id` = ? ORDER BY `tags`.`name`', $this->userId)->fetchAll(); - } - - /** - * @param string $tagName - * @param $mediaId - * @return array [id, limit] - */ - public function addTag(string $tagName, $mediaId) - { - $tag = $this->db->query('SELECT * FROM `tags` WHERE `name` = ? LIMIT 1', $tagName)->fetch(); - - $connectedIds = $this->db->query('SELECT `tag_id` FROM `uploads_tags` WHERE `upload_id` = ?', $mediaId)->fetchAll(PDO::FETCH_COLUMN, 0); - - if (!$tag && count($connectedIds) < self::PER_MEDIA_LIMIT) { - $this->db->query('INSERT INTO `tags`(`name`) VALUES (?)', strtolower($tagName)); - - $tagId = $this->db->getPdo()->lastInsertId(); - - $this->db->query('INSERT INTO `uploads_tags`(`upload_id`, `tag_id`) VALUES (?, ?)', [ - $mediaId, - $tagId, - ]); - - return [$tagId, false]; - } - - if (count($connectedIds) >= self::PER_MEDIA_LIMIT || in_array($tag->id, $connectedIds)) { - return [null, true]; - } - - $this->db->query('INSERT INTO `uploads_tags`(`upload_id`, `tag_id`) VALUES (?, ?)', [ - $mediaId, - $tag->id, - ]); - - return [$tag->id, false]; - } - - /** - * @param $tagId - * @param $mediaId - * @return bool - */ - public function removeTag($tagId, $mediaId) - { - $tag = $this->db->query('SELECT * FROM `tags` WHERE `id` = ? LIMIT 1', $tagId)->fetch(); - - if ($tag) { - $this->db->query('DELETE FROM `uploads_tags` WHERE `upload_id` = ? AND `tag_id` = ?', [ - $mediaId, - $tag->id, - ]); - - if ($this->db->query('SELECT COUNT(*) AS `count` FROM `uploads_tags` WHERE `tag_id` = ?', $tag->id)->fetch()->count == 0) { - $this->db->query('DELETE FROM `tags` WHERE `id` = ? ', $tag->id); - } - - return true; - } - - return false; - } -} diff --git a/app/Database/Queries/UserQuery.php b/app/Database/Queries/UserQuery.php deleted file mode 100644 index 36d9190..0000000 --- a/app/Database/Queries/UserQuery.php +++ /dev/null @@ -1,180 +0,0 @@ -database = $db; - $this->session = $session; - } - - /** - * @param DB $db - * @param Session|null $session - * @return UserQuery - */ - public static function make(DB $db, Session $session = null) - { - return new self($db, $session); - } - - /** - * @param Request $request - * @param $id - * @param bool $authorize - * @return mixed - * @throws HttpNotFoundException - * @throws HttpUnauthorizedException - */ - public function get(Request $request, $id, $authorize = false) - { - $user = $this->database->query('SELECT * FROM `users` WHERE `id` = ? LIMIT 1', $id)->fetch(); - - if (!$user) { - throw new HttpNotFoundException($request); - } - - if ($authorize) { - if ($this->session === null) { - throw new \InvalidArgumentException('The session is null.'); - } - - if ($user->id !== $this->session->get('user_id') && !$this->session->get('admin', false)) { - throw new HttpUnauthorizedException($request); - } - } - - return $user; - } - - /** - * @param string $email - * @param string $username - * @param string|null $password - * @param int $isAdmin - * @param int $isActive - * @param int $maxUserQuota - * @param string|null $activateToken - * @param int $ldap - * @param int $hideUploads - * @param int $copyRaw - * @return bool|\PDOStatement|string - */ - public function create(string $email, string $username, string $password = null, int $isAdmin = 0, int $isActive = 0, int $maxUserQuota = -1, string $activateToken = null, int $ldap = 0, int $hideUploads = 0, int $copyRaw = 0) - { - do { - $userCode = humanRandomString(5); - } while ($this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `user_code` = ?', $userCode)->fetch()->count > 0); - - $token = $this->generateUserUploadToken(); - - return $this->database->query('INSERT INTO `users`(`email`, `username`, `password`, `is_admin`, `active`, `user_code`, `token`, `max_disk_quota`, `activate_token`, `ldap`, `hide_uploads`, `copy_raw`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [ - $email, - $username, - $password !== null ? password_hash($password, PASSWORD_DEFAULT) : null, - $isAdmin, - $isActive, - $userCode, - $token, - $maxUserQuota, - $activateToken, - $ldap, - $hideUploads, - $copyRaw, - ]); - } - - /** - * @param $id - * @param string $email - * @param string $username - * @param string|null $password - * @param int $isAdmin - * @param int $isActive - * @param int $maxUserQuota - * @param int $ldap - * @param int $hideUploads - * @param int $copyRaw - * @return bool|\PDOStatement|string - */ - public function update($id, string $email, string $username, string $password = null, int $isAdmin = 0, int $isActive = 0, int $maxUserQuota = -1, int $ldap = 0, int $hideUploads = 0, int $copyRaw = 0) - { - if (!empty($password)) { - return $this->database->query('UPDATE `users` SET `email`=?, `username`=?, `password`=?, `is_admin`=?, `active`=?, `max_disk_quota`=?, `ldap`=?, `hide_uploads`=?, `copy_raw`=? WHERE `id` = ?', [ - $email, - $username, - password_hash($password, PASSWORD_DEFAULT), - $isAdmin, - $isActive, - $maxUserQuota, - $ldap, - $hideUploads, - $copyRaw, - $id, - ]); - } else { - return $this->database->query('UPDATE `users` SET `email`=?, `username`=?, `is_admin`=?, `active`=?, `max_disk_quota`=?, `ldap`=?, `hide_uploads`=?, `copy_raw`=? WHERE `id` = ?', [ - $email, - $username, - $isAdmin, - $isActive, - $maxUserQuota, - $ldap, - $hideUploads, - $copyRaw, - $id, - ]); - } - } - - /** - * @param $id - * @return string - */ - public function refreshToken($id) - { - $token = $this->generateUserUploadToken(); - - $this->database->query('UPDATE `users` SET `token`=? WHERE `id` = ?', [ - $token, - $id, - ]); - - return $token; - } - - /** - * @return string - */ - protected function generateUserUploadToken(): string - { - do { - $token = 'token_'.md5(uniqid('', true)); - } while ($this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `token` = ?', $token)->fetch()->count > 0); - - return $token; - } -} diff --git a/app/Exceptions/Handlers/AppErrorHandler.php b/app/Exceptions/Handlers/AppErrorHandler.php deleted file mode 100644 index 0da16e0..0000000 --- a/app/Exceptions/Handlers/AppErrorHandler.php +++ /dev/null @@ -1,13 +0,0 @@ -error($error); - } -} diff --git a/app/Exceptions/Handlers/Renderers/HtmlErrorRenderer.php b/app/Exceptions/Handlers/Renderers/HtmlErrorRenderer.php deleted file mode 100644 index e1987fc..0000000 --- a/app/Exceptions/Handlers/Renderers/HtmlErrorRenderer.php +++ /dev/null @@ -1,50 +0,0 @@ -string('errors/maintenance.twig'); - } - - if ($exception instanceof HttpUnauthorizedException || $exception instanceof HttpForbiddenException) { - return view()->string('errors/403.twig'); - } - - if ($exception instanceof HttpMethodNotAllowedException) { - return view()->string('errors/405.twig'); - } - - if ($exception instanceof HttpNotFoundException) { - return view()->string('errors/404.twig'); - } - - if ($exception instanceof HttpBadRequestException) { - return view()->string('errors/400.twig'); - } - - return view()->string('errors/500.twig', ['exception' => $displayErrorDetails ? $exception : null]); - } -} diff --git a/app/Exceptions/MaintenanceException.php b/app/Exceptions/MaintenanceException.php new file mode 100644 index 0000000..7c4085e --- /dev/null +++ b/app/Exceptions/MaintenanceException.php @@ -0,0 +1,15 @@ +getStatusCode(), $previous); - $this->response = $response; - } - - /** - * @return Response - */ - public function response(): Response - { - return $this->response; - } -} diff --git a/app/Factories/ViewFactory.php b/app/Factories/ViewFactory.php deleted file mode 100644 index f20f77d..0000000 --- a/app/Factories/ViewFactory.php +++ /dev/null @@ -1,70 +0,0 @@ -get('config'); - $loader = new FilesystemLoader(BASE_DIR.'resources/templates'); - - $twig = new Environment($loader, [ - 'cache' => BASE_DIR.'resources/cache/twig', - 'autoescape' => 'html', - 'debug' => $config['debug'], - 'auto_reload' => $config['debug'], - ]); - - $request = ServerRequestCreatorFactory::determineServerRequestCreator()->createServerRequestFromGlobals(); - - $twig->addGlobal('config', $config); - $twig->addGlobal('request', $request); - $twig->addGlobal('session', $container->get('session')); - $twig->addGlobal('current_lang', $container->get('lang')->getLang()); - $twig->addGlobal('maxUploadSize', stringToBytes(ini_get('post_max_size'))); - $twig->addGlobal('PLATFORM_VERSION', PLATFORM_VERSION); - - $twig->addFunction(new TwigFunction('route', 'route')); - $twig->addFunction(new TwigFunction('lang', 'lang')); - $twig->addFunction(new TwigFunction('urlFor', 'urlFor')); - $twig->addFunction(new TwigFunction('asset', 'asset')); - $twig->addFunction(new TwigFunction('mime2font', 'mime2font')); - $twig->addFunction(new TwigFunction('queryParams', 'queryParams')); - $twig->addFunction(new TwigFunction('isDisplayableImage', 'isDisplayableImage')); - $twig->addFunction(new TwigFunction('inPath', 'inPath')); - $twig->addFunction(new TwigFunction('humanFileSize', 'humanFileSize')); - $twig->addFunction(new TwigFunction('param', 'param')); - - return new View($twig); - } - - public static function createInstallerInstance(Container $container) - { - $config = $container->get('config'); - $loader = new FilesystemLoader([BASE_DIR.'install/templates', BASE_DIR.'resources/templates']); - - $twig = new Environment($loader, [ - 'cache' => false, - 'autoescape' => 'html', - 'debug' => $config['debug'], - 'auto_reload' => $config['debug'], - ]); - - $request = ServerRequestCreatorFactory::determineServerRequestCreator()->createServerRequestFromGlobals(); - - $twig->addGlobal('config', $config); - $twig->addGlobal('request', $request); - $twig->addGlobal('session', $container->get('session')); - $twig->addGlobal('PLATFORM_VERSION', PLATFORM_VERSION); - - return new View($twig); - } -} diff --git a/app/Middleware/AdminMiddleware.php b/app/Middleware/AdminMiddleware.php index 7ae07c1..b84519f 100644 --- a/app/Middleware/AdminMiddleware.php +++ b/app/Middleware/AdminMiddleware.php @@ -2,30 +2,27 @@ namespace App\Middleware; -use GuzzleHttp\Psr7\Response; -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Server\RequestHandlerInterface as RequestHandler; -use Slim\Exception\HttpUnauthorizedException; +use App\Exceptions\UnauthorizedException; +use Slim\Http\Request; +use Slim\Http\Response; class AdminMiddleware extends Middleware { - /** - * @param Request $request - * @param RequestHandler $handler - * - * @throws HttpUnauthorizedException - * - * @return Response - */ - public function __invoke(Request $request, RequestHandler $handler): ResponseInterface - { - if (!$this->database->query('SELECT `id`, `is_admin` FROM `users` WHERE `id` = ? LIMIT 1', [$this->session->get('user_id')])->fetch()->is_admin) { - $this->session->set('admin', false); + /** + * @param Request $request + * @param Response $response + * @param callable $next + * @return Response + * @throws UnauthorizedException + */ + public function __invoke(Request $request, Response $response, callable $next) + { + if (!$this->database->query('SELECT `id`, `is_admin` FROM `users` WHERE `id` = ? LIMIT 1', [$this->session->get('user_id')])->fetch()->is_admin) { + $this->session->set('admin', false); + throw new UnauthorizedException(); + } - throw new HttpUnauthorizedException($request); - } + return $next($request, $response); + } - return $handler->handle($request); - } -} +} \ No newline at end of file diff --git a/app/Middleware/AuthMiddleware.php b/app/Middleware/AuthMiddleware.php index 36ff679..dc17f0c 100644 --- a/app/Middleware/AuthMiddleware.php +++ b/app/Middleware/AuthMiddleware.php @@ -2,34 +2,33 @@ namespace App\Middleware; -use GuzzleHttp\Psr7\Response; -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Server\RequestHandlerInterface as RequestHandler; +use Slim\Http\Request; +use Slim\Http\Response; class AuthMiddleware extends Middleware { - /** - * @param Request $request - * @param RequestHandler $handler - * - * @return ResponseInterface - */ - public function __invoke(Request $request, RequestHandler $handler): ResponseInterface - { - if (!$this->session->get('logged', false)) { - $this->session->set('redirectTo', (string) $request->getUri()); - return redirect(new Response(), route('login.show')); - } + /** + * @param Request $request + * @param Response $response + * @param callable $next + * @return Response + */ + public function __invoke(Request $request, Response $response, callable $next) + { + if (!$this->session->get('logged', false)) { + $this->session->set('redirectTo', (isset($_SERVER['HTTPS']) ? 'https' : 'http') . "://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]"); + return redirect($response, 'login.show'); + } - if (!$this->database->query('SELECT `id`, `active` FROM `users` WHERE `id` = ? LIMIT 1', [$this->session->get('user_id')])->fetch()->active) { - $this->session->alert(lang('account_disabled'), 'danger'); - $this->session->set('logged', false); + if (!$this->database->query('SELECT `id`, `active` FROM `users` WHERE `id` = ? LIMIT 1', [$this->session->get('user_id')])->fetch()->active) { + $this->session->alert('Your account is not active anymore.', 'danger'); + $this->session->set('logged', false); + $this->session->set('redirectTo', (isset($_SERVER['HTTPS']) ? 'https' : 'http') . "://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]"); + return redirect($response, 'login.show'); + } - return redirect(new Response(), route('login.show')); - } + return $next($request, $response); + } - return $handler->handle($request); - } -} +} \ No newline at end of file diff --git a/app/Middleware/CheckForMaintenanceMiddleware.php b/app/Middleware/CheckForMaintenanceMiddleware.php index 6cce863..a4280ea 100644 --- a/app/Middleware/CheckForMaintenanceMiddleware.php +++ b/app/Middleware/CheckForMaintenanceMiddleware.php @@ -2,27 +2,25 @@ namespace App\Middleware; -use App\Exceptions\UnderMaintenanceException; -use Psr\Http\Message\ResponseInterface as Response; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Server\RequestHandlerInterface as RequestHandler; +use App\Exceptions\MaintenanceException; +use Slim\Http\Request; +use Slim\Http\Response; class CheckForMaintenanceMiddleware extends Middleware { - /** - * @param Request $request - * @param RequestHandler $handler - * - * @throws UnderMaintenanceException - * - * @return Response - */ - public function __invoke(Request $request, RequestHandler $handler): Response - { - if ($this->config['maintenance'] && !$this->database->query('SELECT `id`, `is_admin` FROM `users` WHERE `id` = ? LIMIT 1', [$this->session->get('user_id')])->fetch()->is_admin) { - throw new UnderMaintenanceException($request); - } + /** + * @param Request $request + * @param Response $response + * @param callable $next + * @return Response + * @throws MaintenanceException + */ + public function __invoke(Request $request, Response $response, callable $next) + { + if (isset($this->settings['maintenance']) && $this->settings['maintenance'] && !$this->database->query('SELECT `id`, `is_admin` FROM `users` WHERE `id` = ? LIMIT 1', [$this->session->get('user_id')])->fetch()->is_admin) { + throw new MaintenanceException(); + } - return $handler->handle($request); - } -} + return $next($request, $response); + } +} \ No newline at end of file diff --git a/app/Middleware/InjectMiddleware.php b/app/Middleware/InjectMiddleware.php deleted file mode 100644 index ecc88c6..0000000 --- a/app/Middleware/InjectMiddleware.php +++ /dev/null @@ -1,23 +0,0 @@ -view->getTwig()->addGlobal('customHead', $this->getSetting('custom_head')); - - return $handler->handle($request); - } -} diff --git a/app/Middleware/LangMiddleware.php b/app/Middleware/LangMiddleware.php deleted file mode 100644 index b80c0ad..0000000 --- a/app/Middleware/LangMiddleware.php +++ /dev/null @@ -1,27 +0,0 @@ -getSetting('lang'); - if ($forcedLang !== null) { - $this->lang::setLang($forcedLang); - $request = $request->withAttribute('forced_lang', $forcedLang); - } - - return $handler->handle($request); - } -} diff --git a/app/Middleware/Middleware.php b/app/Middleware/Middleware.php index fde94ab..bf6b24f 100644 --- a/app/Middleware/Middleware.php +++ b/app/Middleware/Middleware.php @@ -2,18 +2,37 @@ namespace App\Middleware; -use App\Controllers\Controller; -use Psr\Http\Message\ResponseInterface as Response; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Server\RequestHandlerInterface as RequestHandler; +use Slim\Container; +use Slim\Http\Request; +use Slim\Http\Response; -abstract class Middleware extends Controller +abstract class Middleware { - /** - * @param Request $request - * @param RequestHandler $handler - * - * @return Response - */ - abstract public function __invoke(Request $request, RequestHandler $handler); -} + /** @var Container */ + protected $container; + + public function __construct(Container $container) + { + $this->container = $container; + } + + /** + * @param $name + * @return mixed|null + * @throws \Interop\Container\Exception\ContainerException + */ + public function __get($name) + { + if ($this->container->has($name)) { + return $this->container->get($name); + } + return null; + } + + /** + * @param Request $request + * @param Response $response + * @param callable $next + */ + public abstract function __invoke(Request $request, Response $response, callable $next); +} \ No newline at end of file diff --git a/app/Middleware/RememberMiddleware.php b/app/Middleware/RememberMiddleware.php deleted file mode 100644 index 9b0fce0..0000000 --- a/app/Middleware/RememberMiddleware.php +++ /dev/null @@ -1,40 +0,0 @@ -session->get('logged', false) && !empty($request->getCookieParams()['remember'])) { - [$selector, $token] = explode(':', $request->getCookieParams()['remember']); - - $user = $this->database->query('SELECT `id`, `email`, `username`,`is_admin`, `active`, `remember_token`, `current_disk_quota`, `max_disk_quota` FROM `users` WHERE `remember_selector` = ? AND `remember_expire` > ? LIMIT 1', - [$selector, date('Y-m-d\TH:i:s', time())] - )->fetch(); - - if ($user && password_verify($token, $user->remember_token) && $user->active) { - $this->session->set('logged', true); - $this->session->set('user_id', $user->id); - $this->session->set('username', $user->username); - $this->session->set('admin', $user->is_admin); - $this->setSessionQuotaInfo($user->current_disk_quota, $user->max_disk_quota); - } - - $this->refreshRememberCookie($user->id); - } - - return $handler->handle($request); - } -} diff --git a/app/Web/Lang.php b/app/Web/Lang.php index aa800a6..59d7a6f 100644 --- a/app/Web/Lang.php +++ b/app/Web/Lang.php @@ -2,155 +2,138 @@ namespace App\Web; + class Lang { - const DEFAULT_LANG = 'en'; - const LANG_PATH = __DIR__.'../../resources/lang/'; - /** @var string */ - protected static $langPath = self::LANG_PATH; + const DEFAULT_LANG = 'en'; + const LANG_PATH = __DIR__ . '../../resources/lang/'; - /** @var string */ - protected static $lang; + /** @var string */ + protected static $langPath = self::LANG_PATH; - /** @var Lang */ - protected static $instance; + /** @var string */ + protected static $lang; - /** @var array */ - protected $cache = []; + /** @var Lang */ + protected static $instance; - /** - * @return Lang - */ - public static function getInstance(): self - { - if (self::$instance === null) { - self::$instance = new self(); - } + /** @var array */ + protected $cache = []; - return self::$instance; - } - /** - * @param string $lang - * @param string $langPath - * - * @return Lang - */ - public static function build($lang = self::DEFAULT_LANG, $langPath = null): self - { - self::$lang = $lang; + /** + * @return Lang + */ + public static function getInstance(): Lang + { + if (self::$instance === null) { + self::$instance = new self(); + } - if ($langPath !== null) { - self::$langPath = $langPath; - } + return self::$instance; + } - self::$instance = new self(); + /** + * @param string $lang + * @param string $langPath + * @return Lang + */ + public static function build($lang = self::DEFAULT_LANG, $langPath = null): Lang + { + self::$lang = $lang; - return self::$instance; - } + if ($langPath !== null) { + self::$langPath = $langPath; + } - /** - * Recognize the current language from the request. - * - * @return bool|string - */ - public static function recognize() - { - if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { - return locale_accept_from_http($_SERVER['HTTP_ACCEPT_LANGUAGE']); - } + self::$instance = new self(); - return self::DEFAULT_LANG; - } + return self::$instance; + } - /** - * @return string - */ - public static function getLang(): string - { - return self::$lang; - } + /** + * Recognize the current language from the request. + * @return bool|string + */ + public static function recognize() + { + if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { + return locale_accept_from_http($_SERVER['HTTP_ACCEPT_LANGUAGE']); + } + return self::DEFAULT_LANG; + } - /** - * @param $lang - */ - public static function setLang($lang) - { - self::$lang = $lang; - } + /** + * @return string + */ + public static function getLang(): string + { + return self::$lang; + } - /** - * @return array - */ - public static function getList() - { - $languages = []; + /** + * @return array + */ + public static function getList() + { + $languages = []; - $default = count(include self::$langPath.self::DEFAULT_LANG.'.lang.php') - 1; + $default = count(include self::$langPath . self::DEFAULT_LANG . '.lang.php') - 1; - foreach (glob(self::$langPath.'*.lang.php') as $file) { - $dict = include $file; + foreach (glob(self::$langPath . '*.lang.php') as $file) { + $dict = include $file; - if (!is_array($dict) || !isset($dict['lang'])) { - continue; - } + $count = count($dict) - 1; + $prepend = "[{$count}/{$default}] "; - $count = count($dict) - 1; - $percent = min(round(($count / $default) * 100), 100); + $languages[str_replace('.lang.php', '', basename($file))] = $prepend . $dict['lang']; + } - $languages[str_replace('.lang.php', '', basename($file))] = "[{$percent}%] ".$dict['lang']; - } + return $languages; + } - return $languages; - } - /** - * @param $key - * @param array $args - * - * @return string - */ - public function get($key, $args = []): string - { - return $this->getString($key, self::$lang, $args); - } + /** + * @param $key + * @param array $args + * @return string + */ + public function get($key, $args = []): string + { + return $this->getString($key, self::$lang, $args); + } - /** - * @param $key - * @param $lang - * @param $args - * - * @return string - */ - private function getString($key, $lang, $args): string - { - $redLang = strtolower(substr($lang, 0, 2)); + /** + * @param $key + * @param $lang + * @param $args + * @return string + */ + private function getString($key, $lang, $args): string + { + $redLang = strtolower(substr($lang, 0, 2)); - if (array_key_exists($lang, $this->cache)) { - $transDict = $this->cache[$lang]; - } else { - if (file_exists(self::$langPath.$lang.'.lang.php')) { - $transDict = include self::$langPath.$lang.'.lang.php'; - $this->cache[$lang] = $transDict; - } else { - if (file_exists(self::$langPath.$redLang.'.lang.php')) { - $transDict = include self::$langPath.$redLang.'.lang.php'; - $this->cache[$lang] = $transDict; - } else { - $transDict = []; - } - } - } + if (array_key_exists($lang, $this->cache)) { + $transDict = $this->cache[$lang]; + } else if (file_exists(self::$langPath . $lang . '.lang.php')) { + $transDict = include self::$langPath . $lang . '.lang.php'; + $this->cache[$lang] = $transDict; + } else if (file_exists(self::$langPath . $redLang . '.lang.php')) { + $transDict = include self::$langPath . $redLang . '.lang.php'; + $this->cache[$lang] = $transDict; + } else { + $transDict = []; + } - if (array_key_exists($key, $transDict)) { - return vsprintf($transDict[$key], $args); - } + if (array_key_exists($key, $transDict)) { + return vsprintf($transDict[$key], $args); + } - if ($lang !== self::DEFAULT_LANG) { - return $this->getString($key, self::DEFAULT_LANG, $args); - } + if ($lang !== self::DEFAULT_LANG) { + return $this->getString($key, self::DEFAULT_LANG, $args); + } - return $key; - } + return $key; + } } diff --git a/app/Web/Mail.php b/app/Web/Mail.php deleted file mode 100644 index a0c4d00..0000000 --- a/app/Web/Mail.php +++ /dev/null @@ -1,126 +0,0 @@ -fromMail = $mail; - $this->fromName = $name; - return $this; - } - - /** - * @param $mail - * @return $this - */ - public function to(string $mail) - { - if (!filter_var($mail, FILTER_VALIDATE_EMAIL)) { - throw new InvalidArgumentException('Mail not valid.'); - } - $this->to = $mail; - return $this; - } - - - /** - * @param $text - * @return $this - */ - public function subject(string $text) - { - $this->subject = htmlentities($text); - return $this; - } - - /** - * @param $text - * @return $this - */ - public function message(string $text) - { - $this->message = htmlentities($text); - return $this; - } - - /** - * @param $header - * @return $this - */ - public function addHeader(string $header) - { - $this->additionalHeaders .= "$header\r\n"; - return $this; - } - - /** - * @param $header - * @return $this - */ - protected function addRequiredHeader(string $header) - { - $this->headers .= "$header\r\n"; - return $this; - } - - /** - * @return int - */ - public function send() - { - if ($this->to === null) { - throw new InvalidArgumentException('Target email cannot be null.'); - } - - if ($this->subject === null) { - throw new InvalidArgumentException('Subject cannot be null.'); - } - - if ($this->message === null) { - throw new InvalidArgumentException('Message cannot be null.'); - } - - if ($this->fromName === null) { - $this->addRequiredHeader("From: $this->fromMail"); - } else { - $this->addRequiredHeader("From: $this->fromName <$this->fromMail>"); - } - - $this->addRequiredHeader('X-Mailer: PHP/'.phpversion()); - $this->addRequiredHeader('MIME-Version: 1.0'); - $this->addRequiredHeader('Content-Type: text/html; charset=iso-8859-1'); - - $this->headers .= $this->additionalHeaders; - - return (int) mail($this->to, $this->subject, $this->message, $this->headers); - } -} diff --git a/app/Web/Session.php b/app/Web/Session.php index c6134a6..28cee27 100644 --- a/app/Web/Session.php +++ b/app/Web/Session.php @@ -2,144 +2,115 @@ namespace App\Web; + use Exception; class Session { - /** - * Session constructor. - * - * @param string $name - * @param string $path - * - * @throws Exception - */ - public function __construct(string $name, $path = '') - { - if (session_status() === PHP_SESSION_NONE) { - if (!is_writable($path) && $path !== '') { - throw new Exception("The given path '{$path}' is not writable."); - } - // Workaround for php <= 7.3 - if (PHP_VERSION_ID < 70300) { - $params = session_get_cookie_params(); - session_set_cookie_params( - $params['lifetime'], - $params['path'].'; SameSite=Lax', - $params['domain'], - $params['secure'], - $params['httponly'] - ); - } + /** + * Session constructor. + * @param string $name + * @param string $path + * @throws Exception + */ + public function __construct(string $name, $path = '') + { + if (session_status() === PHP_SESSION_NONE) { + if (!is_writable($path) && $path !== '') { + throw new Exception("The given path '{$path}' is not writable."); + } - $started = @session_start([ - 'name' => $name, - 'save_path' => $path, - 'cookie_httponly' => true, - 'gc_probability' => 25, - 'cookie_samesite' => 'Lax', // works only for php >= 7.3 - ]); + $started = @session_start([ + 'name' => $name, + 'save_path' => $path, + 'cookie_httponly' => true, + 'gc_probability' => 25, + ]); - if (!$started) { - throw new Exception("Cannot start the HTTP session. The session path '{$path}' is not writable."); - } - } - } + if (!$started) { + throw new Exception("Cannot start the HTTP session. That the session path '{$path}' is writable and your PHP settings."); + } + } + } - /** - * @return string - */ - public function getId() - { - return session_id(); - } + /** + * Destroy the current session + * @return bool + */ + public function destroy(): bool + { + return session_destroy(); + } - /** - * Destroy the current session. - * - * @return bool - */ - public function destroy(): bool - { - return session_destroy(); - } + /** + * Clear all session stored values + */ + public function clear(): void + { + $_SESSION = []; + } - /** - * Clear all session stored values. - */ - public function clear(): void - { - $_SESSION = []; - } + /** + * Check if session has a stored key + * @param $key + * @return bool + */ + public function has($key): bool + { + return isset($_SESSION[$key]); + } - /** - * Check if session has a stored key. - * - * @param $key - * - * @return bool - */ - public function has($key): bool - { - return isset($_SESSION[$key]); - } + /** + * Get the content of the current session + * @return array + */ + public function all(): array + { + return $_SESSION; + } - /** - * Get the content of the current session. - * - * @return array - */ - public function all(): array - { - return $_SESSION; - } + /** + * Returned a value given a key + * @param $key + * @param null $default + * @return mixed + */ + public function get($key, $default = null) + { + return self::has($key) ? $_SESSION[$key] : $default; + } - /** - * Returned a value given a key. - * - * @param $key - * @param null $default - * - * @return mixed - */ - public function get($key, $default = null) - { - return self::has($key) ? $_SESSION[$key] : $default; - } + /** + * Add a key-value pair to the session + * @param $key + * @param $value + */ + public function set($key, $value): void + { + $_SESSION[$key] = $value; + } - /** - * Add a key-value pair to the session. - * - * @param $key - * @param $value - */ - public function set($key, $value): void - { - $_SESSION[$key] = $value; - } + /** + * Set a flash alert + * @param $message + * @param string $type + */ + public function alert($message, string $type = 'info'): void + { + $_SESSION['_flash'][] = [$type => $message]; + } - /** - * Set a flash alert. - * - * @param $message - * @param string $type - */ - public function alert($message, string $type = 'info'): void - { - $_SESSION['_flash'][] = [$type => $message]; - } - /** - * Retrieve flash alerts. - * - * @return array - */ - public function getAlert(): ?array - { - $flash = self::get('_flash'); - self::set('_flash', []); + /** + * Retrieve flash alerts + * @return array + */ + public function getAlert(): ?array + { + $flash = self::get('_flash'); + self::set('_flash', []); + return $flash; + } - return $flash; - } -} +} \ No newline at end of file diff --git a/app/Web/ValidationChecker.php b/app/Web/ValidationChecker.php deleted file mode 100644 index 5aaa3cb..0000000 --- a/app/Web/ValidationChecker.php +++ /dev/null @@ -1,88 +0,0 @@ -rules = $rules; - return $this; - } - - /** - * @param callable $closure - * @return $this - */ - public function onFail(callable $closure) - { - $this->failClosure = $closure; - return $this; - } - - /** - * @return bool - */ - public function fails() - { - foreach ($this->rules as $rule => $condition) { - if (!$condition) { - $this->lastRule = $rule; - if (is_callable($this->failClosure)) { - ($this->failClosure)($rule); - } - return true; - } - } - return false; - } - - /** - * @param string $key - * @return ValidationChecker - */ - public function removeRule(string $key) - { - $this->rules[$key]; - - unset($this->rules[$key]); - - return $this; - } - - /** - * @param string $key - * @param $condition - * @return ValidationChecker - */ - public function addRule(string $key, $condition) - { - $this->rules[$key] = $condition; - return $this; - } - - /** - * @return mixed - */ - public function getLastRule() - { - return $this->lastRule; - } -} diff --git a/app/Web/View.php b/app/Web/View.php deleted file mode 100644 index 42c7a9b..0000000 --- a/app/Web/View.php +++ /dev/null @@ -1,69 +0,0 @@ -twig = $twig; - } - - /** - * @param Response $response - * @param string $view - * @param array|null $parameters - * - * @throws LoaderError - * @throws RuntimeError - * @throws SyntaxError - * - * @return Response - */ - public function render(Response $response, string $view, ?array $parameters = []) - { - $body = $this->twig->render($view, $parameters); - $response->getBody()->write($body); - - return $response; - } - - /** - * @param string $view - * @param array|null $parameters - * - * @throws LoaderError - * @throws RuntimeError - * @throws SyntaxError - * - * @return string - */ - public function string(string $view, ?array $parameters = []) - { - return $this->twig->render($view, $parameters); - } - - /** - * @return Environment - */ - public function getTwig(): Environment - { - return $this->twig; - } -} diff --git a/app/helpers.php b/app/helpers.php index 6f38789..b67699b 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -1,489 +1,318 @@ 0.9; $i++, $size /= 1024) { - } - - if ($iniMode) { - return round($size, $precision).['B', 'K', 'M', 'G', 'T'][$i]; - } - - return round($size, $precision).' '.['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'][$i]; - } + /** + * Generate a human readable file size + * @param $size + * @param int $precision + * @return string + */ + function humanFileSize($size, $precision = 2): string + { + for ($i = 0; ($size / 1024) > 0.9; $i++, $size /= 1024) { + } + return round($size, $precision) . ' ' . ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'][$i]; + } } if (!function_exists('humanRandomString')) { - /** - * @param int $length - * - * @return string - */ - function humanRandomString(int $length = 10): string - { - $result = ''; - $numberOffset = round($length * 0.2); - for ($x = 0; $x < $length - $numberOffset; $x++) { - $result .= ($x % 2) ? HUMAN_RANDOM_CHARS[rand(42, 51)] : HUMAN_RANDOM_CHARS[rand(0, 41)]; - } - for ($x = 0; $x < $numberOffset; $x++) { - $result .= rand(0, 9); - } - - return $result; - } + /** + * @param int $length + * @return string + */ + function humanRandomString(int $length = 13): string + { + $result = ''; + $numberOffset = round($length * 0.2); + for ($x = 0; $x < $length - $numberOffset; $x++) { + $result .= ($x % 2) ? HUMAN_RANDOM_CHARS[rand(42, 51)] : HUMAN_RANDOM_CHARS[rand(0, 41)]; + } + for ($x = 0; $x < $numberOffset; $x++) { + $result .= rand(0, 9); + } + return $result; + } } if (!function_exists('isDisplayableImage')) { - /** - * @param string $mime - * - * @return bool - */ - function isDisplayableImage(string $mime): bool - { - return in_array($mime, [ - 'image/apng', - 'image/bmp', - 'image/gif', - 'image/x-icon', - 'image/jpeg', - 'image/png', - 'image/svg', - 'image/svg+xml', - 'image/tiff', - 'image/webp', - ]); - } + /** + * @param string $mime + * @return bool + */ + function isDisplayableImage(string $mime): bool + { + return in_array($mime, [ + 'image/apng', + 'image/bmp', + 'image/gif', + 'image/x-icon', + 'image/jpeg', + 'image/png', + 'image/svg', + 'image/svg+xml', + 'image/tiff', + 'image/webp', + ]); + } } if (!function_exists('stringToBytes')) { - /** - * @param $str - * - * @return float - */ - function stringToBytes(string $str): float - { - $val = trim($str); - if (is_numeric($val)) { - return (float) $val; - } + /** + * @param $str + * @return float + */ + function stringToBytes(string $str): float + { + $val = trim($str); + if (is_numeric($val)) { + return (float)$val; + } - $last = strtolower($val[strlen($val) - 1]); - $val = substr($val, 0, -1); + $last = strtolower($val[strlen($val) - 1]); + $val = substr($val, 0, -1); - $val = (float) $val; - switch ($last) { - case 't': - $val *= 1024; - case 'g': - $val *= 1024; - case 'm': - $val *= 1024; - case 'k': - $val *= 1024; - } - - return $val; - } + $val = (float)$val; + switch ($last) { + case 'g': + $val *= 1024; + case 'm': + $val *= 1024; + case 'k': + $val *= 1024; + } + return $val; + } } if (!function_exists('removeDirectory')) { - /** - * Remove a directory and it's content. - * - * @param $path - */ - function removeDirectory($path) - { - $files = glob($path.'/*'); - foreach ($files as $file) { - is_dir($file) ? removeDirectory($file) : unlink($file); - } - rmdir($path); - } + /** + * Remove a directory and it's content + * @param $path + */ + function removeDirectory($path) + { + $files = glob($path . '/*'); + foreach ($files as $file) { + is_dir($file) ? removeDirectory($file) : unlink($file); + } + rmdir($path); + return; + } } if (!function_exists('cleanDirectory')) { - /** - * Removes all directory contents. - * - * @param $path - */ - function cleanDirectory($path) - { - $directoryIterator = new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS); - $iteratorIterator = new RecursiveIteratorIterator($directoryIterator, RecursiveIteratorIterator::CHILD_FIRST); - foreach ($iteratorIterator as $file) { - if ($file->getFilename() !== '.gitkeep') { - $file->isDir() ? rmdir($file) : unlink($file); - } - } - } -} - -if (!function_exists('resolve')) { - /** - * Resolve a service from de DI container. - * - * @param string $service - * - * @return mixed - */ - function resolve(string $service) - { - global $app; - - return $app->getContainer()->get($service); - } -} - -if (!function_exists('make')) { - /** - * Resolve a service from de DI container. - * - * @param string $class - * @param array $params - * @return mixed - */ - function make(string $class, array $params = []) - { - global $app; - - return $app->getContainer()->make($class, $params); - } -} - -if (!function_exists('view')) { - /** - * Render a view to the response body. - * - * @return \App\Web\View - */ - function view() - { - return resolve('view'); - } + /** + * Removes all directory contents + * @param $path + */ + function cleanDirectory($path) + { + $directoryIterator = new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS); + $iteratorIterator = new RecursiveIteratorIterator($directoryIterator, RecursiveIteratorIterator::CHILD_FIRST); + foreach ($iteratorIterator as $file) { + if ($file->getFilename() !== '.gitkeep') { + $file->isDir() ? rmdir($file) : unlink($file); + } + } + } } if (!function_exists('redirect')) { - /** - * Set the redirect response. - * - * @param Response $response - * @param string $url - * @param int $status - * - * @return Response - */ - function redirect(Response $response, string $url, $status = 302) - { - return $response - ->withHeader('Location', $url) - ->withStatus($status); - } + /** + * Set the redirect response + * @param \Slim\Http\Response $response + * @param string $path + * @param array $args + * @param null $status + * @return \Slim\Http\Response + */ + function redirect(\Slim\Http\Response $response, string $path, $args = [], $status = null) + { + if (substr($path, 0, 1) === '/' || substr($path, 0, 3) === '../' || substr($path, 0, 2) === './') { + $url = urlFor($path); + } else { + $url = route($path, $args); + } + + return $response->withRedirect($url, $status); + } } if (!function_exists('asset')) { - /** - * Get the asset link with timestamp. - * - * @param string $path - * - * @return string - */ - function asset(string $path): string - { - return urlFor($path, '?'.filemtime(realpath(BASE_DIR.$path))); - } + /** + * Get the asset link with timestamp + * @param string $path + * @return string + */ + function asset(string $path): string + { + return urlFor($path, '?' . filemtime(realpath(BASE_DIR . $path))); + } } if (!function_exists('urlFor')) { - /** - * Generate the app url given a path. - * - * @param string $path - * @param string $append - * - * @return string - */ - function urlFor(string $path = '', string $append = ''): string - { - $baseUrl = resolve('config')['base_url']; - - return $baseUrl.$path.$append; - } + /** + * Generate the app url given a path + * @param string $path + * @param string $append + * @return string + */ + function urlFor(string $path, string $append = ''): string + { + global $app; + $baseUrl = $app->getContainer()->get('settings')['base_url']; + return $baseUrl . $path . $append; + } } if (!function_exists('route')) { - /** - * Generate the app url given a path. - * - * @param string $path - * @param array $args - * @param string $append - * - * @return string - */ - function route(string $path, array $args = [], string $append = ''): string - { - global $app; - $uri = $app->getRouteCollector()->getRouteParser()->relativeUrlFor($path, $args); - - return urlFor($uri, $append); - } -} - -if (!function_exists('param')) { - /** - * Get a parameter from the request. - * - * @param Request $request - * @param string $name - * @param null $default - * - * @return string - */ - function param(Request $request, string $name, $default = null) - { - if ($request->getMethod() === 'GET') { - $params = $request->getQueryParams(); - } else { - $params = $request->getParsedBody(); - } - - if (isset($params[$name])) { - return $params[$name]; - } - - return $default; - } -} - -if (!function_exists('json')) { - /** - * Return a json response. - * - * @param Response $response - * @param $data - * @param int $status - * @param int $options - * - * @return Response - */ - function json(Response $response, $data, int $status = 200, $options = 0): Response - { - $response->getBody()->write(json_encode($data, $options)); - - return $response - ->withStatus($status) - ->withHeader('Content-Type', 'application/json'); - } + /** + * Generate the app url given a path + * @param string $path + * @param array $args + * @param string $append + * @return string + */ + function route(string $path, array $args = [], string $append = ''): string + { + global $app; + $uri = $app->getContainer()->get('router')->relativePathFor($path, $args); + return urlFor($uri, $append); + } } if (!function_exists('lang')) { - /** - * @param string $key - * @param array $args - * - * @return string - */ - function lang(string $key, $args = []): string - { - return resolve('lang')->get($key, $args); - } + /** + * @param string $key + * @param array $args + * @return string + */ + function lang(string $key, $args = []): string + { + global $app; + return $app->getContainer()->get('lang')->get($key, $args); + } } if (!function_exists('isBot')) { - /** - * @param string $userAgent - * - * @return bool - */ - function isBot(string $userAgent) - { - $bots = [ - 'TelegramBot', - 'facebookexternalhit/', - 'Discordbot/', - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:38.0) Gecko/20100101 Firefox/38.0', // The discord service bot? - 'Facebot', - 'curl/', - 'wget/', - 'WhatsApp/', - 'Slackbot', - 'Slack-ImgProxy', - ]; + /** + * @param string $userAgent + * @return boolean + */ + function isBot(string $userAgent) + { + $bots = [ + 'TelegramBot', + 'facebookexternalhit/', + 'Discordbot/', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:38.0) Gecko/20100101 Firefox/38.0', // The discord service bot? + 'Facebot', + 'curl/', + 'wget/', + ]; - foreach ($bots as $bot) { - if (stripos($userAgent, $bot) !== false) { - return true; - } - } + foreach ($bots as $bot) { + if (stripos($userAgent, $bot) !== false) { + return true; + } + } - return false; - } + return false; + } } if (!function_exists('mime2font')) { - /** - * Convert get the icon from the file mimetype. - * - * @param $mime - * - * @return mixed|string - */ - function mime2font($mime) - { - $classes = [ - 'image' => 'fa-file-image', - 'audio' => 'fa-file-audio', - 'video' => 'fa-file-video', - 'application/pdf' => 'fa-file-pdf', - 'application/msword' => 'fa-file-word', - 'application/vnd.ms-word' => 'fa-file-word', - 'application/vnd.oasis.opendocument.text' => 'fa-file-word', - 'application/vnd.openxmlformats-officedocument.wordprocessingml' => 'fa-file-word', - 'application/vnd.ms-excel' => 'fa-file-excel', - 'application/vnd.openxmlformats-officedocument.spreadsheetml' => 'fa-file-excel', - 'application/vnd.oasis.opendocument.spreadsheet' => 'fa-file-excel', - 'application/vnd.ms-powerpoint' => 'fa-file-powerpoint', - 'application/vnd.openxmlformats-officedocument.presentationml' => 'fa-file-powerpoint', - 'application/vnd.oasis.opendocument.presentation' => 'fa-file-powerpoint', - 'text/plain' => 'fa-file-alt', - 'text/html' => 'fa-file-code', - 'text/x-php' => 'fa-file-code', - 'application/json' => 'fa-file-code', - 'application/gzip' => 'fa-file-archive', - 'application/zip' => 'fa-file-archive', - 'application/octet-stream' => 'fa-file-alt', - ]; + /** + * Convert get the icon from the file mimetype + * @param $mime + * @return mixed|string + */ + function mime2font($mime) + { + $classes = [ + 'image' => 'fa-file-image', + 'audio' => 'fa-file-audio', + 'video' => 'fa-file-video', + 'application/pdf' => 'fa-file-pdf', + 'application/msword' => 'fa-file-word', + 'application/vnd.ms-word' => 'fa-file-word', + 'application/vnd.oasis.opendocument.text' => 'fa-file-word', + 'application/vnd.openxmlformats-officedocument.wordprocessingml' => 'fa-file-word', + 'application/vnd.ms-excel' => 'fa-file-excel', + 'application/vnd.openxmlformats-officedocument.spreadsheetml' => 'fa-file-excel', + 'application/vnd.oasis.opendocument.spreadsheet' => 'fa-file-excel', + 'application/vnd.ms-powerpoint' => 'fa-file-powerpoint', + 'application/vnd.openxmlformats-officedocument.presentationml' => 'fa-file-powerpoint', + 'application/vnd.oasis.opendocument.presentation' => 'fa-file-powerpoint', + 'text/plain' => 'fa-file-alt', + 'text/html' => 'fa-file-code', + 'text/x-php' => 'fa-file-code', + 'application/json' => 'fa-file-code', + 'application/gzip' => 'fa-file-archive', + 'application/zip' => 'fa-file-archive', + 'application/octet-stream' => 'fa-file-alt', + ]; - foreach ($classes as $fullMime => $class) { - if (strpos($mime, $fullMime) === 0) { - return $class; - } - } - - return 'fa-file'; - } + foreach ($classes as $fullMime => $class) { + if (strpos($mime, $fullMime) === 0) { + return $class; + } + } + return 'fa-file'; + } } if (!function_exists('dd')) { - /** - * Dumps all the given vars and halt the execution. - */ - function dd() - { - array_map(function ($x) { - echo '
';
-            print_r($x);
-            echo '
'; - }, func_get_args()); - die(); - } + /** + * Dumps all the given vars and halt the execution. + */ + function dd() + { + array_map(function ($x) { + echo '
';
+			print_r($x);
+			echo '
'; + }, func_get_args()); + die(); + } } if (!function_exists('queryParams')) { - /** - * Get the query parameters of the current request. - * - * @param array $replace - * - * @return string - */ - function queryParams(array $replace = []) - { - $request = ServerRequestCreatorFactory::determineServerRequestCreator()->createServerRequestFromGlobals(); + /** + * Get the query parameters of the current request. + * @param array $replace + * @return string + * @throws \Interop\Container\Exception\ContainerException + */ + function queryParams(array $replace = []) + { + global $container; + /** @var \Slim\Http\Request $request */ + $request = $container->get('request'); - $params = array_replace_recursive($request->getQueryParams(), $replace); + $params = array_replace_recursive($request->getQueryParams(), $replace); - return !empty($params) ? '?'.http_build_query($params) : ''; - } -} - -if (!function_exists('inPath')) { - /** - * Check if uri start with a path. - * - * @param string $uri - * @param string $path - * - * @return bool - */ - function inPath(string $uri, string $path): bool - { - $path = parse_url(urlFor($path), PHP_URL_PATH); - - return substr($uri, 0, strlen($path)) === $path; - } + return !empty($params) ? '?' . http_build_query($params) : ''; + } } if (!function_exists('glob_recursive')) { - /** - * Does not support flag GLOB_BRACE. - * - * @param $pattern - * @param int $flags - * - * @return array|false - */ - function glob_recursive($pattern, $flags = 0) - { - $files = glob($pattern, $flags); - foreach (glob(dirname($pattern).'/*', GLOB_ONLYDIR | GLOB_NOSORT) as $dir) { - $files = array_merge($files, glob_recursive($dir.'/'.basename($pattern), $flags)); - } - - return $files; - } -} - -if (!function_exists('dsnFromConfig')) { - /** - * Return the database DSN from config. - * - * @param array $config - * - * @param string $baseDir - * @return string - */ - function dsnFromConfig(array $config, $baseDir = BASE_DIR): string - { - $dsn = $config['db']['connection'] === 'sqlite' ? $baseDir.$config['db']['dsn'] : $config['db']['dsn']; - - return $config['db']['connection'].':'.$dsn; - } -} - -if (!function_exists('platform_mail')) { - /** - * Return the system no-reply mail. - * - * @param string $mailbox - * @return string - */ - function platform_mail($mailbox = 'no-reply'): string - { - return $mailbox.'@'.str_ireplace('www.', '', parse_url(resolve('config')['base_url'], PHP_URL_HOST)); - } -} + /** + * Does not support flag GLOB_BRACE + * @param $pattern + * @param int $flags + * @return array|false + */ + function glob_recursive($pattern, $flags = 0) + { + $files = glob($pattern, $flags); + foreach (glob(dirname($pattern) . '/*', GLOB_ONLYDIR | GLOB_NOSORT) as $dir) { + $files = array_merge($files, glob_recursive($dir . '/' . basename($pattern), $flags)); + } + return $files; + } +} \ No newline at end of file diff --git a/app/routes.php b/app/routes.php index 7af4ea0..bd83bf0 100644 --- a/app/routes.php +++ b/app/routes.php @@ -1,90 +1,68 @@ group('', function (RouteCollectorProxy $group) { - $group->get('/home[/page/{page}]', [DashboardController::class, 'home'])->setName('home'); - $group->get('/upload', [UploadController::class, 'uploadWebPage'])->setName('upload.web.show'); - $group->post('/upload/web', [UploadController::class, 'uploadWeb'])->setName('upload.web'); +$app->group('', function () { + $this->get('/home[/page/{page}]', DashboardController::class . ':home')->setName('home'); - $group->group('', function (RouteCollectorProxy $group) { - $group->get('/home/switchView', [DashboardController::class, 'switchView'])->setName('switchView'); + $this->group('', function () { + $this->get('/home/switchView', DashboardController::class . ':switchView')->setName('switchView'); - $group->get('/system/deleteOrphanFiles', [AdminController::class, 'deleteOrphanFiles'])->setName('system.deleteOrphanFiles'); - $group->get('/system/recalculateUserQuota', [AdminController::class, 'recalculateUserQuota'])->setName('system.recalculateUserQuota'); + $this->get('/system/deleteOrphanFiles', AdminController::class . ':deleteOrphanFiles')->setName('system.deleteOrphanFiles'); - $group->get('/system/themes', [AdminController::class, 'getThemes'])->setName('theme'); + $this->get('/system/themes', ThemeController::class . ':getThemes')->setName('theme'); + $this->post('/system/theme/apply', ThemeController::class . ':applyTheme')->setName('theme.apply'); - $group->post('/system/settings/save', [SettingController::class, 'saveSettings'])->setName('settings.save'); + $this->post('/system/lang/apply', AdminController::class . ':applyLang')->setName('lang.apply'); - $group->post('/system/upgrade', [UpgradeController::class, 'upgrade'])->setName('system.upgrade'); - $group->get('/system/checkForUpdates', [UpgradeController::class, 'checkForUpdates'])->setName('system.checkForUpdates'); + $this->post('/system/upgrade', UpgradeController::class . ':upgrade')->setName('system.upgrade'); + $this->get('/system/checkForUpdates', UpgradeController::class . ':checkForUpdates')->setName('system.checkForUpdates'); - $group->get('/system', [AdminController::class, 'system'])->setName('system'); + $this->get('/system', AdminController::class . ':system')->setName('system'); - $group->get('/users[/page/{page}]', [UserController::class, 'index'])->setName('user.index'); - })->add(AdminMiddleware::class); + $this->get('/users[/page/{page}]', UserController::class . ':index')->setName('user.index'); + })->add(AdminMiddleware::class); - $group->group('/user', function (RouteCollectorProxy $group) { - $group->get('/create', [UserController::class, 'create'])->setName('user.create'); - $group->post('/create', [UserController::class, 'store'])->setName('user.store'); - $group->get('/{id}/edit', [UserController::class, 'edit'])->setName('user.edit'); - $group->post('/{id}', [UserController::class, 'update'])->setName('user.update'); - $group->get('/{id}/delete', [UserController::class, 'delete'])->setName('user.delete'); - $group->get('/{id}/clear', [UserController::class, 'clearUserMedia'])->setName('user.clear'); - })->add(AdminMiddleware::class); + $this->group('/user', function () { - $group->get('/profile', [ProfileController::class, 'profile'])->setName('profile'); - $group->post('/profile/{id}', [ProfileController::class, 'profileEdit'])->setName('profile.update'); - $group->post('/user/{id}/refreshToken', [UserController::class, 'refreshToken'])->setName('refreshToken'); - $group->get('/user/{id}/config/sharex', [ClientController::class, 'getShareXConfig'])->setName('config.sharex'); - $group->get('/user/{id}/config/script', [ClientController::class, 'getBashScript'])->setName('config.script'); + $this->get('/create', UserController::class . ':create')->setName('user.create'); + $this->post('/create', UserController::class . ':store')->setName('user.store'); + $this->get('/{id}/edit', UserController::class . ':edit')->setName('user.edit'); + $this->post('/{id}', UserController::class . ':update')->setName('user.update'); + $this->get('/{id}/delete', UserController::class . ':delete')->setName('user.delete'); + })->add(AdminMiddleware::class); - $group->get('/user/{id}/export', [ExportController::class, 'downloadData'])->setName('export.data'); + $this->get('/profile', UserController::class . ':profile')->setName('profile'); + $this->post('/profile/{id}', UserController::class . ':profileEdit')->setName('profile.update'); + $this->post('/user/{id}/refreshToken', UserController::class . ':refreshToken')->setName('refreshToken'); + $this->get('/user/{id}/config/sharex', UserController::class . ':getShareXconfigFile')->setName('config.sharex'); + $this->get('/user/{id}/config/script', UserController::class . ':getUploaderScriptFile')->setName('config.script'); - $group->post('/upload/{id}/publish', [MediaController::class, 'togglePublish'])->setName('upload.publish'); - $group->post('/upload/{id}/unpublish', [MediaController::class, 'togglePublish'])->setName('upload.unpublish'); - $group->get('/upload/{id}/raw', [MediaController::class, 'getRawById'])->add(AdminMiddleware::class)->setName('upload.raw'); - $group->map(['GET', 'POST'], '/upload/{id}/delete', [MediaController::class, 'delete'])->setName('upload.delete'); + $this->post('/upload/{id}/publish', UploadController::class . ':togglePublish')->setName('upload.publish'); + $this->post('/upload/{id}/unpublish', UploadController::class . ':togglePublish')->setName('upload.unpublish'); + $this->get('/upload/{id}/raw', UploadController::class . ':getRawById')->add(AdminMiddleware::class)->setName('upload.raw'); + $this->post('/upload/{id}/delete', UploadController::class . ':delete')->setName('upload.delete'); - $group->post('/tag/add', [TagController::class, 'addTag'])->setName('tag.add'); - $group->post('/tag/remove', [TagController::class, 'removeTag'])->setName('tag.remove'); })->add(App\Middleware\CheckForMaintenanceMiddleware::class)->add(AuthMiddleware::class); -$app->get('/', [DashboardController::class, 'redirects'])->setName('root'); -$app->get('/register', [RegisterController::class, 'registerForm'])->setName('register.show'); -$app->post('/register', [RegisterController::class, 'register'])->setName('register'); -$app->get('/activate/{activateToken}', [RegisterController::class, 'activateUser'])->setName('activate'); -$app->get('/recover', [PasswordRecoveryController::class, 'recover'])->setName('recover'); -$app->post('/recover/mail', [PasswordRecoveryController::class, 'recoverMail'])->setName('recover.mail'); -$app->get('/recover/password/{resetToken}', [PasswordRecoveryController::class, 'recoverPasswordForm'])->setName('recover.password.view'); -$app->post('/recover/password/{resetToken}', [PasswordRecoveryController::class, 'recoverPassword'])->setName('recover.password'); -$app->get('/login', [LoginController::class, 'show'])->setName('login.show'); -$app->post('/login', [LoginController::class, 'login'])->setName('login'); -$app->map(['GET', 'POST'], '/logout', [LoginController::class, 'logout'])->setName('logout'); +$app->get('/', DashboardController::class . ':redirects')->setName('root'); +$app->get('/login', LoginController::class . ':show')->setName('login.show'); +$app->post('/login', LoginController::class . ':login')->setName('login'); +$app->map(['GET', 'POST'], '/logout', LoginController::class . ':logout')->setName('logout'); -$app->post('/upload', [UploadController::class, 'uploadEndpoint'])->setName('upload'); +$app->post('/upload', UploadController::class . ':upload')->setName('upload'); -$app->get('/{userCode}/{mediaCode}', [MediaController::class, 'show'])->setName('public'); -$app->get('/{userCode}/{mediaCode}/delete/{token}', [MediaController::class, 'show'])->setName('public.delete.show')->add(CheckForMaintenanceMiddleware::class); -$app->post('/{userCode}/{mediaCode}/delete/{token}', [MediaController::class, 'deleteByToken'])->setName('public.delete')->add(CheckForMaintenanceMiddleware::class); -$app->get('/{userCode}/{mediaCode}/raw[.{ext}]', [MediaController::class, 'getRaw'])->setName('public.raw'); -$app->get('/{userCode}/{mediaCode}/download', [MediaController::class, 'download'])->setName('public.download'); +$app->get('/{userCode}/{mediaCode}', UploadController::class . ':show')->setName('public'); +$app->get('/{userCode}/{mediaCode}/delete/{token}', UploadController::class . ':show')->setName('public.delete.show')->add(CheckForMaintenanceMiddleware::class); +$app->post('/{userCode}/{mediaCode}/delete/{token}', UploadController::class . ':deleteByToken')->setName('public.delete')->add(CheckForMaintenanceMiddleware::class); +$app->get('/{userCode}/{mediaCode}/raw', UploadController::class . ':showRaw')->setName('public.raw')->setOutputBuffering(false); +$app->get('/{userCode}/{mediaCode}/download', UploadController::class . ':download')->setName('public.download')->setOutputBuffering(false); \ No newline at end of file diff --git a/bin/migrate b/bin/migrate index 62e5414..e05ef46 100644 --- a/bin/migrate +++ b/bin/migrate @@ -2,41 +2,96 @@ = 7 && PHP_MINOR_VERSION >= 1) ?: die('Sorry, PHP 7.1 or above is required to run XBackBone.'); if (php_sapi_name() !== 'cli') { - die(); + die(); } use App\Database\DB; -use App\Database\Migrator; -require __DIR__.'/../vendor/autoload.php'; +require __DIR__ . '/../vendor/autoload.php'; -$config = include __DIR__.'/../config.php'; +$config = include __DIR__ . '/../config.php'; if (!$config) { - die('config.php not found. Please create a new one.'); + die('config.php not found. Please create a new one.'); } -chdir(__DIR__.'/../'); +chdir(__DIR__ . '/../'); + +DB::setDsn($config['db']['connection'] . ':' . $config['db']['dsn'], $config['db']['username'], $config['db']['password']); $firstMigrate = false; -if ($config['db']['connection'] === 'sqlite' && !file_exists(__DIR__.'/../'.$config['db']['dsn'])) { - touch(__DIR__.'/../'.$config['db']['dsn']); - $firstMigrate = true; +if (!file_exists($config['db']['dsn']) && DB::driver() === 'sqlite') { + touch($config['db']['dsn']); + $firstMigrate = true; } -$db = new DB(dsnFromConfig($config, getcwd().DIRECTORY_SEPARATOR), $config['db']['username'], $config['db']['password']); +try { + DB::doQuery('SELECT 1 FROM `migrations` LIMIT 1'); +} catch (PDOException $exception) { + $firstMigrate = true; +} -$migrator = new Migrator($db, 'resources/schemas', $firstMigrate); -$migrator->migrate(); +echo 'Connected.' . PHP_EOL; + +if ($firstMigrate) { + echo 'Creating migrations table...' . PHP_EOL; + DB::raw()->exec(file_get_contents('resources/schemas/migrations.sql')); +} + +$files = glob('resources/schemas/' . DB::driver() . '/*.sql'); + +$names = array_map(function ($path) { + return basename($path); +}, $files); + +$in = str_repeat('?, ', count($names) - 1) . '?'; + +$inMigrationsTable = DB::doQuery("SELECT * FROM `migrations` WHERE `name` IN ($in)", $names)->fetchAll(); + + +foreach ($files as $file) { + + $continue = false; + $exists = false; + + foreach ($inMigrationsTable as $migration) { + if (basename($file) === $migration->name && $migration->migrated) { + $continue = true; + break; + } else if (basename($file) === $migration->name && !$migration->migrated) { + $exists = true; + break; + } + } + if ($continue) continue; + + $sql = file_get_contents($file); + try { + DB::raw()->exec($sql); + if (!$exists) { + DB::doQuery('INSERT INTO `migrations` VALUES (?,?)', [basename($file), 1]); + } else { + DB::doQuery('UPDATE `migrations` SET `migrated`=? WHERE `name`=?', [1, basename($file)]); + } + echo "Migrated '$file'" . PHP_EOL; + } catch (PDOException $exception) { + if (!$exists) { + DB::doQuery('INSERT INTO `migrations` VALUES (?,?)', [basename($file), 0]); + } + echo "Error migrating '$file' (" . $exception->getMessage() . ')' . PHP_EOL; + echo $exception->getTraceAsString() . PHP_EOL; + + } +} if (isset($argv[1]) && $argv[1] === '--install') { - $db->query("INSERT INTO `users` (`email`, `username`, `password`, `is_admin`, `user_code`) VALUES ('admin@example.com', 'admin', ?, 1, ?)", [password_hash('admin', PASSWORD_DEFAULT), humanRandomString(5)]); + DB::doQuery("INSERT INTO `users` (`email`, `username`, `password`, `is_admin`, `user_code`) VALUES ('admin@example.com', 'admin', ?, 1, ?)", [password_hash('admin', PASSWORD_DEFAULT), humanRandomString(5)]); } -if (file_exists(__DIR__.'/../install') && (!isset($config['debug']) || !$config['debug'])) { - removeDirectory(__DIR__.'/../install'); +if (file_exists(__DIR__ . '/../install')) { + removeDirectory(__DIR__ . '/../install'); } -echo 'If you are upgrading from a previous version, please run a "php bin\clean".'.PHP_EOL; -echo 'Done.'.PHP_EOL; +echo 'If you are upgrading from a previous version, please run a "php bin\clean".' . PHP_EOL; +echo 'Done.' . PHP_EOL; exit(0); \ No newline at end of file diff --git a/bootstrap/app.php b/bootstrap/app.php index 2f009c2..752cd9c 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -1,116 +1,225 @@ 'XBackBone', - 'base_url' => isset($_SERVER['HTTPS']) ? 'https://'.$_SERVER['HTTP_HOST'] : 'http://'.$_SERVER['HTTP_HOST'], - 'debug' => false, - 'maintenance' => false, - 'db' => [ - 'connection' => 'sqlite', - 'dsn' => BASE_DIR.'resources/database/xbackbone.db', - 'username' => null, - 'password' => null, - ], - 'storage' => [ - 'driver' => 'local', - 'path' => realpath(__DIR__.'/').DIRECTORY_SEPARATOR.'storage', - ], - 'ldap' => [ - 'enabled' => false, - 'host' => null, - 'port' => null, - 'base_domain' => null, - 'user_domain' => null, - ], -], require BASE_DIR.'config.php'); + 'app_name' => 'XBackBone', + 'base_url' => isset($_SERVER['HTTPS']) ? 'https://' . $_SERVER['HTTP_HOST'] : 'http://' . $_SERVER['HTTP_HOST'], + 'displayErrorDetails' => false, + 'maintenance' => false, + 'db' => [ + 'connection' => 'sqlite', + 'dsn' => BASE_DIR . 'resources/database/xbackbone.db', + 'username' => null, + 'password' => null, + ], + 'storage' => [ + 'driver' => 'local', + 'path' => realpath(__DIR__ . '/') . DIRECTORY_SEPARATOR . 'storage', + ], +], require BASE_DIR . 'config.php'); -$builder = new ContainerBuilder(); - -if (!$config['debug']) { - $builder->enableCompilation(BASE_DIR.'/resources/cache/di'); - $builder->writeProxiesToFile(true, BASE_DIR.'/resources/cache/di'); +if (!$config['displayErrorDetails']) { + $config['routerCacheFile'] = BASE_DIR . 'resources/cache/routes.cache.php'; } -$builder->addDefinitions([ - View::class => factory(function (Container $container) { - return ViewFactory::createAppInstance($container); - }), - 'view' => get(View::class), -]); +$container = new Container(['settings' => $config]); -$builder->addDefinitions(__DIR__.'/container.php'); +$container['config'] = function ($container) use ($config) { + return $config; +}; -$app = Bridge::create($builder->build()); -$app->getContainer()->set('config', $config); -$app->setBasePath(parse_url($config['base_url'], PHP_URL_PATH) ?: ''); +$container['logger'] = function ($container) { + $logger = new Logger('app'); -if (!$config['debug']) { - $app->getRouteCollector()->setCacheFile(BASE_DIR.'resources/cache/routes.cache.php'); -} + $streamHandler = new RotatingFileHandler(BASE_DIR . 'logs/log.txt', 10, Logger::DEBUG); -$app->add(InjectMiddleware::class); -$app->add(LangMiddleware::class); -$app->add(RememberMiddleware::class); + $lineFormatter = new LineFormatter("[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n", "Y-m-d H:i:s"); + $lineFormatter->includeStacktraces(true); + + $streamHandler->setFormatter($lineFormatter); + + $logger->pushHandler($streamHandler); + + return $logger; +}; + +$container['session'] = function ($container) { + return new Session('xbackbone_session', BASE_DIR . 'resources/sessions'); +}; + +$container['database'] = function ($container) use (&$config) { + $dsn = $config['db']['connection'] === 'sqlite' ? BASE_DIR . $config['db']['dsn'] : $config['db']['dsn']; + return new DB($config['db']['connection'] . ':' . $dsn, $config['db']['username'], $config['db']['password']); +}; + +$container['storage'] = function ($container) use (&$config) { + + switch ($config['storage']['driver']) { + case 'local': + return new Filesystem(new Local($config['storage']['path'])); + case 's3': + $client = new S3Client([ + 'credentials' => [ + 'key' => $config['storage']['key'], + 'secret' => $config['storage']['secret'], + ], + 'region' => $config['storage']['region'], + 'version' => 'latest', + ]); + + return new Filesystem(new AwsS3Adapter($client, $config['storage']['bucket'], $config['storage']['path'])); + case 'dropbox': + $client = new DropboxClient($config['storage']['token']); + return new Filesystem(new DropboxAdapter($client), ['case_sensitive' => false]); + case 'ftp': + return new Filesystem(new FtpAdapter([ + 'host' => $config['storage']['host'], + 'username' => $config['storage']['username'], + 'password' => $config['storage']['password'], + 'port' => $config['storage']['port'], + 'root' => $config['storage']['path'], + 'passive' => $config['storage']['passive'], + 'ssl' => $config['storage']['ssl'], + 'timeout' => 30, + ])); + case 'google-cloud': + $client = new StorageClient([ + 'projectId' => $config['storage']['project_id'], + 'keyFilePath' => $config['storage']['key_path'], + ]); + return new Filesystem(new GoogleStorageAdapter($client, $client->bucket($config['storage']['bucket']))); + default: + throw new InvalidArgumentException('The driver specified is not supported.'); + } +}; + +$container['lang'] = function ($container) use (&$config) { + if (isset($config['lang'])) { + return Lang::build($config['lang'], BASE_DIR . 'resources/lang/'); + } + return Lang::build(Lang::recognize(), BASE_DIR . 'resources/lang/'); +}; + +$container['view'] = function ($container) use (&$config) { + $view = new Twig(BASE_DIR . 'resources/templates', [ + 'cache' => BASE_DIR . 'resources/cache', + 'autoescape' => 'html', + 'debug' => $config['displayErrorDetails'], + 'auto_reload' => $config['displayErrorDetails'], + ]); + + // Instantiate and add Slim specific extension + $router = $container->get('router'); + $uri = Uri::createFromEnvironment(new Environment($_SERVER)); + $view->addExtension(new Slim\Views\TwigExtension($router, $uri)); + + $view->getEnvironment()->addGlobal('config', $config); + $view->getEnvironment()->addGlobal('request', $container->get('request')); + $view->getEnvironment()->addGlobal('alerts', $container->get('session')->getAlert()); + $view->getEnvironment()->addGlobal('session', $container->get('session')->all()); + $view->getEnvironment()->addGlobal('current_lang', $container->get('lang')->getLang()); + $view->getEnvironment()->addGlobal('PLATFORM_VERSION', PLATFORM_VERSION); + + $view->getEnvironment()->addFunction(new TwigFunction('route', 'route')); + $view->getEnvironment()->addFunction(new TwigFunction('lang', 'lang')); + $view->getEnvironment()->addFunction(new TwigFunction('urlFor', 'urlFor')); + $view->getEnvironment()->addFunction(new TwigFunction('asset', 'asset')); + $view->getEnvironment()->addFunction(new TwigFunction('mime2font', 'mime2font')); + $view->getEnvironment()->addFunction(new TwigFunction('queryParams', 'queryParams')); + $view->getEnvironment()->addFunction(new TwigFunction('isDisplayableImage', 'isDisplayableImage')); + return $view; +}; + +$container['phpErrorHandler'] = function ($container) { + return function (Request $request, Response $response, Throwable $error) use (&$container) { + $container->logger->critical('Fatal runtime error during app execution', ['exception' => $error]); + return $container->view->render($response->withStatus(500), 'errors/500.twig', ['exception' => $error]); + }; +}; + +$container['errorHandler'] = function ($container) { + return function (Request $request, Response $response, Exception $exception) use (&$container) { + + if ($exception instanceof MaintenanceException) { + return $container->view->render($response->withStatus(503), 'errors/maintenance.twig'); + } + + if ($exception instanceof UnauthorizedException) { + return $container->view->render($response->withStatus(403), 'errors/403.twig'); + } + + $container->logger->critical('Fatal exception during app execution', ['exception' => $exception]); + return $container->view->render($response->withStatus(500), 'errors/500.twig', ['exception' => $exception]); + }; +}; + +$container['notAllowedHandler'] = function ($container) { + return function (Request $request, Response $response, $methods) use (&$container) { + return $container->view->render($response->withStatus(405)->withHeader('Allow', implode(', ', $methods)), 'errors/405.twig'); + }; +}; + +$container['notFoundHandler'] = function ($container) { + return function (Request $request, Response $response) use (&$container) { + $response->withStatus(404)->withHeader('Content-Type', 'text/html'); + return $container->view->render($response, 'errors/404.twig'); + }; +}; + +$app = new App($container); // Permanently redirect paths with a trailing slash to their non-trailing counterpart -$app->add(function (Request $request, RequestHandler $handler) use (&$app, &$config) { - $uri = $request->getUri(); - $path = $uri->getPath(); +$app->add(function (Request $request, Response $response, callable $next) { + $uri = $request->getUri(); + $path = $uri->getPath(); - if ($path !== $app->getBasePath().'/' && substr($path, -1) === '/') { - // permanently redirect paths with a trailing slash - // to their non-trailing counterpart - $uri = $uri->withPath(substr($path, 0, -1)); + if ($path !== '/' && substr($path, -1) === '/') { + $uri = $uri->withPath(substr($path, 0, -1)); - if ($request->getMethod() === 'GET') { - return $app->getResponseFactory() - ->createResponse(301) - ->withHeader('Location', (string) $uri); - } else { - $request = $request->withUri($uri); - } - } + if ($request->getMethod() === 'GET') { + return $response->withRedirect((string)$uri, 301); + } else { + return $next($request->withUri($uri), $response); + } + } - return $handler->handle($request); + return $next($request, $response); }); -$app->addRoutingMiddleware(); - -// Configure the error handler -$errorHandler = new AppErrorHandler($app->getCallableResolver(), $app->getResponseFactory()); -$errorHandler->registerErrorRenderer('text/html', HtmlErrorRenderer::class); - -// Add Error Middleware -$errorMiddleware = $app->addErrorMiddleware($config['debug'], true, true); -$errorMiddleware->setDefaultErrorHandler($errorHandler); - // Load the application routes -require BASE_DIR.'app/routes.php'; +require BASE_DIR . 'app/routes.php'; -return $app; +return $app; \ No newline at end of file diff --git a/bootstrap/container.php b/bootstrap/container.php deleted file mode 100644 index fd23552..0000000 --- a/bootstrap/container.php +++ /dev/null @@ -1,99 +0,0 @@ - factory(function () { - $logger = new Logger('app'); - - $streamHandler = new RotatingFileHandler(BASE_DIR.'logs/log.txt', 10, Logger::DEBUG); - - $lineFormatter = new LineFormatter("[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n", 'Y-m-d H:i:s'); - $lineFormatter->includeStacktraces(true); - - $streamHandler->setFormatter($lineFormatter); - - $logger->pushHandler($streamHandler); - - return $logger; - }), - 'logger' => get(Logger::class), - - Session::class => factory(function () { - return new Session('xbackbone_session', BASE_DIR.'resources/sessions'); - }), - 'session' => get(Session::class), - - DB::class => factory(function (Container $container) { - $config = $container->get('config'); - - return new DB(dsnFromConfig($config), $config['db']['username'], $config['db']['password']); - }), - 'database' => get(DB::class), - - Filesystem::class => factory(function (Container $container) { - $config = $container->get('config'); - switch ($config['storage']['driver']) { - case 'local': - return new Filesystem(new Local($config['storage']['path'])); - case 's3': - $client = new S3Client([ - 'credentials' => [ - 'key' => $config['storage']['key'], - 'secret' => $config['storage']['secret'], - ], - 'region' => $config['storage']['region'], - 'version' => 'latest', - ]); - - return new Filesystem(new AwsS3Adapter($client, $config['storage']['bucket'], $config['storage']['path'])); - case 'dropbox': - $client = new DropboxClient($config['storage']['token']); - - return new Filesystem(new DropboxAdapter($client), ['case_sensitive' => false]); - case 'ftp': - return new Filesystem(new FtpAdapter([ - 'host' => $config['storage']['host'], - 'username' => $config['storage']['username'], - 'password' => $config['storage']['password'], - 'port' => $config['storage']['port'], - 'root' => $config['storage']['path'], - 'passive' => $config['storage']['passive'], - 'ssl' => $config['storage']['ssl'], - 'timeout' => 30, - ])); - case 'google-cloud': - $client = new StorageClient([ - 'projectId' => $config['storage']['project_id'], - 'keyFilePath' => $config['storage']['key_path'], - ]); - - return new Filesystem(new GoogleStorageAdapter($client, $client->bucket($config['storage']['bucket']))); - default: - throw new InvalidArgumentException('The driver specified is not supported.'); - } - }), - 'storage' => get(Filesystem::class), - - Lang::class => factory(function () { - return Lang::build(Lang::recognize(), BASE_DIR.'resources/lang/'); - }), - 'lang' => get(Lang::class), -]; diff --git a/composer.json b/composer.json index a612a42..178ee6d 100644 --- a/composer.json +++ b/composer.json @@ -1,34 +1,26 @@ { "name": "sergix44/xbackbone", - "version": "3.1", + "version": "2.6.6", "description": "A lightweight ShareX PHP backend", "type": "project", "require": { "php": ">=7.1", - "ext-gd": "*", - "ext-intl": "*", - "ext-json": "*", - "ext-pdo": "*", - "ext-zip": "*", - "guzzlehttp/psr7": "^1.6", - "http-interop/http-factory-guzzle": "^1.0", - "intervention/image": "^2.4", + "slim/slim": "^3.0", + "slim/twig-view": "^2.4", "league/flysystem": "^1.0.45", - "league/flysystem-aws-s3-v3": "^1.0", - "maennchen/zipstream-php": "^2.0", "monolog/monolog": "^1.23", - "php-di/slim-bridge": "^3.0", - "slim/slim": "^4.0", + "intervention/image": "^2.4", + "league/flysystem-aws-s3-v3": "^1.0", "spatie/flysystem-dropbox": "^1.0", "superbalist/flysystem-google-storage": "^7.2", - "twig/twig": "^2.12", - "ext-ldap": "*" - }, - "config": { - "optimize-autoloader": true, - "preferred-install": "dist", - "sort-packages": true + "ext-intl": "*", + "ext-json": "*", + "ext-gd": "*", + "ext-pdo": "*", + "ext-zip": "*" }, + "prefer-stable": true, + "minimum-stability": "dev", "autoload": { "files": [ "app/helpers.php" diff --git a/composer.lock b/composer.lock index 31bae8d..b558c3f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,30 +4,30 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "17be54724e0928ed7049b18bbcb181b9", + "content-hash": "94fb8de56fe9b216b1db862119cb8aa0", "packages": [ { "name": "aws/aws-sdk-php", - "version": "3.133.46", + "version": "3.112.26", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "98d359e61b365f3040ca61c66ae8883334cf5d74" + "reference": "eda066813388de5ab8584f06707dacd0b15908b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/98d359e61b365f3040ca61c66ae8883334cf5d74", - "reference": "98d359e61b365f3040ca61c66ae8883334cf5d74", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/eda066813388de5ab8584f06707dacd0b15908b0", + "reference": "eda066813388de5ab8584f06707dacd0b15908b0", "shasum": "" }, "require": { "ext-json": "*", "ext-pcre": "*", "ext-simplexml": "*", - "guzzlehttp/guzzle": "^5.3.3|^6.2.1|^7.0", - "guzzlehttp/promises": "^1.0", + "guzzlehttp/guzzle": "^5.3.3|^6.2.1", + "guzzlehttp/promises": "~1.0", "guzzlehttp/psr7": "^1.4.1", - "mtdowling/jmespath.php": "^2.5", + "mtdowling/jmespath.php": "~2.2", "php": ">=5.5" }, "require-dev": { @@ -42,8 +42,7 @@ "nette/neon": "^2.3", "phpunit/phpunit": "^4.8.35|^5.4.3", "psr/cache": "^1.0", - "psr/simple-cache": "^1.0", - "sebastian/comparator": "^1.2.3" + "psr/simple-cache": "^1.0" }, "suggest": { "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications", @@ -88,27 +87,58 @@ "s3", "sdk" ], - "time": "2020-03-27T18:15:32+00:00" + "time": "2019-10-22T18:17:52+00:00" }, { - "name": "firebase/php-jwt", - "version": "v5.2.0", + "name": "container-interop/container-interop", + "version": "1.2.0", "source": { "type": "git", - "url": "https://github.com/firebase/php-jwt.git", - "reference": "feb0e820b8436873675fd3aca04f3728eb2185cb" + "url": "https://github.com/container-interop/container-interop.git", + "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/firebase/php-jwt/zipball/feb0e820b8436873675fd3aca04f3728eb2185cb", - "reference": "feb0e820b8436873675fd3aca04f3728eb2185cb", + "url": "https://api.github.com/repos/container-interop/container-interop/zipball/79cbf1341c22ec75643d841642dd5d6acd83bdb8", + "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8", + "shasum": "" + }, + "require": { + "psr/container": "^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Interop\\Container\\": "src/Interop/Container/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Promoting the interoperability of container objects (DIC, SL, etc.)", + "homepage": "https://github.com/container-interop/container-interop", + "time": "2017-02-14T19:40:03+00:00" + }, + { + "name": "firebase/php-jwt", + "version": "v5.0.0", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "9984a4d3a32ae7673d6971ea00bae9d0a1abba0e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/9984a4d3a32ae7673d6971ea00bae9d0a1abba0e", + "reference": "9984a4d3a32ae7673d6971ea00bae9d0a1abba0e", "shasum": "" }, "require": { "php": ">=5.3.0" }, "require-dev": { - "phpunit/phpunit": ">=4.8 <=9" + "phpunit/phpunit": " 4.8.35" }, "type": "library", "autoload": { @@ -134,24 +164,20 @@ ], "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", "homepage": "https://github.com/firebase/php-jwt", - "keywords": [ - "jwt", - "php" - ], - "time": "2020-03-25T18:49:23+00:00" + "time": "2017-06-27T22:17:23+00:00" }, { "name": "google/auth", - "version": "v1.8.0", + "version": "v1.6.0", "source": { "type": "git", "url": "https://github.com/googleapis/google-auth-library-php.git", - "reference": "c7b295feb248f138f462a1e6b7d635e4244204c5" + "reference": "6d5455b4c0f4a58b1f1b4bdf2ba49221123698b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/googleapis/google-auth-library-php/zipball/c7b295feb248f138f462a1e6b7d635e4244204c5", - "reference": "c7b295feb248f138f462a1e6b7d635e4244204c5", + "url": "https://api.github.com/repos/googleapis/google-auth-library-php/zipball/6d5455b4c0f4a58b1f1b4bdf2ba49221123698b3", + "reference": "6d5455b4c0f4a58b1f1b4bdf2ba49221123698b3", "shasum": "" }, "require": { @@ -165,7 +191,6 @@ "require-dev": { "friendsofphp/php-cs-fixer": "^1.11", "guzzlehttp/promises": "0.1.1|^1.3", - "kelvinmo/simplejwt": "^0.2.5", "phpseclib/phpseclib": "^2", "phpunit/phpunit": "^4.8.36|^5.7", "sebastian/comparator": ">=1.2.3" @@ -190,20 +215,20 @@ "google", "oauth2" ], - "time": "2020-03-26T19:47:36+00:00" + "time": "2019-10-01T18:35:05+00:00" }, { "name": "google/cloud-core", - "version": "v1.36.0", + "version": "v1.33.1", "source": { "type": "git", "url": "https://github.com/googleapis/google-cloud-php-core.git", - "reference": "4764850f256a43b7513226a54a04b349f8f385d8" + "reference": "7b8773a5c6d501b3ed31655a8e243e1c17e2862e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/googleapis/google-cloud-php-core/zipball/4764850f256a43b7513226a54a04b349f8f385d8", - "reference": "4764850f256a43b7513226a54a04b349f8f385d8", + "url": "https://api.github.com/repos/googleapis/google-cloud-php-core/zipball/7b8773a5c6d501b3ed31655a8e243e1c17e2862e", + "reference": "7b8773a5c6d501b3ed31655a8e243e1c17e2862e", "shasum": "" }, "require": { @@ -251,24 +276,24 @@ "Apache-2.0" ], "description": "Google Cloud PHP shared dependency, providing functionality useful to all components.", - "time": "2020-03-25T23:00:37+00:00" + "time": "2019-10-02T20:38:25+00:00" }, { "name": "google/cloud-storage", - "version": "v1.19.0", + "version": "v1.14.0", "source": { "type": "git", "url": "https://github.com/googleapis/google-cloud-php-storage.git", - "reference": "49dc19608ebea54023c2b3910e72b91393235d82" + "reference": "7315239270318e618f025791b20ac8cc2586ccf0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/googleapis/google-cloud-php-storage/zipball/49dc19608ebea54023c2b3910e72b91393235d82", - "reference": "49dc19608ebea54023c2b3910e72b91393235d82", + "url": "https://api.github.com/repos/googleapis/google-cloud-php-storage/zipball/7315239270318e618f025791b20ac8cc2586ccf0", + "reference": "7315239270318e618f025791b20ac8cc2586ccf0", "shasum": "" }, "require": { - "google/cloud-core": "^1.35", + "google/cloud-core": "^1.31", "google/crc32": "^0.1.0" }, "require-dev": { @@ -302,7 +327,7 @@ "Apache-2.0" ], "description": "Cloud Storage Client for PHP", - "time": "2020-03-25T23:00:37+00:00" + "time": "2019-08-07T20:57:43+00:00" }, { "name": "google/crc32", @@ -402,46 +427,44 @@ }, { "name": "guzzlehttp/guzzle", - "version": "6.5.2", + "version": "6.3.3", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "43ece0e75098b7ecd8d13918293029e555a50f82" + "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/43ece0e75098b7ecd8d13918293029e555a50f82", - "reference": "43ece0e75098b7ecd8d13918293029e555a50f82", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/407b0cb880ace85c9b63c5f9551db498cb2d50ba", + "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba", "shasum": "" }, "require": { - "ext-json": "*", "guzzlehttp/promises": "^1.0", - "guzzlehttp/psr7": "^1.6.1", + "guzzlehttp/psr7": "^1.4", "php": ">=5.5" }, "require-dev": { "ext-curl": "*", "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0", - "psr/log": "^1.1" + "psr/log": "^1.0" }, "suggest": { - "ext-intl": "Required for Internationalized Domain Name (IDN) support", "psr/log": "Required for using the Log middleware" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "6.5-dev" + "dev-master": "6.3-dev" } }, "autoload": { - "psr-4": { - "GuzzleHttp\\": "src/" - }, "files": [ "src/functions_include.php" - ] + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -465,7 +488,7 @@ "rest", "web service" ], - "time": "2019-12-23T11:57:10+00:00" + "time": "2018-04-22T15:46:56+00:00" }, { "name": "guzzlehttp/promises", @@ -589,68 +612,18 @@ ], "time": "2019-07-01T23:21:34+00:00" }, - { - "name": "http-interop/http-factory-guzzle", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/http-interop/http-factory-guzzle.git", - "reference": "34861658efb9899a6618cef03de46e2a52c80fc0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/http-interop/http-factory-guzzle/zipball/34861658efb9899a6618cef03de46e2a52c80fc0", - "reference": "34861658efb9899a6618cef03de46e2a52c80fc0", - "shasum": "" - }, - "require": { - "guzzlehttp/psr7": "^1.4.2", - "psr/http-factory": "^1.0" - }, - "provide": { - "psr/http-factory-implementation": "^1.0" - }, - "require-dev": { - "http-interop/http-factory-tests": "^0.5", - "phpunit/phpunit": "^6.5" - }, - "type": "library", - "autoload": { - "psr-4": { - "Http\\Factory\\Guzzle\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "An HTTP Factory using Guzzle PSR7", - "keywords": [ - "factory", - "http", - "psr-17", - "psr-7" - ], - "time": "2018-07-31T19:32:56+00:00" - }, { "name": "intervention/image", - "version": "2.5.1", + "version": "2.5.0", "source": { "type": "git", "url": "https://github.com/Intervention/image.git", - "reference": "abbf18d5ab8367f96b3205ca3c89fb2fa598c69e" + "reference": "39eaef720d082ecc54c64bf54541c55f10db546d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Intervention/image/zipball/abbf18d5ab8367f96b3205ca3c89fb2fa598c69e", - "reference": "abbf18d5ab8367f96b3205ca3c89fb2fa598c69e", + "url": "https://api.github.com/repos/Intervention/image/zipball/39eaef720d082ecc54c64bf54541c55f10db546d", + "reference": "39eaef720d082ecc54c64bf54541c55f10db546d", "shasum": "" }, "require": { @@ -707,78 +680,20 @@ "thumbnail", "watermark" ], - "time": "2019-11-02T09:15:47+00:00" - }, - { - "name": "jeremeamia/superclosure", - "version": "2.4.0", - "source": { - "type": "git", - "url": "https://github.com/jeremeamia/super_closure.git", - "reference": "5707d5821b30b9a07acfb4d76949784aaa0e9ce9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/jeremeamia/super_closure/zipball/5707d5821b30b9a07acfb4d76949784aaa0e9ce9", - "reference": "5707d5821b30b9a07acfb4d76949784aaa0e9ce9", - "shasum": "" - }, - "require": { - "nikic/php-parser": "^1.2|^2.0|^3.0|^4.0", - "php": ">=5.4", - "symfony/polyfill-php56": "^1.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.0|^5.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.4-dev" - } - }, - "autoload": { - "psr-4": { - "SuperClosure\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jeremy Lindblom", - "email": "jeremeamia@gmail.com", - "homepage": "https://github.com/jeremeamia", - "role": "Developer" - } - ], - "description": "Serialize Closure objects, including their context and binding", - "homepage": "https://github.com/jeremeamia/super_closure", - "keywords": [ - "closure", - "function", - "lambda", - "parser", - "serializable", - "serialize", - "tokenizer" - ], - "time": "2018-03-21T22:21:57+00:00" + "time": "2019-06-24T14:06:31+00:00" }, { "name": "league/flysystem", - "version": "1.0.66", + "version": "1.0.57", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "021569195e15f8209b1c4bebb78bd66aa4f08c21" + "reference": "0e9db7f0b96b9f12dcf6f65bc34b72b1a30ea55a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/021569195e15f8209b1c4bebb78bd66aa4f08c21", - "reference": "021569195e15f8209b1c4bebb78bd66aa4f08c21", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/0e9db7f0b96b9f12dcf6f65bc34b72b1a30ea55a", + "reference": "0e9db7f0b96b9f12dcf6f65bc34b72b1a30ea55a", "shasum": "" }, "require": { @@ -790,7 +705,7 @@ }, "require-dev": { "phpspec/phpspec": "^3.4", - "phpunit/phpunit": "^5.7.26" + "phpunit/phpunit": "^5.7.10" }, "suggest": { "ext-fileinfo": "Required for MimeType", @@ -849,20 +764,20 @@ "sftp", "storage" ], - "time": "2020-03-17T18:58:12+00:00" + "time": "2019-10-16T21:01:05+00:00" }, { "name": "league/flysystem-aws-s3-v3", - "version": "1.0.24", + "version": "1.0.23", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git", - "reference": "4382036bde5dc926f9b8b337e5bdb15e5ec7b570" + "reference": "15b0cdeab7240bf8e8bffa85ae5275bbc3692bf4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/4382036bde5dc926f9b8b337e5bdb15e5ec7b570", - "reference": "4382036bde5dc926f9b8b337e5bdb15e5ec7b570", + "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/15b0cdeab7240bf8e8bffa85ae5275bbc3692bf4", + "reference": "15b0cdeab7240bf8e8bffa85ae5275bbc3692bf4", "shasum": "" }, "require": { @@ -896,81 +811,20 @@ } ], "description": "Flysystem adapter for the AWS S3 SDK v3.x", - "time": "2020-02-23T13:31:58+00:00" - }, - { - "name": "maennchen/zipstream-php", - "version": "2.0.0", - "source": { - "type": "git", - "url": "https://github.com/maennchen/ZipStream-PHP.git", - "reference": "9ceee828f9620b2e5c075e551ec7ed8a7035ac95" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/9ceee828f9620b2e5c075e551ec7ed8a7035ac95", - "reference": "9ceee828f9620b2e5c075e551ec7ed8a7035ac95", - "shasum": "" - }, - "require": { - "ext-mbstring": "*", - "myclabs/php-enum": "^1.5", - "php": ">= 7.1", - "psr/http-message": "^1.0" - }, - "require-dev": { - "ext-zip": "*", - "guzzlehttp/guzzle": ">= 6.3", - "mikey179/vfsstream": "^1.6", - "phpunit/phpunit": ">= 7.5" - }, - "type": "library", - "autoload": { - "psr-4": { - "ZipStream\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Paul Duncan", - "email": "pabs@pablotron.org" - }, - { - "name": "Jonatan Männchen", - "email": "jonatan@maennchen.ch" - }, - { - "name": "Jesse Donat", - "email": "donatj@gmail.com" - }, - { - "name": "András Kolesár", - "email": "kolesar@kolesar.hu" - } - ], - "description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.", - "keywords": [ - "stream", - "zip" - ], - "time": "2020-02-23T01:48:39+00:00" + "time": "2019-06-05T17:18:29+00:00" }, { "name": "monolog/monolog", - "version": "1.25.3", + "version": "1.25.1", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "fa82921994db851a8becaf3787a9e73c5976b6f1" + "reference": "70e65a5470a42cfec1a7da00d30edb6e617e8dcf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/fa82921994db851a8becaf3787a9e73c5976b6f1", - "reference": "fa82921994db851a8becaf3787a9e73c5976b6f1", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/70e65a5470a42cfec1a7da00d30edb6e617e8dcf", + "reference": "70e65a5470a42cfec1a7da00d30edb6e617e8dcf", "shasum": "" }, "require": { @@ -1035,29 +889,27 @@ "logging", "psr-3" ], - "time": "2019-12-20T14:15:16+00:00" + "time": "2019-09-06T13:49:17+00:00" }, { "name": "mtdowling/jmespath.php", - "version": "2.5.0", + "version": "2.4.0", "source": { "type": "git", "url": "https://github.com/jmespath/jmespath.php.git", - "reference": "52168cb9472de06979613d365c7f1ab8798be895" + "reference": "adcc9531682cf87dfda21e1fd5d0e7a41d292fac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/52168cb9472de06979613d365c7f1ab8798be895", - "reference": "52168cb9472de06979613d365c7f1ab8798be895", + "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/adcc9531682cf87dfda21e1fd5d0e7a41d292fac", + "reference": "adcc9531682cf87dfda21e1fd5d0e7a41d292fac", "shasum": "" }, "require": { - "php": ">=5.4.0", - "symfony/polyfill-mbstring": "^1.4" + "php": ">=5.4.0" }, "require-dev": { - "composer/xdebug-handler": "^1.2", - "phpunit/phpunit": "^4.8.36|^7.5.15" + "phpunit/phpunit": "~4.0" }, "bin": [ "bin/jp.php" @@ -1065,7 +917,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -1092,53 +944,7 @@ "json", "jsonpath" ], - "time": "2019-12-30T18:03:34+00:00" - }, - { - "name": "myclabs/php-enum", - "version": "1.7.6", - "source": { - "type": "git", - "url": "https://github.com/myclabs/php-enum.git", - "reference": "5f36467c7a87e20fbdc51e524fd8f9d1de80187c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/myclabs/php-enum/zipball/5f36467c7a87e20fbdc51e524fd8f9d1de80187c", - "reference": "5f36467c7a87e20fbdc51e524fd8f9d1de80187c", - "shasum": "" - }, - "require": { - "ext-json": "*", - "php": ">=7.1" - }, - "require-dev": { - "phpunit/phpunit": "^7", - "squizlabs/php_codesniffer": "1.*", - "vimeo/psalm": "^3.8" - }, - "type": "library", - "autoload": { - "psr-4": { - "MyCLabs\\Enum\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP Enum contributors", - "homepage": "https://github.com/myclabs/php-enum/graphs/contributors" - } - ], - "description": "PHP Enum implementation", - "homepage": "http://github.com/myclabs/php-enum", - "keywords": [ - "enum" - ], - "time": "2020-02-14T08:15:52+00:00" + "time": "2016-12-03T22:08:25+00:00" }, { "name": "nikic/fast-route", @@ -1187,236 +993,54 @@ "time": "2018-02-13T20:26:39+00:00" }, { - "name": "nikic/php-parser", - "version": "v4.3.0", + "name": "pimple/pimple", + "version": "v3.2.3", "source": { "type": "git", - "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "9a9981c347c5c49d6dfe5cf826bb882b824080dc" + "url": "https://github.com/silexphp/Pimple.git", + "reference": "9e403941ef9d65d20cba7d54e29fe906db42cf32" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/9a9981c347c5c49d6dfe5cf826bb882b824080dc", - "reference": "9a9981c347c5c49d6dfe5cf826bb882b824080dc", + "url": "https://api.github.com/repos/silexphp/Pimple/zipball/9e403941ef9d65d20cba7d54e29fe906db42cf32", + "reference": "9e403941ef9d65d20cba7d54e29fe906db42cf32", "shasum": "" }, "require": { - "ext-tokenizer": "*", - "php": ">=7.0" + "php": ">=5.3.0", + "psr/container": "^1.0" }, "require-dev": { - "ircmaxell/php-yacc": "0.0.5", - "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0" + "symfony/phpunit-bridge": "^3.2" }, - "bin": [ - "bin/php-parse" - ], "type": "library", "extra": { "branch-alias": { - "dev-master": "4.3-dev" + "dev-master": "3.2.x-dev" } }, "autoload": { - "psr-4": { - "PhpParser\\": "lib/PhpParser" + "psr-0": { + "Pimple": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Nikita Popov" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" } ], - "description": "A PHP parser written in PHP", + "description": "Pimple, a simple Dependency Injection Container", + "homepage": "http://pimple.sensiolabs.org", "keywords": [ - "parser", - "php" - ], - "time": "2019-11-08T13:50:10+00:00" - }, - { - "name": "php-di/invoker", - "version": "2.0.0", - "source": { - "type": "git", - "url": "https://github.com/PHP-DI/Invoker.git", - "reference": "540c27c86f663e20fe39a24cd72fa76cdb21d41a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/PHP-DI/Invoker/zipball/540c27c86f663e20fe39a24cd72fa76cdb21d41a", - "reference": "540c27c86f663e20fe39a24cd72fa76cdb21d41a", - "shasum": "" - }, - "require": { - "psr/container": "~1.0" - }, - "require-dev": { - "athletic/athletic": "~0.1.8", - "phpunit/phpunit": "~4.5" - }, - "type": "library", - "autoload": { - "psr-4": { - "Invoker\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Generic and extensible callable invoker", - "homepage": "https://github.com/PHP-DI/Invoker", - "keywords": [ - "callable", - "dependency", - "dependency-injection", - "injection", - "invoke", - "invoker" - ], - "time": "2017-03-20T19:28:22+00:00" - }, - { - "name": "php-di/php-di", - "version": "6.0.11", - "source": { - "type": "git", - "url": "https://github.com/PHP-DI/PHP-DI.git", - "reference": "9bdcc2f41f5fb700ddd01bc4fa8d5bd7b3f94620" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/9bdcc2f41f5fb700ddd01bc4fa8d5bd7b3f94620", - "reference": "9bdcc2f41f5fb700ddd01bc4fa8d5bd7b3f94620", - "shasum": "" - }, - "require": { - "jeremeamia/superclosure": "^2.0", - "nikic/php-parser": "^2.0|^3.0|^4.0", - "php": ">=7.0.0", - "php-di/invoker": "^2.0", - "php-di/phpdoc-reader": "^2.0.1", - "psr/container": "^1.0" - }, - "provide": { - "psr/container-implementation": "^1.0" - }, - "require-dev": { - "doctrine/annotations": "~1.2", - "friendsofphp/php-cs-fixer": "^2.4", - "mnapoli/phpunit-easymock": "~1.0", - "ocramius/proxy-manager": "~2.0.2", - "phpstan/phpstan": "^0.9.2", - "phpunit/phpunit": "~6.4" - }, - "suggest": { - "doctrine/annotations": "Install it if you want to use annotations (version ~1.2)", - "ocramius/proxy-manager": "Install it if you want to use lazy injection (version ~2.0)" - }, - "type": "library", - "autoload": { - "psr-4": { - "DI\\": "src/" - }, - "files": [ - "src/functions.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "The dependency injection container for humans", - "homepage": "http://php-di.org/", - "keywords": [ - "PSR-11", "container", - "container-interop", - "dependency injection", - "di", - "ioc", - "psr11" + "dependency injection" ], - "time": "2019-12-12T07:58:02+00:00" - }, - { - "name": "php-di/phpdoc-reader", - "version": "2.1.1", - "source": { - "type": "git", - "url": "https://github.com/PHP-DI/PhpDocReader.git", - "reference": "15678f7451c020226807f520efb867ad26fbbfcf" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/PHP-DI/PhpDocReader/zipball/15678f7451c020226807f520efb867ad26fbbfcf", - "reference": "15678f7451c020226807f520efb867ad26fbbfcf", - "shasum": "" - }, - "require": { - "php": ">=5.4.0" - }, - "require-dev": { - "phpunit/phpunit": "~4.6" - }, - "type": "library", - "autoload": { - "psr-4": { - "PhpDocReader\\": "src/PhpDocReader" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "PhpDocReader parses @var and @param values in PHP docblocks (supports namespaced class names with the same resolution rules as PHP)", - "keywords": [ - "phpdoc", - "reflection" - ], - "time": "2019-09-26T11:24:58+00:00" - }, - { - "name": "php-di/slim-bridge", - "version": "3.0.1", - "source": { - "type": "git", - "url": "https://github.com/PHP-DI/Slim-Bridge.git", - "reference": "e7a568c00ecf6b0128f2a751990e06e77bedec68" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/PHP-DI/Slim-Bridge/zipball/e7a568c00ecf6b0128f2a751990e06e77bedec68", - "reference": "e7a568c00ecf6b0128f2a751990e06e77bedec68", - "shasum": "" - }, - "require": { - "php": "~7.1", - "php-di/invoker": "^2.0.0", - "php-di/php-di": "^6.0.0", - "slim/slim": "^4.2.0" - }, - "require-dev": { - "phpunit/phpunit": "~6.0", - "zendframework/zend-diactoros": "^2.1" - }, - "type": "library", - "autoload": { - "psr-4": { - "DI\\Bridge\\Slim\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "PHP-DI integration in Slim", - "time": "2020-01-13T19:11:12+00:00" + "time": "2018-01-21T07:42:36+00:00" }, { "name": "psr/cache", @@ -1513,58 +1137,6 @@ ], "time": "2017-02-14T16:28:37+00:00" }, - { - "name": "psr/http-factory", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-factory.git", - "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be", - "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be", - "shasum": "" - }, - "require": { - "php": ">=7.0.0", - "psr/http-message": "^1.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Message\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Common interfaces for PSR-7 HTTP message factories", - "keywords": [ - "factory", - "http", - "message", - "psr", - "psr-17", - "psr-7", - "request", - "response" - ], - "time": "2019-04-30T12:38:16+00:00" - }, { "name": "psr/http-message", "version": "1.0.1", @@ -1615,124 +1187,18 @@ ], "time": "2016-08-06T14:39:51+00:00" }, - { - "name": "psr/http-server-handler", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-server-handler.git", - "reference": "aff2f80e33b7f026ec96bb42f63242dc50ffcae7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/aff2f80e33b7f026ec96bb42f63242dc50ffcae7", - "reference": "aff2f80e33b7f026ec96bb42f63242dc50ffcae7", - "shasum": "" - }, - "require": { - "php": ">=7.0", - "psr/http-message": "^1.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Server\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Common interface for HTTP server-side request handler", - "keywords": [ - "handler", - "http", - "http-interop", - "psr", - "psr-15", - "psr-7", - "request", - "response", - "server" - ], - "time": "2018-10-30T16:46:14+00:00" - }, - { - "name": "psr/http-server-middleware", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-server-middleware.git", - "reference": "2296f45510945530b9dceb8bcedb5cb84d40c5f5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/2296f45510945530b9dceb8bcedb5cb84d40c5f5", - "reference": "2296f45510945530b9dceb8bcedb5cb84d40c5f5", - "shasum": "" - }, - "require": { - "php": ">=7.0", - "psr/http-message": "^1.0", - "psr/http-server-handler": "^1.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Server\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Common interface for HTTP server-side middleware", - "keywords": [ - "http", - "http-interop", - "middleware", - "psr", - "psr-15", - "psr-7", - "request", - "response" - ], - "time": "2018-10-30T17:12:04+00:00" - }, { "name": "psr/log", - "version": "1.1.3", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc" + "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc", - "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc", + "url": "https://api.github.com/repos/php-fig/log/zipball/6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", + "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", "shasum": "" }, "require": { @@ -1741,7 +1207,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1.x-dev" + "dev-master": "1.0.x-dev" } }, "autoload": { @@ -1766,7 +1232,7 @@ "psr", "psr-3" ], - "time": "2020-03-23T09:12:05+00:00" + "time": "2018-11-20T15:27:04+00:00" }, { "name": "ralouphie/getallheaders", @@ -1854,48 +1320,35 @@ }, { "name": "slim/slim", - "version": "4.4.0", + "version": "3.12.2", "source": { "type": "git", "url": "https://github.com/slimphp/Slim.git", - "reference": "207acac048652a35d4762a737d59e317aedc02df" + "reference": "200c6143f15baa477601879b64ab2326847aac0b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slimphp/Slim/zipball/207acac048652a35d4762a737d59e317aedc02df", - "reference": "207acac048652a35d4762a737d59e317aedc02df", + "url": "https://api.github.com/repos/slimphp/Slim/zipball/200c6143f15baa477601879b64ab2326847aac0b", + "reference": "200c6143f15baa477601879b64ab2326847aac0b", "shasum": "" }, "require": { + "container-interop/container-interop": "^1.2", "ext-json": "*", - "nikic/fast-route": "^1.3", - "php": "^7.2", + "ext-libxml": "*", + "ext-simplexml": "*", + "nikic/fast-route": "^1.0", + "php": ">=5.5.0", + "pimple/pimple": "^3.0", "psr/container": "^1.0", - "psr/http-factory": "^1.0", - "psr/http-message": "^1.0", - "psr/http-server-handler": "^1.0", - "psr/http-server-middleware": "^1.0" + "psr/http-message": "^1.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" }, "require-dev": { - "adriansuter/php-autoload-override": "^1.0", - "ext-simplexml": "*", - "guzzlehttp/psr7": "^1.5", - "http-interop/http-factory-guzzle": "^1.0", - "laminas/laminas-diactoros": "^2.1", - "nyholm/psr7": "^1.1", - "nyholm/psr7-server": "^0.3.0", - "phpspec/prophecy": "^1.10", - "phpstan/phpstan": "^0.11.5", - "phpunit/phpunit": "^8.5", - "slim/http": "^0.7", - "slim/psr7": "^0.3", - "squizlabs/php_codesniffer": "^3.5" - }, - "suggest": { - "ext-simplexml": "Needed to support XML format in BodyParsingMiddleware", - "ext-xml": "Needed to support XML format in BodyParsingMiddleware", - "php-di/php-di": "PHP-DI is the recommended container library to be used with Slim", - "slim/psr7": "Slim PSR-7 implementation. See https://www.slimframework.com/docs/v4/start/installation.html for more information." + "phpunit/phpunit": "^4.0", + "squizlabs/php_codesniffer": "^2.5" }, "type": "library", "autoload": { @@ -1923,11 +1376,6 @@ "email": "rob@akrabat.com", "homepage": "http://akrabat.com" }, - { - "name": "Pierre Berube", - "email": "pierre@lgse.com", - "homepage": "http://www.lgse.com" - }, { "name": "Gabriel Manricks", "email": "gmanricks@me.com", @@ -1935,27 +1383,78 @@ } ], "description": "Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs", - "homepage": "https://www.slimframework.com", + "homepage": "https://slimframework.com", "keywords": [ "api", "framework", "micro", "router" ], - "time": "2020-01-05T03:51:47+00:00" + "time": "2019-08-20T18:46:05+00:00" }, { - "name": "spatie/dropbox-api", - "version": "1.12.0", + "name": "slim/twig-view", + "version": "2.5.0", "source": { "type": "git", - "url": "https://github.com/spatie/dropbox-api.git", - "reference": "f599374697e7fefa2b0d64ca25ad7079abf91970" + "url": "https://github.com/slimphp/Twig-View.git", + "reference": "06ef39b58d60b11a9546893fd0b7fff2bd901798" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/dropbox-api/zipball/f599374697e7fefa2b0d64ca25ad7079abf91970", - "reference": "f599374697e7fefa2b0d64ca25ad7079abf91970", + "url": "https://api.github.com/repos/slimphp/Twig-View/zipball/06ef39b58d60b11a9546893fd0b7fff2bd901798", + "reference": "06ef39b58d60b11a9546893fd0b7fff2bd901798", + "shasum": "" + }, + "require": { + "php": ">=5.5.0", + "psr/http-message": "^1.0", + "twig/twig": "^1.38|^2.7" + }, + "require-dev": { + "phpunit/phpunit": "^4.8|^5.7", + "slim/slim": "^3.10" + }, + "type": "library", + "autoload": { + "psr-4": { + "Slim\\Views\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Josh Lockhart", + "email": "hello@joshlockhart.com", + "homepage": "http://joshlockhart.com" + } + ], + "description": "Slim Framework 3 view helper built on top of the Twig 2 templating component", + "homepage": "http://slimframework.com", + "keywords": [ + "framework", + "slim", + "template", + "twig", + "view" + ], + "time": "2019-04-06T16:34:38+00:00" + }, + { + "name": "spatie/dropbox-api", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/dropbox-api.git", + "reference": "47c6a14bcdc639ba90f86f98b2ada38ac9cc8e30" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/dropbox-api/zipball/47c6a14bcdc639ba90f86f98b2ada38ac9cc8e30", + "reference": "47c6a14bcdc639ba90f86f98b2ada38ac9cc8e30", "shasum": "" }, "require": { @@ -1964,7 +1463,7 @@ "php": "^7.1" }, "require-dev": { - "phpunit/phpunit": "^7.5.15|^8.5" + "phpunit/phpunit": "^6.0" }, "type": "library", "autoload": { @@ -1978,16 +1477,16 @@ ], "authors": [ { - "name": "Alex Vanderbist", - "email": "alex.vanderbist@gmail.com", - "homepage": "https://spatie.be", - "role": "Developer" + "name": "Freek Van der Herten", + "role": "Developer", + "email": "freek@spatie.be", + "homepage": "https://spatie.be" }, { - "name": "Freek Van der Herten", - "email": "freek@spatie.be", - "homepage": "https://spatie.be", - "role": "Developer" + "name": "Alex Vanderbist", + "role": "Developer", + "email": "alex.vanderbist@gmail.com", + "homepage": "https://spatie.be" } ], "description": "A minimal implementation of Dropbox API v2", @@ -1999,20 +1498,20 @@ "spatie", "v2" ], - "time": "2020-02-04T07:32:05+00:00" + "time": "2019-07-04T19:04:14+00:00" }, { "name": "spatie/flysystem-dropbox", - "version": "1.2.2", + "version": "1.2.1", "source": { "type": "git", "url": "https://github.com/spatie/flysystem-dropbox.git", - "reference": "512e8d59b3f9b8a6710f932c421032cb490e9869" + "reference": "8aa19e6f63d36f66d716f13a4d4dec23012d16e2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/flysystem-dropbox/zipball/512e8d59b3f9b8a6710f932c421032cb490e9869", - "reference": "512e8d59b3f9b8a6710f932c421032cb490e9869", + "url": "https://api.github.com/repos/spatie/flysystem-dropbox/zipball/8aa19e6f63d36f66d716f13a4d4dec23012d16e2", + "reference": "8aa19e6f63d36f66d716f13a4d4dec23012d16e2", "shasum": "" }, "require": { @@ -2051,7 +1550,7 @@ "spatie", "v2" ], - "time": "2019-12-04T08:18:17+00:00" + "time": "2019-09-14T17:00:10+00:00" }, { "name": "superbalist/flysystem-google-storage", @@ -2102,16 +1601,16 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.15.0", + "version": "v1.12.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "4719fa9c18b0464d399f1a63bf624b42b6fa8d14" + "reference": "550ebaac289296ce228a706d0867afc34687e3f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/4719fa9c18b0464d399f1a63bf624b42b6fa8d14", - "reference": "4719fa9c18b0464d399f1a63bf624b42b6fa8d14", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/550ebaac289296ce228a706d0867afc34687e3f4", + "reference": "550ebaac289296ce228a706d0867afc34687e3f4", "shasum": "" }, "require": { @@ -2123,7 +1622,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.15-dev" + "dev-master": "1.12-dev" } }, "autoload": { @@ -2156,20 +1655,20 @@ "polyfill", "portable" ], - "time": "2020-02-27T09:26:54+00:00" + "time": "2019-08-06T08:03:45+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.15.0", + "version": "v1.12.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "81ffd3a9c6d707be22e3012b827de1c9775fc5ac" + "reference": "b42a2f66e8f1b15ccf25652c3424265923eb4f17" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/81ffd3a9c6d707be22e3012b827de1c9775fc5ac", - "reference": "81ffd3a9c6d707be22e3012b827de1c9775fc5ac", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/b42a2f66e8f1b15ccf25652c3424265923eb4f17", + "reference": "b42a2f66e8f1b15ccf25652c3424265923eb4f17", "shasum": "" }, "require": { @@ -2181,7 +1680,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.15-dev" + "dev-master": "1.12-dev" } }, "autoload": { @@ -2215,128 +1714,20 @@ "portable", "shim" ], - "time": "2020-03-09T19:04:49+00:00" - }, - { - "name": "symfony/polyfill-php56", - "version": "v1.15.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php56.git", - "reference": "d51ec491c8ddceae7dca8dd6c7e30428f543f37d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php56/zipball/d51ec491c8ddceae7dca8dd6c7e30428f543f37d", - "reference": "d51ec491c8ddceae7dca8dd6c7e30428f543f37d", - "shasum": "" - }, - "require": { - "php": ">=5.3.3", - "symfony/polyfill-util": "~1.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.15-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php56\\": "" - }, - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 5.6+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "time": "2020-03-09T19:04:49+00:00" - }, - { - "name": "symfony/polyfill-util", - "version": "v1.15.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-util.git", - "reference": "d8e76c104127675d0ea3df3be0f2ae24a8619027" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-util/zipball/d8e76c104127675d0ea3df3be0f2ae24a8619027", - "reference": "d8e76c104127675d0ea3df3be0f2ae24a8619027", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.15-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Util\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony utilities for portability of PHP codes", - "homepage": "https://symfony.com", - "keywords": [ - "compat", - "compatibility", - "polyfill", - "shim" - ], - "time": "2020-03-02T11:55:35+00:00" + "time": "2019-08-06T08:03:45+00:00" }, { "name": "twig/twig", - "version": "v2.12.5", + "version": "v2.12.1", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "18772e0190734944277ee97a02a9a6c6555fcd94" + "reference": "ddd4134af9bfc6dba4eff7c8447444ecc45b9ee5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/18772e0190734944277ee97a02a9a6c6555fcd94", - "reference": "18772e0190734944277ee97a02a9a6c6555fcd94", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/ddd4134af9bfc6dba4eff7c8447444ecc45b9ee5", + "reference": "ddd4134af9bfc6dba4eff7c8447444ecc45b9ee5", "shasum": "" }, "require": { @@ -2346,7 +1737,8 @@ }, "require-dev": { "psr/container": "^1.0", - "symfony/phpunit-bridge": "^4.4|^5.0" + "symfony/debug": "^3.4|^4.2", + "symfony/phpunit-bridge": "^4.4@dev|^5.0" }, "type": "library", "extra": { @@ -2375,6 +1767,7 @@ }, { "name": "Twig Team", + "homepage": "https://twig.symfony.com/contributors", "role": "Contributors" }, { @@ -2388,30 +1781,30 @@ "keywords": [ "templating" ], - "time": "2020-02-11T15:31:23+00:00" + "time": "2019-10-17T07:34:53+00:00" } ], "packages-dev": [ { "name": "composer/xdebug-handler", - "version": "1.4.1", + "version": "1.3.3", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "1ab9842d69e64fb3a01be6b656501032d1b78cb7" + "reference": "46867cbf8ca9fb8d60c506895449eb799db1184f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/1ab9842d69e64fb3a01be6b656501032d1b78cb7", - "reference": "1ab9842d69e64fb3a01be6b656501032d1b78cb7", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/46867cbf8ca9fb8d60c506895449eb799db1184f", + "reference": "46867cbf8ca9fb8d60c506895449eb799db1184f", "shasum": "" }, "require": { - "php": "^5.3.2 || ^7.0 || ^8.0", + "php": "^5.3.2 || ^7.0", "psr/log": "^1.0" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 8" + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5" }, "type": "library", "autoload": { @@ -2429,12 +1822,12 @@ "email": "john-stevenson@blueyonder.co.uk" } ], - "description": "Restarts a process without Xdebug.", + "description": "Restarts a process without xdebug.", "keywords": [ "Xdebug", "performance" ], - "time": "2020-03-01T12:26:26+00:00" + "time": "2019-05-27T17:52:04+00:00" }, { "name": "jean85/pretty-package-versions", @@ -2565,25 +1958,25 @@ }, { "name": "nette/di", - "version": "v3.0.3", + "version": "v3.0.1", "source": { "type": "git", "url": "https://github.com/nette/di.git", - "reference": "77d69061cbf8f9cfb7363dd983136f51213d3e41" + "reference": "4aff517a1c6bb5c36fa09733d4cea089f529de6d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/di/zipball/77d69061cbf8f9cfb7363dd983136f51213d3e41", - "reference": "77d69061cbf8f9cfb7363dd983136f51213d3e41", + "url": "https://api.github.com/repos/nette/di/zipball/4aff517a1c6bb5c36fa09733d4cea089f529de6d", + "reference": "4aff517a1c6bb5c36fa09733d4cea089f529de6d", "shasum": "" }, "require": { "ext-tokenizer": "*", "nette/neon": "^3.0", - "nette/php-generator": "^3.3.3", + "nette/php-generator": "^3.2.2", "nette/robot-loader": "^3.2", "nette/schema": "^1.0", - "nette/utils": "^3.1", + "nette/utils": "^3.0", "php": ">=7.1" }, "conflict": { @@ -2591,7 +1984,6 @@ }, "require-dev": { "nette/tester": "^2.2", - "phpstan/phpstan": "^0.12", "tracy/tracy": "^2.3" }, "type": "library", @@ -2603,13 +1995,16 @@ "autoload": { "classmap": [ "src/" + ], + "files": [ + "src/compatibility.php" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause", - "GPL-2.0-only", - "GPL-3.0-only" + "GPL-2.0", + "GPL-3.0" ], "authors": [ { @@ -2632,24 +2027,24 @@ "nette", "static" ], - "time": "2020-01-20T12:14:54+00:00" + "time": "2019-08-07T12:11:33+00:00" }, { "name": "nette/finder", - "version": "v2.5.2", + "version": "v2.5.1", "source": { "type": "git", "url": "https://github.com/nette/finder.git", - "reference": "4ad2c298eb8c687dd0e74ae84206a4186eeaed50" + "reference": "14164e1ddd69e9c5f627ff82a10874b3f5bba5fe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/finder/zipball/4ad2c298eb8c687dd0e74ae84206a4186eeaed50", - "reference": "4ad2c298eb8c687dd0e74ae84206a4186eeaed50", + "url": "https://api.github.com/repos/nette/finder/zipball/14164e1ddd69e9c5f627ff82a10874b3f5bba5fe", + "reference": "14164e1ddd69e9c5f627ff82a10874b3f5bba5fe", "shasum": "" }, "require": { - "nette/utils": "^2.4 || ^3.0", + "nette/utils": "^2.4 || ~3.0.0", "php": ">=7.1" }, "conflict": { @@ -2657,7 +2052,6 @@ }, "require-dev": { "nette/tester": "^2.0", - "phpstan/phpstan": "^0.12", "tracy/tracy": "^2.3" }, "type": "library", @@ -2695,36 +2089,35 @@ "iterator", "nette" ], - "time": "2020-01-03T20:35:40+00:00" + "time": "2019-07-11T18:02:17+00:00" }, { "name": "nette/neon", - "version": "v3.1.2", + "version": "v3.0.0", "source": { "type": "git", "url": "https://github.com/nette/neon.git", - "reference": "3c3dcbc6bf6c80dc97b1fc4ba9a22ae67930fc0e" + "reference": "cbff32059cbdd8720deccf9e9eace6ee516f02eb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/neon/zipball/3c3dcbc6bf6c80dc97b1fc4ba9a22ae67930fc0e", - "reference": "3c3dcbc6bf6c80dc97b1fc4ba9a22ae67930fc0e", + "url": "https://api.github.com/repos/nette/neon/zipball/cbff32059cbdd8720deccf9e9eace6ee516f02eb", + "reference": "cbff32059cbdd8720deccf9e9eace6ee516f02eb", "shasum": "" }, "require": { "ext-iconv": "*", "ext-json": "*", - "php": ">=7.1" + "php": ">=7.0" }, "require-dev": { "nette/tester": "^2.0", - "phpstan/phpstan": "^0.12", "tracy/tracy": "^2.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -2735,8 +2128,8 @@ "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause", - "GPL-2.0-only", - "GPL-3.0-only" + "GPL-2.0", + "GPL-3.0" ], "authors": [ { @@ -2748,8 +2141,8 @@ "homepage": "https://nette.org/contributors" } ], - "description": "🍸 Nette NEON: encodes and decodes NEON file format.", - "homepage": "https://ne-on.org", + "description": "? Nette NEON: encodes and decodes NEON file format.", + "homepage": "http://ne-on.org", "keywords": [ "export", "import", @@ -2757,91 +2150,28 @@ "nette", "yaml" ], - "time": "2020-03-04T11:47:04+00:00" + "time": "2019-02-05T21:30:40+00:00" }, { "name": "nette/php-generator", - "version": "v3.3.4", - "source": { - "type": "git", - "url": "https://github.com/nette/php-generator.git", - "reference": "8fe7e699dca7db186f56d75800cb1ec32e39c856" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nette/php-generator/zipball/8fe7e699dca7db186f56d75800cb1ec32e39c856", - "reference": "8fe7e699dca7db186f56d75800cb1ec32e39c856", - "shasum": "" - }, - "require": { - "nette/utils": "^2.4.2 || ^3.0", - "php": ">=7.1" - }, - "require-dev": { - "nette/tester": "^2.0", - "phpstan/phpstan": "^0.12", - "tracy/tracy": "^2.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.3-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause", - "GPL-2.0-only", - "GPL-3.0-only" - ], - "authors": [ - { - "name": "David Grudl", - "homepage": "https://davidgrudl.com" - }, - { - "name": "Nette Community", - "homepage": "https://nette.org/contributors" - } - ], - "description": "🐘 Nette PHP Generator: generates neat PHP code for you. Supports new PHP 7.4 features.", - "homepage": "https://nette.org", - "keywords": [ - "code", - "nette", - "php", - "scaffolding" - ], - "time": "2020-02-09T14:39:09+00:00" - }, - { - "name": "nette/robot-loader", "version": "v3.2.3", "source": { "type": "git", - "url": "https://github.com/nette/robot-loader.git", - "reference": "726c462e73e739e965ec654a667407074cfe83c0" + "url": "https://github.com/nette/php-generator.git", + "reference": "aea6e81437bb238e5f0e5b5ce06337433908e63b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/robot-loader/zipball/726c462e73e739e965ec654a667407074cfe83c0", - "reference": "726c462e73e739e965ec654a667407074cfe83c0", + "url": "https://api.github.com/repos/nette/php-generator/zipball/aea6e81437bb238e5f0e5b5ce06337433908e63b", + "reference": "aea6e81437bb238e5f0e5b5ce06337433908e63b", "shasum": "" }, "require": { - "ext-tokenizer": "*", - "nette/finder": "^2.5 || ^3.0", - "nette/utils": "^3.0", + "nette/utils": "^2.4.2 || ~3.0.0", "php": ">=7.1" }, "require-dev": { "nette/tester": "^2.0", - "phpstan/phpstan": "^0.12", "tracy/tracy": "^2.3" }, "type": "library", @@ -2858,8 +2188,8 @@ "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause", - "GPL-2.0-only", - "GPL-3.0-only" + "GPL-2.0", + "GPL-3.0" ], "authors": [ { @@ -2871,7 +2201,68 @@ "homepage": "https://nette.org/contributors" } ], - "description": "🍀 Nette RobotLoader: high performance and comfortable autoloader that will search and autoload classes within your application.", + "description": "🐘 Nette PHP Generator: generates neat PHP code for you. Supports new PHP 7.3 features.", + "homepage": "https://nette.org", + "keywords": [ + "code", + "nette", + "php", + "scaffolding" + ], + "time": "2019-07-05T13:01:56+00:00" + }, + { + "name": "nette/robot-loader", + "version": "v3.2.0", + "source": { + "type": "git", + "url": "https://github.com/nette/robot-loader.git", + "reference": "0712a0e39ae7956d6a94c0ab6ad41aa842544b5c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/robot-loader/zipball/0712a0e39ae7956d6a94c0ab6ad41aa842544b5c", + "reference": "0712a0e39ae7956d6a94c0ab6ad41aa842544b5c", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "nette/finder": "^2.5", + "nette/utils": "^3.0", + "php": ">=7.1" + }, + "require-dev": { + "nette/tester": "^2.0", + "tracy/tracy": "^2.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0", + "GPL-3.0" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "? Nette RobotLoader: high performance and comfortable autoloader that will search and autoload classes within your application.", "homepage": "https://nette.org", "keywords": [ "autoload", @@ -2880,34 +2271,35 @@ "nette", "trait" ], - "time": "2020-02-28T13:10:07+00:00" + "time": "2019-03-08T21:57:24+00:00" }, { "name": "nette/schema", - "version": "v1.0.2", + "version": "v1.0.0", "source": { "type": "git", "url": "https://github.com/nette/schema.git", - "reference": "febf71fb4052c824046f5a33f4f769a6e7fa0cb4" + "reference": "6241d8d4da39e825dd6cb5bfbe4242912f4d7e4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/schema/zipball/febf71fb4052c824046f5a33f4f769a6e7fa0cb4", - "reference": "febf71fb4052c824046f5a33f4f769a6e7fa0cb4", + "url": "https://api.github.com/repos/nette/schema/zipball/6241d8d4da39e825dd6cb5bfbe4242912f4d7e4d", + "reference": "6241d8d4da39e825dd6cb5bfbe4242912f4d7e4d", "shasum": "" }, "require": { - "nette/utils": "^3.1", + "nette/utils": "^3.0.1", "php": ">=7.1" }, "require-dev": { "nette/tester": "^2.2", - "phpstan/phpstan-nette": "^0.12", "tracy/tracy": "^2.3" }, "type": "library", "extra": { - "branch-alias": [] + "branch-alias": { + "dev-master": "1.0-dev" + } }, "autoload": { "classmap": [ @@ -2936,20 +2328,20 @@ "config", "nette" ], - "time": "2020-01-06T22:52:48+00:00" + "time": "2019-04-03T15:53:25+00:00" }, { "name": "nette/utils", - "version": "v3.1.1", + "version": "v3.0.2", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "2c17d16d8887579ae1c0898ff94a3668997fd3eb" + "reference": "c133e18c922dcf3ad07673077d92d92cef25a148" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/2c17d16d8887579ae1c0898ff94a3668997fd3eb", - "reference": "2c17d16d8887579ae1c0898ff94a3668997fd3eb", + "url": "https://api.github.com/repos/nette/utils/zipball/c133e18c922dcf3ad07673077d92d92cef25a148", + "reference": "c133e18c922dcf3ad07673077d92d92cef25a148", "shasum": "" }, "require": { @@ -2957,7 +2349,6 @@ }, "require-dev": { "nette/tester": "~2.0", - "phpstan/phpstan": "^0.12", "tracy/tracy": "^2.3" }, "suggest": { @@ -2972,7 +2363,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -2983,8 +2374,8 @@ "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause", - "GPL-2.0-only", - "GPL-3.0-only" + "GPL-2.0", + "GPL-3.0" ], "authors": [ { @@ -3014,20 +2405,71 @@ "utility", "validation" ], - "time": "2020-02-09T14:10:55+00:00" + "time": "2019-10-21T20:40:16+00:00" }, { - "name": "ocramius/package-versions", - "version": "1.4.2", + "name": "nikic/php-parser", + "version": "v4.2.4", "source": { "type": "git", - "url": "https://github.com/Ocramius/PackageVersions.git", - "reference": "44af6f3a2e2e04f2af46bcb302ad9600cba41c7d" + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "97e59c7a16464196a8b9c77c47df68e4a39a45c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Ocramius/PackageVersions/zipball/44af6f3a2e2e04f2af46bcb302ad9600cba41c7d", - "reference": "44af6f3a2e2e04f2af46bcb302ad9600cba41c7d", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/97e59c7a16464196a8b9c77c47df68e4a39a45c4", + "reference": "97e59c7a16464196a8b9c77c47df68e4a39a45c4", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.2-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "time": "2019-09-01T07:51:21+00:00" + }, + { + "name": "ocramius/package-versions", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/Ocramius/PackageVersions.git", + "reference": "a4d4b60d0e60da2487bd21a2c6ac089f85570dbb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Ocramius/PackageVersions/zipball/a4d4b60d0e60da2487bd21a2c6ac089f85570dbb", + "reference": "a4d4b60d0e60da2487bd21a2c6ac089f85570dbb", "shasum": "" }, "require": { @@ -3039,7 +2481,7 @@ "doctrine/coding-standard": "^5.0.1", "ext-zip": "*", "infection/infection": "^0.7.1", - "phpunit/phpunit": "^7.5.17" + "phpunit/phpunit": "^7.0.0" }, "type": "composer-plugin", "extra": { @@ -3064,7 +2506,7 @@ } ], "description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)", - "time": "2019-11-15T16:17:10+00:00" + "time": "2019-02-21T12:16:21+00:00" }, { "name": "phpstan/phpdoc-parser", @@ -3190,28 +2632,27 @@ }, { "name": "symfony/console", - "version": "v4.4.7", + "version": "v4.3.5", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "10bb3ee3c97308869d53b3e3d03f6ac23ff985f7" + "reference": "929ddf360d401b958f611d44e726094ab46a7369" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/10bb3ee3c97308869d53b3e3d03f6ac23ff985f7", - "reference": "10bb3ee3c97308869d53b3e3d03f6ac23ff985f7", + "url": "https://api.github.com/repos/symfony/console/zipball/929ddf360d401b958f611d44e726094ab46a7369", + "reference": "929ddf360d401b958f611d44e726094ab46a7369", "shasum": "" }, "require": { "php": "^7.1.3", "symfony/polyfill-mbstring": "~1.0", "symfony/polyfill-php73": "^1.8", - "symfony/service-contracts": "^1.1|^2" + "symfony/service-contracts": "^1.1" }, "conflict": { "symfony/dependency-injection": "<3.4", - "symfony/event-dispatcher": "<4.3|>=5", - "symfony/lock": "<4.4", + "symfony/event-dispatcher": "<4.3", "symfony/process": "<3.3" }, "provide": { @@ -3219,12 +2660,12 @@ }, "require-dev": { "psr/log": "~1.0", - "symfony/config": "^3.4|^4.0|^5.0", - "symfony/dependency-injection": "^3.4|^4.0|^5.0", + "symfony/config": "~3.4|~4.0", + "symfony/dependency-injection": "~3.4|~4.0", "symfony/event-dispatcher": "^4.3", - "symfony/lock": "^4.4|^5.0", - "symfony/process": "^3.4|^4.0|^5.0", - "symfony/var-dumper": "^4.3|^5.0" + "symfony/lock": "~3.4|~4.0", + "symfony/process": "~3.4|~4.0", + "symfony/var-dumper": "^4.3" }, "suggest": { "psr/log": "For using the console logger", @@ -3235,7 +2676,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.4-dev" + "dev-master": "4.3-dev" } }, "autoload": { @@ -3262,20 +2703,20 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2020-03-30T11:41:10+00:00" + "time": "2019-10-07T12:36:49+00:00" }, { "name": "symfony/finder", - "version": "v4.4.7", + "version": "v4.3.5", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "5729f943f9854c5781984ed4907bbb817735776b" + "reference": "5e575faa95548d0586f6bedaeabec259714e44d1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/5729f943f9854c5781984ed4907bbb817735776b", - "reference": "5729f943f9854c5781984ed4907bbb817735776b", + "url": "https://api.github.com/repos/symfony/finder/zipball/5e575faa95548d0586f6bedaeabec259714e44d1", + "reference": "5e575faa95548d0586f6bedaeabec259714e44d1", "shasum": "" }, "require": { @@ -3284,7 +2725,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.4-dev" + "dev-master": "4.3-dev" } }, "autoload": { @@ -3311,20 +2752,20 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2020-03-27T16:54:36+00:00" + "time": "2019-09-16T11:29:48+00:00" }, { "name": "symfony/polyfill-php73", - "version": "v1.15.0", + "version": "v1.12.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "0f27e9f464ea3da33cbe7ca3bdf4eb66def9d0f7" + "reference": "2ceb49eaccb9352bff54d22570276bb75ba4a188" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/0f27e9f464ea3da33cbe7ca3bdf4eb66def9d0f7", - "reference": "0f27e9f464ea3da33cbe7ca3bdf4eb66def9d0f7", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/2ceb49eaccb9352bff54d22570276bb75ba4a188", + "reference": "2ceb49eaccb9352bff54d22570276bb75ba4a188", "shasum": "" }, "require": { @@ -3333,7 +2774,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.15-dev" + "dev-master": "1.12-dev" } }, "autoload": { @@ -3369,20 +2810,20 @@ "portable", "shim" ], - "time": "2020-02-27T09:26:54+00:00" + "time": "2019-08-06T08:03:45+00:00" }, { "name": "symfony/service-contracts", - "version": "v1.1.8", + "version": "v1.1.7", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "ffc7f5692092df31515df2a5ecf3b7302b3ddacf" + "reference": "ffcde9615dc5bb4825b9f6aed07716f1f57faae0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/ffc7f5692092df31515df2a5ecf3b7302b3ddacf", - "reference": "ffc7f5692092df31515df2a5ecf3b7302b3ddacf", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/ffcde9615dc5bb4825b9f6aed07716f1f57faae0", + "reference": "ffcde9615dc5bb4825b9f6aed07716f1f57faae0", "shasum": "" }, "require": { @@ -3427,19 +2868,19 @@ "interoperability", "standards" ], - "time": "2019-10-14T12:27:06+00:00" + "time": "2019-09-17T11:12:18+00:00" } ], "aliases": [], - "minimum-stability": "stable", + "minimum-stability": "dev", "stability-flags": [], - "prefer-stable": false, + "prefer-stable": true, "prefer-lowest": false, "platform": { "php": ">=7.1", - "ext-gd": "*", "ext-intl": "*", "ext-json": "*", + "ext-gd": "*", "ext-pdo": "*", "ext-zip": "*" }, diff --git a/config.example.php b/config.example.php index d9e4667..83805da 100644 --- a/config.example.php +++ b/config.example.php @@ -1,15 +1,14 @@ 'https://localhost', // no trailing slash - 'db' => [ - 'connection' => 'sqlite', - 'dsn' => 'resources/database/xbackbone.db', - 'username' => null, - 'password' => null, - ], - 'storage' => [ - 'driver' => 'local', - 'path' => './storage', - ], + 'base_url' => 'http://localhost', + 'db' => [ + 'connection' => 'sqlite', + 'dsn' => 'resources/database/xbackbone.db', + 'username' => null, + 'password' => null, + ], + 'storage' => [ + 'driver' => 'local', + 'path' => './storage', + ], ]; diff --git a/docs/_config.yml b/docs/_config.yml deleted file mode 100644 index 4291907..0000000 --- a/docs/_config.yml +++ /dev/null @@ -1,7 +0,0 @@ -remote_theme: pmarsceill/just-the-docs -logo: "img/xbackbone.png" -title: "XBackBone" -aux_links: - "XBackBone on GitHub": - - "//github.com/SergiX44/XBackBone" -footer_content: "Copyright © 2020 Sergio Brighenti. Distributed by an AGPL v3.0 license." \ No newline at end of file diff --git a/docs/basic_usage.md b/docs/basic_usage.md deleted file mode 100644 index 55865c6..0000000 --- a/docs/basic_usage.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -layout: default -title: Basic Usage -nav_order: 6 ---- - -# Basic Usage - -## Users -Some functions that every user that use XBackBone should know: -+ You can hide/publish every upload, once it's hidden, it's visible only by yourself. -+ You can download your ShareX configuration file from your profile page. -+ You can upload files directly from the upload page. -+ You can change you upload token anytime. -+ You can export all your uploads in a single zip files in your profile page. -+ (`v3.1`+) With the right click on the uploaded media, you can select them, and then remove them in bulk. -+ (`v3.1`+) You can add additional tag to your uploads, clicking on the **+** button on the media. -+ (`v3.1`+) You can add you can delete tags, by right-click on them. -+ (`v3.1`+) You can choose in your profile options if hide the uploads by default. -+ (`v3.1`+) You can choose in your profile options if always copy the raw url (from the web interface). - -## Administrator -In addition, from the system page, and administrator can: -+ Perform maintenance actions. -+ Change theme. -+ Force system languages. -+ Enabled recaptcha. -+ Enable user disk quota. -+ ... and more. - -In the users page, it can add/remove users, and edit per user configurations. \ No newline at end of file diff --git a/docs/changelog.md b/docs/changelog.md deleted file mode 100644 index 59474da..0000000 --- a/docs/changelog.md +++ /dev/null @@ -1,195 +0,0 @@ ---- -layout: default -title: Changelog -nav_order: 8 ---- - -## v.3.1 (WIP) -+ Added tagging system (add, delete, search of tagged files). -+ Added basic media auto-tagging on upload. -+ Added registration system. -+ Added password recovery system. -+ Added ability to export all media of an account. -+ Added ability to choose between default and raw url on copy. -+ Added hide by default option. -+ Added user disk quota. -+ Added reCAPTCHA login protection. -+ Added bulk delete. -+ Added account clean function. -+ Added user disk quota system. -+ Added notification option on account create. -+ Added LDAP authentication. -+ Fixed bug html files raws are rendered in a browser. -+ The theme is now re-applied after every system update. -+ Updated system settings page. -+ Updated translations. -+ Improved grid layout. -+ Fixes and improvements. - -## v.3.0.2 -+ Fixed error with migrate command. -+ Updated translations. - -## v.3.0.1 -+ Fixed error with older mysql versions. -+ Fixed config is compiled with the di container. -+ Small installer update. - -## v.3.0 -+ Upgraded from Slim3 to Slim 4. -+ Added web upload. -+ Added ability to add custom HTML in \ tag. -+ Added ability to show a preview of PDF files. -+ Added remember me functionality. -+ Added delete button on the preview page if the user is logged in. -+ New project icon (by [@SerenaItalia](https://www.deviantart.com/serenaitalia)). -+ Raw URL now accept file extensions. -+ The linux script can be used on headless systems. -+ Improved installer. -+ Improved thumbnail generation. -+ Replaced videojs player with Plyr. -+ Implemented SameSite XSS protection. -+ Small fixes and improvements. - -## v.2.6.6 -+ Ability to choose between releases and prereleases with the web updater. -+ Updated translations. - -## v2.6.5 -+ Fixed error after orphaned files removal #74. -+ Fixed update password not correctly removed from log files #74. -+ Changed color to some buttons to address visibility with some themes. - -## v2.6.4 -+ Filter on displayable images. -+ Fixed during upload error on php compiled for 32 bit. -+ Fixed icons on the installer page. -+ The generated random strings are now more human readable. - -## v2.6.3 -+ Fixed #67. -+ Fixed bad preload statement. -+ Fixed wrong redirect after install in subdirs. - -## v2.6.2 -+ Use the font awesome web font for better performances. -+ Changed background default color. -+ Added method for cache busting when updating/change theme. -+ Added russian translation from [Weblate](https://hosted.weblate.org/projects/xbackbone/xbackbone/). - -## v2.6.1 -+ Fixed bad redirects on the web installer (#62). -+ Fixed login page with dark themes. -+ Improved shell commands. -+ Added alert if required extensions are not loaded. -+ Updated translations. - -## v2.6 -+ Added support to use AWS S3, Google Cloud Storage, Dropbox and FTP(s) accounts as storage location. -+ Fixed missing icon. -+ Added german and norwegian translations from [Weblate](https://hosted.weblate.org/projects/xbackbone/xbackbone/). -+ Improved lang detection. -+ Added ability to force system language. - -## v2.5.3 -+ Fixed bad css loading on Firefox (#35). -+ Fixed wrong style for publish/unpublish button. -+ Improved exception stacktrace logging. - -## v2.5.2 -+ Improved session handling. -+ Fixed telegram share not working. -+ Fix for big text file now are not rendered in the browser. -+ Added preloading for some resources to improve performances. -+ Added check for block execution on EOL and unsupported PHP versions. -+ Other minor improvements. - -## v2.5.1 -+ Fixed bad redirect if the theme folder is not writable. (#27) -+ Improved HTTP partial content implementation for large files. - -## v2.5 -+ Updated project license to AGPL v3.0 (now releases ships with the new license). -+ **[BETA]** Added self update feature. -+ Added partial content implementation (stream seeking on chromium based browsers). -+ Improved video.js alignment with large videos. -+ Optimized output zip release size. -+ Templates cleanup and optimizations. -+ Improved error handling. -+ Added project favicon. - -## v2.4.1 -+ Fixed error message when the file is too large. (#15) -+ Fixed button alignment. - -## v2.4 -+ Added function to remove orphaned files. -+ Switch between tab and gallery mode using an admin account. -+ Multiple uploads sorting methods. -+ Search in uploads. -+ Internal refactoring and improvements -+ Updated js dependencies. - -## v2.3.1 -+ Fixed en lang. -+ Fixed forced background with dark themes. -+ Added checks during the installation wizard. -+ cURL and Wget can now directly download the file. - -## v2.3 -+ Improved image scaling in user gallery. -+ Added overlay on user gallery images. -+ Fixed IT translation. -+ Fontawesome icon match the single file mime-type. -+ Enable audio player with video.js. -+ Video and audio now starts with volume at 50%. -+ Added linux script to allow uploads from linux screenshot tools. -+ Minor layout fixes. - -## v2.2 -+ Added multi-language support. -+ Improved routing. -+ Fixed HTTP/2 push is resetting the current session. -+ Minor improvements and bug fixes. - -## v2.1 -+ Improved theme style. -+ Improved page redirecting. -+ Allow e-mail login. -+ Support for ShareX deletion URL. -+ Fixed HTTP/2 push preload. -+ Added video.js support. - -## v2.0 -+ Migrated from Flight to Slim 3 framework. -+ Added install wizard (using the CLI is no longer required). -+ Allow discord bot to display the preview. -+ Theme switcher on the web UI. -+ Added used space indicator per user. -+ MySQL support. -+ Improvements under the hood. - -## v1.3 -+ Added command to switch between bootswatch.com themes. -+ Added popever to write the telegram message when sharing. -+ Packaging improvements. -+ Updated some dependencies. -+ Allow Facebook bots to display the preview. - -## v1.2 -+ Previews are now scaled for better page load. -+ Added auto config generator for ShareX. -+ Show upload file size on the dashboard. -+ Fixed insert for admin user (running `php bin\migrate --install`). -+ Removed HTTP2 push from the dashboard to improve loading time. - -## v1.1 -+ Added logging. -+ Fixed back to top when click delete or publish/unpublish. -+ Improved migrate system. -+ Login redirect back to the requested page. -+ Updated Bootstrap theme. -+ Added share to Telegram. - -## v1.0 -+ Initial version. diff --git a/docs/clients.md b/docs/clients.md deleted file mode 100644 index ae1d4f9..0000000 --- a/docs/clients.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -layout: default -title: Clients -nav_order: 5 ---- - -# Clients Configuration - -## ShareX Configuration -Once you are logged in, just go in your profile settings and download the ShareX config file for your account. - -## Linux/Mac Support -Since ShareX does not support Linux, XBackBone can generate a script that allows you to share an item from any tool: -+ Login into your account -+ Navigate to your profile and download the Linux script for your account. -+ Place the script where you want (ex. in your user home: `/home/`). -+ Add execution permissions (`chmod +x xbackbone_uploader_XXX.sh`) -+ Run the script for the first time to create the desktop entry: `./xbackbone_uploader_XXX.sh -desktop-entry`. - -Now, to upload a media, just use the right click on the file > "Open with ..." > search XBackBone Uploader (XXX) in the app list. -You can use this feature in combination with tools like [Flameshot](https://github.com/lupoDharkael/flameshot), just use the "Open with ..." button once you have done the screenshot. - -The script requires `xclip`, `curl`, and `notify-send` on a desktop distribution. - -*Note: XXX is the username of your XBackBone account.* \ No newline at end of file diff --git a/docs/common_issues.md b/docs/common_issues.md deleted file mode 100644 index 129eb61..0000000 --- a/docs/common_issues.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -layout: default -title: Common Issues -nav_order: 7 ---- - -# Common Issues - -### Error 404 after installation -If you have apache, check if it's reading the file `.htaccess` and the `mod_rewrite` is enabled. - -### [Discord, Telegram, ...] is not showing the image/video preview of the link. -If you have Cloudflare enabled, check if it's blocking bots. If this function is enabled the Discord bot, Telebot, etc that fetch the preview will be blocked. - -### How to increase the max file size? -Increase the post_max_size and upload_max_filesize in your php.ini \ No newline at end of file diff --git a/docs/configuration.md b/docs/configuration.md deleted file mode 100644 index 1c85a5c..0000000 --- a/docs/configuration.md +++ /dev/null @@ -1,156 +0,0 @@ ---- -layout: default -title: Configuration -nav_order: 3 ---- -# Configuration - -## Web Server -*Apache need the `mod_rewrite` extension to make XBackBone work properly*. - -If you do not use Apache, or the Apache `.htaccess` is not enabled, set your web server so that the `static/` folder is the only one accessible from the outside, otherwise even private uploads and logs will be accessible! - -If you are using NGINX, you can find an example configuration [`nginx.conf`](https://github.com/SergiX44/XBackBone/blob/master/nginx.conf) in the project repository. - -## Maintenance Mode -Maintenance mode is automatically enabled during an upgrade using the upgrade manager. You can activate it manually by editing the `config.php`, and adding this line: - -```php -return array( - ... - 'maintenance' => true, -); -``` - -## Database support - -Currently, is supported `MySQL/MariaDB` and `SQLite3`. - -For big installations, `MySQL/MariaDB` is recommended. - -Example config: -```php -return array( - ..., - 'db' => array ( - 'connection' => 'mysql', // sqlite or mysql - 'dsn' => 'host=localhost;port=3306;dbname=xbackbone', // the path to db, if sqlite - 'username' => 'xbackbone', // null, if sqlite - 'password' => 's3cr3t', // null, if sqlite - ), -) -``` - -## LDAP Authentication - -Since the release 3.1, the LDAP integration can be configured. - -Edit the `config.php`, and add the following lines: -```php -return array( - ... - 'ldap' => array( - 'enabled' => true, // enable it - 'host' => 'ad.example.com', // set the ldap host - 'port' => 389, // ldap port - 'base_domain' => 'dc=example,dc=com', // the base_dn string - 'user_domain' => 'ou=Users', // the user dn string - ) -); -``` - -By activating this function, it will not be possible for users logging in via LDAP to reset the password from the application (for obvious reasons), and it will also be possible to bring existing users under LDAP authentication. - - -## Storage drivers - -XBackBone supports these storage drivers (with some configuration examples): - -+ Local Storage (default) -```php -return array( - ... - 'storage' => array ( - 'driver' => 'local', - 'path' => '/path/to/storage/folder', - ) -); -``` - -+ Amazon S3 -```php -return array( - ... - 'storage' => array ( - 'driver' => 's3', - 'key' => 'the-key', - 'secret' => 'the-secret', - 'region' => 'the-region', - 'bucket' => 'bucket-name', - 'path' => 'optional/path/prefix', - ) -); -``` - -+ Dropbox -```php -return array( - ... - 'storage' => array ( - 'driver' => 'dropbox', - 'token' => 'the-token', - ) -); -``` - -+ FTP(s) -```php -return array( - ... - 'storage' => array ( - 'driver' => 'ftp', - 'host' => 'ftp.example.com', - 'port' => 21, - 'username' => 'the-username', - 'password' => 'the-password', - 'path' => 'the/prefix/path/', - 'passive' => true/false, - 'ssl' => true/false, - ) -); -``` - -+ Google Cloud Storage -```php -return array( - ... - 'storage' => array ( - 'driver' => 'google-client', - 'project_id' => 'the-project-id', - 'key_path' => 'the-key-path', - 'bucket' => 'bucket-name', - ) -); -``` - -## Changing themes -XBackBone supports all [bootswatch.com](https://bootswatch.com/) themes. - -From the web UI: -+ Navigate to the web interface as admin -> System Menu -> Choose a theme from the dropdown. - -From the CLI: -+ Run the command `php bin/theme` to see the available themes. -+ Use the same command with the argument name (`php bin/theme `) to choose a theme. -+ If you want to revert back to the original bootstrap theme, run the command `php bin/theme default`. - -*Clear the browser cache once you have applied.* - -## Change app install name -Add to the `config.php` file an array element like this: -```php -return array( - 'app_name' => 'This line will overwrite "XBackBone"', - ... -); -``` \ No newline at end of file diff --git a/docs/img/xbackbone.png b/docs/img/xbackbone.png deleted file mode 100644 index 5f56b18..0000000 Binary files a/docs/img/xbackbone.png and /dev/null differ diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index fa7e26d..0000000 --- a/docs/index.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -layout: default -title: Home -nav_order: 1 ---- - -

- -

- -XBackBone is a simple and lightweight PHP file manager that support the instant sharing tool ShareX and *NIX systems. It supports uploading and displaying images, GIF, video, code, formatted text, pdf, and file downloading and uploading. Also have a web UI with multi user management, media gallery and search support. -{: .fs-5 .fw-300 } - - -[Download](https://github.com/SergiX44/XBackBone/releases/latest){: .btn .btn-green } -[GitHub](https://github.com/SergiX44/XBackBone){: .btn .btn-blue } - - -## Main Features -+ Supports every upload type from ShareX. -+ Config generator for ShareX. -+ Low memory footprint. -+ Multiple backends support: Local storage, AWS S3, Google Cloud, Dropbox, FTP(s). -+ Web file upload. -+ Code uploads syntax highlighting. -+ Video and audio uploads webplayer. -+ PDF viewer. -+ Files preview page. -+ Bootswatch themes support. -+ Responsive theme for mobile use. -+ Multi language support. -+ User management, multi user features, roles and disk quota. -+ Public and private uploads. -+ Logging system. -+ Share to Telegram. -+ Linux supported via a per-user custom generated script (server and desktop). -+ Direct downloads using curl or wget commands. -+ Direct images links support on Discord, Telegram, Facebook, etc. -+ System updates without FTP or CLI. -+ Easy web installer. -+ LDAP authentication. -+ Registration system. -+ Automatic uploads tagging system. -+ Tag uploads with custom tags for categorization. -+ ... and more. - -## Translations -You can help translating the project on [Weblate](https://hosted.weblate.org/projects/xbackbone/xbackbone/). - - -Stato traduzione - - -#### Demo GIF - -![img](https://i.imgur.com/iV8Rirn.gif) \ No newline at end of file diff --git a/docs/installation.md b/docs/installation.md deleted file mode 100644 index cf35d29..0000000 --- a/docs/installation.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -layout: default -title: Installation -nav_order: 2 ---- - -# Installation - -### Prerequisites -XBackBone require PHP >= `7.1`, with installed the required extensions: -+ `php-sqlite3` for SQLite. -+ `php-mysql` for MariaDB/MySQL. -+ `php-gd` image manipualtion library. -+ `php-json` json file support. -+ `php-intl` internationalization functions. -+ `php-fileinfo` file related functions. -+ (optional) `php-ftp` to use the FTP remote storage driver. -+ (optional) `php-ldap` to use LDAP authentication. - -## Web installation -+ Download latest release from GitHub: [Latest Release](https://github.com/SergiX44/XBackBone/releases/latest) -+ Extract the release zip to your document root. -+ Navigate to the webspace root (ex. `http://example.com/xbackbone`, this should auto redirect your browser to the install page `http://example.com/xbackbone/install/`) -+ Follow the instructions. - -For futher and advanced configurations, see the [configuration page](configuration.md). - -## Manual installation -+ Download latest release from GitHub: [Latest Release](https://github.com/SergiX44/XBackBone/releases/latest) -+ Extract the release zip to your document root. -+ Copy and edit the config file: -```sh -cp config.example.php config.php && nano config.php -``` -By default, XBackBone will use Sqlite3 as DB engine, and a `storage` dir in the main directory. You can leave these settings unchanged for a simple personal installation. -You must set the `base_url`, or remove it for get dynamically the url from request (not recommended). - -```php -return [ - 'base_url' => 'https://example.com', // no trailing slash - 'storage' => [ - 'driver' => 'local', - 'path' => 'storage', - ], - 'db' => [ - 'connection' => 'sqlite', // current support for sqlite and mysql - 'dsn' => 'resources/database/xbackbone.db', - 'username' => null, // username and password not needed for sqlite - 'password' => null, - ] -]; -``` -+ Finally, run the migrate script to setup the database - -```sh -php bin/migrate --install -``` -+ Delete the `/install` directory. -+ Now just login with `admin/admin`, **be sure to change these credentials after your first login**. - - -For futher and advanced configurations, see the [configuration page](configuration.md). - -## Docker deployment -Alternatively, a docker container is available. - -[Docker container](https://hub.docker.com/r/pe46dro/xbackbone-docker){: .btn .btn-purple } \ No newline at end of file diff --git a/docs/license.md b/docs/license.md deleted file mode 100644 index db7994d..0000000 --- a/docs/license.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -layout: default -title: License & Credits -nav_order: 9 ---- - -# License -This software is licensed under the GNU Affero General Public License v3.0, available in this repository. -As a "copyright notice" it is sufficient to keep the small footer at the bottom of the page, also to help other people to learn about this project! - -# Built with -+ Project logo by [@Sere](https://www.deviantart.com/serenaitalia) -+ Slim 3 since `v2.0`, and Slim 4 since `v3.0` (https://www.slimframework.com/) and some great PHP packages (Flysystem, Intervention Image, Twig, etc) -+ FlightPHP, up to `v1.x` (http://flightphp.com/) -+ Bootstrap 4 (https://getbootstrap.com/) -+ Font Awesome 5 (http://fontawesome.com) -+ ClipboardJS (https://clipboardjs.com/) -+ HighlightJS (https://highlightjs.org/) -+ JQuery (https://jquery.com/) -+ Plyr.io (https://plyr.io/) -+ Dropzone.js (https://www.dropzonejs.com/) \ No newline at end of file diff --git a/docs/upgrading.md b/docs/upgrading.md deleted file mode 100644 index badd5e8..0000000 --- a/docs/upgrading.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -layout: default -title: Upgrading -nav_order: 4 ---- - -# How to update - -The system updates can be applied via the web interface by an administrator, or manually via CLI. - -## Self-update (since v2.5) -+ Navigate to the system page as administrator. -+ Click the check for update button, and finally the upgrade button. -+ Wait until the browser redirect to the install page. -+ Click the update button. -+ Done. - - -## Manual update -+ Download and extract the release zip to your document root, overwriting any file. -+ Navigate to the `/install` path (es: `http://example.com/` -> `http://example.com/install/`) -+ Click the update button. -+ Done. - -## CLI update -If, for whatever reason, the web UI is not accessible, you can upgrade from CLI: -+ Download and extract the release zip to your document root, overwriting any file. -+ Run the command `php\migrate`. -+ Run the command `php\clean`. -+ Done. - -### Pre-release channel - -From the system page, you can also choose to check from beta/RC releases, these are NOT considered stable enough for every day use, but only for testing purposes, **take a backup before upgrading to these versions**. \ No newline at end of file diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..5d03b38 Binary files /dev/null and b/favicon.ico differ diff --git a/index.php b/index.php index 6427241..668c5b7 100644 --- a/index.php +++ b/index.php @@ -1,10 +1,9 @@ = 7 && PHP_MINOR_VERSION >= 1) ?: die('Sorry, PHP 7.1 or above is required to run XBackBone.'); -require __DIR__.'/vendor/autoload.php'; +require __DIR__ . '/vendor/autoload.php'; -define('BASE_DIR', realpath(__DIR__).DIRECTORY_SEPARATOR); +define('BASE_DIR', realpath(__DIR__) . DIRECTORY_SEPARATOR); define('PLATFORM_VERSION', json_decode(file_get_contents('composer.json'))->version); -$app = require_once __DIR__.'/bootstrap/app.php'; +$app = require_once __DIR__ . '/bootstrap/app.php'; $app->run(); diff --git a/install/index.php b/install/index.php index 2538b7e..f5272b1 100644 --- a/install/index.php +++ b/install/index.php @@ -1,243 +1,333 @@ = 7 && PHP_MINOR_VERSION >= 1) ?: die('Sorry, PHP 7.1 or above is required to run XBackBone.'); -require __DIR__.'/../vendor/autoload.php'; +require __DIR__ . '/../vendor/autoload.php'; use App\Database\DB; -use App\Database\Migrator; -use App\Factories\ViewFactory; -use App\Web\Media; use App\Web\Session; -use App\Web\View; -use DI\Bridge\Slim\Bridge; -use DI\ContainerBuilder; -use function DI\factory; -use function DI\get; -use function DI\value; +use Aws\S3\S3Client; +use Google\Cloud\Storage\StorageClient; +use League\Flysystem\Adapter\Local; +use League\Flysystem\AwsS3v3\AwsS3Adapter; +use League\Flysystem\Adapter\Ftp as FtpAdapter; use League\Flysystem\FileExistsException; +use Spatie\Dropbox\Client as DropboxClient; use League\Flysystem\Filesystem; -use Psr\Container\ContainerInterface as Container; -use Psr\Http\Message\ResponseInterface as Response; -use Psr\Http\Message\ServerRequestInterface as Request; +use Slim\App; +use Slim\Container; +use Slim\Http\Environment; +use Slim\Http\Request; +use Slim\Http\Response; +use Slim\Http\Uri; +use Slim\Views\Twig; +use Spatie\FlysystemDropbox\DropboxAdapter; +use Superbalist\Flysystem\GoogleStorage\GoogleStorageAdapter; -define('PLATFORM_VERSION', json_decode(file_get_contents(__DIR__.'/../composer.json'))->version); -define('BASE_DIR', realpath(__DIR__.'/../').DIRECTORY_SEPARATOR); +define('PLATFORM_VERSION', json_decode(file_get_contents(__DIR__ . '/../composer.json'))->version); // default config $config = [ - 'base_url' => str_replace('/install/', '', (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http')."://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]"), - 'debug' => true, - 'db' => [ - 'connection' => 'sqlite', - 'dsn' => realpath(__DIR__.'/../').implode(DIRECTORY_SEPARATOR, ['resources', 'database', 'xbackbone.db']), - 'username' => null, - 'password' => null, - ], - 'storage' => [ - 'driver' => 'local', - 'path' => realpath(__DIR__.'/../').DIRECTORY_SEPARATOR.'storage', - ], + 'base_url' => str_replace('/install/', '', (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http') . "://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]"), + 'displayErrorDetails' => true, + 'db' => [ + 'connection' => 'sqlite', + 'dsn' => realpath(__DIR__ . '/../') . implode(DIRECTORY_SEPARATOR, ['resources', 'database', 'xbackbone.db']), + 'username' => null, + 'password' => null, + ], + 'storage' => [ + 'driver' => 'local', + 'path' => realpath(__DIR__ . '/../') . DIRECTORY_SEPARATOR . 'storage', + ], ]; -if (file_exists(__DIR__.'/../config.php')) { - $config = array_replace_recursive($config, require __DIR__.'/../config.php'); +if (file_exists(__DIR__ . '/../config.php')) { + $config = array_replace_recursive($config, require __DIR__ . '/../config.php'); } -$builder = new ContainerBuilder(); +$container = new Container(['settings' => $config]); -$builder->addDefinitions([ - 'config' => value($config), - View::class => factory(function (Container $container) { - return ViewFactory::createInstallerInstance($container); - }), - 'view' => get(View::class), -]); -$builder->addDefinitions(__DIR__.'/../bootstrap/container.php'); +$container['session'] = function ($container) { + return new Session('xbackbone_session'); +}; -$app = Bridge::create($builder->build()); -$app->setBasePath(parse_url($config['base_url'].'/install', PHP_URL_PATH)); -$app->addRoutingMiddleware(); +$container['view'] = function ($container) use (&$config) { + $view = new Twig([__DIR__ . '/templates', __DIR__ . '/../resources/templates'], [ + 'cache' => false, + 'autoescape' => 'html', + 'debug' => $config['displayErrorDetails'], + 'auto_reload' => $config['displayErrorDetails'], + ]); -$app->get('/', function (Response $response, View $view, Session $session) use (&$config) { - if (!extension_loaded('gd')) { - $session->alert('The required "gd" extension is not loaded.', 'danger'); - } + // Instantiate and add Slim specific extension + $router = $container->get('router'); + $uri = Uri::createFromEnvironment(new Environment($_SERVER)); + $view->addExtension(new Slim\Views\TwigExtension($router, $uri)); - if (!extension_loaded('intl')) { - $session->alert('The required "intl" extension is not loaded.', 'danger'); - } + $view->getEnvironment()->addGlobal('config', $config); + $view->getEnvironment()->addGlobal('request', $container->get('request')); + $view->getEnvironment()->addGlobal('alerts', $container->get('session')->getAlert()); + $view->getEnvironment()->addGlobal('session', $container->get('session')->all()); + $view->getEnvironment()->addGlobal('PLATFORM_VERSION', PLATFORM_VERSION); + return $view; +}; - if (!extension_loaded('json')) { - $session->alert('The required "json" extension is not loaded.', 'danger'); - } +$container['storage'] = function ($container) use (&$config) { - if (!extension_loaded('fileinfo')) { - $session->alert('The required "fileinfo" extension is not loaded.', 'danger'); - } + switch ($config['storage']['driver']) { + case 'local': + return new Filesystem(new Local($config['storage']['path'])); + case 's3': + $client = new S3Client([ + 'credentials' => [ + 'key' => $config['storage']['key'], + 'secret' => $config['storage']['secret'], + ], + 'region' => $config['storage']['region'], + 'version' => 'latest', + ]); - if (!is_writable(__DIR__.'/../resources/cache')) { - $session->alert('The cache folder is not writable ('.__DIR__.'/../resources/cache'.')', 'danger'); - } + return new Filesystem(new AwsS3Adapter($client, $config['storage']['bucket'], $config['storage']['path'])); + case 'dropbox': + $client = new DropboxClient($config['storage']['token']); + return new Filesystem(new DropboxAdapter($client), ['case_sensitive' => false]); + case 'ftp': + return new Filesystem(new FtpAdapter([ + 'host' => $config['storage']['host'], + 'username' => $config['storage']['username'], + 'password' => $config['storage']['password'], + 'port' => $config['storage']['port'], + 'root' => $config['storage']['path'], + 'passive' => $config['storage']['passive'], + 'ssl' => $config['storage']['ssl'], + 'timeout' => 30, + ])); + case 'google-cloud': + $client = new StorageClient([ + 'projectId' => $config['storage']['project_id'], + 'keyFilePath' => $config['storage']['key_path'], + ]); + return new Filesystem(new GoogleStorageAdapter($client, $client->bucket($config['storage']['bucket']))); + default: + throw new InvalidArgumentException('The driver specified is not supported.'); + } +}; - if (!is_writable(__DIR__.'/../resources/database')) { - $session->alert('The database folder is not writable ('.__DIR__.'/../resources/database'.')', 'danger'); - } +function migrate($config) { + $firstMigrate = false; + if ($config['db']['connection'] === 'sqlite' && !file_exists(__DIR__ . '/../' . $config['db']['dsn'])) { + touch(__DIR__ . '/../' . $config['db']['dsn']); + $firstMigrate = true; + } - if (!is_writable(__DIR__.'/../resources/sessions')) { - $session->alert('The sessions folder is not writable ('.__DIR__.'/../resources/sessions'.')', 'danger'); - } + try { + DB::doQuery('SELECT 1 FROM `migrations` LIMIT 1'); + } catch (PDOException $exception) { + $firstMigrate = true; + } - $installed = file_exists(__DIR__.'/../config.php'); + if ($firstMigrate) { + DB::raw()->exec(file_get_contents(__DIR__ . '/../resources/schemas/migrations.sql')); + } - return $view->render($response, 'install.twig', [ - 'installed' => $installed, - ]); -})->setName('install'); + $files = glob(__DIR__ . '/../resources/schemas/' . DB::driver() . '/*.sql'); -$app->post('/', function (Request $request, Response $response, Filesystem $storage, Session $session, DB $db) use (&$config) { + $names = array_map(function ($path) { + return basename($path); + }, $files); - // Check if there is a previous installation, if not, setup the config file - $installed = true; + $in = str_repeat('?, ', count($names) - 1) . '?'; - // disable debug in production - unset($config['debug']); - if (!file_exists(__DIR__.'/../config.php')) { - $installed = false; + $inMigrationsTable = DB::doQuery("SELECT * FROM `migrations` WHERE `name` IN ($in)", $names)->fetchAll(); - // config file setup - $config['base_url'] = param($request, 'base_url'); - $config['storage']['driver'] = param($request, 'storage_driver'); - $config['db']['connection'] = param($request, 'connection'); - $config['db']['dsn'] = param($request, 'dsn'); - $config['db']['username'] = param($request, 'db_user'); - $config['db']['password'] = param($request, 'db_password'); - // setup storage configuration - switch ($config['storage']['driver']) { - case 's3': - $config['storage']['key'] = param($request, 'storage_key'); - $config['storage']['secret'] = param($request, 'storage_secret'); - $config['storage']['region'] = param($request, 'storage_region'); - $config['storage']['bucket'] = param($request, 'storage_bucket'); - $config['storage']['path'] = param($request, 'storage_path'); - break; - case 'dropbox': - $config['storage']['token'] = param($request, 'storage_token'); - break; - case 'ftp': - if (!extension_loaded('ftp')) { - $session->alert('The "ftp" extension is not loaded.', 'danger'); + foreach ($files as $file) { - return redirect($response, urlFor('/')); - } - $config['storage']['host'] = param($request, 'storage_host'); - $config['storage']['username'] = param($request, 'storage_username'); - $config['storage']['password'] = param($request, 'storage_password'); - $config['storage']['port'] = param($request, 'storage_port'); - $config['storage']['path'] = param($request, 'storage_path'); - $config['storage']['passive'] = param($request, 'storage_passive') === '1'; - $config['storage']['ssl'] = param($request, 'storage_ssl') === '1'; - break; - case 'google-cloud': - $config['storage']['project_id'] = param($request, 'storage_project_id'); - $config['storage']['key_path'] = param($request, 'storage_key_path'); - $config['storage']['bucket'] = param($request, 'storage_bucket'); - break; - case 'local': - default: - $config['storage']['path'] = param($request, 'storage_path'); - break; - } - } + $continue = false; + $exists = false; - // check if the storage is valid - $storageTestFile = 'storage_test.xbackbone.txt'; + foreach ($inMigrationsTable as $migration) { + if (basename($file) === $migration->name && $migration->migrated) { + $continue = true; + break; + } else if (basename($file) === $migration->name && !$migration->migrated) { + $exists = true; + break; + } + } + if ($continue) continue; - try { - try { - $success = $storage->write($storageTestFile, 'XBACKBONE_TEST_FILE'); - } catch (FileExistsException $fileExistsException) { - $success = $storage->update($storageTestFile, 'XBACKBONE_TEST_FILE'); - } + $sql = file_get_contents($file); + try { + DB::raw()->exec($sql); + if (!$exists) { + DB::doQuery('INSERT INTO `migrations` VALUES (?,?)', [basename($file), 1]); + } else { + DB::doQuery('UPDATE `migrations` SET `migrated`=? WHERE `name`=?', [1, basename($file)]); + } + } catch (PDOException $exception) { + if (!$exists) { + DB::doQuery('INSERT INTO `migrations` VALUES (?,?)', [basename($file), 0]); + } + throw $exception; + } + } +} - if (!$success) { - throw new Exception('The storage is not writable.'); - } - $storage->readAndDelete($storageTestFile); - } catch (Exception $e) { - $session->alert("Storage setup error: {$e->getMessage()} [{$e->getCode()}]", 'danger'); +$app = new App($container); - return redirect($response, urlFor('/install')); - } +$app->get('/', function (Request $request, Response $response) { - // if from older installations with no support of other than local driver - // update the config - if ($installed && isset($config['storage_dir'])) { - $config['storage']['driver'] = 'local'; - $config['storage']['path'] = $config['storage_dir']; - unset($config['storage_dir']); - } + if (!extension_loaded('gd')) { + $this->session->alert('The required "gd" extension is not loaded.', 'danger'); + } - // Build the dns string and run the migrations - try { - $firstMigrate = false; - if ($config['db']['connection'] === 'sqlite' && !file_exists(__DIR__.'/../'.$config['db']['dsn'])) { - touch(__DIR__.'/../'.$config['db']['dsn']); - $firstMigrate = true; - } + if (!extension_loaded('intl')) { + $this->session->alert('The required "intl" extension is not loaded.', 'danger'); + } - $migrator = new Migrator($db, __DIR__.'/../resources/schemas', $firstMigrate); - $migrator->migrate(); - $migrator->reSyncQuotas($storage); - } catch (PDOException $e) { - $session->alert("Cannot connect to the database: {$e->getMessage()} [{$e->getCode()}]", 'danger'); + if (!extension_loaded('json')) { + $this->session->alert('The required "json" extension is not loaded.', 'danger'); + } - return redirect($response, urlFor('/install')); - } + if (!is_writable(__DIR__ . '/../resources/cache')) { + $this->session->alert('The cache folder is not writable (' . __DIR__ . '/../resources/cache' . ')', 'danger'); + } - // if not installed, create the default admin account - if (!$installed) { - $db->query("INSERT INTO `users` (`email`, `username`, `password`, `is_admin`, `user_code`) VALUES (?, 'admin', ?, 1, ?)", [param($request, 'email'), password_hash(param($request, 'password'), PASSWORD_DEFAULT), humanRandomString(5)]); - } + if (!is_writable(__DIR__ . '/../resources/database')) { + $this->session->alert('The database folder is not writable (' . __DIR__ . '/../resources/database' . ')', 'danger'); + } - // re-apply the previous theme if is present - $css = $db->query('SELECT `value` FROM `settings` WHERE `key` = \'css\'')->fetch()->value; - if ($css) { - $content = file_get_contents($css); - if ($content !== false) { - file_put_contents(BASE_DIR.'static/bootstrap/css/bootstrap.min.css', $content); - } - } + if (!is_writable(__DIR__ . '/../resources/sessions')) { + $this->session->alert('The sessions folder is not writable (' . __DIR__ . '/../resources/sessions' . ')', 'danger'); + } - // if is upgrading and existing installation, put it out maintenance - if ($installed) { - unset($config['maintenance']); + $installed = file_exists(__DIR__ . '/../config.php'); - // remove old config from old versions - unset($config['lang']); - unset($config['displayErrorDetails']); - } - - // Finally write the config - $ret = file_put_contents(__DIR__.'/../config.php', 'alert('The config folder is not writable ('.__DIR__.'/../config.php'.')', 'danger'); - - return redirect($response, '/install'); - } - - // post install cleanup - cleanDirectory(__DIR__.'/../resources/cache'); - cleanDirectory(__DIR__.'/../resources/sessions'); - - if (!isset($config['debug']) || !$config['debug']) { - removeDirectory(__DIR__.'/../install'); - } - - // Installed successfully, destroy the installer session - $session->destroy(); - - return redirect($response, urlFor('/?afterInstall=true')); + return $this->view->render($response, 'install.twig', [ + 'installed' => $installed, + ]); }); -$app->run(); +$app->post('/', function (Request $request, Response $response) use (&$config) { + + // Check if there is a previous installation, if not, setup the config file + $installed = true; + if (!file_exists(__DIR__ . '/../config.php')) { + $installed = false; + + // config file setup + $config['base_url'] = $request->getParam('base_url'); + $config['storage']['driver'] = $request->getParam('storage_driver'); + unset($config['displayErrorDetails']); + $config['db']['connection'] = $request->getParam('connection'); + $config['db']['dsn'] = $request->getParam('dsn'); + $config['db']['username'] = $request->getParam('db_user'); + $config['db']['password'] = $request->getParam('db_password'); + + + // setup storage configuration + switch ($config['storage']['driver']) { + case 's3': + $config['storage']['key'] = $request->getParam('storage_key'); + $config['storage']['secret'] = $request->getParam('storage_secret'); + $config['storage']['region'] = $request->getParam('storage_region'); + $config['storage']['bucket'] = $request->getParam('storage_bucket'); + $config['storage']['path'] = $request->getParam('storage_path'); + break; + case 'dropbox': + $config['storage']['token'] = $request->getParam('storage_token'); + break; + case 'ftp': + $config['storage']['host'] = $request->getParam('storage_host'); + $config['storage']['username'] = $request->getParam('storage_username'); + $config['storage']['password'] = $request->getParam('storage_password'); + $config['storage']['port'] = $request->getParam('storage_port'); + $config['storage']['path'] = $request->getParam('storage_path'); + $config['storage']['passive'] = $request->getParam('storage_passive') === '1'; + $config['storage']['ssl'] = $request->getParam('storage_ssl') === '1'; + break; + case 'google-cloud': + $config['storage']['project_id'] = $request->getParam('storage_project_id'); + $config['storage']['key_path'] = $request->getParam('storage_key_path'); + $config['storage']['bucket'] = $request->getParam('storage_bucket'); + break; + case 'local': + default: + $config['storage']['path'] = $request->getParam('storage_path'); + break; + } + + // check if the storage is valid + $storageTestFile = 'storage_test.xbackbone.txt'; + try { + try { + $success = $this->storage->write($storageTestFile, 'XBACKBONE_TEST_FILE'); + } catch (FileExistsException $fileExistsException) { + $success = $this->storage->update($storageTestFile, 'XBACKBONE_TEST_FILE'); + } + + if (!$success) { + throw new Exception('The storage is not writable.'); + } + $this->storage->readAndDelete($storageTestFile); + } catch (Exception $e) { + $this->session->alert("Storage setup error: {$e->getMessage()} [{$e->getCode()}]", 'danger'); + return redirect($response, '/install'); + } + + $ret = file_put_contents(__DIR__ . '/../config.php', 'session->alert('The config folder is not writable (' . __DIR__ . '/../config.php' . ')', 'danger'); + return redirect($response, '/install'); + } + } + + // if from older installations with no support of other than local driver + // update the config + if ($installed && isset($config['storage_dir'])) { + $config['storage']['driver'] = 'local'; + $config['storage']['path'] = $config['storage_dir']; + unset($config['storage_dir']); + } + + + // Build the dns string and run the migrations + try { + + $dsn = $config['db']['connection'] === 'sqlite' ? __DIR__ . '/../' . $config['db']['dsn'] : $config['db']['dsn']; + DB::setDsn($config['db']['connection'] . ':' . $dsn, $config['db']['username'], $config['db']['password']); + + migrate($config); + } catch (PDOException $e) { + $this->session->alert("Cannot connect to the database: {$e->getMessage()} [{$e->getCode()}]", 'danger'); + return redirect($response, '/install'); + } + + // if not installed, create the default admin account + if (!$installed) { + DB::doQuery("INSERT INTO `users` (`email`, `username`, `password`, `is_admin`, `user_code`) VALUES (?, 'admin', ?, 1, ?)", [$request->getParam('email'), password_hash($request->getParam('password'), PASSWORD_DEFAULT), humanRandomString(5)]); + } + + // post install cleanup + cleanDirectory(__DIR__ . '/../resources/cache'); + cleanDirectory(__DIR__ . '/../resources/sessions'); + + removeDirectory(__DIR__ . '/../install'); + + // if is upgrading and existing installation, put it out maintenance + if ($installed) { + unset($config['maintenance']); + + $ret = file_put_contents(__DIR__ . '/../config.php', 'session->alert('The config folder is not writable (' . __DIR__ . '/../config.php' . ')', 'danger'); + return redirect($response, '/install'); + } + } + + // Installed successfully, destroy the installer session + session_destroy(); + return $response->withRedirect("{$config['base_url']}/?afterInstall=true"); +}); + +$app->run(); \ No newline at end of file diff --git a/install/templates/install.twig b/install/templates/install.twig index 673daf5..6d452aa 100644 --- a/install/templates/install.twig +++ b/install/templates/install.twig @@ -14,7 +14,7 @@ - +
{% include 'comp/alert.twig' %} @@ -24,7 +24,7 @@
Install XBackBone
-
+ {% if not installed %}
@@ -184,7 +184,7 @@
-
@@ -193,7 +193,7 @@ {% else %}
-
@@ -205,7 +205,7 @@
-
-
- -
-
{{ lang('system_settings') }}
+
+
+
{{ lang('theme') }}
-
+
- -
- -
-
-
- -
+
- -
- - {{ lang('default_lang_behavior') }} +
+
-
- -
- - {{ lang('custom_head_html_hint') }} -
-
-
-
- -
- -
-
-
- -
- - 512M, 2G, 1T, ... -
-
-
-
- -
- -
{{ lang('only_recaptcha_v3') }} -
-
-
- -
- -
-
-
- -
- -
-
-
-
+
+
{{ lang('enforce_language') }}
+
+
+
+
+ + [{{ lang('translated_strings') }} / {{ lang('total_strings') }}] {{ lang('lang_name') }}. {{ lang('default_lang_behavior') }} +
+
+
+
+ +
+
+
+
+
+
{{ lang('updates') }} v{{ PLATFORM_VERSION }}
@@ -150,7 +118,7 @@
-
+
@@ -158,12 +126,10 @@
-
-
-
{{ lang('php_info') }}
+
+
+
{{ lang('system_info') }}
- PHP Version: {{ php_version }}
- Max memory usage: {{ max_memory }}
Max upload size:
  • post_max_size: {{ post_max_size }}
  • @@ -171,38 +137,12 @@
-
+ -
-
{{ lang('donation') }}
-
-

{{ lang('donate_text') }}

-
- - -
-
-
-
-
-
- diff --git a/resources/templates/errors/400.twig b/resources/templates/errors/400.twig deleted file mode 100644 index 8ed0c96..0000000 --- a/resources/templates/errors/400.twig +++ /dev/null @@ -1,14 +0,0 @@ -{% extends 'base.twig' %} - -{% block title %}Forbidden{% endblock %} - -{% block content %} -
-
-

400 Bad Request

-

The server cannot or will not process the request due to an apparent client error.

-
-
-{% endblock %} - -{% block footer %}{% endblock %} \ No newline at end of file diff --git a/resources/templates/errors/500.twig b/resources/templates/errors/500.twig index 8464baa..7bbd974 100644 --- a/resources/templates/errors/500.twig +++ b/resources/templates/errors/500.twig @@ -12,7 +12,7 @@
- {% if exception is not null %} + {% if config.displayErrorDetails %}
diff --git a/resources/templates/scripts/xbackbone_uploader.sh.twig b/resources/templates/scripts/xbackbone_uploader.sh.twig index ef9f225..4884ba4 100755 --- a/resources/templates/scripts/xbackbone_uploader.sh.twig +++ b/resources/templates/scripts/xbackbone_uploader.sh.twig @@ -33,58 +33,40 @@ upload() { if jq -e . >/dev/null 2>&1 <<<"$RESPONSE"; then # Response is JSON - echo "Upload Completed."; + echo; else - if [ "${DESKTOP_SESSION}" != "" ]; then - notify-send "Error: Uploading returned unexpected response" "Response: ${RESPONSE}"; - else - echo "Error: Uploading returned unexpected response: ${RESPONSE}" - fi + notify-send "Error: Uploading returned unexpected response" "Response: ${RESPONSE}"; exit 1; fi if [[ "$(echo "${RESPONSE}" | jq -r '.message')" == "OK." ]]; then URL="$(echo "${RESPONSE}" | jq -r '.url')"; - if [ "${DESKTOP_SESSION}" != "" ]; then - echo "${URL}" | xclip -selection c; - notify-send "Upload completed!" "${URL}"; - else - echo "${URL}"; - fi + echo "${URL}" | xclip -selection c; + notify-send "Upload completed!" "${URL}"; else - if [ "${DESKTOP_SESSION}" != "" ]; then - notify-send "Error!" "$(echo "${RESPONSE}" | jq -r '.message')"; - else - echo "Error! $(echo "${RESPONSE}" | jq -r '.message')"; - fi + notify-send "Error!" "$(echo "${RESPONSE}" | jq -r '.message')"; fi } check() { - ERRORS=0; - - if [ ! -x "$(command -v jq)" ]; then - echo "jq command not found."; - ERRORS=1; + if [ ! -x "$(command -v xclip)" ]; then + echo "xclip command not found." + exit; + fi + + if [ ! -x "$(command -v jq)" ]; then + echo "jq command not found." + exit; fi if [ ! -x "$(command -v curl)" ]; then - echo "curl command not found."; - ERRORS=1; + echo "curl command not found." + exit; fi - if [ ! -x "$(command -v xclip)" ] && [ "${DESKTOP_SESSION}" != "" ]; then - echo "xclip command not found."; - ERRORS=1; - fi - - if [ ! -x "$(command -v notify-send)" ] && [ "${DESKTOP_SESSION}" != "" ]; then - echo "notify-send command not found."; - ERRORS=1; - fi - - if [ "${ERRORS}" -eq 1 ]; then - exit 1; + if [ ! -x "$(command -v notify-send)" ]; then + echo "notify-send command not found." + exit; fi } @@ -98,9 +80,5 @@ fi if [ -f "${1}" ]; then upload "${1}"; else - if [ "${DESKTOP_SESSION}" != "" ]; then - notify-send "Error!" "File specified not exists."; - else - echo "Error! File specified not exists."; - fi + notify-send "Error!" "File specified not exists."; fi diff --git a/resources/templates/upload/public.twig b/resources/templates/upload/public.twig index 0e21d56..53a1a3f 100644 --- a/resources/templates/upload/public.twig +++ b/resources/templates/upload/public.twig @@ -3,8 +3,8 @@ {% block title %}{{ media.filename }}{% endblock %} {% block head %} - {% if type == 'image' %} - + {% if type in ['image', 'text'] %} + {% endif %} {% endblock %} @@ -17,13 +17,10 @@
@@ -33,12 +30,12 @@
{% if delete_token is not null %} -
+

{{ lang('public.delete_text') }}

- {{ lang('no') }} + {{ lang('no') }}
@@ -46,7 +43,7 @@ {% if type is same as ('image') %}
- {{ media.filename }} + {{ media.filename }}
@@ -66,26 +63,19 @@
{% elseif type is same as ('audio') %} -
-
- {% include 'comp/modal_delete.twig' %} {% endblock %} \ No newline at end of file diff --git a/resources/templates/upload/web.twig b/resources/templates/upload/web.twig deleted file mode 100644 index 4e248a2..0000000 --- a/resources/templates/upload/web.twig +++ /dev/null @@ -1,36 +0,0 @@ -{% extends 'base.twig' %} - -{% block title %}{{ lang('upload') }}{% endblock %} - -{% block content %} - {% include 'comp/navbar.twig' %} -
- {% include 'comp/alert.twig' %} -
- {{ lang('upload_max_file_size', [max_file_size]) }} -
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
- -
-
-
-
-
-
-
-{% endblock %} \ No newline at end of file diff --git a/resources/templates/user/create.twig b/resources/templates/user/create.twig index 3a0e25a..e6fbeaf 100644 --- a/resources/templates/user/create.twig +++ b/resources/templates/user/create.twig @@ -7,72 +7,53 @@
{% include 'comp/alert.twig' %}
-
+
{{ lang('user.create') }}
- -
+ +
- -
+ +
- -
- - {{ lang('user_create_password') }} + +
+
- -
- +
+
+
+ + +
- -
- -
-
- {% if quota_enabled == 'on' %} -
- -
- - 512M, 2G, 1T, ... (-1=∞) -
-
- {% endif %} -
- -
- -
-
-
- -
- -
-
-
-
- -
- +
+
+
+ + +
-
+
diff --git a/resources/templates/user/edit.twig b/resources/templates/user/edit.twig index d25df8c..b0be519 100644 --- a/resources/templates/user/edit.twig +++ b/resources/templates/user/edit.twig @@ -7,7 +7,7 @@
{% include 'comp/alert.twig' %}
-
+
{% if not profile %}
{{ lang('user.edit') }}
@@ -15,14 +15,14 @@
- -
+ +
- -
+ +
{% if profile %} {% else %} @@ -31,14 +31,14 @@
- -
- + +
+
- -
+ +
@@ -49,77 +49,40 @@
- -
- -
-
-
- -
- -
-
-
- -
+ +
-
- - -
{% if not profile %} -
{{ lang('danger_zone') }}
-
- {% if quota_enabled == 'on' %} -
- -
- - 512M, 2G, 1T, ... (-1=∞) -
-
- {% endif %}
- -
- +
+
+
+ + +
- -
- -
-
- {% if config.ldap.enabled %} -
- -
- -
-
- {% endif %} -
- -
-
- {{ lang('clear_account') }} +
+
+
+ +
{% endif %}
-
+
diff --git a/resources/templates/user/index.twig b/resources/templates/user/index.twig index 180bc6b..cccff9b 100644 --- a/resources/templates/user/index.twig +++ b/resources/templates/user/index.twig @@ -19,11 +19,7 @@ Email {{ lang('username') }} {{ lang('user_code') }} - {{ lang('used_space') }} {{ lang('active') }} - {% if config.ldap.enabled %} - {{ lang('LDAP') }} - {% endif %} {{ lang('admin') }} {{ lang('reg_date') }} @@ -38,24 +34,14 @@
{{ user.user_code|default(lang('none')) }}
- {{ humanFileSize(user.current_disk_quota) }}{% if quota_enabled == 'on' %}/{{ user.max_disk_quota > 0 ? humanFileSize(user.max_disk_quota) : '∞' }}{% endif %} - + {% if user.active %} {% else %} {% endif %} - {% if config.ldap.enabled %} - - {% if user.ldap %} - - {% else %} - - {% endif %} - - {% endif %} - + {% if user.is_admin %} {% else %} @@ -68,7 +54,7 @@
- +
@@ -82,5 +68,23 @@
- {% include 'comp/modal_delete.twig' %} + {% endblock %} \ No newline at end of file diff --git a/src/css/app.css b/src/css/app.css index a681a08..0c2064f 100644 --- a/src/css/app.css +++ b/src/css/app.css @@ -28,23 +28,22 @@ body { z-index: 2; } -.form-signin input.first { +.form-signin input[type="text"] { margin-bottom: -1px; border-bottom-right-radius: 0; border-bottom-left-radius: 0; } -.form-signin input.middle { - margin-bottom: -1px; - border-radius: 0; -} - -.form-signin input.last { - margin-bottom: .50rem; +.form-signin input[type="password"] { + margin-bottom: 10px; border-top-left-radius: 0; border-top-right-radius: 0; } +.admin-img { + max-height: 25px; +} + .user-title { white-space: nowrap; overflow: hidden; @@ -52,6 +51,28 @@ body { max-width: 200px; } +.user-img-overlay { + opacity: 0; + transition: opacity .5s ease-in-out; + -moz-transition: opacity .5s ease-in-out; + -webkit-transition: opacity .5s ease-in-out; +} + +.card-img-overlay:hover .user-img-overlay { + opacity: 1; + transition: opacity .2s ease-in-out; + -moz-transition: opacity .2s ease-in-out; + -webkit-transition: opacity .2s ease-in-out; +} + +.btn-outlink { + padding: 25% 0; +} + +.btn-outlink:hover, .btn-outlink:active, .btn-outlink:focus, .btn-outlink:not(:disabled):not(.disabled):focus { + background-color: transparent; +} + .footer { position: absolute; bottom: 0; @@ -61,16 +82,12 @@ body { text-align: right; } -@media (max-width: 576px) { +@media (min-width: 576px) { .media-player { width: 100%; margin-right: auto; margin-left: auto; } - - .media-audio { - margin-top: 40vh; - } } @media (min-width: 768px) { @@ -79,125 +96,4 @@ body { margin-right: auto; margin-left: auto; } - - .media-audio { - margin-top: 40vh; - } -} - -.dropzone { - min-height: 300px; - border: 2px dashed rgba(0, 0, 0, 0.3); - background: none; - padding: 20px 20px; -} - -.dropzone .dz-image { - border-radius: .25rem !important; -} - -.system-tile { - font-size: 2.5rem; -} - -.pdf-viewer { - width: 100%; - height: 85vh -} - -.grecaptcha-badge { - top: 5px !important; -} - -.text-maxlen { - display: inline-block; - max-width: 330px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.text-maxlen:hover { - overflow: auto; - text-overflow: unset; -} - -.image-card { - position: relative; -} - -.image-card:hover .overlay { - opacity: 1; - transition: opacity .1s ease-in-out; - -moz-transition: opacity .1s ease-in-out; - -webkit-transition: opacity .1s ease-in-out; -} - -.overlay { - position: absolute; - width: 100%; - height: 100%; - opacity: 0; - transition: opacity .5s ease-in-out; - -moz-transition: opacity .5s ease-in-out; - -webkit-transition: opacity .5s ease-in-out; -} - -.overlay-rows { - display: flex; - flex-direction: column; - align-items: stretch; - height: 100%; -} - -.overlay-rows-top, -.overlay-rows-bottom { - flex: 0 1 auto; -} - -.overlay-rows-center { - flex: 1 1 auto; - display: flex; - align-items: center; - justify-content: center; -} - -.overlay-rows-top { - display: flex; - width: 100%; -} - -.overlay-rows-top > div { - flex: 0 1 100%; -} - -.content-image { - background-size: cover; - background-position: center; - background-repeat: no-repeat; - height: 267px; -} - -.text-shadow-link { - text-shadow: 3px 3px 2px black; -} - -.pt-2d5 { - padding-top: .8rem -} - -.bg-light { - border: 0 !important; -} - -.form-control-verysm { - height: calc(1em + .2rem + 2px); - font-size: .8rem; -} - -.tag-item { - cursor: pointer; - max-width: 145px; - text-overflow: ellipsis; - overflow: hidden; } \ No newline at end of file diff --git a/src/images/android-chrome-192x192.png b/src/images/android-chrome-192x192.png deleted file mode 100644 index ea3c265..0000000 Binary files a/src/images/android-chrome-192x192.png and /dev/null differ diff --git a/src/images/android-chrome-512x512.png b/src/images/android-chrome-512x512.png deleted file mode 100644 index 2f7da9a..0000000 Binary files a/src/images/android-chrome-512x512.png and /dev/null differ diff --git a/src/images/apple-touch-icon.png b/src/images/apple-touch-icon.png deleted file mode 100644 index 89174f9..0000000 Binary files a/src/images/apple-touch-icon.png and /dev/null differ diff --git a/src/images/browserconfig.xml b/src/images/browserconfig.xml deleted file mode 100644 index 31b5050..0000000 --- a/src/images/browserconfig.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - #603cba - - - diff --git a/src/images/favicon-16x16.png b/src/images/favicon-16x16.png deleted file mode 100644 index 16d9c4b..0000000 Binary files a/src/images/favicon-16x16.png and /dev/null differ diff --git a/src/images/favicon-32x32.png b/src/images/favicon-32x32.png deleted file mode 100644 index d433f71..0000000 Binary files a/src/images/favicon-32x32.png and /dev/null differ diff --git a/src/images/favicon.ico b/src/images/favicon.ico deleted file mode 100644 index 12da661..0000000 Binary files a/src/images/favicon.ico and /dev/null differ diff --git a/src/images/mstile-150x150.png b/src/images/mstile-150x150.png deleted file mode 100644 index 2e9f6d9..0000000 Binary files a/src/images/mstile-150x150.png and /dev/null differ diff --git a/src/images/safari-pinned-tab.svg b/src/images/safari-pinned-tab.svg deleted file mode 100644 index e4ae4db..0000000 --- a/src/images/safari-pinned-tab.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/images/site.webmanifest b/src/images/site.webmanifest deleted file mode 100644 index e209d89..0000000 --- a/src/images/site.webmanifest +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "", - "short_name": "", - "icons": [ - { - "src": "/static/images/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "/static/images/android-chrome-512x512.png", - "sizes": "512x512", - "type": "image/png" - } - ], - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" -} diff --git a/src/js/app.js b/src/js/app.js index 9814aa0..8b7b144 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -1,44 +1,26 @@ var app = { - init: function () { - Dropzone.options.uploadDropzone = { - paramName: 'upload', - maxFilesize: window.AppConfig.max_upload_size / Math.pow(1024, 2), // MB - dictDefaultMessage: window.AppConfig.lang.dropzone, - error: function (file, response) { - this.defaultOptions.error(file, response.message); - }, - totaluploadprogress: function (uploadProgress) { - var text = Math.round(uploadProgress) + '%'; - $('#uploadProgess').css({'width': text}).text(text); - }, - timeout: 0 - }; - }, run: function () { $('[data-toggle="tooltip"]').tooltip(); $('[data-toggle="popover"]').popover(); $('.user-delete').click(app.modalDelete); - $('.public-delete').click(app.modalDelete); $('.media-delete').click(app.mediaDelete); $('.publish-toggle').click(app.publishToggle); $('.refresh-token').click(app.refreshToken); $('#themes').mousedown(app.loadThemes); $('.checkForUpdatesButton').click(app.checkForUpdates); - $('.bulk-selector').contextmenu(app.bulkSelect); - $('#bulk-delete').click(app.bulkDelete); - - $('.tag-add').click(app.addTag); - $('.tag-item').contextmenu(app.removeTag); - - - $('.alert').not('.alert-permanent').fadeTo(10000, 500).slideUp(500, function () { + $('.alert').fadeTo(4000, 500).slideUp(500, function () { $('.alert').slideUp(500); }); new ClipboardJS('.btn-clipboard'); - new Plyr($('#player'), {ratio: '16:9'}); + + if ($('#player').length > 0) { + videojs('player').ready(function () { + this.volume(0.5); + }); + } $('.footer').fadeIn(600); @@ -101,12 +83,13 @@ var app = { var $themes = $('#themes'); $.get(window.AppConfig.base_url + '/system/themes', function (data) { $themes.empty(); - $.each(data, function (key, value) { + Object.keys(data).forEach(function (key) { var opt = document.createElement('option'); - opt.value = value; + opt.value = data[key]; opt.innerHTML = key; $themes.append(opt); }); + $('#themes-apply').prop('disabled', false); }); $themes.unbind('mousedown'); }, @@ -114,7 +97,7 @@ var app = { window.open($('#telegram-share-button').data('url') + $('#telegram-share-text').val(), '_blank'); }, checkForUpdates: function () { - $('#checkForUpdatesMessage').empty().html(''); + $('#checkForUpdatesMessage').empty().text('...'); $('#doUpgradeButton').prop('disabled', true); $.get(window.AppConfig.base_url + '/system/checkForUpdates?prerelease=' + $(this).data('prerelease'), function (data) { $('#checkForUpdatesMessage').empty().text(data.message); @@ -124,98 +107,7 @@ var app = { $('#doUpgradeButton').prop('disabled', true); } }); - }, - bulkSelect: function (e) { - e.preventDefault(); - $(this).toggleClass('bg-light').toggleClass('text-danger').toggleClass('bulk-selected'); - var $bulkDelete = $('#bulk-delete'); - if ($bulkDelete.hasClass('disabled')) { - $bulkDelete.removeClass('disabled'); - } - }, - bulkDelete: function () { - $('.bulk-selected').each(function (index, media) { - $.post(window.AppConfig.base_url + '/upload/' + $(media).data('id') + '/delete', function () { - $(media).fadeOut(200, function () { - $(this).remove(); - }); - }); - }); - $(this).addClass('disabled'); - }, - addTag: function (e) { - var $caller = $(this); - var $newAddTag = $caller.clone() - .click(app.addTag) - .appendTo($caller.parent()); - - var tagInput = $(document.createElement('input')) - .addClass('form-control form-control-verysm tag-input') - .attr('data-id', $caller.data('id')) - .attr('maxlength', 32) - .css('width', '90px') - .attr('onchange', 'this.value = this.value.toLowerCase();') - .keydown(function (e) { - if (e.keyCode === 13) { // enter -> save tag - app.saveTag.call($(this)); // change context - return false; - } - if (e.keyCode === 32) { // space -> save and add new tag - $newAddTag.click(); - return false; - } - }) - .focusout(app.saveTag); - - $caller.off() - .removeClass('badge-success badge-light') - .html(tagInput) - .children() - .focus(); - }, - saveTag: function () { - var tag = $(this).val(); - var mediaId = $(this).data('id'); - var $parent = $(this).parent(); - if (tag === '') { - $parent.remove(); - return false; - } - $.ajax({ - type: 'POST', - url: window.AppConfig.base_url + '/tag/add' + window.location.search, - data: {'tag': tag, 'mediaId': mediaId}, - dataType: 'json', - success: function (data) { - if (!data.limitReached) { - $parent.replaceWith( - $(document.createElement('a')) - .addClass('badge badge-pill badge-light shadow-sm tag-item mr-1') - .attr('data-id', data.tagId) - .attr('data-media', mediaId) - .attr('href', data.href) - .contextmenu(app.removeTag) - .text(tag) - ); - } else { - $parent.remove(); - } - } - }); - }, - removeTag: function (e) { - e.preventDefault(); - e.stopPropagation(); - var $tag = $(this); - - $.post(window.AppConfig.base_url + '/tag/remove', { - 'tagId': $tag.data('id'), - 'mediaId': $tag.data('media') - }, function () { - $tag.remove(); - }); } }; -app.init(); $(document).ready(app.run);