Merge pull request #172 from SergiX44/revert-170-master

Revert "Translated using Weblate (French)"
This commit is contained in:
Sergio Brighenti 2020-04-04 12:37:06 +02:00 committed by GitHub
commit 303e607f7d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
132 changed files with 4128 additions and 8700 deletions

6
.github/FUNDING.yml vendored
View file

@ -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

View file

@ -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.

BIN
.github/xbackbone.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

View file

@ -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 \<head\> 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.

View file

@ -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'}
],
},

172
README.md
View file

@ -1,48 +1,168 @@
# [![Weblate](https://hosted.weblate.org/widgets/xbackbone/-/xbackbone/svg-badge.svg)](https://hosted.weblate.org/engage/xbackbone/?utm_source=widget) <a href="https://codeclimate.com/github/SergiX44/XBackBone/maintainability"><img src="https://api.codeclimate.com/v1/badges/bf8ee4a8df9c9f0dfa08/maintainability" /></a> [![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&currency_code=EUR&source=url)
<p align="center">
<img src=".github/xbackbone.png" width="350px">
</p>
# 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/).
<a href="https://hosted.weblate.org/engage/xbackbone/?utm_source=widget">
<img src="https://hosted.weblate.org/widgets/xbackbone/-/xbackbone/multi-auto.svg" alt="Stato traduzione" />
</a>
## 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 <THEME-NAME>`) 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/<username>`).
+ 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 <a href="https://choosealicense.com/licenses/agpl-3.0/">GNU Affero General Public License v3.0</a>, 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/)

View file

@ -2,53 +2,52 @@
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
*
* @throws FileNotFoundException
*/
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),
$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;
$medias = $this->database->query('SELECT `uploads`.`storage_path` FROM `uploads`')->fetchAll();
$totalSize = 0;
$filesystem = $this->storage;
foreach ($medias as $media) {
$totalSize += $filesystem->getSize($media->storage_path);
}
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(),
'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 Request $request
* @param Response $response
*
* @return Response
*/
public function deleteOrphanFiles(Response $response): Response
public function deleteOrphanFiles(Request $request, Response $response): Response
{
$orphans = $this->database->query('SELECT * FROM `uploads` WHERE `user_id` IS NULL')->fetchAll();
@ -67,37 +66,28 @@ class AdminController extends Controller
$this->session->alert(lang('deleted_orphans', [$deleted]));
return redirect($response, route('system'));
}
/**
* @param Response $response
*
* @return Response
*/
public function getThemes(Response $response): Response
{
$apiJson = json_decode(file_get_contents('https://bootswatch.com/api/4.json'));
$out = [];
$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;
}
return json($response, $out);
return redirect($response, 'system');
}
/**
* @param Request $request
* @param Response $response
* @return Response
*/
public function recalculateUserQuota(Response $response): Response
public function applyLang(Request $request, Response $response): Response
{
$migrator = new Migrator($this->database, null);
$migrator->reSyncQuotas($this->storage);
$this->session->alert(lang('quota_recalculated'));
return redirect($response, route('system'));
$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', '<?php' . PHP_EOL . 'return ' . var_export($config, true) . ';');
$this->session->alert(lang('lang_set', [$request->getParam('lang')]));
return redirect($response, 'system');
}
}

View file

@ -1,180 +0,0 @@
<?php
namespace App\Controllers\Auth;
use App\Controllers\Controller;
use App\Database\Queries\UserQuery;
use App\Web\ValidationChecker;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
class LoginController extends Controller
{
/**
* @param Response $response
*
* @return Response
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*
* @throws \Twig\Error\LoaderError
*/
public function show(Response $response): Response
{
if ($this->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;
}
}

View file

@ -1,135 +0,0 @@
<?php
namespace App\Controllers\Auth;
use App\Controllers\Controller;
use App\Web\Mail;
use App\Web\ValidationChecker;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Exception\HttpNotFoundException;
class PasswordRecoveryController extends Controller
{
/**
* @param Request $request
* @param Response $response
* @return Response
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*/
public function recover(Request $request, Response $response): Response
{
return view()->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'));
}
}

View file

@ -1,129 +0,0 @@
<?php
namespace App\Controllers\Auth;
use App\Controllers\Controller;
use App\Database\Queries\UserQuery;
use App\Web\Mail;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Exception\HttpNotFoundException;
class RegisterController extends Controller
{
/**
* @param Request $request
* @param Response $response
* @return Response
* @throws HttpNotFoundException
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*/
public function registerForm(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);
}
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'));
}
}

View file

@ -1,75 +0,0 @@
<?php
namespace App\Controllers;
use App\Database\Queries\UserQuery;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
class ClientController extends Controller
{
/**
* @param Request $request
* @param Response $response
* @param int $id
*
* @return Response
*/
public function getShareXConfig(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'));
}
$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,
]
);
}
}

View file

@ -3,30 +3,25 @@
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;
@ -37,155 +32,36 @@ abstract class Controller
/**
* @param $name
*
* @return mixed|null
* @throws NotFoundException
*
* @throws DependencyException
* @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
* @param $id
* @return int
*/
protected function getSetting($key, $default = null)
protected function getUsedSpaceByUser($id): int
{
return $this->database->query('SELECT `value` FROM `settings` WHERE `key` = '.$this->database->getPdo()->quote($key))->fetch()->value ?? $default;
}
$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;
$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]);
}
}
/**
* @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);
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;
}
}

View file

@ -3,43 +3,40 @@
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')) {
if ($request->getParam('afterInstall') !== null && !is_dir(BASE_DIR . 'install')) {
$this->session->alert(lang('installed'), 'success');
}
return redirect($response, route('home'));
return redirect($response, 'home');
}
/**
* @param Request $request
* @param Response $response
* @param int|null $page
*
* @param $args
* @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
public function home(Request $request, Response $response, $args): Response
{
$page = isset($args['page']) ? (int)$args['page'] : 0;
$page = max(0, --$page);
switch (param($request, 'sort', 'time')) {
$query = new MediaQuery($this->database, $this->session->get('admin', false), $this->storage);
switch ($request->getParam('sort', 'time')) {
case 'size':
$order = MediaQuery::ORDER_SIZE;
break;
@ -52,42 +49,32 @@ class DashboardController extends Controller
break;
}
/** @var MediaQuery $query */
$query = make(MediaQuery::class, ['isAdmin' => (bool) $this->session->get('admin', false)])
->orderBy($order, param($request, 'order', 'DESC'))
$query->orderBy($order, $request->getParam('order', 'DESC'))
->withUserId($this->session->get('user_id'))
->search(param($request, 'search', null))
->filterByTag(param($request, 'tag'))
->search($request->getParam('search', null))
->run($page);
$tags = make(TagQuery::class, [
'isAdmin' => (bool) $this->session->get('admin', false),
'userId' => $this->session->get('user_id')
])->all();
return view()->render(
return $this->view->render(
$response,
($this->session->get('admin', false) && $this->session->get('gallery_view', true)) ? 'dashboard/list.twig' : 'dashboard/grid.twig',
($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,
'copy_raw' => $this->session->get('copy_raw', false),
'tags' => $tags,
]
);
}
/**
* @param Request $request
* @param Response $response
*
* @param $args
* @return Response
*/
public function switchView(Response $response): Response
public function switchView(Request $request, Response $response, $args): Response
{
$this->session->set('gallery_view', !$this->session->get('gallery_view', true));
return redirect($response, route('home'));
return redirect($response, 'home');
}
}

View file

@ -1,49 +0,0 @@
<?php
namespace App\Controllers;
use App\Database\Queries\UserQuery;
use League\Flysystem\FileNotFoundException;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use ZipStream\Option\Archive;
use ZipStream\ZipStream;
class ExportController extends Controller
{
/**
* @param Request $request
* @param Response $response
* @param int|null $id
* @return Response
* @throws \ZipStream\Exception\OverflowException
*/
public function downloadData(Request $request, Response $response, int $id): Response
{
$user = make(UserQuery::class)->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);
}
}

View file

@ -0,0 +1,79 @@
<?php
namespace App\Controllers;
use Slim\Http\Request;
use Slim\Http\Response;
class LoginController extends Controller
{
/**
* @param Request $request
* @param Response $response
* @return Response
*/
public function show(Request $request, Response $response): Response
{
if ($this->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');
}
}

View file

@ -1,418 +0,0 @@
<?php
namespace App\Controllers;
use App\Database\Queries\UserQuery;
use GuzzleHttp\Psr7\Stream;
use Intervention\Image\Constraint;
use Intervention\Image\ImageManagerStatic as Image;
use League\Flysystem\FileNotFoundException;
use League\Flysystem\Filesystem;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Exception\HttpBadRequestException;
use Slim\Exception\HttpNotFoundException;
use Slim\Exception\HttpUnauthorizedException;
class MediaController extends Controller
{
/**
* @param Request $request
* @param Response $response
* @param string $userCode
* @param string $mediaCode
* @param string|null $token
*
* @return Response
* @throws HttpNotFoundException
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
* @throws FileNotFoundException
*
*/
public function show(Request $request, Response $response, string $userCode, string $mediaCode, string $token = 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);
}
$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);
}
}

View file

@ -1,84 +0,0 @@
<?php
namespace App\Controllers;
use App\Database\Queries\UserQuery;
use App\Web\ValidationChecker;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
class ProfileController extends Controller
{
/**
* @param Request $request
* @param Response $response
*
* @return Response
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*/
public function profile(Request $request, Response $response): Response
{
$user = make(UserQuery::class)->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'));
}
}

View file

@ -1,99 +0,0 @@
<?php
namespace App\Controllers;
use App\Database\Queries\UserQuery;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
class SettingController extends Controller
{
/**
* @param Request $request
* @param Response $response
*
* @return Response
*/
public function saveSettings(Request $request, Response $response): Response
{
if (!preg_match('/[0-9]+[K|M|G|T]/i', param($request, 'default_user_quota', '1G'))) {
$this->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);
}
}
}

View file

@ -1,78 +0,0 @@
<?php
namespace App\Controllers;
use App\Database\Queries\TagQuery;
use App\Web\ValidationChecker;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Exception\HttpBadRequestException;
use Slim\Exception\HttpNotFoundException;
class TagController extends Controller
{
const PER_MEDIA_LIMIT = 10;
/**
* @param Request $request
* @param Response $response
* @return Response
* @throws HttpBadRequestException
*/
public function addTag(Request $request, Response $response): Response
{
$validator = $this->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'),
]);
}
}

View file

@ -0,0 +1,41 @@
<?php
namespace App\Controllers;
use Slim\Http\Request;
use Slim\Http\Response;
class ThemeController extends Controller
{
/**
* @param Request $request
* @param Response $response
* @return Response
*/
public function getThemes(Request $request, Response $response): Response
{
$apiJson = json_decode(file_get_contents('https://bootswatch.com/api/4.json'));
$out = [];
$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;
}
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');
}
}

View file

@ -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 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,15 +85,12 @@ 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
*
* @return Response
*/
public function checkForUpdates(Request $request, Response $response): Response
@ -109,7 +101,7 @@ class UpgradeController extends Controller
'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);
}
}

View file

@ -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 = [
/**
* @param Request $request
* @param Response $response
* @return Response
* @throws FileExistsException
*/
public function upload(Request $request, Response $response): Response
{
$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')));
return view()->render($response, 'upload/web.twig', [
'max_file_size' => humanFileSize($maxFileSize),
]);
if ($this->settings['maintenance']) {
$json['message'] = 'Endpoint under maintenance.';
return $response->withJson($json, 503);
}
/**
* @param Request $request
* @param Response $response
* @return Response
* @throws Exception
*/
public function uploadWeb(Request $request, Response $response): Response
{
try {
$file = $this->validateFile($request, $response);
$user = make(UserQuery::class)->get($request, $this->session->get('user_id'));
$this->validateUser($request, $response, $file, $user);
} catch (ValidationException $e) {
return $e->response();
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);
}
if (!$this->updateUserQuota($request, $user->id, $file->getSize())) {
$this->json['message'] = 'User disk quota exceeded.';
return json($response, $this->json, 507);
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);
}
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 ($request->getParam('token') === null) {
$json['message'] = 'Token not specified.';
return $response->withJson($json, 400);
}
return $response;
}
$user = $this->database->query('SELECT * FROM `users` WHERE `token` = ? LIMIT 1', $request->getParam('token'))->fetch();
/**
* @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.';
return json($response, $this->json, 503);
}
try {
$file = $this->validateFile($request, $response);
if (param($request, 'token') === null) {
$this->json['message'] = 'Token not specified.';
return json($response, $this->json, 400);
}
$user = $this->database->query('SELECT * FROM `users` WHERE `token` = ? LIMIT 1', param($request, 'token'))->fetch();
$this->validateUser($request, $response, $file, $user);
} catch (ValidationException $e) {
return $e->response();
}
if (!$this->updateUserQuota($request, $user->id, $file->getSize())) {
$this->json['message'] = 'User disk quota exceeded.';
return json($response, $this->json, 507);
}
try {
$response = $this->saveMedia($response, $file, $user);
} catch (Exception $e) {
$this->updateUserQuota($request, $user->id, $file->getSize(), true);
throw $e;
}
return $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?).';
throw new ValidationException(json($response, $this->json, 400));
}
$file = array_values($request->getUploadedFiles());
/** @var UploadedFileInterface|null $file */
$file = $file[0] ?? null;
if ($file === null) {
$this->json['message'] = 'Request without file attached.';
throw new ValidationException(json($response, $this->json, 400));
}
if ($file->getError() === UPLOAD_ERR_INI_SIZE) {
$this->json['message'] = 'File too large (upload_max_filesize too low?).';
throw new ValidationException(json($response, $this->json, 400));
}
return $file;
}
/**
* @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.';
throw new ValidationException(json($response, $this->json, 404));
$json['message'] = 'Token specified not found.';
return $response->withJson($json, 404);
}
if (!$user->active) {
$this->json['message'] = 'Account disabled.';
throw new ValidationException(json($response, $this->json, 401));
}
$json['message'] = 'Account disabled.';
return $response->withJson($json, 401);
}
/**
* @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);
/** @var \Psr\Http\Message\UploadedFileInterface $file */
$file = $request->getUploadedFiles()['upload'];
$fileInfo = pathinfo($file->getClientFilename());
$storagePath = "$user->user_code/$code.$fileInfo[extension]";
$this->storage->writeStream($storagePath, $file->getStream()->detach());
$this->database->query('INSERT INTO `uploads`(`user_id`, `code`, `filename`, `storage_path`, `published`) VALUES (?, ?, ?, ?, ?)', [
$this->database->query('INSERT INTO `uploads`(`user_id`, `code`, `filename`, `storage_path`) VALUES (?, ?, ?, ?)', [
$user->id,
$code,
$file->getClientFilename(),
$storagePath,
$user->hide_uploads == '1' ? 0 : 1,
]);
$mediaId = $this->database->getPdo()->lastInsertId();
$this->autoTag($mediaId, $storagePath);
$json['message'] = 'OK.';
$json['url'] = urlFor("/$user->user_code/$code.$fileInfo[extension]");
$this->json['message'] = 'OK';
$this->json['url'] = urlFor("/{$user->user_code}/{$code}.{$fileInfo['extension']}");
$this->logger->info("User $user->username uploaded new media.", [$this->database->raw()->lastInsertId()]);
$this->logger->info("User $user->username uploaded new media.", [$mediaId]);
return json($response, $this->json, 201);
return $response->withJson($json, 201);
}
/**
* @param $mediaId
* @param $storagePath
* @throws \League\Flysystem\FileNotFoundException
* @param Request $request
* @param Response $response
* @param $args
* @return Response
* @throws FileNotFoundException
* @throws NotFoundException
*/
protected function autoTag($mediaId, $storagePath)
public function show(Request $request, Response $response, $args): Response
{
$mime = $this->storage->getMimetype($storagePath);
$media = $this->getMedia($args['userCode'], $args['mediaCode']);
[$type, $subtype] = explode('/', $mime);
if (!$media || (!$media->published && $this->session->get('user_id') !== $media->user_id && !$this->session->get('admin', false))) {
throw new NotFoundException($request, $response);
}
/** @var TagQuery $query */
$query = make(TagQuery::class);
$query->addTag($type, $mediaId);
$filesystem = $this->storage;
if ($type === 'application') {
$query->addTag($subtype, $mediaId);
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);
$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 NotFoundException($request, $response);
}
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),
]);
}
}
/**
* @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']);
if (!$media) {
throw new NotFoundException($request, $response);
}
$user = $this->database->query('SELECT `id`, `active` FROM `users` WHERE `token` = ? LIMIT 1', $args['token'])->fetch();
if (!$user) {
$this->session->alert(lang('token_not_found'), 'danger');
return $response->withRedirect($request->getHeaderLine('HTTP_REFERER'));
}
if (!$user->active) {
$this->session->alert(lang('account_disabled'), 'danger');
return $response->withRedirect($request->getHeaderLine('HTTP_REFERER'));
}
if ($this->session->get('admin', false) || $user->id === $media->user_id) {
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 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']);
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');
}
/**
* @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();
}
if (!$media) {
throw new NotFoundException($request, $response);
}
$this->database->query('UPDATE `uploads` SET `published`=? WHERE `id`=?', [$media->published ? 0 : 1, $media->id]);
return $response->withStatus(200);
}
/**
* @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();
if (!$media) {
throw new NotFoundException($request, $response);
}
if ($this->session->get('admin', false) || $media->user_id === $this->session->get('user_id')) {
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();
}
return $response->withStatus(200);
}
/**
* @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);
}
}
}

View file

@ -2,226 +2,214 @@
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;
/**
* @param Request $request
* @param Response $response
* @param int|null $page
*
* @param $args
* @return Response
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*
* @throws \Twig\Error\LoaderError
*/
public function index(Response $response, int $page = 0): 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();
$pages = $this->database->query('SELECT COUNT(*) AS `count` FROM `users`')->fetch()->count / self::PER_PAGE;
return view()->render($response,
return $this->view->render($response,
'user/index.twig',
[
'users' => $users,
'next' => $page < floor($pages),
'previous' => $page >= 1,
'current_page' => ++$page,
'quota_enabled' => $this->getSetting('quota_enabled'),
]
);
}
/**
* @param Request $request
* @param Response $response
*
* @return Response
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*
* @throws \Twig\Error\LoaderError
*/
public function create(Response $response): Response
public function create(Request $request, 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'),
]);
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');
if ($validator->fails()) {
return redirect($response, route('user.create'));
if ($request->getParam('email') === null) {
$this->session->alert(lang('email_required'), '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 ($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');
}
if ($maxUserQuotaStr !== '-1') {
$maxUserQuota = stringToBytes($maxUserQuotaStr);
}
if ($request->getParam('username') === null) {
$this->session->alert(lang('username_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 (param($request, 'send_notification') !== null) {
$this->sendCreateNotification($hasPassword, $request);
if ($request->getParam('password') === null) {
$this->session->alert(lang('password_required'), 'danger');
return redirect($response, 'user.create');
}
$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']))]);
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');
}
return redirect($response, route('user.index'));
do {
$userCode = humanRandomString(5);
} while ($this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `user_code` = ?', $userCode)->fetch()->count > 0);
$token = $this->generateNewToken();
$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,
]);
$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 redirect($response, 'user.index');
}
/**
* @param Request $request
* @param Response $response
* @param int $id
*
* @param $args
* @return Response
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
* @throws NotFoundException
*/
public function edit(Request $request, Response $response, int $id): Response
public function edit(Request $request, Response $response, $args): Response
{
$user = make(UserQuery::class)->get($request, $id);
$user = $this->database->query('SELECT * FROM `users` WHERE `id` = ? LIMIT 1', $args['id'])->fetch();
return view()->render($response, 'user/edit.twig', [
if (!$user) {
throw new NotFoundException($request, $response);
}
return $this->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,
]);
}
/**
* @param Request $request
* @param Response $response
* @param int $id
*
* @param $args
* @return Response
* @throws NotFoundException
*/
public function update(Request $request, Response $response, int $id): Response
public function update(Request $request, Response $response, $args): Response
{
$user = make(UserQuery::class)->get($request, $id);
$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'),
];
$this->session->alert($alerts[$rule], 'danger');
});
if ($validator->fails()) {
return redirect($response, route('user.edit', ['id' => $id]));
if (!$user) {
throw new NotFoundException($request, $response);
}
$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 ($request->getParam('email') === null) {
$this->session->alert(lang('email_required'), 'danger');
return redirect($response, 'user.edit', ['id' => $args['id']]);
}
if ($maxUserQuota !== '-1') {
$user->max_disk_quota = stringToBytes($maxUserQuota);
}
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']]);
}
make(UserQuery::class)->update(
if ($request->getParam('username') === null) {
$this->session->alert(lang('username_required'), 'danger');
return redirect($response, 'user.edit', ['id' => $args['id']]);
}
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']]);
}
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']]);
}
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,
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 ($user->id === $this->session->get('user_id')) {
$this->setSessionQuotaInfo($user->current_disk_quota, $user->max_disk_quota);
]);
} 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,
]);
}
$this->session->alert(lang('user_updated', [param($request, 'username')]), 'success');
$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->getParsedBody(), array_flip(['password'])),
array_diff_key($request->getParams(), array_flip(['password'])),
]);
return redirect($response, route('user.index'));
return redirect($response, 'user.index');
}
/**
* @param Request $request
* @param Response $response
* @param int $id
*
* @param $args
* @return Response
* @throws NotFoundException
*/
public function delete(Request $request, Response $response, int $id): Response
public function delete(Request $request, Response $response, $args): Response
{
$user = make(UserQuery::class)->get($request, $id);
$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->alert(lang('cannot_delete'), 'danger');
return redirect($response, route('user.index'));
return redirect($response, 'user.index');
}
$this->database->query('DELETE FROM `users` WHERE `id` = ?', $user->id);
@ -229,94 +217,205 @@ class UserController extends Controller
$this->session->alert(lang('user_deleted'), 'success');
$this->logger->info('User ' . $this->session->get('username') . " deleted $user->id.");
return redirect($response, route('user.index'));
return redirect($response, 'user.index');
}
/**
* @param Request $request
* @param Response $response
* @param int $id
* @return Response
* @throws NotFoundException
* @throws UnauthorizedException
*/
public function clearUserMedia(Request $request, Response $response, int $id): Response
public function profile(Request $request, Response $response): Response
{
$user = make(UserQuery::class)->get($request, $id, true);
$user = $this->database->query('SELECT * FROM `users` WHERE `id` = ? LIMIT 1', $this->session->get('user_id'))->fetch();
$medias = $this->database->query('SELECT * FROM `uploads` WHERE `user_id` = ?', $user->id);
foreach ($medias as $media) {
try {
$this->storage->delete($media->storage_path);
} catch (FileNotFoundException $e) {
}
if (!$user) {
throw new NotFoundException($request, $response);
}
$this->database->query('DELETE FROM `uploads` WHERE `user_id` = ?', $user->id);
$this->database->query('UPDATE `users` SET `current_disk_quota`=? WHERE `id` = ?', [
0,
if ($user->id !== $this->session->get('user_id') && !$this->session->get('admin', false)) {
throw new UnauthorizedException();
}
return $this->view->render($response, 'user/edit.twig', [
'profile' => true,
'user' => $user,
]);
}
/**
* @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();
if (!$user) {
throw new NotFoundException($request, $response);
}
if ($user->id !== $this->session->get('user_id') && !$this->session->get('admin', false)) {
throw new UnauthorizedException();
}
if ($request->getParam('email') === null) {
$this->session->alert(lang('email_required'), 'danger');
return redirect($response, 'profile');
}
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->session->alert(lang('account_media_deleted'), 'success');
return redirect($response, route('user.edit', ['id' => $id]));
}
/**
* @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);
$this->logger->info('User ' . $this->session->get('username') . " refreshed token of user $user->id.");
$response->getBody()->write($query->refreshToken($user->id));
$response->getBody()->write($token);
return $response;
}
/**
* @param $hasPassword
* @param $request
* @throws \Exception
* @param Request $request
* @param Response $response
* @param $args
* @return Response
* @throws NotFoundException
* @throws UnauthorizedException
*/
private function sendCreateNotification($hasPassword, $request)
public function getShareXconfigFile(Request $request, Response $response, $args): Response
{
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));
$user = $this->database->query('SELECT * FROM `users` WHERE `id` = ? LIMIT 1', $args['id'])->fetch();
$this->database->query('UPDATE `users` SET `reset_token`=? WHERE `id` = ?', [
$resetToken,
$this->database->getPdo()->lastInsertId(),
]);
$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 (!$user) {
throw new NotFoundException($request, $response);
}
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 ($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;
}
}

View file

@ -2,21 +2,34 @@
namespace App\Database;
use PDO;
class DB
{
/** @var DB */
protected static $instance;
/** @var string */
private static $password;
/** @var string */
private static $username;
/** @var PDO */
protected $pdo;
/** @var string */
protected static $dsn = 'sqlite:database.db';
/** @var string */
protected $currentDriver;
public function __construct(string $dsn, string $username = null, string $password = null)
{
self::setDsn($dsn, $username, $password);
$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);
@ -39,13 +52,11 @@ class DB
}
$query->execute();
return $query;
}
/**
* Get the PDO instance.
*
* Get the PDO instance
* @return PDO
*/
public function getPdo(): PDO
@ -54,12 +65,65 @@ class DB
}
/**
* Get the current PDO driver.
*
* 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;
}
}

View file

@ -1,125 +0,0 @@
<?php
namespace App\Database;
use League\Flysystem\FileNotFoundException;
use League\Flysystem\Filesystem;
use PDOException;
class Migrator
{
/**
* @var DB
*/
private $db;
/**
* @var string
*/
private $schemaPath;
/**
* @var bool
*/
private $firstMigrate;
/**
* Migrator constructor.
*
* @param DB $db
* @param string $schemaPath
* @param bool $firstMigrate
*/
public function __construct(DB $db, ?string $schemaPath, bool $firstMigrate = false)
{
$this->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,
]);
}
}
}

View file

@ -2,10 +2,11 @@
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
{
@ -16,6 +17,7 @@ class MediaQuery
const ORDER_NAME = 1;
const ORDER_SIZE = 2;
/** @var DB */
protected $db;
@ -39,117 +41,98 @@ class MediaQuery
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)
public function __construct(DB $db, bool $isAdmin, Filesystem $storage)
{
$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;
}
/**
* @param int $page
*/
public function run(int $page)
{
if ($this->orderBy == self::ORDER_SIZE) {
$this->runWithFileSort($page);
} else {
$this->runWithDbSort($page);
$this->runWithOrderBySize($page);
return;
}
return $this;
}
$queryPages = 'SELECT COUNT(*) AS `count` FROM `uploads`';
public function runWithDbSort(int $page)
{
$params = [];
if ($this->isAdmin) {
[$queryMedia, $queryPages] = $this->buildAdminQueries();
$constPage = self::PER_PAGE_ADMIN;
$queryMedia = 'SELECT `uploads`.*, `users`.`user_code`, `users`.`username` FROM `uploads` LEFT JOIN `users` ON `uploads`.`user_id` = `users`.`id` %s LIMIT ? OFFSET ?';
} else {
[$queryMedia, $queryPages] = $this->buildUserQueries();
$params[] = $this->userId;
$constPage = self::PER_PAGE;
$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` = ?';
}
$orderAndSearch = '';
$params = [];
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) . '%';
}
$queryMedia .= $this->buildOrderBy().' LIMIT ? OFFSET ?';
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 = $this->db->query($queryMedia, array_merge($params, [$constPage, $page * $constPage]))->fetchAll();
$this->pages = $this->db->query($queryPages, $params)->fetch()->count / $constPage;
$queryMedia = sprintf($queryMedia, $orderAndSearch);
$tags = $this->getTags(array_column($this->media, 'id'));
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;
}
foreach ($this->media as $media) {
try {
@ -160,182 +143,73 @@ class MediaQuery
$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 = [];
}
}
return $this;
}
public function runWithFileSort(int $page)
/**
* @param int $page
*/
private function runWithOrderBySize(int $page)
{
$this->storage->addPlugin(new ListWith());
$this->storage->addPlugin(new ListFiles());
if ($this->isAdmin) {
$files = $this->storage->listWith(['size', 'mimetype'], '/', true);
$files = $this->storage->listFiles('/', true);
$this->pages = count($files) / self::PER_PAGE_ADMIN;
$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);
$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;
$offset = $page * self::PER_PAGE;
$limit = self::PER_PAGE;
}
$files = array_filter($files, function ($file) {
return $file['type'] !== 'dir';
});
array_multisort(array_column($files, 'size'), ($this->orderMode === 'ASC') ? SORT_ASC : SORT_DESC, SORT_NUMERIC, $files);
array_multisort(array_column($files, 'size'), $this->buildOrderBy(), SORT_NUMERIC, $files);
$params = [];
if ($this->text !== null) {
if ($this->isAdmin) {
[$queryMedia,] = $this->buildAdminQueries();
$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 {
[$queryMedia,] = $this->buildUserQueries();
$params[] = $this->userId;
$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();
}
$params[] = '%'.htmlentities($this->text).'%';
$paths = array_column($files, 'path');
} else {
if ($this->tagId !== null) {
$files = array_slice($files, $offset, $limit);
$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).'")';
}
}
$medias = $this->db->query($queryMedia, $params)->fetchAll();
$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();
}
$paths = array_flip($paths);
foreach ($medias as $media) {
$paths[$media->storage_path] = $media;
}
$tags = $this->getTags(array_column($medias, 'id'));
$this->media = [];
foreach ($files as $file) {
$media = $paths[$file['path']];
if (is_object($media)) {
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'];
$media->mimetype = $file['mimetype'];
$this->media[] = $media;
if (array_key_exists($media->id, $tags)) {
$media->tags = $tags[$media->id];
} else {
$media->tags = [];
}
}
}
$this->pages = count($this->media) / $limit;
if ($this->text !== null || $this->tagId !== null) {
$this->media = array_slice($this->media, $offset, $limit, true);
}
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`';
if ($this->text !== null || $this->tagId !== null) {
$queryMedia .= ' WHERE';
$queryPages .= ' WHERE';
}
if ($this->text !== null) {
$queryMedia .= ' `uploads`.`filename` LIKE ?';
$queryPages .= ' `filename` LIKE ?';
$this->media = array_slice($this->media, $offset, $limit);
}
if ($this->tagId !== null) {
if ($this->text !== null) {
$queryMedia .= ' AND';
$queryPages .= ' AND';
}
$ids = $this->getMediaIdsByTagId($this->tagId);
$queryMedia .= ' `uploads`.`id` IN ('.implode(',', $ids).')';
$queryPages .= ' `uploads`.`id` IN ('.implode(',', $ids).')';
}
return [$queryMedia, $queryPages];
}
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` = ?';
if ($this->text !== null) {
$queryMedia .= ' AND `uploads`.`filename` LIKE ? ';
$queryPages .= ' AND `filename` LIKE ?';
}
if ($this->tagId !== null) {
$ids = $this->getMediaIdsByTagId($this->tagId);
$queryMedia .= ' AND `uploads`.`id` IN ('.implode(',', $ids).')';
$queryPages .= ' AND `uploads`.`id` IN ('.implode(',', $ids).')';
}
return [$queryMedia, $queryPages];
}
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 '';
}
}
/**
* @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;
}
/**
* @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;
}
/**
@ -344,6 +218,7 @@ class MediaQuery
public function getMedia()
{
return $this->media;
}
/**

View file

@ -1,105 +0,0 @@
<?php
namespace App\Database\Queries;
use App\Database\DB;
use PDO;
class TagQuery
{
const PER_MEDIA_LIMIT = 10;
/**
* @var DB
*/
private $db;
/**
* @var null|bool
*/
private $isAdmin;
/**
* @var null|int|string
*/
private $userId;
public function __construct(DB $db, $isAdmin = null, $userId = null)
{
$this->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;
}
}

View file

@ -1,180 +0,0 @@
<?php
namespace App\Database\Queries;
use App\Database\DB;
use App\Web\Session;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Exception\HttpNotFoundException;
use Slim\Exception\HttpUnauthorizedException;
class UserQuery
{
/**
* @var DB
*/
private $database;
/**
* @var Session
*/
private $session;
/**
* UserQuery constructor.
* @param DB $db
* @param Session|null $session
*/
public function __construct(DB $db, ?Session $session)
{
$this->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;
}
}

View file

@ -1,13 +0,0 @@
<?php
namespace App\Exception\Handlers;
use Slim\Handlers\ErrorHandler;
class AppErrorHandler extends ErrorHandler
{
protected function logError(string $error): void
{
resolve('logger')->error($error);
}
}

View file

@ -1,50 +0,0 @@
<?php
namespace App\Exception\Handlers\Renderers;
use App\Exceptions\UnderMaintenanceException;
use Slim\Exception\HttpBadRequestException;
use Slim\Exception\HttpForbiddenException;
use Slim\Exception\HttpMethodNotAllowedException;
use Slim\Exception\HttpNotFoundException;
use Slim\Exception\HttpUnauthorizedException;
use Slim\Interfaces\ErrorRendererInterface;
use Throwable;
class HtmlErrorRenderer implements ErrorRendererInterface
{
/**
* @param Throwable $exception
* @param bool $displayErrorDetails
*
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*
* @return string
*/
public function __invoke(Throwable $exception, bool $displayErrorDetails): string
{
if ($exception instanceof UnderMaintenanceException) {
return view()->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]);
}
}

View file

@ -0,0 +1,15 @@
<?php
namespace App\Exceptions;
use Exception;
use Throwable;
class MaintenanceException extends Exception
{
public function __construct(string $message = 'Under Maintenance', int $code = 503, Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
}

View file

@ -0,0 +1,15 @@
<?php
namespace App\Exceptions;
use Exception;
use Throwable;
class UnauthorizedException extends Exception
{
public function __construct(string $message = 'Forbidden', int $code = 403, Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
}

View file

@ -1,13 +0,0 @@
<?php
namespace App\Exceptions;
use Slim\Exception\HttpSpecializedException;
class UnderMaintenanceException extends HttpSpecializedException
{
protected $code = 503;
protected $message = 'Platform Under Maintenance.';
protected $title = '503 Service Unavailable';
protected $description = 'We\'ll be back very soon! :)';
}

View file

@ -1,30 +0,0 @@
<?php
namespace App\Exceptions;
use Exception;
use Psr\Http\Message\ResponseInterface as Response;
use Throwable;
class ValidationException extends Exception
{
/**
* @var Response
*/
private $response;
public function __construct(Response $response, $message = "", $code = 0, Throwable $previous = null)
{
parent::__construct($message, $response->getStatusCode(), $previous);
$this->response = $response;
}
/**
* @return Response
*/
public function response(): Response
{
return $this->response;
}
}

View file

@ -1,70 +0,0 @@
<?php
namespace App\Factories;
use App\Web\View;
use Psr\Container\ContainerInterface as Container;
use Slim\Factory\ServerRequestCreatorFactory;
use Twig\Environment;
use Twig\Loader\FilesystemLoader;
use Twig\TwigFunction;
class ViewFactory
{
public static function createAppInstance(Container $container)
{
$config = $container->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);
}
}

View file

@ -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
*
* @param Response $response
* @param callable $next
* @return Response
* @throws UnauthorizedException
*/
public function __invoke(Request $request, RequestHandler $handler): ResponseInterface
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 HttpUnauthorizedException($request);
throw new UnauthorizedException();
}
return $handler->handle($request);
return $next($request, $response);
}
}

View file

@ -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
* @param Response $response
* @param callable $next
* @return Response
*/
public function __invoke(Request $request, RequestHandler $handler): ResponseInterface
public function __invoke(Request $request, Response $response, callable $next)
{
if (!$this->session->get('logged', false)) {
$this->session->set('redirectTo', (string) $request->getUri());
return redirect(new Response(), route('login.show'));
$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->alert('Your account is not active anymore.', 'danger');
$this->session->set('logged', false);
return redirect(new Response(), route('login.show'));
$this->session->set('redirectTo', (isset($_SERVER['HTTPS']) ? 'https' : 'http') . "://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]");
return redirect($response, 'login.show');
}
return $handler->handle($request);
return $next($request, $response);
}
}

View file

@ -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
*
* @param Response $response
* @param callable $next
* @return Response
* @throws MaintenanceException
*/
public function __invoke(Request $request, RequestHandler $handler): Response
public function __invoke(Request $request, Response $response, callable $next)
{
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);
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);
}
}

View file

@ -1,23 +0,0 @@
<?php
namespace App\Middleware;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
class InjectMiddleware extends Middleware
{
/**
* @param Request $request
* @param RequestHandler $handler
*
* @return Response
*/
public function __invoke(Request $request, RequestHandler $handler)
{
$this->view->getTwig()->addGlobal('customHead', $this->getSetting('custom_head'));
return $handler->handle($request);
}
}

View file

@ -1,27 +0,0 @@
<?php
namespace App\Middleware;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
class LangMiddleware extends Middleware
{
/**
* @param Request $request
* @param RequestHandler $handler
*
* @return Response
*/
public function __invoke(Request $request, RequestHandler $handler)
{
$forcedLang = $this->getSetting('lang');
if ($forcedLang !== null) {
$this->lang::setLang($forcedLang);
$request = $request->withAttribute('forced_lang', $forcedLang);
}
return $handler->handle($request);
}
}

View file

@ -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
{
/** @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 RequestHandler $handler
*
* @return Response
* @param Response $response
* @param callable $next
*/
abstract public function __invoke(Request $request, RequestHandler $handler);
public abstract function __invoke(Request $request, Response $response, callable $next);
}

View file

@ -1,40 +0,0 @@
<?php
namespace App\Middleware;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
class RememberMiddleware extends Middleware
{
/**
* @param Request $request
* @param RequestHandler $handler
*
* @return Response
* @throws \Exception
*/
public function __invoke(Request $request, RequestHandler $handler)
{
if (!$this->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);
}
}

View file

@ -2,8 +2,10 @@
namespace App\Web;
class Lang
{
const DEFAULT_LANG = 'en';
const LANG_PATH = __DIR__ . '../../resources/lang/';
@ -19,10 +21,11 @@ class Lang
/** @var array */
protected $cache = [];
/**
* @return Lang
*/
public static function getInstance(): self
public static function getInstance(): Lang
{
if (self::$instance === null) {
self::$instance = new self();
@ -34,10 +37,9 @@ class Lang
/**
* @param string $lang
* @param string $langPath
*
* @return Lang
*/
public static function build($lang = self::DEFAULT_LANG, $langPath = null): self
public static function build($lang = self::DEFAULT_LANG, $langPath = null): Lang
{
self::$lang = $lang;
@ -52,7 +54,6 @@ class Lang
/**
* Recognize the current language from the request.
*
* @return bool|string
*/
public static function recognize()
@ -60,7 +61,6 @@ class Lang
if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
return locale_accept_from_http($_SERVER['HTTP_ACCEPT_LANGUAGE']);
}
return self::DEFAULT_LANG;
}
@ -72,14 +72,6 @@ class Lang
return self::$lang;
}
/**
* @param $lang
*/
public static function setLang($lang)
{
self::$lang = $lang;
}
/**
* @return array
*/
@ -92,23 +84,19 @@ class Lang
foreach (glob(self::$langPath . '*.lang.php') as $file) {
$dict = include $file;
if (!is_array($dict) || !isset($dict['lang'])) {
continue;
}
$count = count($dict) - 1;
$percent = min(round(($count / $default) * 100), 100);
$prepend = "[{$count}/{$default}] ";
$languages[str_replace('.lang.php', '', basename($file))] = "[{$percent}%] ".$dict['lang'];
$languages[str_replace('.lang.php', '', basename($file))] = $prepend . $dict['lang'];
}
return $languages;
}
/**
* @param $key
* @param array $args
*
* @return string
*/
public function get($key, $args = []): string
@ -120,7 +108,6 @@ class Lang
* @param $key
* @param $lang
* @param $args
*
* @return string
*/
private function getString($key, $lang, $args): string
@ -129,19 +116,15 @@ class Lang
if (array_key_exists($lang, $this->cache)) {
$transDict = $this->cache[$lang];
} else {
if (file_exists(self::$langPath.$lang.'.lang.php')) {
} 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')) {
} 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);

View file

@ -1,126 +0,0 @@
<?php
namespace App\Web;
use InvalidArgumentException;
class Mail
{
protected $fromMail = 'no-reply@example.com';
protected $fromName;
protected $to;
protected $subject;
protected $message;
protected $additionalHeaders = '';
protected $headers = '';
/**
* @return Mail
*/
public static function make()
{
return new self();
}
/**
* @param $mail
* @param $name
* @return $this
*/
public function from(string $mail, string $name)
{
$this->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);
}
}

View file

@ -2,16 +2,16 @@
namespace App\Web;
use Exception;
class Session
{
/**
* Session constructor.
*
* @param string $name
* @param string $path
*
* @throws Exception
*/
public function __construct(string $name, $path = '')
@ -21,43 +21,21 @@ class Session
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']
);
}
$started = @session_start([
'name' => $name,
'save_path' => $path,
'cookie_httponly' => true,
'gc_probability' => 25,
'cookie_samesite' => 'Lax', // works only for php >= 7.3
]);
if (!$started) {
throw new Exception("Cannot start the HTTP session. The session path '{$path}' is not writable.");
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.
*
* Destroy the current session
* @return bool
*/
public function destroy(): bool
@ -66,7 +44,7 @@ class Session
}
/**
* Clear all session stored values.
* Clear all session stored values
*/
public function clear(): void
{
@ -74,10 +52,8 @@ class Session
}
/**
* Check if session has a stored key.
*
* Check if session has a stored key
* @param $key
*
* @return bool
*/
public function has($key): bool
@ -86,8 +62,7 @@ class Session
}
/**
* Get the content of the current session.
*
* Get the content of the current session
* @return array
*/
public function all(): array
@ -96,11 +71,9 @@ class Session
}
/**
* Returned a value given a key.
*
* Returned a value given a key
* @param $key
* @param null $default
*
* @return mixed
*/
public function get($key, $default = null)
@ -109,8 +82,7 @@ class Session
}
/**
* Add a key-value pair to the session.
*
* Add a key-value pair to the session
* @param $key
* @param $value
*/
@ -120,8 +92,7 @@ class Session
}
/**
* Set a flash alert.
*
* Set a flash alert
* @param $message
* @param string $type
*/
@ -130,16 +101,16 @@ class Session
$_SESSION['_flash'][] = [$type => $message];
}
/**
* Retrieve flash alerts.
*
* Retrieve flash alerts
* @return array
*/
public function getAlert(): ?array
{
$flash = self::get('_flash');
self::set('_flash', []);
return $flash;
}
}

View file

@ -1,88 +0,0 @@
<?php
namespace App\Web;
class ValidationChecker
{
protected $rules = [];
protected $failClosure;
protected $lastRule;
/**
* @return ValidationChecker
*/
public static function make()
{
return new self();
}
/**
* @param array $rules
* @return $this
*/
public function rules(array $rules)
{
$this->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;
}
}

View file

@ -1,69 +0,0 @@
<?php
namespace App\Web;
use Psr\Http\Message\ResponseInterface as Response;
use Twig\Environment;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Error\SyntaxError;
class View
{
/**
* @var Environment
*/
private $twig;
/**
* View constructor.
*
* @param Environment $twig
*/
public function __construct(Environment $twig)
{
$this->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;
}
}

View file

@ -1,43 +1,30 @@
<?php
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\ServerRequestCreatorFactory;
if (!defined('HUMAN_RANDOM_CHARS')) {
define('HUMAN_RANDOM_CHARS', 'bcdfghjklmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZaeiouAEIOU');
}
if (!function_exists('humanFileSize')) {
/**
* Generate a human readable file size.
*
* Generate a human readable file size
* @param $size
* @param int $precision
*
* @param bool $iniMode
* @return string
*/
function humanFileSize($size, $precision = 2, $iniMode = false): string
function humanFileSize($size, $precision = 2): string
{
for ($i = 0; ($size / 1024) > 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];
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
function humanRandomString(int $length = 13): string
{
$result = '';
$numberOffset = round($length * 0.2);
@ -47,7 +34,6 @@ if (!function_exists('humanRandomString')) {
for ($x = 0; $x < $numberOffset; $x++) {
$result .= rand(0, 9);
}
return $result;
}
}
@ -55,7 +41,6 @@ if (!function_exists('humanRandomString')) {
if (!function_exists('isDisplayableImage')) {
/**
* @param string $mime
*
* @return bool
*/
function isDisplayableImage(string $mime): bool
@ -78,7 +63,6 @@ if (!function_exists('isDisplayableImage')) {
if (!function_exists('stringToBytes')) {
/**
* @param $str
*
* @return float
*/
function stringToBytes(string $str): float
@ -93,8 +77,6 @@ if (!function_exists('stringToBytes')) {
$val = (float)$val;
switch ($last) {
case 't':
$val *= 1024;
case 'g':
$val *= 1024;
case 'm':
@ -102,15 +84,13 @@ if (!function_exists('stringToBytes')) {
case 'k':
$val *= 1024;
}
return $val;
}
}
if (!function_exists('removeDirectory')) {
/**
* Remove a directory and it's content.
*
* Remove a directory and it's content
* @param $path
*/
function removeDirectory($path)
@ -120,13 +100,13 @@ if (!function_exists('removeDirectory')) {
is_dir($file) ? removeDirectory($file) : unlink($file);
}
rmdir($path);
return;
}
}
if (!function_exists('cleanDirectory')) {
/**
* Removes all directory contents.
*
* Removes all directory contents
* @param $path
*/
function cleanDirectory($path)
@ -141,74 +121,31 @@ if (!function_exists('cleanDirectory')) {
}
}
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');
}
}
if (!function_exists('redirect')) {
/**
* Set the redirect response.
*
* @param Response $response
* @param string $url
* @param int $status
*
* @return Response
* Set the redirect response
* @param \Slim\Http\Response $response
* @param string $path
* @param array $args
* @param null $status
* @return \Slim\Http\Response
*/
function redirect(Response $response, string $url, $status = 302)
function redirect(\Slim\Http\Response $response, string $path, $args = [], $status = null)
{
return $response
->withHeader('Location', $url)
->withStatus($status);
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.
*
* Get the asset link with timestamp
* @param string $path
*
* @return string
*/
function asset(string $path): string
@ -219,105 +156,52 @@ if (!function_exists('asset')) {
if (!function_exists('urlFor')) {
/**
* Generate the app url given a path.
*
* Generate the app url given a path
* @param string $path
* @param string $append
*
* @return string
*/
function urlFor(string $path = '', string $append = ''): string
function urlFor(string $path, string $append = ''): string
{
$baseUrl = resolve('config')['base_url'];
global $app;
$baseUrl = $app->getContainer()->get('settings')['base_url'];
return $baseUrl . $path . $append;
}
}
if (!function_exists('route')) {
/**
* Generate the app url given a path.
*
* 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);
$uri = $app->getContainer()->get('router')->relativePathFor($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');
}
}
if (!function_exists('lang')) {
/**
* @param string $key
* @param array $args
*
* @return string
*/
function lang(string $key, $args = []): string
{
return resolve('lang')->get($key, $args);
global $app;
return $app->getContainer()->get('lang')->get($key, $args);
}
}
if (!function_exists('isBot')) {
/**
* @param string $userAgent
*
* @return bool
* @return boolean
*/
function isBot(string $userAgent)
{
@ -329,9 +213,6 @@ if (!function_exists('isBot')) {
'Facebot',
'curl/',
'wget/',
'WhatsApp/',
'Slackbot',
'Slack-ImgProxy',
];
foreach ($bots as $bot) {
@ -346,10 +227,8 @@ if (!function_exists('isBot')) {
if (!function_exists('mime2font')) {
/**
* Convert get the icon from the file mimetype.
*
* Convert get the icon from the file mimetype
* @param $mime
*
* @return mixed|string
*/
function mime2font($mime)
@ -383,7 +262,6 @@ if (!function_exists('mime2font')) {
return $class;
}
}
return 'fa-file';
}
}
@ -406,14 +284,15 @@ if (!function_exists('dd')) {
if (!function_exists('queryParams')) {
/**
* Get the query parameters of the current request.
*
* @param array $replace
*
* @return string
* @throws \Interop\Container\Exception\ContainerException
*/
function queryParams(array $replace = [])
{
$request = ServerRequestCreatorFactory::determineServerRequestCreator()->createServerRequestFromGlobals();
global $container;
/** @var \Slim\Http\Request $request */
$request = $container->get('request');
$params = array_replace_recursive($request->getQueryParams(), $replace);
@ -421,30 +300,11 @@ if (!function_exists('queryParams')) {
}
}
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;
}
}
if (!function_exists('glob_recursive')) {
/**
* Does not support flag GLOB_BRACE.
*
* Does not support flag GLOB_BRACE
* @param $pattern
* @param int $flags
*
* @return array|false
*/
function glob_recursive($pattern, $flags = 0)
@ -453,37 +313,6 @@ if (!function_exists('glob_recursive')) {
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));
}
}

View file

@ -1,90 +1,68 @@
<?php
// Auth routes
use App\Controllers\AdminController;
use App\Controllers\Auth\LoginController;
use App\Controllers\Auth\PasswordRecoveryController;
use App\Controllers\Auth\RegisterController;
use App\Controllers\ClientController;
use App\Controllers\DashboardController;
use App\Controllers\ExportController;
use App\Controllers\MediaController;
use App\Controllers\ProfileController;
use App\Controllers\SettingController;
use App\Controllers\TagController;
use App\Controllers\LoginController;
use App\Controllers\ThemeController;
use App\Controllers\UpgradeController;
use App\Controllers\UploadController;
use App\Controllers\UserController;
use App\Middleware\AdminMiddleware;
use App\Middleware\AuthMiddleware;
use App\Middleware\CheckForMaintenanceMiddleware;
use Slim\Routing\RouteCollectorProxy;
global $app;
$app->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');
$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');
$this->group('/user', function () {
$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('/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('/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->get('/user/{id}/export', [ExportController::class, 'downloadData'])->setName('export.data');
$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('/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');
$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);

View file

@ -6,7 +6,6 @@ if (php_sapi_name() !== 'cli') {
}
use App\Database\DB;
use App\Database\Migrator;
require __DIR__ . '/../vendor/autoload.php';
@ -18,22 +17,78 @@ if (!$config) {
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']);
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']);
$migrator = new Migrator($db, 'resources/schemas', $firstMigrate);
$migrator->migrate();
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)]);
try {
DB::doQuery('SELECT 1 FROM `migrations` LIMIT 1');
} catch (PDOException $exception) {
$firstMigrate = true;
}
if (file_exists(__DIR__.'/../install') && (!isset($config['debug']) || !$config['debug'])) {
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::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')) {
removeDirectory(__DIR__ . '/../install');
}

View file

@ -1,35 +1,43 @@
<?php
use App\Exception\Handlers\AppErrorHandler;
use App\Exception\Handlers\Renderers\HtmlErrorRenderer;
use App\Factories\ViewFactory;
use App\Middleware\InjectMiddleware;
use App\Middleware\LangMiddleware;
use App\Middleware\RememberMiddleware;
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 Psr\Container\ContainerInterface as Container;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use App\Database\DB;
use App\Exceptions\MaintenanceException;
use App\Exceptions\UnauthorizedException;
use App\Web\Lang;
use App\Web\Session;
use Aws\S3\S3Client;
use Google\Cloud\Storage\StorageClient;
use League\Flysystem\Adapter\Ftp as FtpAdapter;
use League\Flysystem\Adapter\Local;
use League\Flysystem\AwsS3v3\AwsS3Adapter;
use League\Flysystem\Filesystem;
use Monolog\Formatter\LineFormatter;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Logger;
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\Dropbox\Client as DropboxClient;
use Spatie\FlysystemDropbox\DropboxAdapter;
use Superbalist\Flysystem\GoogleStorage\GoogleStorageAdapter;
use Twig\TwigFunction;
if (!file_exists('config.php') && is_dir('install/')) {
header('Location: ./install/');
exit();
} else {
if (!file_exists('config.php') && !is_dir('install/')) {
} else if (!file_exists('config.php') && !is_dir('install/')) {
exit('Cannot find the config file.');
}
}
// Load the config
$config = array_replace_recursive([
'app_name' => 'XBackBone',
'base_url' => isset($_SERVER['HTTPS']) ? 'https://' . $_SERVER['HTTP_HOST'] : 'http://' . $_SERVER['HTTP_HOST'],
'debug' => false,
'displayErrorDetails' => false,
'maintenance' => false,
'db' => [
'connection' => 'sqlite',
@ -41,75 +49,176 @@ $config = array_replace_recursive([
'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');
$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]);
$container['config'] = function ($container) use ($config) {
return $config;
};
$container['logger'] = function ($container) {
$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;
};
$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',
]);
$builder->addDefinitions(__DIR__.'/container.php');
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.');
}
};
$app = Bridge::create($builder->build());
$app->getContainer()->set('config', $config);
$app->setBasePath(parse_url($config['base_url'], PHP_URL_PATH) ?: '');
$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/');
};
if (!$config['debug']) {
$app->getRouteCollector()->setCacheFile(BASE_DIR.'resources/cache/routes.cache.php');
$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');
}
$app->add(InjectMiddleware::class);
$app->add(LangMiddleware::class);
$app->add(RememberMiddleware::class);
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) {
$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
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);
return $response->withRedirect((string)$uri, 301);
} else {
$request = $request->withUri($uri);
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';

View file

@ -1,99 +0,0 @@
<?php
use App\Database\DB;
use App\Web\Lang;
use App\Web\Session;
use Aws\S3\S3Client;
use function DI\factory;
use function DI\get;
use Google\Cloud\Storage\StorageClient;
use League\Flysystem\Adapter\Ftp as FtpAdapter;
use League\Flysystem\Adapter\Local;
use League\Flysystem\AwsS3v3\AwsS3Adapter;
use League\Flysystem\Filesystem;
use Monolog\Formatter\LineFormatter;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Logger;
use Psr\Container\ContainerInterface as Container;
use Spatie\Dropbox\Client as DropboxClient;
use Spatie\FlysystemDropbox\DropboxAdapter;
use Superbalist\Flysystem\GoogleStorage\GoogleStorageAdapter;
return [
Logger::class => 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),
];

View file

@ -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"

1473
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,6 @@
<?php
return [
'base_url' => 'https://localhost', // no trailing slash
'base_url' => 'http://localhost',
'db' => [
'connection' => 'sqlite',
'dsn' => 'resources/database/xbackbone.db',

View file

@ -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 &copy; 2020 Sergio Brighenti. Distributed by an <a href=\"https://github.com/SergiX44/XBackBone/blob/master/LICENSE\">AGPL v3.0 license.</a>"

View file

@ -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.

View file

@ -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 \<head\> 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 <a href="https://choosealicense.com/licenses/agpl-3.0/">AGPL v3.0</a> (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.

View file

@ -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/<username>`).
+ 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.*

View file

@ -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

View file

@ -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 <THEME-NAME>`) 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"',
...
);
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

View file

@ -1,56 +0,0 @@
---
layout: default
title: Home
nav_order: 1
---
<p align="center">
<img src="img/xbackbone.png" width="400px">
</p>
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/).
<a href="https://hosted.weblate.org/engage/xbackbone/?utm_source=widget">
<img src="https://hosted.weblate.org/widgets/xbackbone/-/xbackbone/multi-auto.svg" alt="Stato traduzione" />
</a>
#### Demo GIF
![img](https://i.imgur.com/iV8Rirn.gif)

View file

@ -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 }

View file

@ -1,21 +0,0 @@
---
layout: default
title: License & Credits
nav_order: 9
---
# License
This software is licensed under the <a href="https://choosealicense.com/licenses/agpl-3.0/">GNU Affero General Public License v3.0</a>, 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/)

View file

@ -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**.

BIN
favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -1,5 +1,4 @@
<?php
(PHP_MAJOR_VERSION >= 7 && PHP_MINOR_VERSION >= 1) ?: die('Sorry, PHP 7.1 or above is required to run XBackBone.');
require __DIR__ . '/vendor/autoload.php';

View file

@ -1,32 +1,33 @@
<?php
(PHP_MAJOR_VERSION >= 7 && PHP_MINOR_VERSION >= 1) ?: die('Sorry, PHP 7.1 or above is required to run XBackBone.');
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);
// default config
$config = [
'base_url' => str_replace('/install/', '', (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http') . "://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]"),
'debug' => true,
'displayErrorDetails' => true,
'db' => [
'connection' => 'sqlite',
'dsn' => realpath(__DIR__ . '/../') . implode(DIRECTORY_SEPARATOR, ['resources', 'database', 'xbackbone.db']),
@ -43,131 +44,242 @@ 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),
$container['session'] = function ($container) {
return new Session('xbackbone_session');
};
$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'],
]);
$builder->addDefinitions(__DIR__.'/../bootstrap/container.php');
$app = Bridge::create($builder->build());
$app->setBasePath(parse_url($config['base_url'].'/install', PHP_URL_PATH));
$app->addRoutingMiddleware();
// 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('PLATFORM_VERSION', PLATFORM_VERSION);
return $view;
};
$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.');
}
};
function migrate($config) {
$firstMigrate = false;
if ($config['db']['connection'] === 'sqlite' && !file_exists(__DIR__ . '/../' . $config['db']['dsn'])) {
touch(__DIR__ . '/../' . $config['db']['dsn']);
$firstMigrate = true;
}
try {
DB::doQuery('SELECT 1 FROM `migrations` LIMIT 1');
} catch (PDOException $exception) {
$firstMigrate = true;
}
if ($firstMigrate) {
DB::raw()->exec(file_get_contents(__DIR__ . '/../resources/schemas/migrations.sql'));
}
$files = glob(__DIR__ . '/../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)]);
}
} catch (PDOException $exception) {
if (!$exists) {
DB::doQuery('INSERT INTO `migrations` VALUES (?,?)', [basename($file), 0]);
}
throw $exception;
}
}
}
$app = new App($container);
$app->get('/', function (Request $request, Response $response) {
$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');
$this->session->alert('The required "gd" extension is not loaded.', 'danger');
}
if (!extension_loaded('intl')) {
$session->alert('The required "intl" extension is not loaded.', 'danger');
$this->session->alert('The required "intl" extension is not loaded.', 'danger');
}
if (!extension_loaded('json')) {
$session->alert('The required "json" extension is not loaded.', 'danger');
}
if (!extension_loaded('fileinfo')) {
$session->alert('The required "fileinfo" extension is not loaded.', 'danger');
$this->session->alert('The required "json" extension is not loaded.', 'danger');
}
if (!is_writable(__DIR__ . '/../resources/cache')) {
$session->alert('The cache folder is not writable ('.__DIR__.'/../resources/cache'.')', 'danger');
$this->session->alert('The cache folder is not writable (' . __DIR__ . '/../resources/cache' . ')', 'danger');
}
if (!is_writable(__DIR__ . '/../resources/database')) {
$session->alert('The database folder is not writable ('.__DIR__.'/../resources/database'.')', 'danger');
$this->session->alert('The database folder is not writable (' . __DIR__ . '/../resources/database' . ')', 'danger');
}
if (!is_writable(__DIR__ . '/../resources/sessions')) {
$session->alert('The sessions folder is not writable ('.__DIR__.'/../resources/sessions'.')', 'danger');
$this->session->alert('The sessions folder is not writable (' . __DIR__ . '/../resources/sessions' . ')', 'danger');
}
$installed = file_exists(__DIR__ . '/../config.php');
return $view->render($response, 'install.twig', [
return $this->view->render($response, 'install.twig', [
'installed' => $installed,
]);
})->setName('install');
});
$app->post('/', function (Request $request, Response $response, Filesystem $storage, Session $session, DB $db) use (&$config) {
$app->post('/', function (Request $request, Response $response) use (&$config) {
// Check if there is a previous installation, if not, setup the config file
$installed = true;
// disable debug in production
unset($config['debug']);
if (!file_exists(__DIR__ . '/../config.php')) {
$installed = false;
// 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');
$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'] = 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');
$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'] = param($request, 'storage_token');
$config['storage']['token'] = $request->getParam('storage_token');
break;
case 'ftp':
if (!extension_loaded('ftp')) {
$session->alert('The "ftp" extension is not loaded.', 'danger');
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';
$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'] = param($request, 'storage_project_id');
$config['storage']['key_path'] = param($request, 'storage_key_path');
$config['storage']['bucket'] = param($request, 'storage_bucket');
$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'] = param($request, 'storage_path');
$config['storage']['path'] = $request->getParam('storage_path');
break;
}
}
// check if the storage is valid
$storageTestFile = 'storage_test.xbackbone.txt';
try {
try {
$success = $storage->write($storageTestFile, 'XBACKBONE_TEST_FILE');
$success = $this->storage->write($storageTestFile, 'XBACKBONE_TEST_FILE');
} catch (FileExistsException $fileExistsException) {
$success = $storage->update($storageTestFile, 'XBACKBONE_TEST_FILE');
$success = $this->storage->update($storageTestFile, 'XBACKBONE_TEST_FILE');
}
if (!$success) {
throw new Exception('The storage is not writable.');
}
$storage->readAndDelete($storageTestFile);
$this->storage->readAndDelete($storageTestFile);
} catch (Exception $e) {
$session->alert("Storage setup error: {$e->getMessage()} [{$e->getCode()}]", 'danger');
$this->session->alert("Storage setup error: {$e->getMessage()} [{$e->getCode()}]", 'danger');
return redirect($response, '/install');
}
return redirect($response, urlFor('/install'));
$ret = file_put_contents(__DIR__ . '/../config.php', '<?php' . PHP_EOL . 'return ' . var_export($config, true) . ';');
if ($ret === false) {
$this->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
@ -178,66 +290,44 @@ $app->post('/', function (Request $request, Response $response, Filesystem $stor
unset($config['storage_dir']);
}
// 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;
}
$migrator = new Migrator($db, __DIR__.'/../resources/schemas', $firstMigrate);
$migrator->migrate();
$migrator->reSyncQuotas($storage);
$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) {
$session->alert("Cannot connect to the database: {$e->getMessage()} [{$e->getCode()}]", 'danger');
return redirect($response, urlFor('/install'));
$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->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)]);
}
// 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 upgrading and existing installation, put it out maintenance
if ($installed) {
unset($config['maintenance']);
// remove old config from old versions
unset($config['lang']);
unset($config['displayErrorDetails']);
}
// Finally write the config
$ret = file_put_contents(__DIR__.'/../config.php', '<?php'.PHP_EOL.'return '.var_export($config, true).';');
if ($ret === false) {
$session->alert('The config folder is not writable ('.__DIR__.'/../config.php'.')', 'danger');
return redirect($response, '/install');
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');
if (!isset($config['debug']) || !$config['debug']) {
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', '<?php' . PHP_EOL . 'return ' . var_export($config, true) . ';');
if ($ret === false) {
$this->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 redirect($response, urlFor('/?afterInstall=true'));
session_destroy();
return $response->withRedirect("{$config['base_url']}/?afterInstall=true");
});
$app->run();

View file

@ -14,7 +14,7 @@
<script src="../static/bootstrap/js/bootstrap.bundle.min.js"></script>
<script src="installer.js"></script>
</head>
<body class="bg-light">
<body>
<div class="container">
<div class="mt-4">
{% include 'comp/alert.twig' %}
@ -24,7 +24,7 @@
<div class="card mt-3">
<div class="card-header">Install XBackBone</div>
<div class="card-body">
<form method="post" onsubmit="$('#modalLoading').modal({backdrop: 'static', keyboard: false})">
<form method="post" action="">
{% if not installed %}
<div class="form-group row">
<label for="base_url" class="col-sm-3 col-form-label">Base URL</label>
@ -184,7 +184,7 @@
<div class="form-group row justify-content-md-end">
<div class="col-sm-9">
<button type="submit" class="btn btn-outline-success">
<button type="submit" class="btn btn-outline-success" onsubmit="$('#modalLoading').modal({backdrop: 'static', keyboard: false})">
<i class="fas fa-save fa-fw"></i> Install
</button>
</div>
@ -193,7 +193,7 @@
{% else %}
<div class="form-group row">
<div class="col-sm-12 d-flex justify-content-center">
<button type="submit" class="btn btn-lg btn-outline-primary">
<button type="submit" class="btn btn-lg btn-outline-primary" onsubmit="$('#modalLoading').modal({backdrop: 'static', keyboard: false})">
<i class="fas fa-sync fa-fw"></i> Update database
</button>
</div>
@ -205,7 +205,7 @@
</div>
</div>
</div>
<div class="modal" id="modalLoading" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal fade" id="modalLoading" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header">

615
package-lock.json generated
View file

@ -2,10 +2,32 @@
"requires": true,
"lockfileVersion": 1,
"dependencies": {
"@babel/runtime": {
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.6.0.tgz",
"integrity": "sha512-89eSBLJsxNxOERC0Op4vd+0Bqm6wRMqMbFtV3i0/fbaWw/mJ8Q3eBvgX0G4SyrOOLCtbu98HspF8o09MRT+KzQ==",
"requires": {
"regenerator-runtime": "^0.13.2"
}
},
"@fortawesome/fontawesome-free": {
"version": "5.13.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.13.0.tgz",
"integrity": "sha512-xKOeQEl5O47GPZYIMToj6uuA2syyFlq9EMSl2ui0uytjY9xbe8XS0pexNWmxrdcCyNGyDmLyYw5FtKsalBUeOg=="
"version": "5.10.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.10.2.tgz",
"integrity": "sha512-9pw+Nsnunl9unstGEHQ+u41wBEQue6XPBsILXtJF/4fNN1L3avJcMF/gGF86rIjeTAgfLjTY9ndm68/X4f4idQ=="
},
"@videojs/http-streaming": {
"version": "1.10.3",
"resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-1.10.3.tgz",
"integrity": "sha512-fxXtwVrQBdhOFh6GymPAPCb4utCI01Zs5fdyZgtR6FSsaz/zGmnzfNS5GvNjBi/hZviMsbNPFaOTTFMMNLNA3A==",
"requires": {
"aes-decrypter": "3.0.0",
"global": "^4.3.0",
"m3u8-parser": "4.3.0",
"mpd-parser": "0.8.1",
"mux.js": "5.1.3",
"url-toolkit": "^2.1.3",
"video.js": "^6.8.0 || ^7.0.0"
}
},
"abbrev": {
"version": "1.1.1",
@ -13,6 +35,16 @@
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
"dev": true
},
"aes-decrypter": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-3.0.0.tgz",
"integrity": "sha1-eEihwUW5/b9Xrj4rWxvHzwZEqPs=",
"requires": {
"commander": "^2.9.0",
"global": "^4.3.2",
"pkcs7": "^1.0.2"
}
},
"ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
@ -103,14 +135,9 @@
}
},
"bootstrap": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.4.1.tgz",
"integrity": "sha512-tbx5cHubwE6e2ZG7nqM3g/FZ5PQEDMWmMGNrCUBVRPHXTJaH7CBDdsLeu3eCh3B1tzAxTnAbtmrzvWEvT2NNEA=="
},
"bootstrap4-toggle": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/bootstrap4-toggle/-/bootstrap4-toggle-3.6.1.tgz",
"integrity": "sha512-eRejcTc9YurhZ64nHY9Ii9DQn+F9/R74H9RPoeANVM3N1+C2lZ2tUuFCx1w3orOJ1y/iG4A7iCwdDZphMDIrYg=="
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.3.1.tgz",
"integrity": "sha512-rXqOmH1VilAt2DyPzluTi2blhk17bO7ef+zLLPlWvG494pDxcM234pJ8wTc/6R40UWizAIIMgxjvxZg5kmsbag=="
},
"brace-expansion": {
"version": "1.1.11",
@ -122,6 +149,12 @@
"concat-map": "0.0.1"
}
},
"builtin-modules": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
"integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=",
"dev": true
},
"bytes": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-1.0.0.tgz",
@ -191,9 +224,9 @@
}
},
"clipboard": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.6.tgz",
"integrity": "sha512-g5zbiixBRk/wyKakSwCKd7vQXDjFnAMGHoEyBogG/bw9kTD9GvdAvaoRR1ALcEzt3pVKxZR0pViekPMIS0QyGg==",
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.4.tgz",
"integrity": "sha512-Vw26VSLRpJfBofiVaFb/I8PVfdI1OxKcYShe6fm0sP/DtmiWQNCjhM/okTvdCo0G+lMMm1rMYbk4IK4x1X+kgQ==",
"requires": {
"good-listener": "^1.2.2",
"select": "^1.1.2",
@ -227,6 +260,11 @@
"integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=",
"dev": true
},
"commander": {
"version": "2.20.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz",
"integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ=="
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -248,11 +286,6 @@
"integrity": "sha1-vXJ6f67XfnH/OYWskzUakSczrQ8=",
"dev": true
},
"core-js": {
"version": "3.6.4",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.4.tgz",
"integrity": "sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw=="
},
"core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
@ -268,11 +301,6 @@
"array-find-index": "^1.0.1"
}
},
"custom-event-polyfill": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/custom-event-polyfill/-/custom-event-polyfill-1.0.7.tgz",
"integrity": "sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w=="
},
"date-now": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz",
@ -304,6 +332,14 @@
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
"dev": true
},
"define-properties": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
"integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
"requires": {
"object-keys": "^1.0.12"
}
},
"delegate": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz",
@ -333,6 +369,11 @@
}
}
},
"dom-walk": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.1.tgz",
"integrity": "sha1-ZyIm3HTI95mtNTB9+TaroRrNYBg="
},
"domelementtype": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz",
@ -358,11 +399,6 @@
"domelementtype": "1"
}
},
"dropzone": {
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/dropzone/-/dropzone-5.7.0.tgz",
"integrity": "sha512-kOltiZXH5cO/72I22JjE+w6BoT6uaVLfWdFMsi1PMKFkU6BZWpqRwjnsRm0o6ANGTBuZar5Piu7m/CbKqRPiYg=="
},
"duplexer": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz",
@ -386,26 +422,47 @@
}
},
"error-ex": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
"integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz",
"integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=",
"dev": true,
"requires": {
"is-arrayish": "^0.2.1"
}
},
"es-abstract": {
"version": "1.14.2",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.14.2.tgz",
"integrity": "sha512-DgoQmbpFNOofkjJtKwr87Ma5EW4Dc8fWhD0R+ndq7Oc456ivUfGOOP6oAZTTKl5/CcNMP+EN+e3/iUzgE0veZg==",
"requires": {
"es-to-primitive": "^1.2.0",
"function-bind": "^1.1.1",
"has": "^1.0.3",
"has-symbols": "^1.0.0",
"is-callable": "^1.1.4",
"is-regex": "^1.0.4",
"object-inspect": "^1.6.0",
"object-keys": "^1.1.1",
"string.prototype.trimleft": "^2.0.0",
"string.prototype.trimright": "^2.0.0"
}
},
"es-to-primitive": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz",
"integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==",
"requires": {
"is-callable": "^1.1.4",
"is-date-object": "^1.0.1",
"is-symbol": "^1.0.2"
}
},
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
"dev": true
},
"esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"dev": true
},
"eventemitter2": {
"version": "0.4.14",
"resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz",
@ -477,12 +534,25 @@
}
}
},
"for-each": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
"integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
"requires": {
"is-callable": "^1.1.3"
}
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true
},
"function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
},
"gaze": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz",
@ -518,6 +588,15 @@
"path-is-absolute": "^1.0.0"
}
},
"global": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/global/-/global-4.3.2.tgz",
"integrity": "sha1-52mJJopsdMOJCLEwWxD8DjlOnQ8=",
"requires": {
"min-document": "^2.19.0",
"process": "~0.5.1"
}
},
"globule": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz",
@ -554,15 +633,15 @@
}
},
"graceful-fs": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz",
"integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==",
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
"integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=",
"dev": true
},
"grunt": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/grunt/-/grunt-1.1.0.tgz",
"integrity": "sha512-+NGod0grmviZ7Nzdi9am7vuRS/h76PcWDsV635mEXF0PEQMUV6Kb+OjTdsVxbi0PZmfQOjCMKb3w8CVZcqsn1g==",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/grunt/-/grunt-1.0.4.tgz",
"integrity": "sha512-PYsMOrOC+MsdGEkFVwMaMyc6Ob7pKmq+deg1Sjr+vvMWp35sztfwKE7qoN51V+UEtHsyNuMcGdgMLFkBHvMxHQ==",
"dev": true,
"requires": {
"coffeescript": "~1.10.0",
@ -576,14 +655,20 @@
"grunt-legacy-log": "~2.0.0",
"grunt-legacy-util": "~1.1.1",
"iconv-lite": "~0.4.13",
"js-yaml": "~3.13.1",
"js-yaml": "~3.13.0",
"minimatch": "~3.0.2",
"mkdirp": "~1.0.3",
"mkdirp": "~0.5.1",
"nopt": "~3.0.6",
"path-is-absolute": "~1.0.0",
"rimraf": "~2.6.2"
},
"dependencies": {
"esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"dev": true
},
"grunt-cli": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.2.0.tgz",
@ -596,11 +681,15 @@
"resolve": "~1.1.0"
}
},
"resolve": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz",
"integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=",
"dev": true
"js-yaml": {
"version": "3.13.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
"integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
"dev": true,
"requires": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
}
}
}
},
@ -744,17 +833,17 @@
}
},
"lodash": {
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
"version": "4.17.11",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
"dev": true
}
}
},
"grunt-known-options": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/grunt-known-options/-/grunt-known-options-1.1.1.tgz",
"integrity": "sha512-cHwsLqoighpu7TuYj5RonnEuxGVFnztcUqTqp5rXFGYL4OuPFofwC4Ycg7n9fYwvK6F5WbYgeVOwph9Crs2fsQ==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/grunt-known-options/-/grunt-known-options-1.1.0.tgz",
"integrity": "sha1-pCdO6zL6dl2lp6OxcSYXzjsUQUk=",
"dev": true
},
"grunt-legacy-log": {
@ -847,6 +936,14 @@
"duplexer": "^0.1.1"
}
},
"has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"requires": {
"function-bind": "^1.1.1"
}
},
"has-ansi": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
@ -862,10 +959,15 @@
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
"dev": true
},
"has-symbols": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz",
"integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q="
},
"highlightjs": {
"version": "9.16.2",
"resolved": "https://registry.npmjs.org/highlightjs/-/highlightjs-9.16.2.tgz",
"integrity": "sha512-FK1vmMj8BbEipEy8DLIvp71t5UsC7n2D6En/UfM/91PCwmOpj6f2iu0Y0coRC62KSRHHC+dquM2xMULV/X7NFg=="
"version": "9.12.0",
"resolved": "https://registry.npmjs.org/highlightjs/-/highlightjs-9.12.0.tgz",
"integrity": "sha512-eAhWMtDZaOZIQdxIP4UEB1vNp/CVXQPdMSihTSuaExhFIRC0BVpXbtP3mTP1hDoGOyh7nbB3cuC3sOPhG5wGDA=="
},
"hooker": {
"version": "0.2.3",
@ -874,9 +976,9 @@
"dev": true
},
"hosted-git-info": {
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
"integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==",
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.6.0.tgz",
"integrity": "sha512-lIbgIIQA3lz5XaB6vxakj6sDHADJiZadYEJB+FgA+C4nubM1NwcuvUr9EJPmnH1skZqpqUzWborWo8EIUi0Sdw==",
"dev": true
},
"htmlparser2": {
@ -899,9 +1001,9 @@
"dev": true
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"version": "0.4.23",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
"integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==",
"dev": true,
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
@ -916,6 +1018,11 @@
"repeating": "^2.0.0"
}
},
"individual": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/individual/-/individual-2.0.0.tgz",
"integrity": "sha1-gzsJfa0jKU52EXqY+zjg2a1hu5c="
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@ -938,11 +1045,54 @@
"integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
"dev": true
},
"is-builtin-module": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz",
"integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=",
"dev": true,
"requires": {
"builtin-modules": "^1.0.0"
}
},
"is-callable": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz",
"integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA=="
},
"is-date-object": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz",
"integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY="
},
"is-finite": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz",
"integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==",
"dev": true
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz",
"integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=",
"dev": true,
"requires": {
"number-is-nan": "^1.0.0"
}
},
"is-function": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.1.tgz",
"integrity": "sha1-Es+5i2W1fdPRk6MSH19uL0N2ArU="
},
"is-regex": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz",
"integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=",
"requires": {
"has": "^1.0.1"
}
},
"is-symbol": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz",
"integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==",
"requires": {
"has-symbols": "^1.0.0"
}
},
"is-utf8": {
"version": "0.2.1",
@ -967,16 +1117,6 @@
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.4.1.tgz",
"integrity": "sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw=="
},
"js-yaml": {
"version": "3.13.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
"integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
"dev": true,
"requires": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
}
},
"jszip": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-2.5.0.tgz",
@ -986,6 +1126,11 @@
"pako": "~0.2.5"
}
},
"keycode": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/keycode/-/keycode-2.2.0.tgz",
"integrity": "sha1-PQr1bce4uOXLqNCpfxByBO7CKwQ="
},
"livereload-js": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-2.3.0.tgz",
@ -1017,11 +1162,6 @@
"strip-bom": "^2.0.0"
}
},
"loadjs": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/loadjs/-/loadjs-4.2.0.tgz",
"integrity": "sha512-AgQGZisAlTPbTEzrHPb6q+NYBMD+DP9uvGSIjSUM5uG+0jG15cb8axWpxuOIqrmQjn6scaaH8JwloiP27b2KXA=="
},
"locate-path": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
@ -1041,9 +1181,9 @@
}
},
"lodash": {
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
"version": "4.17.11",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
"dev": true
},
"loud-rejection": {
@ -1056,6 +1196,14 @@
"signal-exit": "^3.0.0"
}
},
"m3u8-parser": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-4.3.0.tgz",
"integrity": "sha512-bVbjuBMoVIgFL1vpXVIxjeaoB5TPDJRb0m5qiTdM738SGqv/LAmsnVVPlKjM4fulm/rr1XZsKM+owHm+zvqxYA==",
"requires": {
"global": "^4.3.2"
}
},
"map-obj": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz",
@ -1119,6 +1267,14 @@
"trim-newlines": "^1.0.0"
}
},
"min-document": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz",
"integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=",
"requires": {
"dom-walk": "^0.1.0"
}
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
@ -1129,16 +1285,36 @@
}
},
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"dev": true
},
"mkdirp": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.3.tgz",
"integrity": "sha512-6uCP4Qc0sWsgMLy1EOqqS/3rjDHOEnsStVr/4vtAIK2Y5i2kA7lFFejYrpIyiN9w0pYf4ckeCYT9f1r1P9KX5g==",
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"requires": {
"minimist": "0.0.8"
},
"dependencies": {
"minimist": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
"dev": true
}
}
},
"mpd-parser": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-0.8.1.tgz",
"integrity": "sha512-WBTJ1bKk8OLUIxBh6s1ju1e2yz/5CzhPbgi6P3F3kJHKhGy1Z+ElvEnuzEbtC/dnbRcJtMXazE3f93N5LLdp9Q==",
"requires": {
"global": "^4.3.2",
"url-toolkit": "^2.1.1"
}
},
"ms": {
"version": "2.0.0",
@ -1158,6 +1334,11 @@
"minimatch": "^3.0.0"
}
},
"mux.js": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/mux.js/-/mux.js-5.1.3.tgz",
"integrity": "sha512-FhDcysLvAkO9H8ftBJ2sK1O4Rmz0AWnMS+2uqP7WjrnaAyE/ox11GEiZkRzrWIdp8at9R9qBHDqdURY3/h/xTg=="
},
"nopt": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
@ -1168,13 +1349,13 @@
}
},
"normalize-package-data": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
"integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
"integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==",
"dev": true,
"requires": {
"hosted-git-info": "^2.1.4",
"resolve": "^1.10.0",
"is-builtin-module": "^1.0.0",
"semver": "2 || 3 || 4 || 5",
"validate-npm-package-license": "^3.0.1"
}
@ -1200,6 +1381,16 @@
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
"dev": true
},
"object-inspect": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.6.0.tgz",
"integrity": "sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ=="
},
"object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@ -1239,6 +1430,15 @@
"integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=",
"dev": true
},
"parse-headers": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.2.tgz",
"integrity": "sha512-/LypJhzFmyBIDYP9aDVgeyEb5sQfbfY5mnDq4hVhlQ69js87wXfmEI5V3xI6vvXasqebp0oCytYFLxsBVfCzSg==",
"requires": {
"for-each": "^0.3.3",
"string.prototype.trim": "^1.1.2"
}
},
"parse-json": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
@ -1269,12 +1469,6 @@
"integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
"dev": true
},
"path-parse": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
"dev": true
},
"path-type": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz",
@ -1307,6 +1501,11 @@
"pinkie": "^2.0.0"
}
},
"pkcs7": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/pkcs7/-/pkcs7-1.0.2.tgz",
"integrity": "sha1-ttulJ1KMKUK/wSLOLa/NteWQdOc="
},
"pkg-up": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-2.0.0.tgz",
@ -1327,22 +1526,10 @@
}
}
},
"plyr": {
"version": "3.5.10",
"resolved": "https://registry.npmjs.org/plyr/-/plyr-3.5.10.tgz",
"integrity": "sha512-wbbSuzk3yKVOmYWQUnxG1bxikqZNkxZmL3OjS1DFVU0D2Uko1evGY72LuD9rm/HnNCNzcTuc0c6MCn7bRRpUTA==",
"requires": {
"core-js": "^3.6.4",
"custom-event-polyfill": "^1.0.7",
"loadjs": "^4.2.0",
"rangetouch": "^2.0.0",
"url-polyfill": "^1.1.8"
}
},
"popper.js": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
"integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ=="
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.15.0.tgz",
"integrity": "sha512-w010cY1oCUmI+9KwwlWki+r5jxKfTFDVoadl7MSrIujHU5MJ5OR6HTDj6Xo8aoR/QsA56x8jKjA59qGH4ELtrA=="
},
"pretty-bytes": {
"version": "3.0.1",
@ -1353,17 +1540,17 @@
"number-is-nan": "^1.0.0"
}
},
"process": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/process/-/process-0.5.2.tgz",
"integrity": "sha1-FjjYqONML0QKkduVq5rrZ3/Bhc8="
},
"qs": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
"dev": true
},
"rangetouch": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/rangetouch/-/rangetouch-2.0.0.tgz",
"integrity": "sha512-y66wTFbwh7KafYligRsmIYYR1kZY8U9tGHH9PgbVhBUFmGzPMsOSjslXPedgR5D3M9W1QKVbAf1AtaVAt7JJTw=="
},
"raw-body": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-1.1.7.tgz",
@ -1417,6 +1604,11 @@
"strip-indent": "^1.0.1"
}
},
"regenerator-runtime": {
"version": "0.13.3",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz",
"integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw=="
},
"repeating": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz",
@ -1427,13 +1619,10 @@
}
},
"resolve": {
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz",
"integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==",
"dev": true,
"requires": {
"path-parse": "^1.0.6"
}
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz",
"integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=",
"dev": true
},
"resolve-from": {
"version": "2.0.0",
@ -1451,28 +1640,20 @@
}
},
"rimraf": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
"integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz",
"integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==",
"dev": true,
"requires": {
"glob": "^7.1.3"
"glob": "^7.0.5"
}
},
"dependencies": {
"glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"dev": true,
"rust-result": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/rust-result/-/rust-result-1.0.0.tgz",
"integrity": "sha1-NMdbLm3Dn+WHXlveyFteD5FTb3I=",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
}
"individual": "^2.0.0"
}
},
"safe-json-parse": {
@ -1493,9 +1674,9 @@
"integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0="
},
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
"integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==",
"dev": true
},
"shelljs": {
@ -1505,9 +1686,9 @@
"dev": true
},
"signal-exit": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==",
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
"dev": true
},
"source-map": {
@ -1517,9 +1698,9 @@
"dev": true
},
"spdx-correct": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz",
"integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz",
"integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==",
"dev": true,
"requires": {
"spdx-expression-parse": "^3.0.0",
@ -1543,9 +1724,9 @@
}
},
"spdx-license-ids": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz",
"integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.3.tgz",
"integrity": "sha512-uBIcIl3Ih6Phe3XHK1NqboJLdGfwr1UN3k6wSD1dZpmPsIkb8AGNbZYJ1fOBk834+Gxy8rpfDxrS6XLEMZMY2g==",
"dev": true
},
"sprintf-js": {
@ -1560,6 +1741,34 @@
"integrity": "sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0=",
"dev": true
},
"string.prototype.trim": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.0.tgz",
"integrity": "sha512-9EIjYD/WdlvLpn987+ctkLf0FfvBefOCuiEr2henD8X+7jfwPnyvTdmW8OJhj5p+M0/96mBdynLWkxUr+rHlpg==",
"requires": {
"define-properties": "^1.1.3",
"es-abstract": "^1.13.0",
"function-bind": "^1.1.1"
}
},
"string.prototype.trimleft": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz",
"integrity": "sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw==",
"requires": {
"define-properties": "^1.1.3",
"function-bind": "^1.1.1"
}
},
"string.prototype.trimright": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz",
"integrity": "sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg==",
"requires": {
"define-properties": "^1.1.3",
"function-bind": "^1.1.1"
}
},
"string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
@ -1609,9 +1818,9 @@
}
},
"tiny-emitter": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.0.2.tgz",
"integrity": "sha512-2NM0auVBGft5tee/OxP4PI3d8WItkDM+fPnaRAVo6xTDI2knbz9eC5ArWGqtGlYqiH3RU5yMpdyTTO7MguC4ow=="
},
"tiny-lr": {
"version": "1.1.1",
@ -1628,9 +1837,9 @@
}
},
"tooltip.js": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tooltip.js/-/tooltip.js-1.3.3.tgz",
"integrity": "sha512-XWWuy/dBdF/F/YpRE955yqBZ4VdLfiTAUdOqoU+wJm6phJlMpEzl/iYHZ+qJswbeT9VG822bNfsETF9wzmoy5A==",
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/tooltip.js/-/tooltip.js-1.3.2.tgz",
"integrity": "sha512-DeDr9JxYx/lSvQ53ZCRFLxXrmrSyU3fLz6k+ITUTw69AIYtpWij/NmOJQscJ7BwY5lcEwWJWSfqqQWVvTMYZiw==",
"requires": {
"popper.js": "^1.0.2"
}
@ -1641,6 +1850,11 @@
"integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=",
"dev": true
},
"tsml": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/tsml/-/tsml-1.0.1.tgz",
"integrity": "sha1-ifghi52eJX9H1/a1bQHFpNLGj8M="
},
"underscore.string": {
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.5.tgz",
@ -1657,10 +1871,10 @@
"integrity": "sha1-l0fwGDWJM8Md4PzP2C0TjmcmLjI=",
"dev": true
},
"url-polyfill": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/url-polyfill/-/url-polyfill-1.1.8.tgz",
"integrity": "sha512-Ey61F4FEqhcu1vHSOMmjl0Vd/RPRLEjMj402qszD/dhMBrVfoUsnIj8KSZo2yj+eIlxJGKFdnm6ES+7UzMgZ3Q=="
"url-toolkit": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/url-toolkit/-/url-toolkit-2.1.6.tgz",
"integrity": "sha512-UaZ2+50am4HwrV2crR/JAf63Q4VvPYphe63WGeoJxeu8gmOm0qxPt+KsukfakPNrX9aymGNEkkaoICwn+OuvBw=="
},
"util-deprecate": {
"version": "1.0.2",
@ -1669,15 +1883,54 @@
"dev": true
},
"validate-npm-package-license": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
"integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz",
"integrity": "sha512-63ZOUnL4SIXj4L0NixR3L1lcjO38crAbgrTpl28t8jjrfuiOBL5Iygm+60qPs/KsZGzPNg6Smnc/oY16QTjF0g==",
"dev": true,
"requires": {
"spdx-correct": "^3.0.0",
"spdx-expression-parse": "^3.0.0"
}
},
"video.js": {
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/video.js/-/video.js-7.6.0.tgz",
"integrity": "sha512-A0HSKzAmcYkd1xyExqUlM6n8bkghcX54iCvW08bPvvl3UHt8d8zijuylfIWu8vo1Z8fYyk9HPOFs1i3Cldr/cw==",
"requires": {
"@babel/runtime": "^7.4.5",
"@videojs/http-streaming": "1.10.3",
"global": "4.3.2",
"keycode": "^2.2.0",
"safe-json-parse": "4.0.0",
"tsml": "1.0.1",
"videojs-font": "3.2.0",
"videojs-vtt.js": "^0.14.1",
"xhr": "2.4.0"
},
"dependencies": {
"safe-json-parse": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/safe-json-parse/-/safe-json-parse-4.0.0.tgz",
"integrity": "sha1-fA9XjPzNEtM6ccDgVBPi7KFx6qw=",
"requires": {
"rust-result": "^1.0.0"
}
}
}
},
"videojs-font": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-3.2.0.tgz",
"integrity": "sha512-g8vHMKK2/JGorSfqAZQUmYYNnXmfec4MLhwtEFS+mMs2IDY398GLysy6BH6K+aS1KMNu/xWZ8Sue/X/mdQPliA=="
},
"videojs-vtt.js": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/videojs-vtt.js/-/videojs-vtt.js-0.14.1.tgz",
"integrity": "sha512-YxOiywx6N9t3J5nqsE5WN2Sw4CSqVe3zV+AZm2T4syOc2buNJaD6ZoexSdeszx2sHLU/RRo2r4BJAXFDQ7Qo2Q==",
"requires": {
"global": "^4.3.1"
}
},
"websocket-driver": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.0.tgz",
@ -1709,11 +1962,21 @@
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true
},
"xhr": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/xhr/-/xhr-2.4.0.tgz",
"integrity": "sha1-4W5mpF+GmGHu76tBbV7/ci3ECZM=",
"requires": {
"global": "~4.3.0",
"is-function": "^1.0.1",
"parse-headers": "^2.0.0",
"xtend": "^4.0.0"
}
},
"xtend": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
"integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=",
"dev": true
"integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68="
}
}
}

View file

@ -1,18 +1,16 @@
{
"dependencies": {
"@fortawesome/fontawesome-free": "^5.13.0",
"bootstrap": "^4.4.1",
"bootstrap4-toggle": "^3.6.1",
"clipboard": "^2.0.6",
"dropzone": "^5.7.0",
"highlightjs": "^9.16.2",
"@fortawesome/fontawesome-free": "^5.10.2",
"bootstrap": "^4.3.1",
"clipboard": "^2.0.4",
"highlightjs": "^9.12.0",
"jquery": "^3.4.1",
"plyr": "^3.5.10",
"popper.js": "^1.16.1",
"tooltip.js": "^1.3.3"
"popper.js": "^1.15.0",
"tooltip.js": "^1.3.2",
"video.js": "^7.6.0"
},
"devDependencies": {
"grunt": "^1.1.0",
"grunt": "^1.0.4",
"grunt-contrib-copy": "^1.0.0",
"grunt-contrib-cssmin": "^3.0.0",
"grunt-contrib-jshint": "^2.1.0",

View file

@ -1,142 +0,0 @@
<?php
return [
'lang' => 'Arabian',
'yes' => 'نعم',
'no' => 'لا',
'send' => 'إرسال',
'no_media' => 'لا توجد أي وسائط.',
'login.username' => 'اسم المستخدم أو البريد الإلكتروني',
'password' => 'كلمة المرور',
'login' => 'تسجيل الدخول',
'username' => 'اسم المستخدم',
'home' => 'الرئيسية',
'users' => 'مستخدمون',
'system' => 'نظام',
'profile' => 'ملف شخصي',
'logout' => 'تسجيل الخروج',
'pager.next' => 'التالي',
'pager.previous' => 'السابق',
'copy_link' => 'نسخ الرابط',
'public.telegram' => 'مشاركة عبر تلغرام',
'public.delete_text' => 'هل تريد بالتأكيد حذف هذا العنصر؟ سيحذف نهائيا!',
'preview' => 'السابق',
'filename' => 'اسم الملف',
'size' => 'حجم',
'public' => 'عام',
'owner' => 'مالك',
'date' => 'تاريخ',
'raw' => 'عرض الخام',
'download' => 'تنزيل',
'upload' => 'رفع',
'delete' => 'حذف',
'publish' => 'نشر',
'hide' => 'إخفاء',
'files' => 'ملفات',
'theme' => 'سمة',
'click_to_load' => 'انقر للتحميل...',
'apply' => 'تطبيق',
'save' => 'حفظ',
'used' => 'مستخدَم',
'system_settings' => 'إعدادات النظام',
'user.create' => 'إنشاء مستخدم',
'user.edit' => 'تعديل مستخدم',
'is_active' => 'نشط؟',
'is_admin' => 'مسؤول؟',
'your_profile' => 'ملفك الشخصي',
'copy' => 'نسخ',
'update' => 'تحديث',
'edit' => 'تعديل',
'user_code' => 'رمز المستخدم',
'active' => 'نشط',
'admin' => 'مسؤول',
'reg_date' => 'تاريخ التسجيل',
'none' => 'بلا',
'open' => 'فتح',
'confirm' => 'تأكيد',
'confirm_string' => 'هل أنت متأكد؟',
'installed' => 'اكتمل التنصيب بنجاح!',
'bad_login' => 'اعتمادات خاطئة.',
'account_disabled' => 'حسابك معطل.',
'welcome' => 'مرحبا، %s!',
'goodbye' => 'وداعا!',
'email_required' => 'البريد الإلكتروني مطلوب.',
'email_taken' => 'البريد الإلكتروني قيد الاستخدام.',
'username_required' => 'اسم المستخدم مطلوب.',
'username_taken' => 'اسم المستخدم قيد الاستخدام.',
'password_required' => 'كلمة المرور مطلوبة.',
'user_created' => 'المستخدم "%s" أنشئ!',
'user_updated' => 'المستخدم "%s" تم تحديثه!',
'profile_updated' => 'تم تحديث الملف الشخصي بنجاح!',
'user_deleted' => 'تم حذف المستخدم.',
'cannot_delete' => 'لا يمكنك حذف نفسك.',
'cannot_write_file' => 'مسار الوجهة غير قابل للكتابة.',
'switch_to' => 'تبديل إلى',
'gallery' => 'معرض',
'table' => 'جدول',
'dotted_search' => 'بحث...',
'order_by' => 'ترتيب حسب...',
'time' => 'وقت',
'name' => 'اسم',
'maintenance' => 'صيانة',
'path_not_writable' => 'مسار الإخراج غير قابل للكتابة.',
'already_latest_version' => 'لديك الإصدار الأخير حاليا.',
'new_version_available' => 'إصدار جديد %s متوفر!',
'cannot_retrieve_file' => 'لا يمكن استلام الملف.',
'file_size_no_match' => 'الملف المنزل لا يطابق الحجم الصحيح.',
'check_for_updates' => 'تحقق من التحديثات',
'upgrade' => 'ترقية',
'updates' => 'تحديثات',
'cancel' => 'إلغاء',
'auto_set' => 'حدد تلقائيا',
'prerelease_channel' => 'قناة إصدار تجريبي',
'drop_to_upload' => 'انقر أو اسحب ملفاتك هنا للرفع.',
'donation' => 'تبرع',
'remember_me' => 'تذكرني',
'please_wait' => 'انتظر رجاء…',
'dont_close' => 'لا تغلق هذا اللسان حتى الاكتمال.',
'register_enabled' => 'التسجيلات مفعلة',
'hide_by_default' => 'أخف الوسائط افتراضيا',
'settings_saved' => 'تم حفظ إعدادات النظام!',
'export_data' => 'استخراج البيانات',
'password_recovery' => 'استعادة كلمة المرور',
'no_account' => 'لا تملك حسابا؟',
'register' => 'تسجيل',
'register_success' => 'تم إنشاء الحساب، تم إرسال رسالة تأكيد إلكترونية.',
'mail.activate_text' => 'مرحبا %s!
شكرات لإنشاء حسابك %s (%s)، انقر على الرابط لتفعيله:
%s',
'mail.activate_account' => '%s - تفعيل الحساب',
'mail.recover_text' => 'مرحبا %
تم طلب استعادة كلمة المرور لهذا الحساب. لإكمال العملية انقر على الرابط التالي:
%s
إذا لم تكن من طلب استعادة كلمة المرور، تجاهل هذه الرسالة.',
'mail.recover_password' => '%s - استعادة كلمة المرور',
'recover_email_sent' => 'إن وجد، سيتم إرسال رسالة إلكترونية لاستعادة كلمة المرور إلى الحساب المحدد.',
'account_activated' => 'تم تفعيل الحساب، الآن يمكنك الدخول!',
'password_repeat' => 'تكرار كلمة المرور',
'password_match' => 'يجب أن تتطابق كلمة المرور وتكراره كلمة المرور.',
'password_restored' => 'استعادة كلمة المرور.',
'used_space' => 'مساحة مستخدمة',
'delete_selected' => 'حذف المحدد',
'delete_all' => 'حذف الكل',
'clear_account' => 'مسح الحساب',
'account_media_deleted' => 'كل الوسائط في الحساب تم حذفها.',
'danger_zone' => 'منطقة خطرة',
'send_notification' => 'إرسال رسالة إشعار إلكترونية',
'mail.new_account' => '%s - إنشاء حساب جديد',
'mail.new_account_text_with_reset' => 'مرحبا %s!
تم إنشائ حساب جديد لك على %s (%s)، انقر على الرابط التالي لتحديد كلمة مرور وتفعيله:
%s',
'mail.new_account_text_with_pw' => 'مرحبا %s!
تم إنشاء حساب جديد على %s (%s)، بالاعتمادات التالية:
اسم المستخدم: %s
كلمة المرور: %s
انقر على الرابط التالي للذهاب إلى صفحة تسجيل الدخول:
%s',
];

View file

@ -1,5 +1,4 @@
<?php
return [
'lang' => 'Bulgarian',
'enforce_language' => 'Изберете език',

View file

@ -1,47 +0,0 @@
<?php
return [
'lang' => 'Danish',
'yes' => 'Ja',
'no' => 'Nej',
'send' => 'Send',
'no_media' => 'Intet media fundet.',
'login.username' => 'Brugernavn eller E-Mail',
'password' => 'Adgangskode',
'login' => 'Log på',
'username' => 'Brugernavn',
'home' => 'Hjem',
'users' => 'Brugere',
'system' => 'System',
'profile' => 'Profil',
'logout' => 'Log ud',
'pager.next' => 'Næste',
'pager.previous' => 'Forrige',
'copy_link' => 'Kopier link',
'public.telegram' => 'Del på Telegram',
'public.delete_text' => 'Er du sikker på du vil slette denne genstand? Det vil blive slettet permanent!',
'preview' => 'Forhåndsvisning',
'filename' => 'Filnavn',
'size' => 'Størrelse',
'public' => 'Offentligt',
'owner' => 'Ejer',
'date' => 'Dato',
'raw' => 'Vis rå',
'download' => 'Hent',
'delete' => 'Slet',
'publish' => 'Offentliggøre',
'hide' => 'Skjul',
'files' => 'Filer',
'orphaned_files' => 'Forældreløse filer',
'theme' => 'Tema',
'click_to_load' => 'Klik for at indlæse...',
'apply' => 'Anvend',
'save' => 'Gem',
'used' => 'Brugt',
'system_info' => 'System information',
'user.create' => 'Ny bruger',
'user.edit' => 'Rediger bruger',
'is_active' => 'Er aktiv',
'is_admin' => 'Er administrator',
'your_profile' => 'Din Profil',
];

View file

@ -21,7 +21,7 @@ return [
'public.delete_text' => 'Möchtest du das wirklich löschen? Es wird dann für immer weg sein!',
'preview' => 'Vorschau',
'filename' => 'Dateiname',
'size' => 'Größe aller Datein',
'size' => 'Größe',
'public' => 'Öffentlich',
'owner' => 'Besitzer',
'date' => 'Datum',
@ -84,10 +84,10 @@ return [
'maintenance' => 'Wartungsarbeiten',
'clean_orphaned_uploads' => 'Leere verwaiste Uploads',
'path_not_writable' => 'Der Speicherort ist nicht beschreibbar.',
'already_latest_version' => 'Du hast bereits die neueste Version.',
'already_latest_version' => 'Sie haben bereits die neueste Version.',
'new_version_available' => 'Neue Version %s ist verfügbar!',
'cannot_retrieve_file' => 'Die Datei kann nicht abgerufen werden.',
'file_size_no_match' => 'Die heruntergeladene Datei stimmt mit der richtigen Dateigröße nicht überein.',
'file_size_no_match' => 'Die heruntergeladene Datei stimmt nicht mit der richtigen Dateigröße überein.',
'check_for_updates' => 'Auf Updates prüfen',
'upgrade' => 'Aktualisierung',
'updates' => 'Updates',
@ -99,79 +99,6 @@ return [
'translated_strings' => 'übersetzte Zeichen',
'total_strings' => 'Übersetzt',
'lang_name' => 'Name von der Sprache',
'default_lang_behavior' => 'XBackBone wird versuchen die Sprache deines Browsers herauszufinden (Standardsprache is Englisch).',
'default_lang_behavior' => 'XBackBone versucht von dein Browser die Sprache herauszufinden (Standard: English).',
'lang_set' => 'Sprache ist jetzt "%s"',
'prerelease_channel' => 'Beta Channel',
'upload' => 'Hochladen',
'no_upload_token' => 'Du hast keinen persönlichen Token. (Erstelle einen und versuche es erneut.)',
'drop_to_upload' => 'Hier klicken oder Dateien hierher ziehen.',
'donation' => 'Spenden',
'donate_text' => 'Wenn dir XBackBone gefällt und du die Entwicklung unterstützen möchtest, spende einen kleinen Beitrag!',
'custom_head_html' => 'Eigenes HTML Head Content',
'custom_head_html_hint' => 'Dieser Inhalt wird auf jeder Seite am Tag <head> hinzugefügt.',
'custom_head_set' => 'Benutzerdefinierter HTML Head wurde angewendet.',
'remember_me' => 'Erinnere mich daran',
'please_wait' => 'Bitte warten…',
'dont_close' => 'Schließe diesen Tab erst wenn fertig.',
'php_info' => 'PHP Informationen',
'system_settings' => 'Systemeinstellungen',
'register_enabled' => 'Registrierungen aktivieren',
'hide_by_default' => 'Medien standardmäßig ausblenden',
'copy_url_behavior' => 'URL kopieren Mode',
'settings_saved' => 'Systemeinstellungen gespeichert!',
'export_data' => 'Daten exportieren',
'password_recovery' => 'Passwort wiederherstellen',
'no_account' => 'Du hast noch kein Account?',
'register' => 'Registrieren',
'default_user_quota' => 'Standard Speicherplatz',
'invalid_quota' => 'Ungültiger Wert für den Speicherplatz.',
'mail.activate_text' => 'Hey %s!
Vielen Dank, dass Sie Ihr Konto auf %s (%s) erstellt haben. Klicken Sie auf den folgenden Link, um es zu aktivieren:
%s',
'register_success' => 'Das Konto wurde erstellt, eine Bestätigungs-E-Mail wurde gesendet.',
'mail.activate_account' => '%s - Konto Aktivierung',
'mail.recover_text' => 'Hey %s,
Für Ihr Konto wurde ein Zurücksetzen des Passworts angefordert. Um den Vorgang abzuschließen, klicken Sie auf den folgenden Link:
%s
Wenn Sie nicht das Zurücksetzen des Passworts angefordert haben, ignorieren Sie diese E-Mail einfach.',
'mail.recover_password' => '%s - Passwort Zurücksetzung',
'recover_email_sent' => 'Falls vorhanden, wurde eine Wiederherstellungs-E-Mail an das angegebene Konto gesendet.',
'account_activated' => 'Account wurde aktiviert, du kannst dich jetzt einloggen!',
'quota_enabled' => 'Aktiviere Speicherlimit',
'password_repeat' => 'Passwort wiederholen',
'password_match' => 'Das Passwort und das wiederholte Passwort muss das gleiche sein.',
'password_restored' => 'Passwort wurde zurückgesetzt.',
'recalculate_user_quota' => 'Benutzer Speicher wird neu berechnet',
'quota_recalculated' => 'Die Berechnung vom Benutzer Speicher war erfolgreich.',
'used_space' => 'Belegter Speicherplatz',
'max_user_quota' => 'Max. Benutzerkontingent',
'delete_selected' => 'Ausgewähltes löschen',
'delete_all' => 'Alle löschen',
'clear_account' => 'Konto löschen',
'account_media_deleted' => 'Alle Medien in dem Konto wurden gelöscht.',
'danger_zone' => 'Gefahrenzone',
'recaptcha_failed' => 'reCAPTCHA Fehlgeschlagen',
'recaptcha_enabled' => 'reCAPTCHA Aktiviert',
'recaptcha_keys_required' => 'reCAPTCHA-Schlüssel ist erforderlich.',
'only_recaptcha_v3' => 'Es wird nur reCAPTCHA v3 unterstützt.',
'recaptcha_site_key' => 'reCAPTCHA Websiteschlüssel',
'recaptcha_secret_key' => 'reCAPTCHA geheimen Schlüssel',
'send_notification' => 'Mail-Benachrichtigung senden',
'mail.new_account' => '%s - Erstellung von Konto',
'mail.new_account_text_with_reset' => 'Hallo %s!
Ein neues Konto wurde für Sie auf %s (%s) erstellt, klicken Sie auf den folgenden Link, um ein Passwort festzulegen und es zu aktivieren:
%s',
'mail.new_account_text_with_pw' => 'Hallo %s!
Ein neues Konto wurde für Sie auf %s (%s) mit den folgenden Anmeldeinformationen erstellt:
Benutzername: %s
Kennwort: %s
Klicken Sie auf den folgenden Link, um zur Login-Seite zu gelangen:
%s',
'user_create_password' => 'Wenn Sie keine Angaben gemacht haben, können Sie eine Benachrichtigung an die Benutzer-E-Mail senden.',
];

View file

@ -0,0 +1,4 @@
<?php
return [
'lang' => 'Greek',
];

View file

@ -28,7 +28,6 @@ return [
'date' => 'Date',
'raw' => 'Show raw',
'download' => 'Download',
'upload' => 'Upload',
'delete' => 'Delete',
'publish' => 'Publish',
'hide' => 'Hide',
@ -39,8 +38,7 @@ return [
'apply' => 'Apply',
'save' => 'Save',
'used' => 'Used',
'php_info' => 'PHP Informations',
'system_settings' => 'System Settings',
'system_info' => 'System Information',
'user.create' => 'Create User',
'user.edit' => 'Edit User',
'is_active' => 'Is active',
@ -98,60 +96,10 @@ return [
'maintenance_in_progress' => 'Platform under maintenance, try again later...',
'cancel' => 'Cancel',
'auto_set' => 'Set automatically',
'default_lang_behavior' => 'XBackBone will try to match the browser language by default (the fallback is English).',
'translated_strings' => 'translated strings',
'total_strings' => 'total strings',
'lang_name' => 'language name',
'default_lang_behavior' => 'XBackBone will try to match the browser language by default (fallback is English).',
'lang_set' => 'System language enforced to "%s"',
'prerelease_channel' => 'Prerelease Channel',
'no_upload_token' => 'You don\'t have a personal upload token. (Generate one and try again.)',
'drop_to_upload' => 'Click or drop your files here to upload.',
'donation' => 'Donation',
'donate_text' => 'If you like XBackBone, consider a donation to support development!',
'custom_head_html' => 'Custom HTML Head content',
'custom_head_html_hint' => 'This content will be added at the <head> tag on every page.',
'custom_head_set' => 'Custom HTML head applied.',
'remember_me' => 'Remember me',
'please_wait' => 'Please wait…',
'dont_close' => 'Do not close this tab until completion.',
'register_enabled' => 'Registrations enabled',
'hide_by_default' => 'Hide media by default',
'copy_url_behavior' => 'Copy URL mode',
'settings_saved' => 'System settings saved!',
'export_data' => 'Export data',
'password_recovery' => 'Recover password',
'no_account' => 'Don\'t have an account?',
'register' => 'Register',
'register_success' => 'The account has been created, a confirmation email has been sent.',
'default_user_quota' => 'Default User Quota',
'max_user_quota' => 'Max User Quota',
'invalid_quota' => 'Invalid values as default user quota.',
'mail.activate_text' => "Hi %s!\nthank you for creating your account on %s (%s), click on the following link to activate it:\n\n%s",
'mail.activate_account' => '%s - Account Activation',
'mail.recover_text' => "Hi %s,\na password reset has been requested for your account. To complete the procedure click on the following link:\n\n%s\n\nIf it wasn\'t you who requested the password reset, simply ignore this email.",
'mail.recover_password' => '%s - Password Recovery',
'recover_email_sent' => 'If present, a recovery email was sent to the specified account.',
'account_activated' => 'Account activated, now you can login!',
'quota_enabled' => 'Enable user quota',
'password_repeat' => 'Repeat Password',
'password_match' => 'Password and repeat password must be the same.',
'password_restored' => 'Password reset.',
'recalculate_user_quota' => 'Recalculate user quota from disk',
'quota_recalculated' => 'User quota recalculated from the disk successfully.',
'used_space' => 'Used Space',
'delete_selected' => 'Delete Selected',
'delete_all' => 'Delete All',
'clear_account' => 'Clear Account',
'account_media_deleted' => 'All media in the account have been deleted.',
'danger_zone' => 'Danger Zone',
'recaptcha_failed' => 'reCAPTCHA Failed',
'recaptcha_enabled' => 'reCAPTCHA Enabled',
'recaptcha_keys_required' => 'All reCAPTCHA keys are required.',
'only_recaptcha_v3' => 'Only reCAPTCHA v3 is supported.',
'recaptcha_site_key' => 'reCAPTCHA Site Key',
'recaptcha_secret_key' => 'reCAPTCHA Secret Key',
'send_notification' => 'Send Mail Notification',
'mail.new_account' => '%s - New Account Creation',
'mail.new_account_text_with_reset' => "Hi %s!\na new account was created for you on %s (%s), click on the following link to set a password and activate it:\n\n%s",
'mail.new_account_text_with_pw' => "Hi %s!\na new account was created for you on %s (%s), with the following credentials:\n\nUsername: %s\nPassword: %s\n\nClick on the following link to go to the login page:\n%s",
'user_create_password' => 'If leaved empty, you might want to send a notification to the user email.',
'ldap_cant_connect' => 'Can\'t connect to the LDAP auth server.',
'upload_max_file_size' => 'The max file size is currently %s.',
'no_tags' => 'No tags added'
];

View file

@ -40,7 +40,7 @@ return [
'system_info' => 'Información del Sistema',
'user.create' => 'Crear Usuario',
'user.edit' => 'Editar Usuario',
'is_active' => 'Es activo',
'is_active' => 'Es activo',
'is_admin' => 'Es administrador',
'your_profile' => 'Tu Perfil',
'token' => 'Ficha',
@ -99,10 +99,4 @@ return [
'lang_name' => 'nombre del lenguaje',
'default_lang_behavior' => 'XBackBone tratará de coincidir con el lenguaje predeterminado del navegador (recurrimos al Inglés automáticamente).',
'lang_set' => 'Lenguaje del sistema impuesto a "%s"',
'upload' => 'Subir',
'delete_all' => 'Borrar Todo',
'danger_zone' => 'Zona De Peligro',
'recaptcha_failed' => 'reCAPTCHA Falló',
'php_info' => 'Información PHP',
'system_settings' => 'Configuracion de Systema',
];

View file

@ -1,113 +1,4 @@
<?php
return [
'lang' => 'French',
'yes' => 'Oui',
'no' => 'Non',
'send' => 'Envoyer',
'no_media' => 'Pas de média trouvé.',
'login.username' => 'Nom d\'utilisateur ou E-mail',
'password' => 'Mot de passe',
'login' => 'Identification',
'username' => 'Nom d\'utilisateur',
'home' => 'Menu',
'users' => 'Utilisateurs',
'system' => 'Système',
'profile' => 'Profil',
'logout' => 'Déconnexion',
'pager.next' => 'Suivant',
'pager.previous' => 'Précédent',
'copy_link' => 'Copier le lien',
'public.telegram' => 'Partager via Telegram',
'public.delete_text' => 'Etes-vous sûr de vouloir supprimer ce fichier ? Il disparaitra pour toujours !',
'preview' => 'Aperçu',
'filename' => 'Nom de fichier',
'size' => 'Taille',
'public' => 'Publique',
'owner' => 'Propriétaire',
'date' => 'Date',
'raw' => 'Montrer le texte brut',
'download' => 'Télécharger',
'delete' => 'Supprimer',
'publish' => 'Publier',
'hide' => 'Cacher',
'files' => 'Fichiers',
'orphaned_files' => 'Fichiers orphelins.',
'theme' => 'Thème',
'click_to_load' => 'Cliquer pour charger...',
'apply' => 'Appliquer',
'save' => 'Sauvegarder',
'used' => 'Utilisé',
'system_info' => 'Informations du système',
'user.create' => 'Créer un utilisateur',
'user.edit' => 'Editer l\'utilisateur',
'is_active' => 'Est actif',
'is_admin' => 'Est administrateur',
'your_profile' => 'Votre profil',
'token' => 'Jeton',
'copy' => 'Copier',
'update' => 'Mettre à jour',
'edit' => 'Editer',
'client_config' => 'Configuration du client',
'user_code' => 'Code de l\'utilisateur',
'active' => 'Actif',
'admin' => 'Administrateur',
'reg_date' => 'Date d\'enregistrement',
'none' => 'Aucun',
'open' => 'Ouvert',
'confirm' => 'Confirmation',
'confirm_string' => 'Etes-vous sûr ?',
'installed' => 'L\'installation s\'est déroulée avec succès!',
'bad_login' => 'Mauvais identifiants.',
'account_disabled' => 'Votre compte est désactivé.',
'table' => 'Table',
'dotted_search' => 'Rechercher...',
'order_by' => 'Trier par...',
'time' => 'Temps',
'name' => 'Nom',
'maintenance' => 'Maintenance',
'clean_orphaned_uploads' => 'Nettoyer les fichiers orphelins',
'path_not_writable' => 'Le dossier en sortie ne peut être écrit.',
'already_latest_version' => 'Vous avez déjà la dernière version.',
'new_version_available' => 'La nouvelle version %s est disponible!',
'cannot_retrieve_file' => 'Impossible de retrouver le fichier.',
'file_size_no_match' => 'Le fichier téléchargé n\'a pas la taille correcte.',
'check_for_updates' => 'Vérifier les mises à jour',
'upgrade' => 'Mettre à jour',
'updates' => 'Mises à jour',
'maintenance_in_progress' => 'Plateforme en maintenance, réessayez plus tard...',
'cancel' => 'Annuler',
'enforce_language' => 'Forcer la langue',
'auto_set' => 'Définir automatiquement',
'default_lang_behavior' => 'XBackBone va essayer de trouver la langue par défaut du navigateur (l\'Anglais en recours).',
'lang_set' => 'Langue du système forcée en "%s"',
'prerelease_channel' => 'Canal de pré-distribution',
'upload' => 'Envoyer',
'no_upload_token' => 'Vous n\'avez pas un jeton personnel d\'envoi. ( Générez-en un et réessayer.)',
'drop_to_upload' => 'Cliquer ou déposer vos fichiers ici pour les envoyer.',
'donation' => 'Don',
'donate_text' => 'Si vous aimez XBackBone, pensez à donner pour soutenir le développement!',
'custom_head_html' => 'En-tête de contenu HTML personnalisé',
'custom_head_html_hint' => 'Ce contenu sera ajouté à la balise <head> dans chaque page.',
'custom_head_set' => 'En-tête HTML personnalisé appliqué.',
'remember_me' => 'Se souvenir de moi',
'please_wait' => 'Veuillez patienter…',
'dont_close' => 'Ne fermez pas cette fenêtre jusqu\'à complétion.',
'welcome' => 'Bienvenue, %s!',
'goodbye' => 'Au revoir!',
'token_not_found' => 'Le token spécifié n\'a pas été trouvé.',
'email_required' => 'Une adresse e-mail est requise.',
'email_taken' => 'L\'adresse e-mail est déjà utilisée.',
'username_required' => 'Un nom d\'utilisateur est requis.',
'username_taken' => 'Le nom d\'utilisateur est déjà utilisé.',
'password_required' => 'Le mot de passe est requis.',
'user_created' => 'L\'utilisateur"%s" à été créé !',
'user_updated' => 'L\'utilisateur"%s" à été mis à jour!',
'profile_updated' => 'Profil mis-à-jour avec succès!',
'user_deleted' => 'Utilisateur Supprimé.',
'cannot_delete' => 'Vous ne pouvez pas vous supprimer.',
'cannot_demote' => 'Vous ne pouvez pas vous rétrograder.',
'cannot_write_file' => 'Le chemin de destination n\'est pas accessible en écriture.',
'deleted_orphans' => 'Fichiers orphelins supprimés avec succès.',
'switch_to' => 'Changer à',
'gallery' => 'Galerie',
];

View file

@ -27,7 +27,6 @@ return [
'date' => 'Data',
'raw' => 'Vedi raw',
'download' => 'Scarica',
'upload' => 'Carica',
'delete' => 'Elimina',
'publish' => 'Pubblica',
'hide' => 'Nascondi',
@ -97,81 +96,10 @@ return [
'cancel' => 'Annulla',
'enforce_language' => 'Imponi lingua',
'auto_set' => 'Imposta automaticamente',
'default_lang_behavior' => 'Per impostazione predefinita, XBackbone cercherà di abbinare la lingua del browser (il fallback è l\'Inglese).',
'translated_strings' => 'stringhe tradotte',
'total_strings' => 'stringhe totali',
'lang_name' => 'nome della lingua',
'default_lang_behavior' => 'Per impostazione predefinita, XBackbone cercherà di abbinare la lingua del browser (il fallback è l\'inglese).',
'lang_set' => 'Lingua di sistema applicata a "%s"',
'prerelease_channel' => 'Canale prerelease',
'no_upload_token' => 'Non hai un token personale per l\'upload associato. (Generane uno e riprova)',
'drop_to_upload' => 'Clicca o lascia i tuoi file qui per caricarli.',
'donation' => 'Donazione',
'donate_text' => 'Se ti piace XBackBone, prendi in considerazione una donazione per sostenere lo sviluppo!',
'custom_head_html' => 'Contenuto Head HTML personalizzato',
'custom_head_html_hint' => 'Questo contenuto sarà aggiunto al tag <head> in ogni pagina.',
'custom_head_set' => 'Custom Head HTML applicato con successo.',
'remember_me' => 'Ricordami',
'please_wait' => 'Attendere prego...',
'dont_close' => 'Non chiudere questa scheda fino al completamento.',
'php_info' => 'Informazioni PHP',
'system_settings' => 'Impostazioni di sistema',
'register_enabled' => 'Registrazioni abilitate',
'hide_by_default' => 'Nascondi media per impostazione predefinita',
'copy_url_behavior' => 'Modalità di copia URL',
'settings_saved' => 'Impostazioni di sistema salvate!',
'export_data' => 'Esporta dati',
'password_recovery' => 'Recupera password',
'no_account' => 'Non hai un account?',
'register' => 'Registrati',
'default_user_quota' => 'Quota utente predefinita',
'invalid_quota' => 'Valore non valido per la quota utente predefinita.',
'mail.activate_text' => 'Ciao %s!
grazie per aver creato il tuo account su %s (%s), fai clic sul seguente link per attivarlo:
%s',
'register_success' => 'L\'account è stato creato, è stata inviata un\'e-mail di conferma.',
'mail.activate_account' => '%s - Attivazione account',
'mail.recover_text' => 'Ciao %s,
è stata richiesta una reimpostazione della password per il tuo account. Per completare la procedura clicca sul seguente link:
%s
Se non sei stato tu a richiedere la reimpostazione della password, ignora semplicemente questa e-mail.',
'mail.recover_password' => '%s - Recupero password',
'recover_email_sent' => 'Se presente, è stata inviata un\'e-mail di recupero all\'account specificato.',
'account_activated' => 'Account attivato, ora è possibile effettuare il login!',
'quota_enabled' => 'Abilita quota utente',
'password_repeat' => 'Ripeti la password',
'password_match' => 'Le password devono coincidere.',
'password_restored' => 'Password reimpostata.',
'recalculate_user_quota' => 'Ricalcolare la quota utente dal disco',
'quota_recalculated' => 'Quota utente ricalcolata correttamente dal disco.',
'used_space' => 'Spazio usato',
'max_user_quota' => 'Quota massima utente',
'delete_selected' => 'Elimina selezionati',
'delete_all' => 'Elimina tutto',
'clear_account' => 'Pulisci account',
'account_media_deleted' => 'Tutti i media nell\'account sono stati eliminati.',
'danger_zone' => 'Zona Pericolosa',
'recaptcha_failed' => 'reCAPTCHA fallito',
'recaptcha_enabled' => 'reCAPTCHA Abilitato',
'recaptcha_keys_required' => 'Tutte le chiavi reCAPTCHA sono obbligatorie.',
'only_recaptcha_v3' => 'È supportato solo reCAPTCHA v3.',
'recaptcha_site_key' => 'reCAPTCHA Site Key',
'recaptcha_secret_key' => 'reCAPTCHA Secret Key',
'no_tags' => 'Nessun tag aggiunto',
'upload_max_file_size' => 'La dimensione massima del file è attualmente %s.',
'ldap_cant_connect' => 'Impossibile connettersi al server di autenticazione LDAP.',
'user_create_password' => 'Se lasciato vuoto, si consiglia di inviare una notifica all\'utente via e-mail.',
'mail.new_account_text_with_pw' => 'Ciao %s!
un nuovo account è stato creato per te su %s (%s), con le seguenti credenziali:
Username: %s
Password: %s
Clicca su questo link per andare alla pagina di login:
%s',
'mail.new_account_text_with_reset' => 'Ciao %s!
un nuovo account è stato creato per te su %s (%s), clicca sul seguente link per impostare una password e attivarlo:
%s',
'mail.new_account' => '%s - Nuovo account creato',
'send_notification' => 'Invia notifica e-mail',
];

View file

@ -1,76 +1,4 @@
<?php
return [
'lang' => 'Japanese',
'enforce_language' => '言語設定',
'yes' => 'はい',
'no' => 'いいえ',
'send' => '送信',
'no_media' => 'メディアが見つかりません。',
'login.username' => 'ユーザー名またはメールアドレス',
'password' => 'パスワード',
'login' => 'ログイン',
'username' => 'ユーザー名',
'home' => 'ホーム',
'users' => 'ユーザー',
'system' => 'システム',
'profile' => 'プロフィール',
'logout' => 'ログアウト',
'pager.next' => '次へ',
'pager.previous' => '戻る',
'copy_link' => 'リンクをコピー',
'public.telegram' => 'Telegramにシェア',
'public.delete_text' => '本当に削除しますか?この操作は元に戻せません。',
'preview' => 'プレビュー',
'filename' => 'ファイル名',
'size' => 'サイズ',
'public' => '公開',
'owner' => '所有者',
'date' => '日付',
'raw' => 'ファイルのみを表示',
'download' => 'ダウンロード',
'upload' => 'アップロード',
'delete' => '削除',
'publish' => '公開する',
'hide' => '非公開にする',
'files' => 'ファイル',
'orphaned_files' => '所有者のいないファイル',
'theme' => 'テーマ',
'click_to_load' => 'クリックしてロード...',
'apply' => '適用',
'save' => '保存',
'system_info' => 'システム情報',
'user.create' => 'ユーザーを追加',
'user.edit' => 'ユーザーを編集',
'is_active' => 'アクティブ',
'is_admin' => '管理者',
'your_profile' => 'あなたのプロフィール',
'token' => 'トークン',
'copy' => 'コピー',
'update' => '更新',
'edit' => '編集',
'client_config' => 'クライアント設定',
'user_code' => 'ユーザーコード',
'active' => 'アクティブ',
'admin' => '管理者',
'reg_date' => '登録日',
'none' => 'None',
'open' => '開く',
'confirm' => '確認',
'confirm_string' => '本当によろしいですか?',
'installed' => 'インストールが正常に完了しました!',
'bad_login' => 'ユーザー名またはパスワードが違います。',
'account_disabled' => 'あなたのアカウントは無効化されています。',
'welcome' => 'ようこそ、 %sさん',
'goodbye' => 'さようなら!',
'token_not_found' => '指定されたトークンが見つかりません。',
'email_required' => 'メールアドレスが必要です。',
'email_taken' => '入力されたメールアドレスはすでに使用されています。',
'username_required' => 'ユーザー名が必要です。',
'username_taken' => '入力されたユーザー名はすでに使用されています。',
'password_required' => 'パスワードが必要です。',
'user_created' => 'ユーザー "%s" が作成されました!',
'user_updated' => 'ユーザー "%s" が更新されました!',
'profile_updated' => 'プロフィールは正常に更新されました!',
'user_deleted' => 'ユーザーが削除されました。',
'cannot_delete' => '自分自身を削除することはできません。',
];

View file

@ -12,9 +12,9 @@ return [
'path_not_writable' => 'Utdatastien kan ikke skrives til.',
'edit' => 'Rediger',
'no_media' => 'Fant inne noe media.',
'account_disabled' => 'Kontoen din er deaktivert.',
'user_created' => 'Bruker "%s" opprettet!',
'installed' => 'Installasjon vellykket!',
'account_disabled' => 'Kontoen din er avskrudd.',
'user_created' => 'Bruker "%s" opprettet.',
'installed' => 'Installert.',
'check_for_updates' => 'Se etter oppdateringer',
'home' => 'Hjem',
'date' => 'Dato',
@ -22,7 +22,7 @@ return [
'password' => 'Passord',
'cannot_write_file' => 'Målstien kan ikke skrives til.',
'confirm' => 'Bekreftelse',
'new_version_available' => 'Ny versjon %s tilgjengelig!',
'new_version_available' => 'Ny versjon %s tilgjengelig.',
'user_code' => 'Brukerkode',
'order_by' => 'Sorter etter…',
'send' => 'Send',
@ -47,7 +47,7 @@ return [
'lang' => 'Norwegian Bokmål',
'upgrade' => 'Oppgrader',
'updates' => 'Oppdateringer',
'user_updated' => 'Bruker "%s" oppdatert!',
'user_updated' => 'Bruker "%s" oppdatert.',
'system' => 'System',
'copy' => 'Kopier',
'maintenance_in_progress' => 'Det utføres vedlikehold på plattformen, prøv igjen senere…',
@ -58,11 +58,11 @@ return [
'none' => 'Ingen',
'profile' => 'Profil',
'files' => 'Filer',
'token' => 'Token',
'token' => 'Symbol',
'publish' => 'Publiser',
'no' => 'Nei',
'open' => 'Åpen',
'welcome' => 'Velkommen %s!',
'open' => 'Åpne',
'welcome' => 'Velkommen %s.',
'login.username' => 'Brukernavn eller e-post',
'reg_date' => 'Registreringsdato',
'confirm_string' => 'Er du sikker?',
@ -78,13 +78,13 @@ return [
'table' => 'Tabell',
'cannot_retrieve_file' => 'Kan ikke hente filen.',
'update' => 'Oppdater',
'public.delete_text' => 'Er du sikker på at du ønsker å slette dette elementet? Det vil gå tapt for alltid!',
'token_not_found' => 'Angitt token ble ikke funnet.',
'profile_updated' => 'Profiloppdatering vellykket!',
'public.delete_text' => 'Er du sikker på at du ønsker å slette dette elementet? Det vil gå tapt for alltid.',
'token_not_found' => 'Angitt symbol ble ikke funnet.',
'profile_updated' => 'Profil oppdatert.',
'username_required' => 'Brukernavn kreves.',
'user.create' => 'Opprett bruker',
'login' => 'Logg inn',
'goodbye' => 'Adjø!',
'goodbye' => 'Adjø.',
'raw' => 'Vis rådata',
'download' => 'Last ned',
'click_to_load' => 'Klikk for å laste inn…',
@ -101,62 +101,4 @@ return [
'lang_name' => 'språknavn',
'default_lang_behavior' => 'Som forvalg, vil XBackBone prøve å samsvare nettleserspråk (tilbakefallsspråk er engelsk).',
'lang_set' => 'Systemspråk er satt til "%s"',
'prerelease_channel' => 'Forhåndsslipp-kanal',
'upload' => 'Last opp',
'donation' => 'Donasjon',
'custom_head_html' => 'Egendefinert HTML-hodeinnhold',
'custom_head_html_hint' => 'Dette inneholdet vil bli lagt til i <head>-taggen på hver side.',
'custom_head_set' => 'Egendefinert HTML-hode lagt til.',
'remember_me' => 'Husk meg',
'please_wait' => 'Vent…',
'no_upload_token' => 'Du har ikke et personlig opplastningssymbol. (Generer et og prøv igjen.)',
'drop_to_upload' => 'Klikk og dra filene dine hit for å laste opp.',
'donate_text' => 'Hvis du liker XBackBone, vurder en donasjon for å støtte utviklingen!',
'dont_close' => 'Ikke lukk denne fanen før fullførelse.',
'php_info' => 'PHP-informasjon',
'system_settings' => 'Systeminnstillinger',
'register_enabled' => 'Registrering påskrudd',
'hide_by_default' => 'Skjul media som forvalg',
'copy_url_behavior' => 'Modus for kopiering av nettadresser',
'settings_saved' => 'Systeminnstillinger lagret!',
'export_data' => 'Eksporter data',
'password_recovery' => 'Gjenopprett passord',
'no_account' => 'Mangler du konto?',
'register' => 'Registrer deg',
'default_user_quota' => 'Forvalgt brukerkvote',
'invalid_quota' => 'Forvalgt brukerkvote har ugyldige verdier.',
'mail.activate_account' => '%s - Kontoaktivering',
'mail.recover_password' => '%s - Passordgjenoppretting',
'mail.activate_text' => 'Hei %s!
takk for at du opprettet en konto hos %s (%s), klikk følgende link for å aktivere den.
%s',
'register_success' => 'Kontoen har blitt opprettet, en bekreftelsese-post har blitt sendt.',
'mail.recover_text' => 'Hei %s,
en tilbakestilling av passord har blitt forespurt for din konto. For å fullføre prosedyren, klikk følgende lenke:
%s
Ignorer denne e-posten hvis forespørselen om å tilbakestille passordet, ikke kommer fra deg.',
'recover_email_sent' => 'Hvis tilstede, har en gjenopprettingse-epost blitt sendt til den spesifiserte kontoen.',
'account_activated' => 'Kontoen er aktivert, du kan nå logge in!',
'quota_enabled' => 'Tillat brukerkvote',
'password_repeat' => 'Gjenta Passord',
'password_match' => 'Passordet og det gjentatte passordet må være like.',
'password_restored' => 'Tilbakestill passord.',
'recalculate_user_quota' => 'Omberegn brukerkvote fra disk',
'quota_recalculated' => 'Omberegning av brukerkvoten fra disken er vellykket.',
'used_space' => 'Brukt plass',
'max_user_quota' => 'Maks brukerkvote',
'delete_selected' => 'Slett valgte',
'delete_all' => 'Slett alle',
'clear_account' => 'Tøm konto',
'account_media_deleted' => 'All media i kontoen har blitt slettet.',
'danger_zone' => 'Faresone',
'recaptcha_failed' => 'reCAPTCHA feilet',
'recaptcha_enabled' => 'reCAPTCHA Aktivert',
'recaptcha_keys_required' => 'Alle reCAPTCHA nøklene kreves.',
'only_recaptcha_v3' => 'Bare reCAPTCHA v3 støttes.',
'recaptcha_site_key' => 'reCAPTCHA Sidenøkkel',
'recaptcha_secret_key' => 'reCAPTCHA Hemmelig Nøkkel',
];

View file

@ -1,5 +1,4 @@
<?php
return [
'lang' => 'Dutch',
'yes' => 'Ja',

View file

@ -1,159 +1,4 @@
<?php
return [
'lang' => 'Polski',
'yes' => 'Tak',
'no' => 'Nie',
'send' => 'Wyślij',
'no_media' => 'Nie znaleziono pliku.',
'login.username' => 'Nazwa użytkownika lub e-mail',
'password' => 'Hasło',
'login' => 'Login',
'username' => 'Nazwa użytkownika',
'users' => 'Użytkownicy',
'system' => 'System',
'profile' => 'Profil',
'logout' => 'Wyloguj',
'pager.next' => 'Następna',
'pager.previous' => 'Poprzednia',
'copy_link' => 'Skopiuj link',
'public.telegram' => 'Udostępnij na Telegram',
'public.delete_text' => 'Jesteś pewien, że chcesz usunąć ten element? Nie będzie się dało go już odzyskać!',
'delete' => 'Usuń',
'hide' => 'Ukryj',
'files' => 'Pliki',
'click_to_load' => 'Kliknij, aby załadować...',
'apply' => 'Zastosuj',
'save' => 'Zapisz',
'system_info' => 'Informacje o systemie',
'user.create' => 'Utwórz Użytkownika',
'user.edit' => 'Edytuj użytkownika',
'is_active' => 'Jest aktywny',
'your_profile' => 'Twój profil',
'token' => 'Token',
'copy' => 'Kopiuj',
'edit' => 'Edytuj',
'client_config' => 'Konfiguracja Klienta',
'user_code' => 'Kod użytkownika',
'active' => 'Aktywny',
'admin' => 'Admin',
'reg_date' => 'Data rejestracji',
'open' => 'Otwórz',
'confirm' => 'Potwierdzenie',
'confirm_string' => 'Jesteś pewien?',
'installed' => 'Instalacja zakończona pomyślnie!',
'welcome' => 'Witaj, %s!',
'email_required' => 'Email jest wymagany.',
'username_required' => 'Nazwa użytkownika jest wymagana.',
'username_taken' => 'Nazwa użytkownika jest już zajęta.',
'password_required' => 'Hasło jest wymagane.',
'user_created' => 'Użytkownik "%s" utworzony!',
'profile_updated' => 'Profil zaktualizowany pomyślnie!',
'enforce_language' => 'Wymuś język',
'home' => 'Strona główna',
'preview' => 'Podgląd',
'filename' => 'Nazwa pliku',
'size' => 'Rozmiar',
'public' => 'Publiczny',
'owner' => 'Właściciel',
'date' => 'Data',
'raw' => 'Pokaż bez formatowania',
'download' => 'Pobierz',
'upload' => 'Prześlij',
'publish' => 'Opublikuj',
'orphaned_files' => 'Porzucone Pliki',
'theme' => 'Motyw',
'used' => 'Wykorzystano',
'is_admin' => 'Jest administratorem',
'update' => 'Zaktualizuj',
'none' => 'Żaden',
'bad_login' => 'Błędne dane uwierzytelniające.',
'account_disabled' => 'Twoje konto jest wyłączone.',
'goodbye' => 'Do zobaczenia!',
'token_not_found' => 'Nie znaleziono określonego tokena.',
'email_taken' => 'Ten email jest już zajęty.',
'user_updated' => 'Użytkownik "%s" zaktualizowany!',
'user_deleted' => 'Użytkownik usunięty.',
'cannot_delete' => 'Nie możesz usunąć siebie.',
'cannot_demote' => 'Nie możesz siebie zdegradować.',
'cannot_write_file' => 'Ścieżka docelowa nie jest zapisywalna.',
'deleted_orphans' => 'Pomyślnie usunięto %d porzuconych plików.',
'switch_to' => 'Przełącz na',
'gallery' => 'Galeria',
'table' => 'Tabela',
'dotted_search' => 'Szukaj...',
'order_by' => 'Sortuj...',
'time' => 'Czas',
'name' => 'Nazwa',
'maintenance' => 'Konserwacja',
'clean_orphaned_uploads' => 'Wyczyść porzucone pliki',
'path_not_writable' => 'Ścieżka wyjściowa nie jest zapisywalna.',
'already_latest_version' => 'Masz już najnowszą wersję.',
'new_version_available' => 'Nowa wersja %s dostępna!',
'cannot_retrieve_file' => 'Nie można pobrać pliku.',
'file_size_no_match' => 'Pobrany plik nie pasuje do odpowiedniego rozmiaru pliku.',
'check_for_updates' => 'Sprawdź aktualizacje',
'upgrade' => 'Aktualizuj',
'updates' => 'Aktualizacje',
'maintenance_in_progress' => 'Platforma w trakcie konserwacji, spróbuj ponownie później...',
'cancel' => 'Anuluj',
'auto_set' => 'Ustaw automatycznie',
'default_lang_behavior' => 'XBackBone domyślnie spróbuje dopasować język przeglądarki (zastępczym jest Angielski).',
'lang_set' => 'Wymuszono język systemu na "%s"',
'prerelease_channel' => 'Kanał Prerelease',
'no_upload_token' => 'Nie masz osobistego tokena przesyłania. (Wygeneruj jeden i spróbuj ponownie.)',
'drop_to_upload' => 'Kliknij lub upuść swoje pliki tutaj, aby przesłać.',
'donation' => 'Darowizna',
'donate_text' => 'Jeśli podoba Ci się XBackBone, rozważ darowiznę na wsparcie rozwoju!',
'custom_head_html' => 'Niestandardowa zawartość Head (HTML)',
'custom_head_html_hint' => 'Ta zawartość zostanie dodana pod tagiem <head> na każdej stronie.',
'custom_head_set' => 'Zastosowano niestandardową zawartość dla elementu head (HTML).',
'remember_me' => 'Zapamiętaj mnie',
'please_wait' => 'Proszę czekać…',
'dont_close' => 'Nie zamykaj tej zakładki do czasu ukończenia.',
'php_info' => 'Informacje PHP',
'system_settings' => 'Ustawienia systemu',
'register_enabled' => 'Rejestracje włączone',
'hide_by_default' => 'Domyślnie ukrywaj pliki',
'copy_url_behavior' => 'Tryb kopiowania URL',
'settings_saved' => 'Zapisano ustawienia systemu!',
'export_data' => 'Eksportuj dane',
'password_recovery' => 'Odzyskaj hasło',
'no_account' => 'Nie posiadasz konta?',
'register' => 'Rejestracja',
'default_user_quota' => 'Domyślny przydział użytkownika',
'invalid_quota' => 'Nieprawidłowe wartości jako domyślny przydział użytkownika.',
'mail.activate_text' => 'Hej %s!
dziękujemy za stworzenie konta na %s (%s), kliknij poniższy link w celu jego aktywacji:
%s',
'register_success' => 'Konto zostało utworzone, email potwierdzający został wysłany.',
'mail.activate_account' => '%s - Aktywacja konta',
'mail.recover_text' => 'Hej %s,
została wysłana prośba o zresetowanie hasła dla twojego konta. Aby ukończyć procedurę, kliknij w poniższy link:
%s
Jeżeli to nie ty zażądałeś zmiany hasła, po prostu zignoruj ten email.',
'mail.recover_password' => '%s - Odzyskiwanie hasła',
'recover_email_sent' => 'Jeżeli istnieje, wiadomość email do odzyskiwania hasła została wysłane na określone konto.',
'account_activated' => 'Konto aktywowane, teraz możesz się zalogować!',
'quota_enabled' => 'Włącz przydział użytkownika',
'password_repeat' => 'Powtórz hasło',
'password_match' => 'Hasło i powtórzone hasło muszą być takie same.',
'password_restored' => 'Zresetowanie hasła.',
'recalculate_user_quota' => 'Przelicz przydział użytkownika z dysku',
'quota_recalculated' => 'Pomyślnie przeliczono z dysku przydział użytkownika.',
'used_space' => 'Użyte miejsce',
'max_user_quota' => 'Maksymalny przydział użytkownika',
'delete_selected' => 'Usuń zaznaczone',
'delete_all' => 'Usuń wszystko',
'clear_account' => 'Wyczyść konto',
'account_media_deleted' => 'Wszystkie pliki na koncie zostały usunięte.',
'danger_zone' => 'Niebezpieczne',
'recaptcha_failed' => 'reCAPTCHA nieudana',
'recaptcha_enabled' => 'reCAPTCHA włączona',
'recaptcha_keys_required' => 'Wszystkie klucze reCAPTCHA są wymagane.',
'only_recaptcha_v3' => 'Tylko reCAPTCHA v3 jest obsługiwana.',
'recaptcha_site_key' => 'Klucz strony reCAPTCHA',
'recaptcha_secret_key' => 'Sekretny klucz reCAPTCHA',
'lang' => 'Polish',
];

View file

@ -43,62 +43,4 @@ return [
'your_profile' => 'O teu perfil',
'token' => 'Token',
'copy' => 'Copiar',
'upload' => 'Enviar',
'click_to_load' => 'Clique para carregar...',
'update' => 'Atualizar',
'edit' => 'Editar',
'client_config' => 'Configuração do cliente',
'user_code' => 'Código do utilizador',
'active' => 'Ativo',
'admin' => 'Admin',
'reg_date' => 'Data de registo',
'none' => 'Nenhum',
'open' => 'Abrir',
'confirm' => 'Confirmação',
'confirm_string' => 'Tem a certeza?',
'installed' => 'Instalação completa com sucesso!',
'bad_login' => 'Login inválido.',
'account_disabled' => 'A conta está desativada.',
'welcome' => 'Bem vindo, %s!',
'goodbye' => 'Adeus!',
'token_not_found' => 'O token indicado não é válido.',
'email_required' => 'O email é necessário.',
'email_taken' => 'Este endereço de email já foi utilizado.',
'username_required' => 'O nome de utilizador é necessário.',
'username_taken' => 'Este nome de utilizador já foi utilizado.',
'password_required' => 'A senha é necessária.',
'user_created' => 'Utilizador "%s" criado!',
'user_updated' => 'Utilizador "%s" atualizado!',
'profile_updated' => 'Perfil atualizado com sucesso!',
'user_deleted' => 'Utilizador apagado.',
'cannot_delete' => 'Você não pode se excluir.',
'cannot_demote' => 'Você não pode se rebaixar.',
'cannot_write_file' => 'O caminho de destino não pode ser escrito.',
'switch_to' => 'Mudar para',
'gallery' => 'Galeria',
'table' => 'Tabela',
'dotted_search' => 'Pesquisar...',
'order_by' => 'Ordenar por...',
'time' => 'Data',
'name' => 'Nome',
'maintenance' => 'Manutenção',
'path_not_writable' => 'O caminho de destino não pode ser escrito.',
'already_latest_version' => 'Já tem a versão mais recente.',
'new_version_available' => 'Nova versão %s está disponível!',
'file_size_no_match' => 'O arquivo baixado não corresponde ao tamanho correto do arquivo.',
'check_for_updates' => 'Verifique se há atualizações',
'upgrade' => 'Atualizar',
'updates' => 'Atualizações',
'maintenance_in_progress' => 'Plataforma em manutenção, tente novamente mais tarde...',
'cancel' => 'Cancelar',
'auto_set' => 'Definir automaticamente',
'default_lang_behavior' => 'O XBackBone tentará corresponder ao idioma do navegador por padrão (a alternativa é o inglês).',
'prerelease_channel' => 'Canal de Pré-Lançamento',
'no_upload_token' => 'Você não tem um token de upload pessoal. (Gere um e tente novamente.)',
'drop_to_upload' => 'Clique ou arraste ficheiros para enviar.',
'donation' => 'Doar',
'donate_text' => 'Se gosta do XBackBone, considere doar para ajudar no desenvolvimento!',
'remember_me' => 'Lembrar-me',
'please_wait' => 'Por favor aguarde…',
'dont_close' => 'Não feche esta janela até a conclusão.',
];

View file

@ -0,0 +1,4 @@
<?php
return [
'lang' => 'Romanian',
];

View file

@ -1,5 +0,0 @@
CREATE TABLE IF NOT EXISTS `settings` (
`key` VARCHAR(32) PRIMARY KEY,
`value` TEXT
);

View file

@ -1,7 +0,0 @@
ALTER TABLE `users`
ADD COLUMN `remember_selector` VARCHAR(16) DEFAULT NULL,
ADD COLUMN `remember_token` VARCHAR(256) DEFAULT NULL,
ADD COLUMN `remember_expire` TIMESTAMP NULL DEFAULT NULL;
ALTER TABLE `users` ADD INDEX (`remember_selector`);

View file

@ -1,8 +0,0 @@
ALTER TABLE `users`
ADD COLUMN `activate_token` VARCHAR(32) DEFAULT NULL,
ADD COLUMN `reset_token` VARCHAR(32) DEFAULT NULL,
ADD COLUMN `current_disk_quota` BIGINT(20) NOT NULL DEFAULT 0,
ADD COLUMN `max_disk_quota` BIGINT(20) NOT NULL DEFAULT -1;
ALTER TABLE `users` ADD INDEX (`activate_token`);
ALTER TABLE `users` ADD INDEX (`reset_token`);

View file

@ -1,18 +0,0 @@
CREATE TABLE IF NOT EXISTS `tags` (
`id` INTEGER PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR(32) NOT NULL,
`timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX (`name`)
);
CREATE TABLE IF NOT EXISTS `uploads_tags` (
`upload_id` INTEGER,
`tag_id` INTEGER,
PRIMARY KEY (`upload_id`, `tag_id`),
FOREIGN KEY (`upload_id`) REFERENCES `uploads` (`id`)
ON UPDATE CASCADE
ON DELETE CASCADE,
FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`)
ON UPDATE CASCADE
ON DELETE CASCADE
);

View file

@ -1,3 +0,0 @@
ALTER TABLE `users` ADD COLUMN `ldap` BOOLEAN NOT NULL DEFAULT 0;
ALTER TABLE `users` ADD COLUMN `hide_uploads` BOOLEAN NOT NULL DEFAULT 0;
ALTER TABLE `users` ADD COLUMN `copy_raw` BOOLEAN NOT NULL DEFAULT 0;

View file

@ -1,4 +0,0 @@
CREATE TABLE IF NOT EXISTS `settings` (
`key` VARCHAR(32) PRIMARY KEY,
`value` TEXT
);

View file

@ -1,6 +0,0 @@
ALTER TABLE `users` ADD COLUMN `remember_selector` VARCHAR(16);
ALTER TABLE `users` ADD COLUMN `remember_token` VARCHAR(256);
ALTER TABLE `users` ADD COLUMN `remember_expire` TIMESTAMP;
CREATE INDEX IF NOT EXISTS `remember_selector_index`
ON `users` (`remember_selector`);

View file

@ -1,11 +0,0 @@
ALTER TABLE `users` ADD COLUMN `activate_token` VARCHAR(32);
ALTER TABLE `users` ADD COLUMN `reset_token` VARCHAR(32);
ALTER TABLE `users` ADD COLUMN `current_disk_quota` BIGINT NOT NULL DEFAULT 0;
ALTER TABLE `users` ADD COLUMN `max_disk_quota` BIGINT NOT NULL DEFAULT -1;
CREATE INDEX IF NOT EXISTS `activate_token_index`
ON `users` (`activate_token`);
CREATE INDEX IF NOT EXISTS `reset_token_index`
ON `users` (`reset_token`);

View file

@ -1,20 +0,0 @@
CREATE TABLE IF NOT EXISTS `tags` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
`name` VARCHAR(32) NOT NULL,
`timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX IF NOT EXISTS `tag_name`
ON `tags` (`name`);
CREATE TABLE IF NOT EXISTS `uploads_tags` (
`upload_id` INTEGER,
`tag_id` INTEGER,
PRIMARY KEY (`upload_id`, `tag_id`),
FOREIGN KEY (`upload_id`) REFERENCES `uploads` (`id`)
ON UPDATE CASCADE
ON DELETE CASCADE,
FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`)
ON UPDATE CASCADE
ON DELETE CASCADE
);

View file

@ -1,3 +0,0 @@
ALTER TABLE `users` ADD COLUMN `ldap` BOOLEAN NOT NULL DEFAULT 0;
ALTER TABLE `users` ADD COLUMN `hide_uploads` BOOLEAN NOT NULL DEFAULT 0;
ALTER TABLE `users` ADD COLUMN `copy_raw` BOOLEAN NOT NULL DEFAULT 0;

View file

@ -26,8 +26,8 @@
{% block content %}
<div class="container-fluid">
<form class="form-signin" method="post" action="{{ route('login') }}">
<div class="row text-center">
<form class="form-signin text-center" method="post" action="{{ route('login') }}">
<div class="row">
<div class="col-md-12">
<h1 class="h3 mb-3 font-weight-normal">{{ config.app_name }}</h1>
{% include 'comp/alert.twig' %}
@ -35,38 +35,17 @@
</div>
<div class="row">
<div class="col-md-12">
{% if recaptcha_site_key is not null %}
<input type="hidden" name="recaptcha_token" id="recaptcha_token">
{% endif %}
<label for="username" class="sr-only">{{ lang('login.username') }}</label>
<input type="text" id="username" class="form-control first" placeholder="{{ lang('login.username') }}" name="username" required autofocus>
<label for="inputEmail" class="sr-only">{{ lang('login.username') }}</label>
<input type="text" id="username" class="form-control" placeholder="{{ lang('login.username') }}" name="username" required autofocus>
<label for="password" class="sr-only">{{ lang('password') }}</label>
<input type="password" id="password" class="form-control last" placeholder="{{ lang('password') }}" name="password" required>
<div class="d-flex justify-content-between">
<div class="form-check">
<input type="checkbox" name="remember" class="form-check-input float-left" id="remember">
<label class="form-check-label" for="remember">{{ lang('remember_me') }}</label>
</div>
<a href="{{ route('recover') }}" class="">{{ lang('password_recovery') }}</a>
<input type="password" id="password" class="form-control" placeholder="{{ lang('password') }}" name="password" required>
</div>
</div>
</div>
<div class="row mt-2">
<div class="row">
<div class="col-md-12">
<button class="btn btn-lg btn-primary btn-block" type="submit">{{ lang('login') }}</button>
{% if register_enabled == 'on' %}
<div class="text-center mt-2">
{{ lang('no_account') }} <a href="{{ route('register.show') }}">{{ lang('register') }}</a>.
</div>
{% endif %}
</div>
</div>
</form>
</div>
{% endblock %}
{% block js %}
{% if recaptcha_site_key is not null %}
{% include 'comp/recaptcha.twig' %}
{% endif %}
{% endblock %}

View file

@ -1,52 +0,0 @@
{% extends 'base.twig' %}
{% block title %}{{ lang('password_recovery') }}{% endblock %}
{% block head %}
<style>
html {
height: 100%;
}
body {
height: 100%;
display: -ms-flexbox;
display: -webkit-box;
display: flex;
-ms-flex-align: center;
-ms-flex-pack: center;
-webkit-box-align: center;
align-items: center;
-webkit-box-pack: center;
justify-content: center;
padding-bottom: 40px;
margin-bottom: 0;
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid">
<form class="form-signin" method="post" action="{{ route('recover.mail') }}">
<div class="row text-center">
<div class="col-md-12">
<h1 class="h3 mb-3 font-weight-normal">{{ config.app_name }}</h1>
{% include 'comp/alert.twig' %}
</div>
</div>
<div class="row">
<div class="col-md-12">
<label for="email" class="sr-only">{{ lang('email') }}</label>
<input type="email" id="email" class="form-control" placeholder="mail@example.com" name="email" required autofocus>
</div>
</div>
<div class="row mt-2">
<div class="col-md-12">
<button class="btn btn-lg btn-primary btn-block" type="submit">{{ lang('password_recovery') }}</button>
<div class="text-center mt-2">
<a href="{{ route('login.show') }}">{{ lang('cancel') }}</a>
</div>
</div>
</div>
</form>
</div>
{% endblock %}

View file

@ -1,51 +0,0 @@
{% extends 'base.twig' %}
{% block title %}{{ lang('password_recovery') }}{% endblock %}
{% block head %}
<style>
html {
height: 100%;
}
body {
height: 100%;
display: -ms-flexbox;
display: -webkit-box;
display: flex;
-ms-flex-align: center;
-ms-flex-pack: center;
-webkit-box-align: center;
align-items: center;
-webkit-box-pack: center;
justify-content: center;
padding-bottom: 40px;
margin-bottom: 0;
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid">
<form class="form-signin" method="post" action="{{ route('recover.password', {'resetToken': reset_token}) }}">
<div class="row text-center">
<div class="col-md-12">
<h1 class="h3 mb-3 font-weight-normal">{{ config.app_name }}</h1>
{% include 'comp/alert.twig' %}
</div>
</div>
<div class="row">
<div class="col-md-12">
<label for="password" class="sr-only">{{ lang('password') }}</label>
<input type="password" id="password" class="form-control first" placeholder="{{ lang('password') }}" name="password" required>
<label for="password_repeat" class="sr-only">{{ lang('password_repeat') }}</label>
<input type="password" id="password_repeat" class="form-control last" placeholder="{{ lang('password_repeat') }}" name="password_repeat" required>
</div>
</div>
<div class="row mt-2">
<div class="col-md-12">
<button class="btn btn-lg btn-primary btn-block" type="submit">{{ lang('password_recovery') }}</button>
</div>
</div>
</form>
</div>
{% endblock %}

Some files were not shown because too many files have changed in this diff Show more