Compare commits

..

3 commits
master ... 2.x

Author SHA1 Message Date
Sergio Brighenti
303e607f7d
Merge pull request #172 from SergiX44/revert-170-master
Revert "Translated using Weblate (French)"
2020-04-04 12:37:06 +02:00
Sergio Brighenti
98db104d7f
Revert "Translated using Weblate (French)" 2020-04-04 12:36:52 +02:00
Sergio Brighenti
d3377979a2
Merge pull request #170 from SergiX44/master
Translated using Weblate (French)
2020-04-04 12:35:04 +02:00
178 changed files with 5103 additions and 22398 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.

View file

@ -1,27 +0,0 @@
name: PHP Composer
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
operating-system: [ubuntu-latest]
php-versions: ["7.3", "7.4", "8.0", "8.1", "8.2"]
steps:
- uses: actions/checkout@v2
- name: Validate composer.json and composer.lock
run: composer validate --no-check-version
- name: Install dependencies
run: composer install --no-progress
- name: Run test suite
run: vendor/bin/phpunit --no-coverage

BIN
.github/xbackbone.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

11
.gitignore vendored
View file

@ -2,10 +2,13 @@
composer.phar
/vendor/
# Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file
# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file
composer.lock
### grunt ###
# Grunt usually compiles files inside this directory
dist/
release*.zip
# Grunt usually preprocesses files such as coffeescript, compass... inside the .tmp directory
.tmp/
@ -74,7 +77,6 @@ typings/
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
.idea/
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/dictionaries
@ -135,7 +137,4 @@ resources/database/*.db
resources/sessions/sess_*
logs/log-*.txt
config.php
release.zip
/.settings/
/.project
/.buildpath
release.zip

View file

@ -1,8 +1,8 @@
Options -Indexes +SymLinksIfOwnerMatch
Options -Indexes
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule ^(app|bin|bootstrap|resources|storage|vendor|logs|CHANGELOG.md)(/.*|)$ - [NC,F]
RewriteRule ^(app|bin|bootstrap|resources|storage|vendor|logs)(/.*|)$ - [NC,F]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [QSA,L]
</IfModule>
</IfModule>

View file

@ -1,450 +1,142 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## Unreleased
## [3.7.0] - 2024-01-14
### Added
- Added support for vanity urls.
- Added KDE integration for linux script.
### Changed
- Updated translations.
- File preview is now clickable to open the file.
### Fixed
- Fixes for LDAP authentication.
## [3.6.3] - 2023-05-27
### Fixed
- Fix LDAP for php >= 8.1
## [3.6.2] - 2023-05-24
### Changed
- Support for PHP 8.2
### Removed
- Azure blob storage driver
## [3.6.1] - 2022-11-27
### Changed
- Upgraded dependencies
- Updated translations
### Fixed
- Fixed error in export data (#499)
- Fixed issues with reverse proxies (#495)
- Fixed duplicated twitter tags
## [3.6.0] - 2022-06-20
### Changed
- Improved embedding on discord of large videos.
- Releases are now compressed for faster downloads
- Updated translations
### Fixed
- Fixed deprecation notices on php >= 8
- Fixed embed UA for Discord.
- Fixed error with post_max_size = 0
### Removed
- Support for php 7.2
## [3.5.1] - 2021-10-22
### Changed
- Fixed embed UA for Discord.
- Updated translations.
## [3.5.0] - 2021-09-05
### Added
- Support for theme-park.dev themes.
- Updated translations.
### Fixed
- Wrong css when reapplying the default theme.
### Removed
- Dropped theme cli command.
## [3.4.1] - 2021-08-11
### Added
- Toggle to disable embeds.
### Changed
- Raw url copying now contains also the file extension.
## [3.4.0] - 2021-08-01
### Added
- Added image support for OG for Discord only.
### Changed
- Updated translations.
- Dropped support for PHP 7.1
### Fixed
- Fixed possible XSS and CSRF attacks.
## [3.3.5] - 2021-04-25
### Fixed
- Removed OG integration for discord.
### Changed
- Updated translations.
## [3.3.4] - 2021-03-07
### Added
- Login failed logging.
- User identifier option for LDAP configurations.
### Fixed
- Fixed open graph meta tags for Discord.
- Fixed custom html tags are not displayed back in the admin setting.
- Fixed python plugin for newer version of Screencloud.
- Fixed accented chars in email subject.
- Fixed error on PHP 8.
## [3.3.3] - 2020-11-13
### Fixed
- Fixed issue with responsive menu on mobile.
## [3.3.2] - 2020-11-12
### Fixed
- Fixed switch not works for the first time for normal users.
## [3.3.1] - 2020-11-12
### Fixed
- Formatting error on the check for updates.
- Fixed default view for normal users.
## [3.3.0] - 2020-11-12
### Added
- Enabled PHP 8 support.
- Added Screencloud client support (https://screencloud.net).
- OpenGraph image tag (issue #269).
- Start adding unit tests.
### Changed
- The list mode is now available also for non-admin accounts (issue #226).
### Fixed
- Linux script strange response code in headless mode.
### Removed
- Dropped Telegram share button.
## [3.2.0] - 2020-09-05
### Added
- Added support to use Azure Blob Storage account as storage location.
- Support for other S3-compatible storage endpoint.
- Line number when showing text files.
### Fixed
- S3 driver file streaming not working properly.
- Fixed Slack image preview.
## [3.1.4] - 2020-04-13
### Changed
- Now the migrate command resync the system quota for each user.
### Fixed
- Fixed error with the migrate command.
## [3.1.3] - 2020-04-13
### Changed
- Added changelog page.
- Updated translations.
## [3.1.2] - 2020-04-12
### Changed
- Improved installer storage checks.
### Fixed
- Fixed upload table lost when updating very old instances.
## [3.1.1] - 2020-04-11
### Fixed
- Fixed error during a fresh installation with sqlite.
## [3.1] - 2020-04-10
### Added
- 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.
### Changed
- The theme is now re-applied after every system update.
- Updated system settings page.
- Updated translations.
- Improved grid layout.
### Fixed
- Fixed bug html files raws are rendered in a browser.
- Fixes and improvements.
## [3.0.2] - 2019-12-04
### Changed
- Updated translations.
### Fixed
- Fixed error with migrate command.
## [3.0.1] - 2019-11-25
### Changed
- Small installer update.
### Fixed
- Fixed error with older mysql versions.
- Fixed config is compiled with the di container.
## [3.0] - 2019-11-23
### Added
- 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)).
- The linux script can be used on headless systems.
- Raw URL now accept file extensions.
- Implemented SameSite XSS protection.
### Changed
- Upgraded from Slim3 to Slim 4.
- Replaced videojs player with Plyr.
- Improved installer.
- Improved thumbnail generation.
- Small fixes and improvements.
## [2.6.6] - 2019-10-23
### Added
- Ability to choose between releases and prereleases with the web updater.
### Changed
- Updated translations.
## [2.6.5] - 2019-09-17
### Changed
- Changed color to some buttons to address visibility with some themes.
### Fixed
- Fixed error after orphaned files removal #74.
- Fixed update password not correctly removed from log files (#74).
## [2.6.4] - 2019-09-15
### Added
- Filter on displayable images.
### Changed
- The generated random strings are now more human readable.
### Fixed
- Fixed during upload error on php compiled for 32 bit.
- Fixed icons on the installer page.
## [2.6.3] - 2019-09-14
### Fixed
- Fixed #67.
- Fixed bad preload statement.
- Fixed wrong redirect after install in subdirs.
## [2.6.2] - 2019-09-06
### Added
- Added method for cache busting when updating/change theme.
- Added russian translation from [Weblate](https://hosted.weblate.org/projects/xbackbone/xbackbone/).
### Changed
- Changed background default color.
- Use the Font Awesome web font for better performances.
## [2.6.1] - 2019-09-04
### Added
- Added alert if required extensions are not loaded.
### Changed
- Improved shell commands.
- Updated translations.
### Fixed
- Fixed bad redirects on the web installer (#62).
- Fixed login page with dark themes.
## [2.6] - 2019-08-20
### Added
- Added support to use AWS S3, Google Cloud Storage, Dropbox and FTP(s) accounts as storage location.
- Added german and norwegian translations from [Weblate](https://hosted.weblate.org/projects/xbackbone/xbackbone/).
- Added ability to force system language.
### Changed
- Improved lang detection.
### Fixed
- Fixed missing icon.
## [2.5.3] - 2019-05-12
### Changed
- Improved exception stacktrace logging.
### Fixed
- Fixed bad css loading on Firefox (#35).
- Fixed wrong style for publish/unpublish button.
## [2.5.2] - 2019-05-09
### Added
- Added preloading for some resources to improve performances.
- Added check for block execution on EOL and unsupported PHP versions.
### Changed
- Improved session handling.
- Other minor improvements.
### Fixed
- Fixed telegram share not working.
- Fix for big text file now are not rendered in the browser.
## [2.5.1] - 2019-04-10
### Changed
- Improved HTTP partial content implementation for large files.
### Fixed
- Fixed bad redirect if the theme folder is not writable. (#27)
## [2.5] - 2019-02-10
### Added
- Added partial content implementation (stream seeking on chromium based browsers).
- **[BETA]** Added self update feature.
- Added project favicon.
### Changed
- Updated project license to [AGPL v3.0](https://choosealicense.com/licenses/agpl-3.0/) (now releases ships with the new license).
- Improved video.js alignment with large videos.
- Optimized output zip release size.
- Templates cleanup and optimizations.
- Improved error handling.
## [2.4.1] - 2019-01-24
### Fixed
- Fixed error message when the file is too large. (#15)
- Fixed button alignment.
## [2.4] - 2019-01-22
### Added
- Added function to remove orphaned files.
- Multiple uploads sorting methods.
- Switch between tab and gallery mode using an admin account.
- Search in uploads.
### Changed
- Updated js dependencies.
- Internal refactoring and improvements
## [2.3.1] - 2018-12-09
### Added
- Added checks during the installation wizard.
- cURL and Wget can now directly download the file.
### Fixed
- Fixed english language.
- Fixed forced background with dark themes.
## [2.3] - 2018-11-30
### Added
- Added overlay on user gallery images.
- Added linux script to allow uploads from linux screenshot tools.
- Enable audio player with video.js.
- Font Awesome icon match the single file mime-type.
### Changed
- Improved image scaling in user gallery.
- Video and audio now starts with volume at 50%.
- Minor layout fixes.
### Fixed
- Fixed IT translation.
## [2.2] - 2018-11-20
### Added
- Added multi-language support.
### Fixed
- Improved routing.
- Minor improvements and bug fixes.
- Fixed HTTP/2 push is resetting the current session.
## [2.1] - 2018-11-20
### Added
- Added video.js support.
- Allow e-mail login.
- Support for ShareX deletion URL.
### Changed
- Improved theme style.
- Improved page redirecting.
### Fixed
- Fixed HTTP/2 push preload.
## [2.0] - 2018-11-13
### Added
- Added install wizard (using the CLI is no longer required).
- Added used space indicator per user.
- Allow discord bot to display the preview.
- Theme switcher on the web UI.
- MySQL support.
### Changed
- Migrated from Flight to Slim 3 framework.
- Improvements under the hood.
## [1.3] - 2018-10-14
### Added
- Added command to switch between bootswatch.com themes.
- Added popover to write the telegram message when sharing.
- Allow Facebook bots to display the preview.
### Changed
- Packaging improvements.
- Updated some dependencies.
## [1.2] - 2018-05-01
### Added
- Added auto config generator for ShareX.
- Show upload file size on the dashboard.
### Changed
- Previews are now scaled for better page load.
### Removed
- Removed HTTP2 push from the dashboard to improve loading time.
### Fixed
- Fixed insert for admin user (running `php bin\migrate --install`).
## [1.1] - 2018-04-28
### Added
- Added logging.
- Added share to Telegram.
### Changed
- Improved migrate system.
- Updated Bootstrap theme.
### Fixed
- Fixed back to top when click delete or publish/unpublish.
- Login redirect back to the requested page.
## [1.0] - 2018-04-28
### Added
- Initial version.
## 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

@ -85,98 +85,54 @@ 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,
cwd: 'node_modules/highlightjs',
src: ['styles/**/*', 'highlight.pack.min.js'],
dest: 'static/highlightjs'
}, {
expand: true,
cwd: 'node_modules/highlightjs-line-numbers.js/dist',
src: ['highlightjs-line-numbers.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'}
],
},
},
zip: {
[releaseFilename]: [
'app/**/*',
'bin/**/*',
'bootstrap/**/*',
'install/**/*',
'logs/**/',
'resources/cache',
'resources/sessions',
'resources/database',
'resources/lang/**/*',
'resources/templates/**/*',
'resources/schemas/**/*',
'resources/lang/**/*',
'static/**/*',
'vendor/**/*',
'.htaccess',
'config.example.php',
'index.php',
'composer.json',
'composer.lock',
'LICENSE',
'favicon.ico',
]
},
shell: {
phpstan: {
command: '"./vendor/bin/phpstan" --level=0 analyse app resources/lang bin install'
command: '"./vendor/bin/phpstan" analyse app resources/lang bin install'
},
composer_no_dev: {
command: 'composer install --no-dev --prefer-dist'
command: 'composer install --no-dev'
}
},
compress: {
main: {
options: {
archive: releaseFilename,
mode: 'zip',
level: 9,
},
files: [{
expand: true,
cwd: './',
src: [
'app/**/*',
'bin/**/*',
'bootstrap/**/*',
'install/**/*',
'logs/**/',
'resources/cache',
'resources/sessions',
'resources/database',
'resources/lang/**/*',
'resources/templates/**/*',
'resources/schemas/**/*',
'resources/lang/**/*',
'resources/uploaders/**/*',
'static/**/*',
'vendor/**/*',
'.htaccess',
'config.example.php',
'index.php',
'composer.json',
'composer.lock',
'LICENSE',
'favicon.ico',
'CHANGELOG.md'
],
dest: '/'
}]
}
},
}
});
@ -185,5 +141,5 @@ module.exports = function (grunt) {
grunt.registerTask('test', ['jshint']);
grunt.registerTask('phpstan', ['shell:phpstan']);
grunt.registerTask('composer_no_dev', ['shell:composer_no_dev']);
grunt.registerTask('build-release', ['default', 'composer_no_dev', 'compress']);
grunt.registerTask('build-release', ['default', 'composer_no_dev', 'zip']);
};

173
README.md
View file

@ -1,53 +1,168 @@
# <a href="https://hosted.weblate.org/engage/xbackbone/?utm_source=widget"><img src="https://hosted.weblate.org/widgets/xbackbone/-/xbackbone/svg-badge.svg" alt="Weblate"></a> <a href="https://codeclimate.com/github/SergiX44/XBackBone/maintainability"><img src="https://api.codeclimate.com/v1/badges/bf8ee4a8df9c9f0dfa08/maintainability" alt="Codeclimate"></a> <a href="http://bit.ly/XBackBonePaypal"><img src="https://img.shields.io/badge/donate-PayPal-yellow" alt="Donations"></a> <a href="https://discord.gg/ksPfXFbhDF"><img src="https://img.shields.io/discord/780922715393359904?label=discord%20chat" alt="Discord"></a>
<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, Azure Blob Storage, Dropbox, FTP(s).
+ Web file upload.
+ Multiple backends support: Local storage, AWS S3, Google Cloud, Dropbox, FTP(s).
+ 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.
## Security Vulnerabilities
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.
If you discover a security vulnerability within XBackBone, please send an e-mail to Sergio at sergio@brighenti.me. All security vulnerabilities will be promptly addressed.
#### 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

@ -1,5 +0,0 @@
# Security Policy
## Security Vulnerabilities
If you discover a security vulnerability within XBackBone, please send an e-mail to Sergio at sergio@brighenti.me. All security vulnerabilities will be promptly addressed.

View file

@ -2,109 +2,92 @@
namespace App\Controllers;
use App\Database\Migrator;
use App\Web\Theme;
use League\Flysystem\FileNotFoundException;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Http\Request;
use Slim\Http\Response;
class AdminController extends Controller
{
/**
* @param Request $request
* @param Response $response
*
* @return Response
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*
*/
public function system(Request $request, Response $response): Response
{
$settings = [];
foreach ($this->database->query('SELECT `key`, `value` FROM `settings`') as $setting) {
$settings[$setting->key] = $setting->value;
}
$settings['default_user_quota'] = humanFileSize(
$this->getSetting('default_user_quota', stringToBytes('1G')),
0,
true
);
/**
* @param Request $request
* @param Response $response
* @return Response
* @throws FileNotFoundException
*/
public function system(Request $request, Response $response): Response
{
$usersCount = $this->database->query('SELECT COUNT(*) AS `count` FROM `users`')->fetch()->count;
$mediasCount = $this->database->query('SELECT COUNT(*) AS `count` FROM `uploads`')->fetch()->count;
$orphanFilesCount = $this->database->query('SELECT COUNT(*) AS `count` FROM `uploads` WHERE `user_id` IS NULL')->fetch()->count;
return view()->render($response, 'dashboard/system.twig', [
'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,
'totalSize' => humanFileSize($this->database->query('SELECT SUM(`current_disk_quota`) AS `sum` FROM `users`')->fetch()->sum ?? 0),
'post_max_size' => ini_get('post_max_size'),
'upload_max_filesize' => ini_get('upload_max_filesize'),
'installed_lang' => $this->lang->getList(),
'forced_lang' => $request->getAttribute('forced_lang'),
'php_version' => PHP_VERSION,
'max_memory' => ini_get('memory_limit'),
'settings' => $settings,
]);
}
$medias = $this->database->query('SELECT `uploads`.`storage_path` FROM `uploads`')->fetchAll();
/**
* @param Response $response
*
* @return Response
*/
public function deleteOrphanFiles(Response $response): Response
{
$orphans = $this->database->query('SELECT * FROM `uploads` WHERE `user_id` IS NULL')->fetchAll();
$totalSize = 0;
$filesystem = $this->storage;
$deleted = 0;
$filesystem = $this->storage;
foreach ($medias as $media) {
$totalSize += $filesystem->getSize($media->storage_path);
}
foreach ($orphans as $orphan) {
try {
$filesystem->delete($orphan->storage_path);
$deleted++;
} catch (FileNotFoundException $e) {
}
}
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(),
]);
}
$this->database->query('DELETE FROM `uploads` WHERE `user_id` IS NULL');
/**
* @param Request $request
* @param Response $response
* @return Response
*/
public function deleteOrphanFiles(Request $request, Response $response): Response
{
$orphans = $this->database->query('SELECT * FROM `uploads` WHERE `user_id` IS NULL')->fetchAll();
$this->session->alert(lang('deleted_orphans', [$deleted]));
$filesystem = $this->storage;
$deleted = 0;
return redirect($response, route('system'));
}
foreach ($orphans as $orphan) {
try {
$filesystem->delete($orphan->storage_path);
$deleted++;
} catch (FileNotFoundException $e) {
}
}
/**
* @param Response $response
*
* @return Response
*/
public function getThemes(Response $response): Response
{
$themes = make(Theme::class)->availableThemes();
$this->database->query('DELETE FROM `uploads` WHERE `user_id` IS NULL');
$out = [];
$this->session->alert(lang('deleted_orphans', [$deleted]));
foreach ($themes as $vendor => $list) {
$out["-- {$vendor} --"] = null;
foreach ($list as $name => $url) {
$out[$name] = "{$vendor}|{$url}";
}
}
return redirect($response, 'system');
}
return json($response, $out);
}
/**
* @param Request $request
* @param Response $response
* @return Response
*/
public function applyLang(Request $request, Response $response): Response
{
$config = require BASE_DIR . 'config.php';
/**
* @param Response $response
* @return Response
*/
public function recalculateUserQuota(Response $response): Response
{
$migrator = new Migrator($this->database, null);
$migrator->reSyncQuotas($this->storage);
$this->session->alert(lang('quota_recalculated'));
return redirect($response, route('system'));
}
}
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,123 +0,0 @@
<?php
namespace App\Controllers\Auth;
use App\Controllers\Controller;
use App\Web\Session;
use App\Web\ValidationHelper;
use Psr\Http\Message\ServerRequestInterface as Request;
abstract class AuthController extends Controller
{
protected function checkRecaptcha(ValidationHelper $validator, Request $request)
{
$validator->callIf($this->getSetting('recaptcha_enabled') === 'on', function (Session $session) use (&$request) {
$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) {
$session->alert(lang('recaptcha_failed'), 'danger');
return false;
}
return true;
});
return $validator;
}
/**
* Connects to LDAP server and logs in with service account (if configured)
* @return \LDAP\Connection|resource|false
*/
public function ldapConnect()
{
if (!extension_loaded('ldap')) {
$this->logger->error('The LDAP extension is not loaded.');
return false;
}
// Building LDAP URI
$ldapSchema=(@is_string($this->config['ldap']['schema'])) ?
strtolower($this->config['ldap']['schema']) : 'ldap';
$ldapURI="$ldapSchema://".$this->config['ldap']['host'].':'.$this->config['ldap']['port'];
// Connecting to LDAP server
$this->logger->debug("Connecting to $ldapURI");
$server = ldap_connect($ldapURI);
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);
} else {
$this->logger->error('LDAP-URI was not parseable');
return false;
}
// Upgrade to StartTLS
$useStartTLS = @is_bool($this->config['ldap']['useStartTLS']) ? $this->config['ldap']['useStartTLS'] : false;
if (($useStartTLS === true) && (ldap_start_tls($server) === false)) {
$this->logger->debug(ldap_error($server));
$this->logger->error("Failed to establish secure LDAP swith StartTLS");
return false;
}
// Authenticating LDAP service account (if configured)
$serviceAccountFQDN= (@is_string($this->config['ldap']['service_account_dn'])) ?
$this->config['ldap']['service_account_dn'] : null;
if (is_string($serviceAccountFQDN)) {
if (ldap_bind($server, $serviceAccountFQDN, $this->config['ldap']['service_account_password']) === false) {
$this->logger->debug(ldap_error($server));
$this->logger->error("Bind with service account ($serviceAccountFQDN) failed.");
return false;
}
}
return $server;
}
/**
* Returns User's LDAP DN
* @param string $username
* @param \LDAP\Connection|resource $server LDAP Server Resource
* @return string|null
*/
protected function getLdapRdn(string $username, $server)
{
//Dynamic LDAP User Binding
if (@is_string($this->config['ldap']['search_filter'])) {
//Replace ???? with username
$searchFilter = str_replace('????', ldap_escape($username, '', LDAP_ESCAPE_FILTER), $this->config['ldap']['search_filter']);
$ldapAddributes = array('dn');
$this->logger->debug("LDAP Search filter: $searchFilter");
$ldapSearchResp = ldap_search(
$server,
$this->config['ldap']['base_domain'],
$searchFilter,
$ldapAddributes
);
if (!$ldapSearchResp) {
$this->logger->debug(ldap_error($server));
$this->logger->error("User LDAP search for user $username failed");
return null;
}
if (ldap_count_entries($server, $ldapSearchResp) !== 1) {
$this->logger->notice("LDAP search for $username not found or had multiple entries");
return null;
}
$ldapEntry = ldap_first_entry($server, $ldapSearchResp);
//Returns full DN
$bindString = ldap_get_dn($server, $ldapEntry);
} else {
// Static LDAP Binding
$bindString = ($this->config['ldap']['rdn_attribute'] ?? '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'];
}
//returns partial DN
}
return $bindString;
}
}

View file

@ -1,183 +0,0 @@
<?php
namespace App\Controllers\Auth;
use App\Database\Repositories\UserRepository;
use App\Web\ValidationHelper;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
class LoginController extends AuthController
{
/**
* @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
{
/** @var ValidationHelper $validator */
$validator = make(ValidationHelper::class);
if ($this->checkRecaptcha($validator, $request)->fails()) {
return redirect($response, route('login'));
}
$username = param($request, 'username');
$password = param($request, 'password');
$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 || $user->ldap ?? true)) {
$user = $this->ldapLogin($request, $username, param($request, 'password'), $user);
}
$validator
->alertIf(!$user || !password_verify($password, $user->password), 'bad_login')
->alertIf(isset($this->config['maintenance']) && $this->config['maintenance'] && !($user->is_admin ?? true), 'maintenance_in_progress', 'info')
->alertIf(!($user->active ?? false), 'account_disabled');
if ($validator->fails()) {
if (!empty($request->getHeaderLine('X-Forwarded-For'))) {
$ip = $request->getHeaderLine('X-Forwarded-For');
} else {
$ip = $request->getServerParams()['REMOTE_ADDR'] ?? null;
}
$this->logger->info("Login failed with username='{$username}', ip={$ip}.");
return redirect($response, route('login'));
}
$this->session->set('logged', true)
->set('user_id', $user->id)
->set('username', $user->username)
->set('admin', $user->is_admin)
->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, 0, '', '', false, true);
}
return redirect($response, route('login.show'));
}
/**
* @param Request $request
* @param string $username
* @param string $password
* @param $dbUser
* @return bool|null
* @throws \Slim\Exception\HttpNotFoundException
* @throws \Slim\Exception\HttpUnauthorizedException
*/
protected function ldapLogin(Request $request, string $username, string $password, $dbUser)
{
// Build LDAP connection
$server = $this->ldapConnect();
if (!$server) {
$this->session->alert(lang('ldap_cant_connect'), 'warning');
return $dbUser;
}
//Get LDAP user's (R)DN
$userDN=$this->getLdapRdn($username, $server);
if (!is_string($userDN)) {
return null;
}
//Bind as user to validate password
if (@ldap_bind($server, $userDN, $password)) {
$this->logger->debug("$userDN authenticated against LDAP sucessfully");
} else {
$this->logger->debug("$userDN authenticated against LDAP unsucessfully");
if ($dbUser && !$dbUser->ldap) {
return $dbUser;
}
return null;
}
if (!$dbUser) {
$email = $username;
if (!filter_var($username, FILTER_VALIDATE_EMAIL)) {
if (@is_string($this->config['ldap']['search_filter'])) {
$search = ldap_read(
$server,
$userDN,
'objectClass=*',
array('mail',$this->config['ldap']['rdn_attribute'])
);
} else {
$search = ldap_search($server, $this->config['ldap']['base_domain'], ($this->config['ldap']['rdn_attribute'] ?? '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 UserRepository $userQuery */
$userQuery = make(UserRepository::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 ($server) {
ldap_close($server);
}
if (!password_verify($password, $dbUser->password)) {
$userQuery = make(UserRepository::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;
}
}

View file

@ -1,130 +0,0 @@
<?php
namespace App\Controllers\Auth;
use App\Web\Mail;
use App\Web\ValidationHelper;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Exception\HttpNotFoundException;
class PasswordRecoveryController extends AuthController
{
/**
* @param Response $response
* @return Response
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*/
public function recover(Response $response): Response
{
return view()->render($response, 'auth/recover_mail.twig', [
'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 recoverMail(Request $request, Response $response): Response
{
if ($this->session->get('logged', false)) {
return redirect($response, route('home'));
}
if ($this->checkRecaptcha(make(ValidationHelper::class), $request)->fails()) {
return redirect($response, route('recover'));
}
$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]),
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);
}
/** @var ValidationHelper $validator */
$validator = make(ValidationHelper::class)
->alertIf(empty(param($request, 'password')), 'password_required')
->alertIf(param($request, 'password') !== param($request, 'password_repeat'), 'password_match');
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,126 +0,0 @@
<?php
namespace App\Controllers\Auth;
use App\Controllers\Controller;
use App\Database\Repositories\UserRepository;
use App\Web\Mail;
use App\Web\ValidationHelper;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Exception\HttpNotFoundException;
class RegisterController extends AuthController
{
/**
* @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->checkRecaptcha(make(ValidationHelper::class), $request)->fails()) {
return redirect($response, route('register.show'));
}
$validator = $this->getUserCreateValidator($request)->alertIf(empty(param($request, 'password')), 'password_required');
if ($validator->fails()) {
return redirect($response, route('register.show'));
}
$activateToken = bin2hex(random_bytes(16));
make(UserRepository::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'],
$this->config['base_url'],
route('activate', ['activateToken' => $activateToken]),
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,148 +0,0 @@
<?php
namespace App\Controllers;
use App\Database\Repositories\UserRepository;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Exception\HttpNotFoundException;
use ZipStream\Option\Archive;
use ZipStream\ZipStream;
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(UserRepository::class)->get($request, $id, true);
if (!$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 string|null $token
* @return Response
* @throws \ZipStream\Exception\FileNotFoundException
* @throws \ZipStream\Exception\FileNotReadableException
* @throws \ZipStream\Exception\OverflowException
* @throws HttpNotFoundException
*/
public function getScreenCloudConfig(Request $request, string $token): Response
{
$user = $this->database->query('SELECT * FROM `users` WHERE `token` = ? LIMIT 1', $token)->fetch();
if (!$user) {
throw new HttpNotFoundException($request);
}
$config = [
'token' => $token,
'host' => route('root'),
];
ob_end_clean();
$options = new Archive();
$options->setSendHttpHeaders(true);
$zip = new ZipStream($user->username.'-screencloud.zip', $options);
$zip->addFileFromPath('main.py', BASE_DIR.'resources/uploaders/screencloud/main.py');
$zip->addFileFromPath('icon.png', BASE_DIR.'static/images/favicon-32x32.png');
$zip->addFileFromPath('metadata.xml', BASE_DIR.'resources/uploaders/screencloud/metadata.xml');
$zip->addFileFromPath('settings.ui', BASE_DIR.'resources/uploaders/screencloud/settings.ui');
$zip->addFile('config.json', json_encode($config, JSON_UNESCAPED_SLASHES));
$zip->finish();
exit(0);
}
/**
* @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(UserRepository::class)->get($request, $id, true);
if (!$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,
]
);
}
/**
* @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 getKDEScript(Request $request, Response $response, int $id): Response
{
$user = make(UserRepository::class)->get($request, $id, true);
if (!$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_kde_uploader.sh.twig',
[
'username' => $user->username,
'upload_url' => route('upload'),
'token' => $user->token,
]
);
}
}

View file

@ -3,152 +3,65 @@
namespace App\Controllers;
use App\Database\DB;
use App\Database\Repositories\UserRepository;
use App\Web\Lang;
use App\Web\Session;
use App\Web\ValidationHelper;
use App\Web\View;
use DI\Container;
use DI\DependencyException;
use DI\NotFoundException;
use Exception;
use League\Flysystem\FileNotFoundException;
use League\Flysystem\Filesystem;
use Monolog\Logger;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Container;
/**
* @property Session session
* @property View view
* @property DB database
* @property Session|null session
* @property mixed|null view
* @property DB|null database
* @property Logger|null logger
* @property Filesystem|null storage
* @property Lang lang
* @property array config
* @property array settings
*/
abstract class Controller
{
/** @var Container */
protected $container;
public function __construct(Container $container)
{
$this->container = $container;
}
/** @var Container */
protected $container;
/**
* @param $name
*
* @return mixed|null
* @throws NotFoundException
*
* @throws DependencyException
*/
public function __get($name)
{
if ($this->container->has($name)) {
return $this->container->get($name);
}
public function __construct(Container $container)
{
$this->container = $container;
}
return null;
}
/**
* @param $name
* @return mixed|null
* @throws \Interop\Container\Exception\ContainerException
*/
public function __get($name)
{
if ($this->container->has($name)) {
return $this->container->get($name);
}
return null;
}
/**
* @param $key
* @param null $default
* @return object
*/
protected function getSetting($key, $default = null)
{
return $this->database->query('SELECT `value` FROM `settings` WHERE `key` = '.$this->database->getPdo()->quote($key))->fetch()->value ?? $default;
}
/**
* @param $id
* @return int
*/
protected function getUsedSpaceByUser($id): int
{
$medias = $this->database->query('SELECT `uploads`.`storage_path` FROM `uploads` WHERE `user_id` = ?', $id);
/**
* @param $current
* @param $max
*/
protected function setSessionQuotaInfo($current, $max)
{
$this->session->set('current_disk_quota', humanFileSize($current));
if ($this->getSetting('quota_enabled', 'off') === 'on') {
if ($max < 0) {
$this->session->set('max_disk_quota', '∞')->set('percent_disk_quota', null);
} else {
$this->session->set('max_disk_quota', humanFileSize($max))->set('percent_disk_quota', round(($current * 100) / $max));
}
} else {
$this->session->set('max_disk_quota', null)->set('percent_disk_quota', null);
}
}
$totalSize = 0;
/**
* @param Request $request
* @param $userId
* @param $fileSize
* @param bool $dec
* @return bool
*/
protected function updateUserQuota(Request $request, $userId, $fileSize, $dec = false)
{
$user = make(UserRepository::class)->get($request, $userId);
$filesystem = $this->storage;
foreach ($medias as $media) {
try {
$totalSize += $filesystem->getSize($media->storage_path);
} catch (FileNotFoundException $e) {
$this->logger->error('Error calculating file size', ['exception' => $e]);
}
}
if ($dec) {
$tot = max($user->current_disk_quota - $fileSize, 0);
} else {
$tot = $user->current_disk_quota + $fileSize;
if ($this->getSetting('quota_enabled') === 'on' && $user->max_disk_quota > 0 && $user->max_disk_quota < $tot) {
return false;
}
}
$this->database->query('UPDATE `users` SET `current_disk_quota`=? WHERE `id` = ?', [
$tot,
$user->id,
]);
return true;
}
/**
* @param $userId
* @throws Exception
*/
protected function refreshRememberCookie($userId)
{
$selector = bin2hex(random_bytes(8));
$token = bin2hex(random_bytes(32));
$expire = time() + 604800; // a week
$this->database->query('UPDATE `users` SET `remember_selector`=?, `remember_token`=?, `remember_expire`=? WHERE `id`=?', [
$selector,
password_hash($token, PASSWORD_DEFAULT),
date('Y-m-d\TH:i:s', $expire),
$userId,
]);
// Workaround for php <= 7.3
if (PHP_VERSION_ID < 70300) {
setcookie('remember', "{$selector}:{$token}", $expire, '; SameSite=Strict', '', isSecure(), true);
} else {
setcookie('remember', "{$selector}:{$token}", [
'expires' => $expire,
'httponly' => true,
'samesite' => 'Strict',
'secure' => isSecure(),
]);
}
}
/**
* @param Request $request
* @return ValidationHelper
*/
public function getUserCreateValidator(Request $request)
{
return make(ValidationHelper::class)
->alertIf(empty(param($request, 'username')), 'username_required')
->alertIf(!filter_var(param($request, 'email'), FILTER_VALIDATE_EMAIL), 'email_required')
->alertIf($this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `email` = ?', param($request, 'email'))->fetch()->count != 0, 'email_taken')
->alertIf($this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `username` = ?', param($request, 'username'))->fetch()->count != 0, 'username_taken');
}
}
return $totalSize;
}
}

View file

@ -2,96 +2,79 @@
namespace App\Controllers;
use App\Database\Repositories\MediaRepository;
use App\Database\Repositories\TagRepository;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use App\Database\Queries\MediaQuery;
use Slim\Http\Request;
use Slim\Http\Response;
class DashboardController extends Controller
{
/**
* @param Request $request
* @param Response $response
*
* @return Response
*/
public function redirects(Request $request, Response $response): Response
{
if (param($request, 'afterInstall') !== null && !is_dir(BASE_DIR.'install')) {
$this->session->alert(lang('installed'), 'success');
}
return redirect($response, route('home'));
}
/**
* @param Request $request
* @param Response $response
* @return Response
*/
public function redirects(Request $request, Response $response): Response
{
if ($request->getParam('afterInstall') !== null && !is_dir(BASE_DIR . 'install')) {
$this->session->alert(lang('installed'), 'success');
}
/**
* @param Request $request
* @param Response $response
* @param int|null $page
*
* @return Response
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*
* @throws \Twig\Error\LoaderError
*/
public function home(Request $request, Response $response, int $page = 0): Response
{
$page = max(0, --$page);
return redirect($response, 'home');
}
switch (param($request, 'sort', 'time')) {
case 'size':
$order = MediaRepository::ORDER_SIZE;
break;
case 'name':
$order = MediaRepository::ORDER_NAME;
break;
default:
case 'time':
$order = MediaRepository::ORDER_TIME;
break;
}
/**
* @param Request $request
* @param Response $response
* @param $args
* @return Response
*/
public function home(Request $request, Response $response, $args): Response
{
$page = isset($args['page']) ? (int)$args['page'] : 0;
$page = max(0, --$page);
$isAdmin = (bool) $this->session->get('admin', false);
$query = new MediaQuery($this->database, $this->session->get('admin', false), $this->storage);
/** @var MediaRepository $query */
$query = make(MediaRepository::class, ['isAdmin' => $isAdmin])
->orderBy($order, param($request, 'order', 'DESC'))
->withUserId($this->session->get('user_id'))
->search(param($request, 'search', null))
->filterByTag(param($request, 'tag'))
->run($page);
switch ($request->getParam('sort', 'time')) {
case 'size':
$order = MediaQuery::ORDER_SIZE;
break;
case 'name':
$order = MediaQuery::ORDER_NAME;
break;
default:
case 'time':
$order = MediaQuery::ORDER_TIME;
break;
}
$tags = make(TagRepository::class, [
'isAdmin' => $isAdmin,
'userId' => $this->session->get('user_id')
])->all();
$query->orderBy($order, $request->getParam('order', 'DESC'))
->withUserId($this->session->get('user_id'))
->search($request->getParam('search', null))
->run($page);
return view()->render(
$response,
($this->session->get('gallery_view', $isAdmin)) ? 'dashboard/list.twig' : 'dashboard/grid.twig',
[
'medias' => $query->getMedia(),
'next' => $page < floor($query->getPages()),
'previous' => $page >= 1,
'current_page' => ++$page,
'copy_raw' => $this->session->get('copy_raw', false),
'tags' => $tags,
]
);
}
return $this->view->render(
$response,
($this->session->get('admin', false) && $this->session->get('gallery_view', true)) ? 'dashboard/admin.twig' : 'dashboard/home.twig',
[
'medias' => $query->getMedia(),
'next' => $page < floor($query->getPages()),
'previous' => $page >= 1,
'current_page' => ++$page,
]
);
}
/**
* @param Response $response
*
* @return Response
*/
public function switchView(Response $response): Response
{
$isAdmin = (bool) $this->session->get('admin', false);
$this->session->set('gallery_view', !$this->session->get('gallery_view', $isAdmin));
return redirect($response, route('home'));
}
}
/**
* @param Request $request
* @param Response $response
* @param $args
* @return Response
*/
public function switchView(Request $request, Response $response, $args): Response
{
$this->session->set('gallery_view', !$this->session->get('gallery_view', true));
return redirect($response, 'home');
}
}

View file

@ -1,49 +0,0 @@
<?php
namespace App\Controllers;
use App\Database\Repositories\UserRepository;
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(UserRepository::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->username, 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,549 +0,0 @@
<?php
namespace App\Controllers;
use App\Database\Repositories\UserRepository;
use App\Web\UA;
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;
use Slim\Psr7\Stream;
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, true);
if (!$media || (!$media->published && $this->session->get('user_id') !== $media->user_id && !$this->session->get(
'admin',
false
))) {
throw new HttpNotFoundException($request);
}
$filesystem = $this->storage;
$userAgent = $request->getHeaderLine('User-Agent');
$mime = $filesystem->getMimetype($media->storage_path);
try {
$media->mimetype = $mime;
$media->extension = pathinfo($media->filename, PATHINFO_EXTENSION);
$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 <= (500 * 1024)) { // less than 500 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);
}
if (
UA::isBot($userAgent) &&
!(
// embed if enabled
(UA::embedsLinks($userAgent) &&
isEmbeddable($mime) &&
$this->getSetting('image_embeds') === 'on') ||
// if the file is too large to be displayed as non embedded
(UA::embedsLinks($userAgent) &&
isEmbeddable($mime) &&
$size >= (8 * 1024 * 1024))
)
) {
return $this->streamMedia($request, $response, $filesystem, $media);
}
return view()->render($response, 'upload/public.twig', [
'delete_token' => $token,
'media' => $media,
'type' => $type,
'url' => urlFor(glue($userCode, $mediaCode)),
'copy_raw' => $this->session->get('copy_raw', false),
]);
}
/**
* @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, false);
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 (must_be_escaped($this->storage->getMimetype($media->storage_path))) {
$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, false);
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 string $vanity
* @param string $id
*
* @return Response
* @throws HttpNotFoundException
* @throws HttpBadRequestException
*/
public function createVanity(Request $request, Response $response, int $id): Response
{
$media = $this->database->query('SELECT * FROM `uploads` WHERE `id` = ? LIMIT 1', $id)->fetch();
$vanity = param($request, 'vanity');
$vanity = preg_replace('/[^a-z0-9]+/', '-', strtolower($vanity));
//handle collisions
$collision = $this->database->query('SELECT * FROM `uploads` WHERE `code` = ? AND `id` != ? LIMIT 1', [$vanity, $id])->fetch();
if (!$media) {
throw new HttpNotFoundException($request);
}
if ($vanity === '' || $collision) {
throw new HttpBadRequestException($request);
}
$this->database->query('UPDATE `uploads` SET `code` = ? WHERE `id` = ?', [$vanity, $media->id]);
$media->code = $vanity;
$response->getBody()->write(json_encode($media));
$this->logger->info('User '.$this->session->get('username').' created a vanity link for media '.$media->id);
return $response;
}
/**
* @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')) {
throw new HttpUnauthorizedException($request);
}
$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(UserRepository::class)->get($request, $media->user_id, true);
$this->setSessionQuotaInfo($user->current_disk_quota, $user->max_disk_quota);
}
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, false);
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);
$this->database->query('DELETE FROM `tags` WHERE `tags`.`id` NOT IN (SELECT `uploads_tags`.`tag_id` FROM `uploads_tags`)');
}
}
/**
* @param $userCode
* @param $mediaCode
*
* @param bool $withTags
* @return mixed
*/
protected function getMedia($userCode, $mediaCode, $withTags = false)
{
$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();
if (!$withTags || !$media) {
return $media;
}
$media->tags = [];
foreach ($this->database->query(
'SELECT `tags`.`id`, `tags`.`name` FROM `uploads_tags` INNER JOIN `tags` ON `uploads_tags`.`tag_id` = `tags`.`id` WHERE `uploads_tags`.`upload_id` = ?',
$media->mediaId
) as $tag) {
$media->tags[$tag->id] = $tag->name;
}
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);
$this->session->close();
$mime = $storage->getMimetype($media->storage_path);
if ((param($request, 'width') !== null || param($request, 'height') !== null) && explode(
'/',
$mime
)[0] === 'image') {
return $this->makeThumbnail(
$storage,
$media,
param($request, 'width'),
param($request, 'height'),
$disposition
);
}
$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();
}
if ($end > $stream->getSize() - 1) {
$end = $stream->getSize() - 1;
}
$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();
fpassthru($stream->detach());
exit(0);
}
}

View file

@ -1,74 +0,0 @@
<?php
namespace App\Controllers;
use App\Database\Repositories\UserRepository;
use App\Web\ValidationHelper;
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(UserRepository::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(UserRepository::class)->get($request, $id, true);
/** @var ValidationHelper $validator */
$validator = make(ValidationHelper::class)
->alertIf(!filter_var(param($request, 'email'), FILTER_VALIDATE_EMAIL), 'email_required')
->alertIf($this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `email` = ? AND `email` <> ?', [param($request, 'email'), $user->email])->fetch()->count != 0, 'email_taken');
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)->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,115 +0,0 @@
<?php
namespace App\Controllers;
use App\Database\Repositories\UserRepository;
use App\Web\Theme;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Exception\HttpBadRequestException;
use Slim\Exception\HttpInternalServerErrorException;
class SettingController extends Controller
{
/**
* @param Request $request
* @param Response $response
*
* @return Response
* @throws HttpInternalServerErrorException
*/
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'));
$this->updateSetting('auto_tagging', param($request, 'auto_tagging', '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(UserRepository::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->updateSetting('image_embeds', param($request, 'image_embeds'));
$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
* @throws HttpInternalServerErrorException
*/
public function applyTheme(Request $request)
{
$css = param($request, 'css');
if ($css === null) {
return;
}
if (!is_writable(BASE_DIR.'static/bootstrap/css/bootstrap.min.css')) {
$this->session->alert(lang('cannot_write_file'), 'danger');
throw new HttpInternalServerErrorException($request);
}
make(Theme::class)->applyTheme($css);
// if is default, remove setting
if ($css !== Theme::default()) {
$this->updateSetting('css', $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,79 +0,0 @@
<?php
namespace App\Controllers;
use App\Database\Repositories\TagRepository;
use App\Web\ValidationHelper;
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
{
/**
* @param Request $request
* @param Response $response
* @return Response
* @throws HttpBadRequestException
*/
public function addTag(Request $request, Response $response): Response
{
$validator = $this->validateTag($request)->failIf(empty(param($request, 'tag')));
if ($validator->fails()) {
throw new HttpBadRequestException($request);
}
[$id, $limit] = make(TagRepository::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);
if ($validator->fails()) {
throw new HttpBadRequestException($request);
}
$result = make(TagRepository::class)->removeTag(param($request, 'tagId'), param($request, 'mediaId'));
if ($result === null) {
throw new HttpNotFoundException($request);
}
$this->logger->info("Tag removed ".param($request, 'tagId').', from media '.param($request, 'mediaId'));
return json($response, [
'deleted' => $result,
]);
}
/**
* @param Request $request
* @return ValidationHelper
*/
protected function validateTag(Request $request)
{
return make(ValidationHelper::class)
->failIf(empty(param($request, 'mediaId')))
->failIf($this->database->query('SELECT COUNT(*) AS `count` FROM `uploads` WHERE `id` = ?', param($request, 'mediaId'))->fetch()->count == 0)
->failIf(!$this->session->get('admin', false) && $this->database->query('SELECT `user_id` 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,71 +2,50 @@
namespace App\Controllers;
use App\Web\Session;
use Monolog\Logger;
use Parsedown;
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;
use function glob_recursive;
use function redirect;
use function removeDirectory;
use function route;
use function urlFor;
class UpgradeController extends Controller
{
const GITHUB_SOURCE_API = 'https://api.github.com/repos/SergiX44/XBackBone/releases';
/**
* @param Request $request
* @param Response $response
*
* @param Logger $logger
* @param Session $session
* @return Response
*/
public function upgrade(Response $response, Logger $logger, Session $session): Response
public function upgrade(Request $request, Response $response): Response
{
if (!extension_loaded('zip')) {
$session->alert(lang('zip_ext_not_loaded'), 'danger');
return redirect($response, route('system'));
}
if (!is_writable(BASE_DIR)) {
$session->alert(lang('path_not_writable', BASE_DIR), 'warning');
return redirect($response, route('system'));
$this->session->alert(lang('path_not_writable', BASE_DIR), 'warning');
return redirect($response, 'system');
}
try {
$json = $this->getApiJson();
} catch (RuntimeException $e) {
$session->alert($e->getMessage(), 'danger');
return redirect($response, route('system'));
} catch (\RuntimeException $e) {
$this->session->alert($e->getMessage(), 'danger');
return redirect($response, 'system');
}
if (version_compare($json[0]->tag_name, PLATFORM_VERSION, '<=')) {
$session->alert(lang('already_latest_version'), 'warning');
return redirect($response, route('system'));
$this->session->alert(lang('already_latest_version'), 'warning');
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) {
$session->alert(lang('cannot_retrieve_file'), 'danger');
return redirect($response, route('system'));
}
$this->session->alert(lang('cannot_retrieve_file'), 'danger');
return redirect($response, 'system');
};
if (filesize($tmpFile) !== $json[0]->assets[0]->size) {
$session->alert(lang('file_size_no_match'), 'danger');
return redirect($response, route('system'));
$this->session->alert(lang('file_size_no_match'), 'danger');
return redirect($response, 'system');
}
$logger->info('System update started.');
$config = require BASE_DIR.'config.php';
$config['maintenance'] = true;
@ -85,6 +64,7 @@ class UpgradeController extends Controller
removeDirectory(BASE_DIR.'vendor/');
$updateZip = new ZipArchive();
$updateZip->open($tmpFile);
@ -98,77 +78,53 @@ class UpgradeController extends Controller
}
}
foreach ($currentFiles as $extraneous) {
unlink($extraneous);
}
$updateZip->close();
unlink($tmpFile);
foreach ($currentFiles as $extraneous) {
if (is_dir($extraneous)) {
removeDirectory($extraneous);
} else {
unlink($extraneous);
}
}
$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
{
$jsonResponse = [
'status' => 'OK',
'message' => lang('already_latest_version'),
'status' => null,
'message' => null,
'upgrade' => false,
];
$acceptPrerelease = param($request, 'prerelease', 'false') === 'true';
$acceptPrerelease = $request->getParam('prerelease', 'false') === 'true';
try {
$json = $this->getApiJson();
$jsonResponse['status'] = 'OK';
foreach ($json as $release) {
if (
$release->prerelease === $acceptPrerelease &&
version_compare($release->tag_name, PLATFORM_VERSION, '>') &&
version_compare($release->tag_name, '4.0.0', '<')
) {
$jsonResponse['message'] = lang('new_version_available', [$release->tag_name]);
if (version_compare($release->tag_name, PLATFORM_VERSION, '>') && ($release->prerelease === $acceptPrerelease)) {
$jsonResponse['message'] = lang('new_version_available', $release->tag_name);
$jsonResponse['upgrade'] = true;
break;
}
if (version_compare($release->tag_name, PLATFORM_VERSION, '<=')) {
$jsonResponse['message'] = lang('already_latest_version');
$jsonResponse['upgrade'] = false;
break;
}
}
} catch (RuntimeException $e) {
} catch (\RuntimeException $e) {
$jsonResponse['status'] = 'ERROR';
$jsonResponse['message'] = $e->getMessage();
}
return json($response, $jsonResponse);
}
/**
* @param Request $request
* @param Response $response
* @return Response
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*/
public function changelog(Request $request, Response $response): Response
{
return view()->render($response, 'dashboard/changelog.twig', [
'content' => Parsedown::instance()->text(file_get_contents('CHANGELOG.md')),
]);
return $response->withJson($jsonResponse);
}
protected function getApiJson()
@ -186,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,236 +2,411 @@
namespace App\Controllers;
use App\Database\Repositories\TagRepository;
use App\Database\Repositories\UserRepository;
use App\Exceptions\ValidationException;
use Exception;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\UploadedFileInterface;
use App\Exceptions\UnauthorizedException;
use Intervention\Image\ImageManagerStatic as Image;
use League\Flysystem\FileExistsException;
use League\Flysystem\FileNotFoundException;
use League\Flysystem\Filesystem;
use Slim\Exception\NotFoundException;
use Slim\Http\Request;
use Slim\Http\Response;
use Slim\Http\Stream;
class UploadController extends Controller
{
private $json = [
'message' => null,
'version' => PLATFORM_VERSION,
];
/**
* @param Response $response
*
* @return Response
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*/
public function uploadWebPage(Response $response): Response
{
$maxFileSize = min(stringToBytes(ini_get('post_max_size')), stringToBytes(ini_get('upload_max_filesize')));
/**
* @param Request $request
* @param Response $response
* @return Response
* @throws FileExistsException
*/
public function upload(Request $request, Response $response): Response
{
return view()->render($response, 'upload/web.twig', [
'max_file_size' => humanFileSize($maxFileSize),
]);
}
$json = [
'message' => null,
'version' => PLATFORM_VERSION,
];
/**
* @param Request $request
* @param Response $response
* @return Response
* @throws Exception
*/
public function uploadWeb(Request $request, Response $response): Response
{
try {
$file = $this->validateFile($request, $response);
if ($this->settings['maintenance']) {
$json['message'] = 'Endpoint under maintenance.';
return $response->withJson($json, 503);
}
$user = make(UserRepository::class)->get($request, $this->session->get('user_id'));
if ($request->getServerParam('CONTENT_LENGTH') > stringToBytes(ini_get('post_max_size'))) {
$json['message'] = 'File too large (post_max_size too low?).';
return $response->withJson($json, 400);
}
$this->validateUser($request, $response, $file, $user);
} catch (ValidationException $e) {
return $e->response();
}
if (isset($request->getUploadedFiles()['upload']) && $request->getUploadedFiles()['upload']->getError() === UPLOAD_ERR_INI_SIZE) {
$json['message'] = 'File too large (upload_max_filesize too low?).';
return $response->withJson($json, 400);
}
if (!$this->updateUserQuota($request, $user->id, $file->getSize())) {
$this->json['message'] = 'User disk quota exceeded.';
if ($request->getParam('token') === null) {
$json['message'] = 'Token not specified.';
return $response->withJson($json, 400);
}
return json($response, $this->json, 507);
}
$user = $this->database->query('SELECT * FROM `users` WHERE `token` = ? LIMIT 1', $request->getParam('token'))->fetch();
try {
$response = $this->saveMedia($response, $file, $user);
$this->setSessionQuotaInfo($user->current_disk_quota + $file->getSize(), $user->max_disk_quota);
} catch (Exception $e) {
$this->updateUserQuota($request, $user->id, $file->getSize(), true);
throw $e;
}
if (!$user) {
$json['message'] = 'Token specified not found.';
return $response->withJson($json, 404);
}
return $response;
}
if (!$user->active) {
$json['message'] = 'Account disabled.';
return $response->withJson($json, 401);
}
/**
* @param Request $request
* @param Response $response
*
* @return Response
* @throws Exception
*/
public function uploadEndpoint(Request $request, Response $response): Response
{
if ($this->config['maintenance']) {
$this->json['message'] = 'Endpoint under maintenance.';
do {
$code = humanRandomString();
} while ($this->database->query('SELECT COUNT(*) AS `count` FROM `uploads` WHERE `code` = ?', $code)->fetch()->count > 0);
return json($response, $this->json, 503);
}
/** @var \Psr\Http\Message\UploadedFileInterface $file */
$file = $request->getUploadedFiles()['upload'];
try {
$file = $this->validateFile($request, $response);
$fileInfo = pathinfo($file->getClientFilename());
$storagePath = "$user->user_code/$code.$fileInfo[extension]";
if (param($request, 'token') === null) {
$this->json['message'] = 'Token not specified.';
$this->storage->writeStream($storagePath, $file->getStream()->detach());
return json($response, $this->json, 400);
}
$this->database->query('INSERT INTO `uploads`(`user_id`, `code`, `filename`, `storage_path`) VALUES (?, ?, ?, ?)', [
$user->id,
$code,
$file->getClientFilename(),
$storagePath,
]);
$user = $this->database->query('SELECT * FROM `users` WHERE `token` = ? LIMIT 1', param($request, 'token'))->fetch();
$json['message'] = 'OK.';
$json['url'] = urlFor("/$user->user_code/$code.$fileInfo[extension]");
$this->validateUser($request, $response, $file, $user);
} catch (ValidationException $e) {
return $e->response();
}
$this->logger->info("User $user->username uploaded new media.", [$this->database->raw()->lastInsertId()]);
if (!$this->updateUserQuota($request, $user->id, $file->getSize())) {
$this->json['message'] = 'User disk quota exceeded.';
return $response->withJson($json, 201);
}
return json($response, $this->json, 507);
}
/**
* @param Request $request
* @param Response $response
* @param $args
* @return Response
* @throws FileNotFoundException
* @throws NotFoundException
*/
public function show(Request $request, Response $response, $args): Response
{
$media = $this->getMedia($args['userCode'], $args['mediaCode']);
try {
$response = $this->saveMedia($response, $file, $user);
} catch (Exception $e) {
$this->updateUserQuota($request, $user->id, $file->getSize(), true);
throw $e;
}
return $response;
}
if (!$media || (!$media->published && $this->session->get('user_id') !== $media->user_id && !$this->session->get('admin', false))) {
throw new NotFoundException($request, $response);
}
/**
* @param Request $request
* @param Response $response
* @return UploadedFileInterface
* @throws ValidationException
*/
protected function validateFile(Request $request, Response $response)
{
$iniValue = ini_get('post_max_size');
$maxPostSize = $iniValue === '0' ? INF : stringToBytes($iniValue);
if ($request->getServerParams()['CONTENT_LENGTH'] > $maxPostSize) {
$this->json['message'] = 'File too large (post_max_size too low?).';
$filesystem = $this->storage;
throw new ValidationException(json($response, $this->json, 400));
}
if (isBot($request->getHeaderLine('User-Agent'))) {
return $this->streamMedia($request, $response, $filesystem, $media);
} else {
try {
$media->mimetype = $filesystem->getMimetype($media->storage_path);
$size = $filesystem->getSize($media->storage_path);
$file = array_values($request->getUploadedFiles());
/** @var UploadedFileInterface|null $file */
$file = $file[0] ?? null;
$type = explode('/', $media->mimetype)[0];
if ($type === 'image' && !isDisplayableImage($media->mimetype)) {
$type = 'application';
$media->mimetype = 'application/octet-stream';
}
if ($type === 'text') {
if ($size <= (200 * 1024)) { // less than 200 KB
$media->text = $filesystem->read($media->storage_path);
} else {
$type = 'application';
$media->mimetype = 'application/octet-stream';
}
}
$media->size = humanFileSize($size);
if ($file === null) {
$this->json['message'] = 'Request without file attached.';
} catch (FileNotFoundException $e) {
throw new NotFoundException($request, $response);
}
throw new ValidationException(json($response, $this->json, 400));
}
return $this->view->render($response, 'upload/public.twig', [
'delete_token' => isset($args['token']) ? $args['token'] : null,
'media' => $media,
'type' => $type,
'extension' => pathinfo($media->filename, PATHINFO_EXTENSION),
]);
}
}
if ($file->getError() === UPLOAD_ERR_INI_SIZE) {
$this->json['message'] = 'File too large (upload_max_filesize too low?).';
/**
* @param Request $request
* @param Response $response
* @param $args
* @return Response
* @throws NotFoundException
* @throws UnauthorizedException
*/
public function deleteByToken(Request $request, Response $response, $args): Response
{
$media = $this->getMedia($args['userCode'], $args['mediaCode']);
throw new ValidationException(json($response, $this->json, 400));
}
if (!$media) {
throw new NotFoundException($request, $response);
}
return $file;
}
$user = $this->database->query('SELECT `id`, `active` FROM `users` WHERE `token` = ? LIMIT 1', $args['token'])->fetch();
/**
* @param Request $request
* @param Response $response
* @param UploadedFileInterface $file
* @param $user
* @return void
* @throws ValidationException
*/
protected function validateUser(Request $request, Response $response, UploadedFileInterface $file, $user)
{
if (!$user) {
$this->json['message'] = 'Token specified not found.';
if (!$user) {
$this->session->alert(lang('token_not_found'), 'danger');
return $response->withRedirect($request->getHeaderLine('HTTP_REFERER'));
}
throw new ValidationException(json($response, $this->json, 404));
}
if (!$user->active) {
$this->session->alert(lang('account_disabled'), 'danger');
return $response->withRedirect($request->getHeaderLine('HTTP_REFERER'));
}
if (!$user->active) {
$this->json['message'] = 'Account disabled.';
if ($this->session->get('admin', false) || $user->id === $media->user_id) {
throw new ValidationException(json($response, $this->json, 401));
}
}
try {
$this->storage->delete($media->storage_path);
} catch (FileNotFoundException $e) {
throw new NotFoundException($request, $response);
} finally {
$this->database->query('DELETE FROM `uploads` WHERE `id` = ?', $media->mediaId);
$this->logger->info('User ' . $user->username . ' deleted a media via token.', [$media->mediaId]);
}
} else {
throw new UnauthorizedException();
}
return redirect($response, 'home');
}
/**
* @param Request $request
* @param Response $response
* @param $args
* @return Response
* @throws NotFoundException
* @throws FileNotFoundException
*/
public function getRawById(Request $request, Response $response, $args): Response
{
$media = $this->database->query('SELECT * FROM `uploads` WHERE `id` = ? LIMIT 1', $args['id'])->fetch();
if (!$media) {
throw new NotFoundException($request, $response);
}
return $this->streamMedia($request, $response, $this->storage, $media);
}
/**
* @param Request $request
* @param Response $response
* @param $args
* @return Response
* @throws NotFoundException
* @throws FileNotFoundException
*/
public function showRaw(Request $request, Response $response, $args): Response
{
$media = $this->getMedia($args['userCode'], $args['mediaCode']);
if (!$media || !$media->published && $this->session->get('user_id') !== $media->user_id && !$this->session->get('admin', false)) {
throw new NotFoundException($request, $response);
}
return $this->streamMedia($request, $response, $this->storage, $media);
}
/**
* @param Response $response
* @param UploadedFileInterface $file
* @param $user
* @return Response
* @throws \League\Flysystem\FileExistsException
* @throws \League\Flysystem\FileNotFoundException
*/
protected function saveMedia(Response $response, UploadedFileInterface $file, $user)
{
do {
$code = humanRandomString();
} while ($this->database->query('SELECT COUNT(*) AS `count` FROM `uploads` WHERE `code` = ?', $code)->fetch()->count > 0);
/**
* @param Request $request
* @param Response $response
* @param $args
* @return Response
* @throws NotFoundException
* @throws FileNotFoundException
*/
public function download(Request $request, Response $response, $args): Response
{
$media = $this->getMedia($args['userCode'], $args['mediaCode']);
$fileInfo = pathinfo($file->getClientFilename());
$storagePath = "$user->user_code/$code.$fileInfo[extension]";
if (!$media || !$media->published && $this->session->get('user_id') !== $media->user_id && !$this->session->get('admin', false)) {
throw new NotFoundException($request, $response);
}
return $this->streamMedia($request, $response, $this->storage, $media, 'attachment');
}
$this->storage->writeStream($storagePath, $file->getStream()->detach());
/**
* @param Request $request
* @param Response $response
* @param $args
* @return Response
* @throws NotFoundException
*/
public function togglePublish(Request $request, Response $response, $args): Response
{
if ($this->session->get('admin')) {
$media = $this->database->query('SELECT * FROM `uploads` WHERE `id` = ? LIMIT 1', $args['id'])->fetch();
} else {
$media = $this->database->query('SELECT * FROM `uploads` WHERE `id` = ? AND `user_id` = ? LIMIT 1', [$args['id'], $this->session->get('user_id')])->fetch();
}
$this->database->query('INSERT INTO `uploads`(`user_id`, `code`, `filename`, `storage_path`, `published`) VALUES (?, ?, ?, ?, ?)', [
$user->id,
$code,
$file->getClientFilename(),
$storagePath,
$user->hide_uploads == '1' ? 0 : 1,
]);
$mediaId = $this->database->getPdo()->lastInsertId();
if (!$media) {
throw new NotFoundException($request, $response);
}
if ($this->getSetting('auto_tagging') === 'on') {
$this->autoTag($mediaId, $storagePath);
}
$this->database->query('UPDATE `uploads` SET `published`=? WHERE `id`=?', [$media->published ? 0 : 1, $media->id]);
$this->json['message'] = 'OK';
$this->json['url'] = urlFor("/{$user->user_code}/{$code}.{$fileInfo['extension']}");
$this->json['raw_url'] = urlFor("/{$user->user_code}/{$code}/raw.{$fileInfo['extension']}");
return $response->withStatus(200);
}
$this->logger->info("User $user->username uploaded new media.", [$mediaId]);
/**
* @param Request $request
* @param Response $response
* @param $args
* @return Response
* @throws NotFoundException
* @throws UnauthorizedException
*/
public function delete(Request $request, Response $response, $args): Response
{
$media = $this->database->query('SELECT * FROM `uploads` WHERE `id` = ? LIMIT 1', $args['id'])->fetch();
return json($response, $this->json, 201);
}
if (!$media) {
throw new NotFoundException($request, $response);
}
/**
* @param $mediaId
* @param $storagePath
* @throws \League\Flysystem\FileNotFoundException
*/
protected function autoTag($mediaId, $storagePath)
{
$mime = $this->storage->getMimetype($storagePath);
if ($this->session->get('admin', false) || $media->user_id === $this->session->get('user_id')) {
[$type, $subtype] = explode('/', $mime);
try {
$this->storage->delete($media->storage_path);
} catch (FileNotFoundException $e) {
throw new NotFoundException($request, $response);
} finally {
$this->database->query('DELETE FROM `uploads` WHERE `id` = ?', $args['id']);
$this->logger->info('User ' . $this->session->get('username') . ' deleted a media.', [$args['id']]);
$this->session->set('used_space', humanFileSize($this->getUsedSpaceByUser($this->session->get('user_id'))));
}
} else {
throw new UnauthorizedException();
}
/** @var TagRepository $query */
$query = make(TagRepository::class);
$query->addTag($type, $mediaId);
return $response->withStatus(200);
}
if ($type === 'application' || $subtype === 'gif') {
$query->addTag($subtype, $mediaId);
}
}
}
/**
* @param $userCode
* @param $mediaCode
* @return mixed
*/
protected function getMedia($userCode, $mediaCode)
{
$mediaCode = pathinfo($mediaCode)['filename'];
$media = $this->database->query('SELECT `uploads`.*, `users`.*, `users`.`id` AS `userId`, `uploads`.`id` AS `mediaId` FROM `uploads` INNER JOIN `users` ON `uploads`.`user_id` = `users`.`id` WHERE `user_code` = ? AND `uploads`.`code` = ? LIMIT 1', [
$userCode,
$mediaCode,
])->fetch();
return $media;
}
/**
* @param Request $request
* @param Response $response
* @param Filesystem $storage
* @param $media
* @param string $disposition
* @return Response
* @throws FileNotFoundException
*/
protected function streamMedia(Request $request, Response $response, Filesystem $storage, $media, string $disposition = 'inline'): Response
{
set_time_limit(0);
$mime = $storage->getMimetype($media->storage_path);
if ($request->getParam('width') !== null && explode('/', $mime)[0] === 'image') {
$image = Image::make($storage->readStream($media->storage_path))
->resizeCanvas(
$request->getParam('width'),
$request->getParam('height'),
'center')
->encode('png');
return $response
->withHeader('Content-Type', 'image/png')
->withHeader('Content-Disposition', $disposition . ';filename="scaled-' . pathinfo($media->filename)['filename'] . '.png"')
->write($image);
} else {
$stream = new Stream($storage->readStream($media->storage_path));
if (!in_array(explode('/', $mime)[0], ['image', 'video', 'audio']) || $disposition === 'attachment') {
return $response->withHeader('Content-Type', $mime)
->withHeader('Content-Disposition', $disposition . '; filename="' . $media->filename . '"')
->withHeader('Content-Length', $stream->getSize())
->withBody($stream);
}
$end = $stream->getSize() - 1;
if ($request->getServerParam('HTTP_RANGE') !== null) {
list(, $range) = explode('=', $request->getServerParam('HTTP_RANGE'), 2);
if (strpos($range, ',') !== false) {
return $response->withHeader('Content-Type', $mime)
->withHeader('Content-Disposition', $disposition . '; filename="' . $media->filename . '"')
->withHeader('Content-Length', $stream->getSize())
->withHeader('Accept-Ranges', 'bytes')
->withHeader('Content-Range', "0,{$stream->getSize()}")
->withStatus(416)
->withBody($stream);
}
if ($range === '-') {
$start = $stream->getSize() - (int)substr($range, 1);
} else {
$range = explode('-', $range);
$start = (int)$range[0];
$end = (isset($range[1]) && is_numeric($range[1])) ? (int)$range[1] : $stream->getSize();
}
$end = ($end > $stream->getSize() - 1) ? $stream->getSize() - 1 : $end;
$stream->seek($start);
header("Content-Type: $mime");
header('Content-Length: ' . ($end - $start + 1));
header('Accept-Ranges: bytes');
header("Content-Range: bytes $start-$end/{$stream->getSize()}");
http_response_code(206);
ob_end_clean();
$buffer = 16348;
$readed = $start;
while ($readed < $end) {
if ($readed + $buffer > $end) {
$buffer = $end - $readed + 1;
}
echo $stream->read($buffer);
$readed += $buffer;
}
exit(0);
}
return $response->withHeader('Content-Type', $mime)
->withHeader('Content-Length', $stream->getSize())
->withHeader('Accept-Ranges', 'bytes')
->withStatus(200)
->withBody($stream);
}
}
}

View file

@ -2,316 +2,420 @@
namespace App\Controllers;
use App\Database\Repositories\UserRepository;
use App\Web\Mail;
use App\Web\ValidationHelper;
use League\Flysystem\FileNotFoundException;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use App\Exceptions\UnauthorizedException;
use Slim\Exception\NotFoundException;
use Slim\Http\Request;
use Slim\Http\Response;
class UserController extends Controller
{
const PER_PAGE = 15;
const PER_PAGE = 15;
/**
* @param Response $response
* @param int|null $page
*
* @return Response
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*
* @throws \Twig\Error\LoaderError
*/
public function index(Response $response, int $page = 0): Response
{
$page = max(0, --$page);
/**
* @param Request $request
* @param Response $response
* @param $args
* @return Response
*/
public function index(Request $request, Response $response, $args): Response
{
$page = isset($args['page']) ? (int)$args['page'] : 0;
$page = max(0, --$page);
$users = $this->database->query('SELECT * FROM `users` LIMIT ? OFFSET ?', [self::PER_PAGE, $page * self::PER_PAGE])->fetchAll();
$users = $this->database->query('SELECT * FROM `users` LIMIT ? OFFSET ?', [self::PER_PAGE, $page * self::PER_PAGE])->fetchAll();
$pages = $this->database->query('SELECT COUNT(*) AS `count` FROM `users`')->fetch()->count / self::PER_PAGE;
$pages = $this->database->query('SELECT COUNT(*) AS `count` FROM `users`')->fetch()->count / self::PER_PAGE;
return view()->render(
$response,
'user/index.twig',
[
'users' => $users,
'next' => $page < floor($pages),
'previous' => $page >= 1,
'current_page' => ++$page,
'quota_enabled' => $this->getSetting('quota_enabled'),
]
);
}
return $this->view->render($response,
'user/index.twig',
[
'users' => $users,
'next' => $page < floor($pages),
'previous' => $page >= 1,
'current_page' => ++$page,
]
);
}
/**
* @param Response $response
*
* @return Response
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*
* @throws \Twig\Error\LoaderError
*/
public function create(Response $response): Response
{
return view()->render($response, 'user/create.twig', [
'default_user_quota' => humanFileSize($this->getSetting('default_user_quota'), 0, true),
'quota_enabled' => $this->getSetting('quota_enabled', 'off'),
]);
}
/**
* @param Request $request
* @param Response $response
* @return Response
*/
public function create(Request $request, Response $response): Response
{
return $this->view->render($response, 'user/create.twig');
}
/**
* @param Request $request
* @param Response $response
*
* @return Response
* @throws \Exception
*/
public function store(Request $request, Response $response): Response
{
$maxUserQuota = -1;
$validator = $this->getUserCreateValidator($request)
->callIf($this->getSetting('quota_enabled') === 'on', function ($session) use (&$maxUserQuota, &$request) {
$maxUserQuota = param($request, 'max_user_quota', humanFileSize($this->getSetting('default_user_quota'), 0, true));
if (!preg_match('/(^[0-9]+[B|K|M|G|T]$)|(^\-1$)/i', $maxUserQuota)) {
$session->alert(lang('invalid_quota', 'danger'));
return false;
}
/**
* @param Request $request
* @param Response $response
* @return Response
*/
public function store(Request $request, Response $response): Response
{
if ($request->getParam('email') === null) {
$this->session->alert(lang('email_required'), 'danger');
return redirect($response, 'user.create');
}
if ($maxUserQuota !== '-1') {
$maxUserQuota = stringToBytes($maxUserQuota);
}
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');
}
return true;
});
if ($request->getParam('username') === null) {
$this->session->alert(lang('username_required'), 'danger');
return redirect($response, 'user.create');
}
if ($validator->fails()) {
return redirect($response, route('user.create'));
}
if ($request->getParam('password') === null) {
$this->session->alert(lang('password_required'), 'danger');
return redirect($response, 'user.create');
}
make(UserRepository::class)->create(
param($request, 'email'),
param($request, 'username'),
param($request, 'password'),
param($request, 'is_admin') !== null ? 1 : 0,
param($request, 'is_active') !== null ? 1 : 0,
$maxUserQuota,
false,
param($request, 'hide_uploads') !== null ? 1 : 0,
param($request, 'copy_raw') !== null ? 1 : 0
);
if ($this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `username` = ?', $request->getParam('username'))->fetch()->count > 0) {
$this->session->alert(lang('username_taken'), 'danger');
return redirect($response, 'user.create');
}
if (param($request, 'send_notification') !== null) {
$resetToken = null;
if (empty(param($request, 'password'))) {
$resetToken = bin2hex(random_bytes(16));
do {
$userCode = humanRandomString(5);
} while ($this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `user_code` = ?', $userCode)->fetch()->count > 0);
$this->database->query('UPDATE `users` SET `reset_token`=? WHERE `id` = ?', [
$resetToken,
$this->database->getPdo()->lastInsertId(),
]);
}
$this->sendCreateNotification($request, $resetToken);
}
$token = $this->generateNewToken();
$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']))]);
$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,
]);
return redirect($response, route('user.index'));
}
$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']))]);
/**
* @param Request $request
* @param Response $response
* @param int $id
*
* @return Response
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*/
public function edit(Request $request, Response $response, int $id): Response
{
$user = make(UserRepository::class)->get($request, $id);
return redirect($response, 'user.index');
}
return view()->render($response, 'user/edit.twig', [
'profile' => false,
'user' => $user,
'quota_enabled' => $this->getSetting('quota_enabled', 'off'),
'max_disk_quota' => $user->max_disk_quota > 0 ? humanFileSize($user->max_disk_quota, 0, true) : -1,
]);
}
/**
* @param Request $request
* @param Response $response
* @param $args
* @return Response
* @throws NotFoundException
*/
public function edit(Request $request, Response $response, $args): Response
{
$user = $this->database->query('SELECT * FROM `users` WHERE `id` = ? LIMIT 1', $args['id'])->fetch();
/**
* @param Request $request
* @param Response $response
* @param int $id
*
* @return Response
*/
public function update(Request $request, Response $response, int $id): Response
{
$user = make(UserRepository::class)->get($request, $id);
$user->max_disk_quota = -1;
if (!$user) {
throw new NotFoundException($request, $response);
}
/** @var ValidationHelper $validator */
$validator = make(ValidationHelper::class)
->alertIf(!filter_var(param($request, 'email'), FILTER_VALIDATE_EMAIL), 'email_required')
->alertIf(empty(param($request, 'username')), 'username_required')
->alertIf($this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `email` = ? AND `email` <> ?', [param($request, 'email'), $user->email])->fetch()->count != 0, 'email_taken')
->alertIf($this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `username` = ? AND `username` <> ?', [param($request, 'username'), $user->username])->fetch()->count != 0, 'username_taken')
->alertIf($user->id === $this->session->get('user_id') && param($request, 'is_admin') === null, 'cannot_demote')
->callIf($this->getSetting('quota_enabled') === 'on', function ($session) use (&$user, &$request) {
$maxUserQuota = param($request, 'max_user_quota', humanFileSize($this->getSetting('default_user_quota'), 0, true));
if (!preg_match('/(^[0-9]+[B|K|M|G|T]$)|(^\-1$)/i', $maxUserQuota)) {
$session->alert(lang('invalid_quota', 'danger'));
return false;
}
return $this->view->render($response, 'user/edit.twig', [
'profile' => false,
'user' => $user,
]);
}
if ($maxUserQuota !== '-1') {
$user->max_disk_quota = stringToBytes($maxUserQuota);
}
/**
* @param Request $request
* @param Response $response
* @param $args
* @return Response
* @throws NotFoundException
*/
public function update(Request $request, Response $response, $args): Response
{
$user = $this->database->query('SELECT * FROM `users` WHERE `id` = ? LIMIT 1', $args['id'])->fetch();
return true;
});
if (!$user) {
throw new NotFoundException($request, $response);
}
if ($validator->fails()) {
return redirect($response, route('user.edit', ['id' => $id]));
}
if ($request->getParam('email') === null) {
$this->session->alert(lang('email_required'), 'danger');
return redirect($response, 'user.edit', ['id' => $args['id']]);
}
make(UserRepository::class)->update(
$user->id,
param($request, 'email'),
param($request, 'username'),
param($request, 'password'),
param($request, 'is_admin') !== null ? 1 : 0,
param($request, 'is_active') !== null ? 1 : 0,
$user->max_disk_quota,
param($request, 'ldap') !== null ? 1 : 0,
param($request, 'hide_uploads') !== null ? 1 : 0,
param($request, 'copy_raw') !== null ? 1 : 0
);
if ($this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `email` = ? AND `email` <> ?', [$request->getParam('email'), $user->email])->fetch()->count > 0) {
$this->session->alert(lang('email_taken'), 'danger');
return redirect($response, 'user.edit', ['id' => $args['id']]);
}
if ($user->id === $this->session->get('user_id')) {
$this->setSessionQuotaInfo($user->current_disk_quota, $user->max_disk_quota);
}
if ($request->getParam('username') === null) {
$this->session->alert(lang('username_required'), 'danger');
return redirect($response, 'user.edit', ['id' => $args['id']]);
}
$this->session->alert(lang('user_updated', [param($request, 'username')]), 'success');
$this->logger->info('User '.$this->session->get('username')." updated $user->id.", [
array_diff_key((array) $user, array_flip(['password'])),
array_diff_key($request->getParsedBody(), array_flip(['password'])),
]);
if ($this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `username` = ? AND `username` <> ?', [$request->getParam('username'), $user->username])->fetch()->count > 0) {
$this->session->alert(lang('username_taken'), 'danger');
return redirect($response, 'user.edit', ['id' => $args['id']]);
}
return redirect($response, route('user.index'));
}
if ($user->id === $this->session->get('user_id') && $request->getParam('is_admin') === null) {
$this->session->alert(lang('cannot_demote'), 'danger');
return redirect($response, 'user.edit', ['id' => $args['id']]);
}
/**
* @param Request $request
* @param Response $response
* @param int $id
*
* @return Response
*/
public function delete(Request $request, Response $response, int $id): Response
{
$user = make(UserRepository::class)->get($request, $id);
if ($request->getParam('password') !== null && !empty($request->getParam('password'))) {
$this->database->query('UPDATE `users` SET `email`=?, `username`=?, `password`=?, `is_admin`=?, `active`=? WHERE `id` = ?', [
$request->getParam('email'),
$request->getParam('username'),
password_hash($request->getParam('password'), PASSWORD_DEFAULT),
$request->getParam('is_admin') !== null ? 1 : 0,
$request->getParam('is_active') !== null ? 1 : 0,
$user->id,
]);
} else {
$this->database->query('UPDATE `users` SET `email`=?, `username`=?, `is_admin`=?, `active`=? WHERE `id` = ?', [
$request->getParam('email'),
$request->getParam('username'),
$request->getParam('is_admin') !== null ? 1 : 0,
$request->getParam('is_active') !== null ? 1 : 0,
$user->id,
]);
}
if ($user->id === $this->session->get('user_id')) {
$this->session->alert(lang('cannot_delete'), 'danger');
$this->session->alert(lang('user_updated', [$request->getParam('username')]), 'success');
$this->logger->info('User ' . $this->session->get('username') . " updated $user->id.", [
array_diff_key((array)$user, array_flip(['password'])),
array_diff_key($request->getParams(), array_flip(['password'])),
]);
return redirect($response, route('user.index'));
}
return redirect($response, 'user.index');
$this->database->query('DELETE FROM `users` WHERE `id` = ?', $user->id);
}
$this->session->alert(lang('user_deleted'), 'success');
$this->logger->info('User '.$this->session->get('username')." deleted $user->id.");
/**
* @param Request $request
* @param Response $response
* @param $args
* @return Response
* @throws NotFoundException
*/
public function delete(Request $request, Response $response, $args): Response
{
$user = $this->database->query('SELECT * FROM `users` WHERE `id` = ? LIMIT 1', $args['id'])->fetch();
return redirect($response, route('user.index'));
}
if (!$user) {
throw new NotFoundException($request, $response);
}
/**
* @param Request $request
* @param Response $response
* @param int $id
* @return Response
*/
public function clearUserMedia(Request $request, Response $response, int $id): Response
{
$user = make(UserRepository::class)->get($request, $id, true);
if ($user->id === $this->session->get('user_id')) {
$this->session->alert(lang('cannot_delete'), 'danger');
return redirect($response, 'user.index');
}
$medias = $this->database->query('SELECT * FROM `uploads` WHERE `user_id` = ?', $user->id);
$this->database->query('DELETE FROM `users` WHERE `id` = ?', $user->id);
foreach ($medias as $media) {
try {
$this->storage->delete($media->storage_path);
} catch (FileNotFoundException $e) {
}
}
$this->session->alert(lang('user_deleted'), 'success');
$this->logger->info('User ' . $this->session->get('username') . " deleted $user->id.");
$this->database->query('DELETE FROM `uploads` WHERE `user_id` = ?', $user->id);
$this->database->query('UPDATE `users` SET `current_disk_quota`=? WHERE `id` = ?', [
0,
$user->id,
]);
return redirect($response, 'user.index');
}
$this->session->alert(lang('account_media_deleted'), 'success');
return redirect($response, route('user.edit', ['id' => $id]));
}
/**
* @param Request $request
* @param Response $response
* @return Response
* @throws NotFoundException
* @throws UnauthorizedException
*/
public function profile(Request $request, Response $response): Response
{
$user = $this->database->query('SELECT * FROM `users` WHERE `id` = ? LIMIT 1', $this->session->get('user_id'))->fetch();
/**
* @param Request $request
* @param Response $response
* @param int $id
*
* @return Response
*/
public function refreshToken(Request $request, Response $response, int $id): Response
{
$query = make(UserRepository::class);
$user = $query->get($request, $id, true);
if (!$user) {
throw new NotFoundException($request, $response);
}
$this->logger->info('User '.$this->session->get('username')." refreshed token of user $user->id.");
if ($user->id !== $this->session->get('user_id') && !$this->session->get('admin', false)) {
throw new UnauthorizedException();
}
$response->getBody()->write($query->refreshToken($user->id));
return $this->view->render($response, 'user/edit.twig', [
'profile' => true,
'user' => $user,
]);
}
return $response;
}
/**
* @param Request $request
* @param Response $response
* @param $args
* @return Response
* @throws NotFoundException
* @throws UnauthorizedException
*/
public function profileEdit(Request $request, Response $response, $args): Response
{
$user = $this->database->query('SELECT * FROM `users` WHERE `id` = ? LIMIT 1', $args['id'])->fetch();
/**
* @param $request
* @param null $resetToken
*/
private function sendCreateNotification($request, $resetToken = null)
{
if ($resetToken === null && !empty(param($request, 'password'))) {
$message = lang('mail.new_account_text_with_pw', [
param($request, 'username'),
$this->config['app_name'],
$this->config['base_url'],
$this->config['base_url'],
param($request, 'username'),
param($request, 'password'),
route('login.show'),
route('login.show'),
]);
} else {
$message = lang('mail.new_account_text_with_reset', [
param($request, 'username'),
$this->config['app_name'],
$this->config['base_url'],
$this->config['base_url'],
route('recover.password', ['resetToken' => $resetToken]),
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 ($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->logger->info('User ' . $this->session->get('username') . " refreshed token of user $user->id.");
$response->getBody()->write($token);
return $response;
}
/**
* @param Request $request
* @param Response $response
* @param $args
* @return Response
* @throws NotFoundException
* @throws UnauthorizedException
*/
public function getShareXconfigFile(Request $request, Response $response, $args): Response
{
$user = $this->database->query('SELECT * FROM `users` WHERE `id` = ? LIMIT 1', $args['id'])->fetch();
if (!$user) {
throw new NotFoundException($request, $response);
}
if ($user->id !== $this->session->get('user_id') && !$this->session->get('admin', false)) {
throw new UnauthorizedException();
}
if ($user->token === null || $user->token === '') {
$this->session->alert('You don\'t have a personal upload token. (Click the update token button and try again)', 'danger');
return $response->withRedirect($request->getHeaderLine('HTTP_REFERER'));
}
$json = [
'DestinationType' => 'ImageUploader, TextUploader, FileUploader',
'RequestURL' => route('upload'),
'FileFormName' => 'upload',
'Arguments' => [
'file' => '$filename$',
'text' => '$input$',
'token' => $user->token,
],
'URL' => '$json:url$',
'ThumbnailURL' => '$json:url$/raw',
'DeletionURL' => '$json:url$/delete/' . $user->token,
];
return $response
->withHeader('Content-Disposition', 'attachment;filename="' . $user->username . '-ShareX.sxcu"')
->withJson($json, 200, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
}
/**
* @param Request $request
* @param Response $response
* @param $args
* @return Response
* @throws NotFoundException
* @throws UnauthorizedException
*/
public function getUploaderScriptFile(Request $request, Response $response, $args): Response
{
$user = $this->database->query('SELECT * FROM `users` WHERE `id` = ? LIMIT 1', $args['id'])->fetch();
if (!$user) {
throw new NotFoundException($request, $response);
}
if ($user->id !== $this->session->get('user_id') && !$this->session->get('admin', false)) {
throw new UnauthorizedException();
}
if ($user->token === null || $user->token === '') {
$this->session->alert('You don\'t have a personal upload token. (Click the update token button and try again)', 'danger');
return $response->withRedirect($request->getHeaderLine('HTTP_REFERER'));
}
return $this->view->render($response->withHeader('Content-Disposition', 'attachment;filename="xbackbone_uploader_' . $user->username . '.sh"'),
'scripts/xbackbone_uploader.sh.twig',
[
'username' => $user->username,
'upload_url' => route('upload'),
'token' => $user->token,
]
);
}
/**
* @return string
*/
protected function generateNewToken(): string
{
do {
$token = 'token_' . md5(uniqid('', true));
} while ($this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `token` = ?', $token)->fetch()->count > 0);
return $token;
}
}

View file

@ -2,64 +2,128 @@
namespace App\Database;
use PDO;
class DB
{
/** @var DB */
protected static $instance;
/** @var PDO */
protected $pdo;
/** @var DB */
protected static $instance;
/** @var string */
protected $currentDriver;
/** @var string */
private static $password;
public function __construct(string $dsn, string $username = null, string $password = null)
{
$this->pdo = new PDO($dsn, $username, $password);
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ);
/** @var string */
private static $username;
$this->currentDriver = $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
if ($this->currentDriver === 'sqlite') {
$this->pdo->exec('PRAGMA foreign_keys = ON');
}
}
/** @var PDO */
protected $pdo;
public function query(string $query, $parameters = [])
{
if (!is_array($parameters)) {
$parameters = [$parameters];
}
$query = $this->pdo->prepare($query);
/** @var string */
protected static $dsn = 'sqlite:database.db';
foreach ($parameters as $index => $parameter) {
$query->bindValue($index + 1, $parameter, is_int($parameter) ? PDO::PARAM_INT : PDO::PARAM_STR);
}
/** @var string */
protected $currentDriver;
$query->execute();
public function __construct(string $dsn, string $username = null, string $password = null)
{
self::setDsn($dsn, $username, $password);
return $query;
}
$this->pdo = new PDO($dsn, $username, $password);
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ);
/**
* Get the PDO instance.
*
* @return PDO
*/
public function getPdo(): PDO
{
return $this->pdo;
}
$this->currentDriver = $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
if ($this->currentDriver === 'sqlite') {
$this->pdo->exec('PRAGMA foreign_keys = ON');
}
}
/**
* Get the current PDO driver.
*
* @return string
*/
public function getCurrentDriver(): string
{
return $this->currentDriver;
}
}
public function query(string $query, $parameters = [])
{
if (!is_array($parameters)) {
$parameters = [$parameters];
}
$query = $this->pdo->prepare($query);
foreach ($parameters as $index => $parameter) {
$query->bindValue($index + 1, $parameter, is_int($parameter) ? PDO::PARAM_INT : PDO::PARAM_STR);
}
$query->execute();
return $query;
}
/**
* Get the PDO instance
* @return PDO
*/
public function getPdo(): PDO
{
return $this->pdo;
}
/**
* Get the current PDO driver
* @return string
*/
public function getCurrentDriver(): string
{
return $this->currentDriver;
}
public static function getInstance(): DB
{
if (self::$instance === null) {
self::$instance = new self(self::$dsn, self::$username, self::$password);
}
return self::$instance;
}
/**
* Perform a query
* @param string $query
* @param array $parameters
* @return bool|\PDOStatement|string
*/
public static function doQuery(string $query, $parameters = [])
{
return self::getInstance()->query($query, $parameters);
}
/**
* Static method to get the current driver name
* @return string
*/
public static function driver(): string
{
return self::getInstance()->getCurrentDriver();
}
/**
* Get directly the PDO instance
* @return PDO
*/
public static function raw(): PDO
{
return self::getInstance()->getPdo();
}
/**
* Set the PDO connection string
* @param string $dsn
* @param string|null $username
* @param string|null $password
*/
public static function setDsn(string $dsn, string $username = null, string $password = null)
{
self::$dsn = $dsn;
self::$username = $username;
self::$password = $password;
}
}

View file

@ -1,111 +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;
/**
* Migrator constructor.
*
* @param DB $db
* @param string|null $schemaPath
*/
public function __construct(DB $db, ?string $schemaPath)
{
$this->db = $db;
$this->schemaPath = $schemaPath;
}
public function migrate(): void
{
$this->db->getPdo()->exec(file_get_contents($this->schemaPath.DIRECTORY_SEPARATOR.'migrations.sql'));
$files = glob($this->schemaPath.'/'.$this->db->getCurrentDriver().'/*.sql');
$names = array_map(static 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;
}
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

@ -0,0 +1,231 @@
<?php
namespace App\Database\Queries;
use App\Database\DB;
use League\Flysystem\FileNotFoundException;
use League\Flysystem\Filesystem;
use League\Flysystem\Plugin\ListFiles;
class MediaQuery
{
const PER_PAGE = 21;
const PER_PAGE_ADMIN = 27;
const ORDER_TIME = 0;
const ORDER_NAME = 1;
const ORDER_SIZE = 2;
/** @var DB */
protected $db;
/** @var bool */
protected $isAdmin;
protected $userId;
/** @var int */
protected $orderBy;
/** @var string */
protected $orderMode;
/** @var string */
protected $text;
/** @var Filesystem */
protected $storage;
private $pages;
private $media;
/**
* MediaQuery constructor.
* @param DB $db
* @param bool $isAdmin
* @param Filesystem $storage
*/
public function __construct(DB $db, bool $isAdmin, Filesystem $storage)
{
$this->db = $db;
$this->isAdmin = $isAdmin;
$this->storage = $storage;
}
/**
* @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;
}
/**
* @param int $page
*/
public function run(int $page)
{
if ($this->orderBy == self::ORDER_SIZE) {
$this->runWithOrderBySize($page);
return;
}
$queryPages = 'SELECT COUNT(*) AS `count` FROM `uploads`';
if ($this->isAdmin) {
$queryMedia = 'SELECT `uploads`.*, `users`.`user_code`, `users`.`username` FROM `uploads` LEFT JOIN `users` ON `uploads`.`user_id` = `users`.`id` %s LIMIT ? OFFSET ?';
} else {
$queryMedia = 'SELECT `uploads`.*,`users`.`user_code`, `users`.`username` FROM `uploads` INNER JOIN `users` ON `uploads`.`user_id` = `users`.`id` WHERE `user_id` = ? %s LIMIT ? OFFSET ?';
$queryPages .= ' WHERE `user_id` = ?';
}
$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) . '%';
}
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;
}
$queryMedia = sprintf($queryMedia, $orderAndSearch);
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 {
$media->size = humanFileSize($this->storage->getSize($media->storage_path));
$media->mimetype = $this->storage->getMimetype($media->storage_path);
} catch (FileNotFoundException $e) {
$media->size = null;
$media->mimetype = null;
}
$media->extension = pathinfo($media->filename, PATHINFO_EXTENSION);
}
}
/**
* @param int $page
*/
private function runWithOrderBySize(int $page)
{
$this->storage->addPlugin(new ListFiles());
if ($this->isAdmin) {
$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->listFiles($userCode);
$this->pages = count($files) / self::PER_PAGE;
$offset = $page * self::PER_PAGE;
$limit = self::PER_PAGE;
}
array_multisort(array_column($files, 'size'), ($this->orderMode === 'ASC') ? SORT_ASC : SORT_DESC, SORT_NUMERIC, $files);
if ($this->text !== null) {
if ($this->isAdmin) {
$medias = $this->db->query('SELECT `uploads`.*, `users`.`user_code`, `users`.`username` FROM `uploads` LEFT JOIN `users` ON `uploads`.`user_id` = `users`.`id` WHERE `uploads`.`filename` LIKE ? ', ['%' . htmlentities($this->text) . '%'])->fetchAll();
} else {
$medias = $this->db->query('SELECT `uploads`.*, `users`.`user_code`, `users`.`username` FROM `uploads` LEFT JOIN `users` ON `uploads`.`user_id` = `users`.`id` WHERE `user_id` = ? AND `uploads`.`filename` LIKE ? ', [$this->userId, '%' . htmlentities($this->text) . '%'])->fetchAll();
}
$paths = array_column($files, 'path');
} else {
$files = array_slice($files, $offset, $limit);
$paths = array_column($files, 'path');
$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;
}
$this->media = [];
foreach ($files as $file) {
$media = $paths[$file['path']];
if (!is_object($media)) {
continue;
}
$media->size = humanFileSize($file['size']);
try {
$media->mimetype = $this->storage->getMimetype($file['path']);
} catch (FileNotFoundException $e) {
$media->mimetype = null;
}
$media->extension = $file['extension'];
$this->media[] = $media;
}
if ($this->text !== null) {
$this->media = array_slice($this->media, $offset, $limit);
}
}
/**
* @return mixed
*/
public function getMedia()
{
return $this->media;
}
/**
* @return mixed
*/
public function getPages()
{
return $this->pages;
}
}

View file

@ -1,387 +0,0 @@
<?php
namespace App\Database\Repositories;
use App\Database\DB;
use League\Flysystem\FileNotFoundException;
use League\Flysystem\Filesystem;
use League\Flysystem\Plugin\ListWith;
class MediaRepository
{
public const PER_PAGE = 21;
public const PER_PAGE_ADMIN = 27;
public const ORDER_TIME = 0;
public const ORDER_NAME = 1;
public const ORDER_SIZE = 2;
/** @var DB */
protected $db;
/** @var bool */
protected $isAdmin;
protected $userId;
/** @var int */
protected $orderBy;
/** @var string */
protected $orderMode;
/** @var string */
protected $text;
/** @var Filesystem */
protected $storage;
private $pages;
private $media;
/**
* @var int
*/
private $tagId;
/**
* MediaQuery constructor.
*
* @param DB $db
* @param bool $isAdmin
* @param Filesystem $storage
*/
public function __construct(DB $db, Filesystem $storage, bool $isAdmin)
{
$this->db = $db;
$this->isAdmin = $isAdmin;
$this->storage = $storage;
}
/**
* @param DB $db
* @param bool $isAdmin
* @param Filesystem $storage
* @return MediaRepository
*/
public static function make(DB $db, Filesystem $storage, bool $isAdmin)
{
return new self($db, $storage, $isAdmin);
}
/**
* @param $id
*
* @return $this
*/
public function withUserId($id): MediaRepository
{
$this->userId = $id;
return $this;
}
/**
* @param string|null $type
* @param string $mode
*
* @return $this
*/
public function orderBy(string $type = null, $mode = 'ASC'): MediaRepository
{
$this->orderBy = $type ?? self::ORDER_TIME;
$this->orderMode = (strtoupper($mode) === 'ASC') ? 'ASC' : 'DESC';
return $this;
}
/**
* @param string|null $text
*
* @return $this
*/
public function search(?string $text): MediaRepository
{
$this->text = $text;
return $this;
}
/**
* @param $tagId
* @return $this
*/
public function filterByTag($tagId): MediaRepository
{
if ($tagId !== null) {
$this->tagId = (int) $tagId;
}
return $this;
}
/**
* @param int $page
* @return $this
*/
public function run(int $page): MediaRepository
{
if ($this->orderBy == self::ORDER_SIZE) {
$this->runWithFileSort($page);
} else {
$this->runWithDbSort($page);
}
return $this;
}
/**
* @param int $page
* @return $this
*/
public function runWithDbSort(int $page): MediaRepository
{
$params = [];
if ($this->isAdmin) {
[$queryMedia, $queryPages] = $this->buildAdminQueries();
$constPage = self::PER_PAGE_ADMIN;
} else {
[$queryMedia, $queryPages] = $this->buildUserQueries();
$params[] = $this->userId;
$constPage = self::PER_PAGE;
}
if ($this->text !== null) {
$params[] = '%'.htmlentities($this->text).'%';
}
$queryMedia .= $this->buildOrderBy().' LIMIT ? OFFSET ?';
$this->media = $this->db->query($queryMedia, array_merge($params, [$constPage, $page * $constPage]))->fetchAll();
$this->pages = $this->db->query($queryPages, $params)->fetch()->count / $constPage;
$tags = $this->getTags(array_column($this->media, 'id'));
foreach ($this->media as $media) {
try {
$media->size = humanFileSize($this->storage->getSize($media->storage_path));
$media->mimetype = $this->storage->getMimetype($media->storage_path);
} catch (FileNotFoundException $e) {
$media->size = null;
$media->mimetype = null;
}
$media->extension = pathinfo($media->filename, PATHINFO_EXTENSION);
if (array_key_exists($media->id, $tags)) {
$media->tags = $tags[$media->id];
} else {
$media->tags = [];
}
}
return $this;
}
/**
* @param int $page
* @return $this
*/
public function runWithFileSort(int $page): MediaRepository
{
$this->storage->addPlugin(new ListWith());
if ($this->isAdmin) {
$files = $this->storage->listWith(['size', 'mimetype'], '/', true);
$offset = $page * self::PER_PAGE_ADMIN;
$limit = self::PER_PAGE_ADMIN;
} else {
$userCode = $this->db->query('SELECT `user_code` FROM `users` WHERE `id` = ?', $this->userId)->fetch()->user_code;
$files = $this->storage->listWith(['size', 'mimetype'], $userCode);
$offset = $page * self::PER_PAGE;
$limit = self::PER_PAGE;
}
$files = array_filter($files, function ($file) {
return $file['type'] !== 'dir';
});
array_multisort(array_column($files, 'size'), $this->buildOrderBy(), SORT_NUMERIC, $files);
$params = [];
$queryPagesParams = [];
if ($this->text !== null) {
if ($this->isAdmin) {
[$queryMedia, $queryPages] = $this->buildAdminQueries();
} else {
[$queryMedia, $queryPages] = $this->buildUserQueries();
$params[] = $this->userId;
}
$params[] = '%'.htmlentities($this->text).'%';
$queryPagesParams = $params;
$paths = array_column($files, 'path');
} elseif ($this->tagId !== null) {
if ($this->isAdmin) {
[, $queryPages] = $this->buildAdminQueries();
} else {
[, $queryPages] = $this->buildUserQueries();
$queryPagesParams[] = $this->userId;
}
$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 {
if ($this->isAdmin) {
[, $queryPages] = $this->buildAdminQueries();
} else {
[, $queryPages] = $this->buildUserQueries();
$queryPagesParams[] = $this->userId;
}
$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();
$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)) {
$media->size = humanFileSize($file['size']);
$media->extension = $file['extension'];
$media->mimetype = $file['mimetype'];
$this->media[] = $media;
if (array_key_exists($media->id, $tags)) {
$media->tags = $tags[$media->id];
} else {
$media->tags = [];
}
}
}
$this->pages = $this->db->query($queryPages, $queryPagesParams)->fetch()->count / $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 ?';
}
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 = [-1];
foreach ($mediaIds as $pivot) {
$ids[] = $pivot->upload_id;
}
return $ids;
}
/**
* @return mixed
*/
public function getMedia()
{
return $this->media;
}
/**
* @return mixed
*/
public function getPages()
{
return $this->pages;
}
}

View file

@ -1,106 +0,0 @@
<?php
namespace App\Database\Repositories;
use App\Database\DB;
use PDO;
class TagRepository
{
public 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 DISTINCT `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;
}
return null;
}
}

View file

@ -1,181 +0,0 @@
<?php
namespace App\Database\Repositories;
use App\Database\DB;
use App\Web\Session;
use InvalidArgumentException;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Exception\HttpNotFoundException;
use Slim\Exception\HttpUnauthorizedException;
class UserRepository
{
/**
* @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 UserRepository
*/
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,
]);
}
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,27 +0,0 @@
<?php
namespace App\Exceptions\Handlers;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Handlers\ErrorHandler;
use Throwable;
class AppErrorHandler extends ErrorHandler
{
protected function logError(string $error): void
{
resolve('logger')->error($error);
}
public function __invoke(ServerRequestInterface $request, Throwable $exception, bool $displayErrorDetails, bool $logErrors, bool $logErrorDetails): ResponseInterface
{
$response = parent::__invoke($request, $exception, $displayErrorDetails, $logErrors, $logErrorDetails);
if ($response->getStatusCode() !== 404) {
$this->writeToErrorLog();
}
return $response;
}
}

View file

@ -1,50 +0,0 @@
<?php
namespace App\Exceptions\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 = "", Throwable $previous = null)
{
parent::__construct($message, $response->getStatusCode(), $previous);
$this->response = $response;
}
/**
* @return Response
*/
public function response(): Response
{
return $this->response;
}
}

View file

@ -1,71 +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'));
$twig->addFunction(new TwigFunction('glue', 'glue'));
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,29 +2,27 @@
namespace App\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Exception\HttpUnauthorizedException;
use App\Exceptions\UnauthorizedException;
use Slim\Http\Request;
use Slim\Http\Response;
class AdminMiddleware extends Middleware
{
/**
* @param Request $request
* @param RequestHandler $handler
*
* @throws HttpUnauthorizedException
*
* @return ResponseInterface
*/
public function __invoke(Request $request, RequestHandler $handler): ResponseInterface
{
if (!$this->database->query('SELECT `id`, `is_admin` FROM `users` WHERE `id` = ? LIMIT 1', [$this->session->get('user_id')])->fetch()->is_admin) {
$this->session->set('admin', false);
/**
* @param Request $request
* @param Response $response
* @param callable $next
* @return Response
* @throws UnauthorizedException
*/
public function __invoke(Request $request, Response $response, callable $next)
{
if (!$this->database->query('SELECT `id`, `is_admin` FROM `users` WHERE `id` = ? LIMIT 1', [$this->session->get('user_id')])->fetch()->is_admin) {
$this->session->set('admin', false);
throw new UnauthorizedException();
}
throw new HttpUnauthorizedException($request);
}
return $next($request, $response);
}
return $handler->handle($request);
}
}
}

View file

@ -2,34 +2,33 @@
namespace App\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Psr7\Factory\ResponseFactory;
use Slim\Http\Request;
use Slim\Http\Response;
class AuthMiddleware extends Middleware
{
/**
* @param Request $request
* @param RequestHandler $handler
*
* @return ResponseInterface
*/
public function __invoke(Request $request, RequestHandler $handler): ResponseInterface
{
if (!$this->session->get('logged', false)) {
$this->session->set('redirectTo', (string) $request->getUri()->getPath());
return redirect((new ResponseFactory())->createResponse(), route('login.show'));
}
/**
* @param Request $request
* @param Response $response
* @param callable $next
* @return Response
*/
public function __invoke(Request $request, Response $response, callable $next)
{
if (!$this->session->get('logged', false)) {
$this->session->set('redirectTo', (isset($_SERVER['HTTPS']) ? 'https' : 'http') . "://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]");
return redirect($response, 'login.show');
}
if (!$this->database->query('SELECT `id`, `active` FROM `users` WHERE `id` = ? LIMIT 1', [$this->session->get('user_id')])->fetch()->active) {
$this->session->alert(lang('account_disabled'), 'danger');
$this->session->set('logged', false);
if (!$this->database->query('SELECT `id`, `active` FROM `users` WHERE `id` = ? LIMIT 1', [$this->session->get('user_id')])->fetch()->active) {
$this->session->alert('Your account is not active anymore.', 'danger');
$this->session->set('logged', false);
$this->session->set('redirectTo', (isset($_SERVER['HTTPS']) ? 'https' : 'http') . "://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]");
return redirect($response, 'login.show');
}
return redirect((new ResponseFactory())->createResponse(), route('login.show'));
}
return $next($request, $response);
}
return $handler->handle($request);
}
}
}

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
*
* @return Response
*/
public function __invoke(Request $request, RequestHandler $handler): Response
{
if ($this->config['maintenance'] && !$this->database->query('SELECT `id`, `is_admin` FROM `users` WHERE `id` = ? LIMIT 1', [$this->session->get('user_id')])->fetch()->is_admin) {
throw new UnderMaintenanceException($request);
}
/**
* @param Request $request
* @param Response $response
* @param callable $next
* @return Response
* @throws MaintenanceException
*/
public function __invoke(Request $request, Response $response, callable $next)
{
if (isset($this->settings['maintenance']) && $this->settings['maintenance'] && !$this->database->query('SELECT `id`, `is_admin` FROM `users` WHERE `id` = ? LIMIT 1', [$this->session->get('user_id')])->fetch()->is_admin) {
throw new MaintenanceException();
}
return $handler->handle($request);
}
}
return $next($request, $response);
}
}

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
{
/**
* @param Request $request
* @param RequestHandler $handler
*
* @return Response
*/
abstract public function __invoke(Request $request, RequestHandler $handler);
}
/** @var Container */
protected $container;
public function __construct(Container $container)
{
$this->container = $container;
}
/**
* @param $name
* @return mixed|null
* @throws \Interop\Container\Exception\ContainerException
*/
public function __get($name)
{
if ($this->container->has($name)) {
return $this->container->get($name);
}
return null;
}
/**
* @param Request $request
* @param Response $response
* @param callable $next
*/
public abstract function __invoke(Request $request, Response $response, callable $next);
}

View file

@ -1,41 +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`, `username`,`is_admin`, `active`, `remember_token`, `current_disk_quota`, `max_disk_quota`, `copy_raw` 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)
->set('user_id', $user->id)
->set('username', $user->username)
->set('admin', $user->is_admin)
->set('copy_raw', $user->copy_raw);
$this->setSessionQuotaInfo($user->current_disk_quota, $user->max_disk_quota);
$this->refreshRememberCookie($user->id);
}
}
return $handler->handle($request);
}
}

View file

@ -2,158 +2,138 @@
namespace App\Web;
class Lang
{
const DEFAULT_LANG = 'en';
const LANG_PATH = __DIR__.'../../resources/lang/';
/** @var string */
protected static $langPath = self::LANG_PATH;
const DEFAULT_LANG = 'en';
const LANG_PATH = __DIR__ . '../../resources/lang/';
/** @var string */
protected static $lang;
/** @var string */
protected static $langPath = self::LANG_PATH;
/** @var Lang */
protected static $instance;
/** @var string */
protected static $lang;
/** @var array */
protected $cache = [];
/** @var Lang */
protected static $instance;
/**
* @return Lang
*/
public static function getInstance(): self
{
if (self::$instance === null) {
self::$instance = new self();
}
/** @var array */
protected $cache = [];
return self::$instance;
}
/**
* @param string $lang
* @param string $langPath
*
* @return Lang
*/
public static function build($lang = self::DEFAULT_LANG, $langPath = null): self
{
self::$lang = $lang;
/**
* @return Lang
*/
public static function getInstance(): Lang
{
if (self::$instance === null) {
self::$instance = new self();
}
if ($langPath !== null) {
self::$langPath = $langPath;
}
return self::$instance;
}
self::$instance = new self();
/**
* @param string $lang
* @param string $langPath
* @return Lang
*/
public static function build($lang = self::DEFAULT_LANG, $langPath = null): Lang
{
self::$lang = $lang;
return self::$instance;
}
if ($langPath !== null) {
self::$langPath = $langPath;
}
/**
* Recognize the current language from the request.
*
* @return bool|string
*/
public static function recognize()
{
if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
return locale_accept_from_http($_SERVER['HTTP_ACCEPT_LANGUAGE']);
}
self::$instance = new self();
return self::DEFAULT_LANG;
}
return self::$instance;
}
/**
* @return string
*/
public static function getLang(): string
{
return self::$lang;
}
/**
* Recognize the current language from the request.
* @return bool|string
*/
public static function recognize()
{
if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
return locale_accept_from_http($_SERVER['HTTP_ACCEPT_LANGUAGE']);
}
return self::DEFAULT_LANG;
}
/**
* @param $lang
*/
public static function setLang($lang)
{
self::$lang = $lang;
}
/**
* @return string
*/
public static function getLang(): string
{
return self::$lang;
}
/**
* @return array
*/
public static function getList()
{
$languages = [];
/**
* @return array
*/
public static function getList()
{
$languages = [];
$default = count(include self::$langPath.self::DEFAULT_LANG.'.lang.php') - 1;
$default = count(include self::$langPath . self::DEFAULT_LANG . '.lang.php') - 1;
foreach (glob(self::$langPath.'*.lang.php') as $file) {
$dict = include $file;
foreach (glob(self::$langPath . '*.lang.php') as $file) {
$dict = include $file;
if (!is_array($dict) || !isset($dict['lang'])) {
continue;
}
$count = count($dict) - 1;
$prepend = "[{$count}/{$default}] ";
$count = count($dict) - 1;
$percent = min(round(($count / $default) * 100), 100);
$languages[str_replace('.lang.php', '', basename($file))] = $prepend . $dict['lang'];
}
$languages[str_replace('.lang.php', '', basename($file))] = "[{$percent}%] ".$dict['lang'];
}
return $languages;
}
return $languages;
}
/**
* @param $key
* @param array $args
*
* @return string
*/
public function get($key, $args = []): string
{
return $this->getString($key, self::$lang, $args);
}
/**
* @param $key
* @param array $args
* @return string
*/
public function get($key, $args = []): string
{
return $this->getString($key, self::$lang, $args);
}
/**
* @param $key
* @param $lang
* @param $args
*
* @return string
*/
private function getString($key, $lang, $args): string
{
$redLang = strtolower(substr($lang, 0, 2));
/**
* @param $key
* @param $lang
* @param $args
* @return string
*/
private function getString($key, $lang, $args): string
{
$redLang = strtolower(substr($lang, 0, 2));
if (array_key_exists($lang, $this->cache)) {
$transDict = $this->cache[$lang];
} else {
if (file_exists(self::$langPath.$lang.'.lang.php')) {
$transDict = include self::$langPath.$lang.'.lang.php';
$this->cache[$lang] = $transDict;
} else {
if (file_exists(self::$langPath.$redLang.'.lang.php')) {
$transDict = include self::$langPath.$redLang.'.lang.php';
$this->cache[$lang] = $transDict;
} else {
$transDict = [];
}
}
}
if (array_key_exists($lang, $this->cache)) {
$transDict = $this->cache[$lang];
} else if (file_exists(self::$langPath . $lang . '.lang.php')) {
$transDict = include self::$langPath . $lang . '.lang.php';
$this->cache[$lang] = $transDict;
} else if (file_exists(self::$langPath . $redLang . '.lang.php')) {
$transDict = include self::$langPath . $redLang . '.lang.php';
$this->cache[$lang] = $transDict;
} else {
$transDict = [];
}
if (array_key_exists($key, $transDict)) {
$string = @vsprintf($transDict[$key], $args);
if ($string !== false) {
return $string;
}
}
if (array_key_exists($key, $transDict)) {
return vsprintf($transDict[$key], $args);
}
if ($lang !== self::DEFAULT_LANG) {
return $this->getString($key, self::DEFAULT_LANG, $args);
}
if ($lang !== self::DEFAULT_LANG) {
return $this->getString($key, self::DEFAULT_LANG, $args);
}
return $key;
}
return $key;
}
}

View file

@ -1,152 +0,0 @@
<?php
namespace App\Web;
use InvalidArgumentException;
class Mail
{
/**
* @var bool
*/
private static $testing = false;
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();
}
/**
* This will skip the email send
*/
public static function fake()
{
self::$testing = true;
}
/**
* @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 = $text;
return $this;
}
/**
* @param $text
* @return $this
*/
public function message(string $text)
{
$this->message = $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;
}
/**
* Set headers before send
*/
protected function setHeaders()
{
if ($this->fromName === null) {
$this->addRequiredHeader("From: $this->fromMail");
} else {
$this->addRequiredHeader("From: $this->fromName <$this->fromMail>");
}
$this->addRequiredHeader('X-Mailer: PHP/'.phpversion())
->addRequiredHeader('MIME-Version: 1.0')
->addRequiredHeader('Content-Type: text/html; charset=utf-8');
}
/**
* @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.');
}
$this->setHeaders();
$this->headers .= $this->additionalHeaders;
$message = html_entity_decode($this->message);
if (self::$testing) {
return 1;
}
return (int) mail($this->to, $this->subject, "<html><body>$message</body></html>", $this->headers);
}
}

View file

@ -2,160 +2,115 @@
namespace App\Web;
use Exception;
class Session
{
/**
* Session constructor.
*
* @param string $name
* @param string $path
*
* @throws Exception
*/
public function __construct(string $name, $path = '')
{
if (session_status() === PHP_SESSION_NONE) {
if (!is_writable($path) && $path !== '') {
throw new Exception("The given path '{$path}' is not writable.");
}
// Workaround for php <= 7.3
if (PHP_VERSION_ID < 70300) {
$params = session_get_cookie_params();
session_set_cookie_params(
$params['lifetime'],
$params['path'].'; SameSite=Strict',
$params['domain'],
isSecure(),
$params['httponly']
);
}
/**
* Session constructor.
* @param string $name
* @param string $path
* @throws Exception
*/
public function __construct(string $name, $path = '')
{
if (session_status() === PHP_SESSION_NONE) {
if (!is_writable($path) && $path !== '') {
throw new Exception("The given path '{$path}' is not writable.");
}
$started = @session_start([
'name' => $name,
'save_path' => $path,
'cookie_httponly' => true,
'gc_probability' => 25,
'cookie_samesite' => 'Strict', // works only for php >= 7.3
'cookie_secure' => isSecure(),
]);
$started = @session_start([
'name' => $name,
'save_path' => $path,
'cookie_httponly' => true,
'gc_probability' => 25,
]);
if (!$started) {
throw new Exception("Cannot start the HTTP session. The session path '{$path}' is not writable.");
}
}
}
if (!$started) {
throw new Exception("Cannot start the HTTP session. That the session path '{$path}' is writable and your PHP settings.");
}
}
}
/**
* @return string
*/
public function getId()
{
return session_id();
}
/**
* Destroy the current session
* @return bool
*/
public function destroy(): bool
{
return session_destroy();
}
/**
* Destroy the current session.
*
* @return bool
*/
public function destroy(): bool
{
return session_destroy();
}
/**
* Clear all session stored values
*/
public function clear(): void
{
$_SESSION = [];
}
/**
* Clear all session stored values.
*/
public function clear(): Session
{
$_SESSION = [];
return $this;
}
/**
* Check if session has a stored key
* @param $key
* @return bool
*/
public function has($key): bool
{
return isset($_SESSION[$key]);
}
/**
* Check if session has a stored key.
*
* @param $key
*
* @return bool
*/
public function has($key): bool
{
return isset($_SESSION[$key]);
}
/**
* Get the content of the current session
* @return array
*/
public function all(): array
{
return $_SESSION;
}
/**
* Get the content of the current session.
*
* @return array
*/
public function all(): array
{
return $_SESSION;
}
/**
* Returned a value given a key
* @param $key
* @param null $default
* @return mixed
*/
public function get($key, $default = null)
{
return self::has($key) ? $_SESSION[$key] : $default;
}
/**
* Returned a value given a key.
*
* @param $key
* @param null $default
*
* @return mixed
*/
public function get($key, $default = null)
{
return self::has($key) ? $_SESSION[$key] : $default;
}
/**
* Add a key-value pair to the session
* @param $key
* @param $value
*/
public function set($key, $value): void
{
$_SESSION[$key] = $value;
}
/**
* Add a key-value pair to the session.
*
* @param $key
* @param $value
* @return Session
*/
public function set($key, $value): Session
{
$_SESSION[$key] = $value;
return $this;
}
/**
* Set a flash alert
* @param $message
* @param string $type
*/
public function alert($message, string $type = 'info'): void
{
$_SESSION['_flash'][] = [$type => $message];
}
/**
* Set a flash alert.
*
* @param $message
* @param string $type
* @return Session
*/
public function alert($message, string $type = 'info'): Session
{
$_SESSION['_flash'][] = [$type => $message];
return $this;
}
/**
* Closes the current session
*
* @return bool|void
*/
public function close()
{
return session_write_close();
}
/**
* Retrieve flash alerts
* @return array
*/
public function getAlert(): ?array
{
$flash = self::get('_flash');
self::set('_flash', []);
return $flash;
}
/**
* Retrieve flash alerts.
*
* @return array
*/
public function getAlert(): ?array
{
$flash = self::get('_flash');
self::set('_flash', []);
return $flash;
}
}
}

View file

@ -1,67 +0,0 @@
<?php
namespace App\Web;
class Theme
{
public const DEFAULT_THEME_URL = 'https://bootswatch.com/4/_vendor/bootstrap/dist/css/bootstrap.min.css';
/**
* @return array
*/
public function availableThemes(): array
{
$apiJson = json_decode(file_get_contents('https://bootswatch.com/api/4.json'));
$default = [];
$default['Default - Bootstrap 4 default theme'] = self::DEFAULT_THEME_URL;
$bootswatch = [];
foreach ($apiJson->themes as $theme) {
$bootswatch["{$theme->name} - {$theme->description}"] = $theme->cssMin;
}
$apiJson = json_decode(file_get_contents('https://theme-park.dev/themes.json'));
$base = $apiJson->applications->xbackbone->base_css;
$themepark = [];
foreach ($apiJson->themes as $name => $urls) {
$themepark[$name] = "{$base},{$urls->url}";
}
return [
'default' => $default,
'bootswatch.com' => $bootswatch,
'theme-park.dev' => $themepark
];
}
/**
* @param string $input
* @return bool
*/
public function applyTheme(string $input): bool
{
[$vendor, $css] = explode('|', $input, 2);
if ($vendor === 'theme-park.dev') {
[$base, $theme] = explode(',', $css);
$data = file_get_contents(self::DEFAULT_THEME_URL).file_get_contents($base).file_get_contents($theme);
} else {
$data = file_get_contents($css ?? self::DEFAULT_THEME_URL);
}
return (bool) file_put_contents(
BASE_DIR.'static/bootstrap/css/bootstrap.min.css',
$data
);
}
/**
* @return string
*/
public static function default(): string
{
return 'default|'.self::DEFAULT_THEME_URL;
}
}

View file

@ -1,55 +0,0 @@
<?php
namespace App\Web;
class UA
{
/**
* bot user agent => perform link embed
* @var string[]
*/
private static $bots = [
'TelegramBot' => false,
'facebookexternalhit/' => false,
'Facebot' => false,
'curl/' => false,
'wget/' => false,
'WhatsApp/' => false,
'Slack' => false,
'Twitterbot/' => false,
'discord' => true,
// discord image bot
'Mozilla/5.0 (Macintosh; Intel Mac OS X 11.6; rv:92.0) Gecko/20100101 Firefox/92.0' => true,
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:38.0) Gecko/20100101 Firefox/38.0' => true,
];
/**
* @param string $userAgent
* @return bool
*/
public static function isBot(string $userAgent): bool
{
foreach (self::$bots as $bot => $embedsLink) {
if (stripos($userAgent, $bot) !== false) {
return true;
}
}
return false;
}
/**
* @param string $userAgent
* @return false|string
*/
public static function embedsLinks(string $userAgent)
{
foreach (self::$bots as $bot => $embedsLink) {
if (stripos($userAgent, $bot) !== false) {
return $embedsLink;
}
}
return false;
}
}

View file

@ -1,66 +0,0 @@
<?php
namespace App\Web;
class ValidationHelper
{
/**
* @var Session
*/
protected $session;
/**
* @var bool
*/
protected $failed;
/**
* Validator constructor.
* @param Session $session
*/
public function __construct(Session $session)
{
$this->session = $session;
$this->failed = false;
}
public function alertIf(bool $condition, string $alert, string $type = 'danger')
{
if (!$this->failed && $condition) {
$this->failed = true;
$this->session->alert(lang($alert), $type);
}
return $this;
}
public function failIf(bool $condition)
{
if (!$this->failed && $condition) {
$this->failed = true;
}
return $this;
}
public function callIf(bool $condition, callable $closure)
{
if (!$this->failed && $condition) {
do {
$result = $closure($this->session);
if (is_callable($result)) {
$closure = $result;
}
} while (!is_bool($result));
$this->failed = !$result;
}
return $this;
}
public function fails()
{
return $this->failed;
}
}

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,530 +1,318 @@
<?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');
define('HUMAN_RANDOM_CHARS', 'bcdfghjklmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZaeiouAEIOU');
}
if (!function_exists('humanFileSize')) {
/**
* Generate a human readable file size.
*
* @param $size
* @param int $precision
*
* @param bool $iniMode
* @return string
*/
function humanFileSize($size, $precision = 2, $iniMode = false): 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];
}
/**
* Generate a human readable file size
* @param $size
* @param int $precision
* @return string
*/
function humanFileSize($size, $precision = 2): string
{
for ($i = 0; ($size / 1024) > 0.9; $i++, $size /= 1024) {
}
return round($size, $precision) . ' ' . ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'][$i];
}
}
if (!function_exists('humanRandomString')) {
/**
* @param int $length
*
* @return string
*/
function humanRandomString(int $length = 10): string
{
$result = '';
$numberOffset = round($length * 0.2);
for ($x = 0; $x < $length - $numberOffset; $x++) {
$result .= ($x % 2) ? HUMAN_RANDOM_CHARS[rand(42, 51)] : HUMAN_RANDOM_CHARS[rand(0, 41)];
}
for ($x = 0; $x < $numberOffset; $x++) {
$result .= rand(0, 9);
}
return $result;
}
/**
* @param int $length
* @return string
*/
function humanRandomString(int $length = 13): string
{
$result = '';
$numberOffset = round($length * 0.2);
for ($x = 0; $x < $length - $numberOffset; $x++) {
$result .= ($x % 2) ? HUMAN_RANDOM_CHARS[rand(42, 51)] : HUMAN_RANDOM_CHARS[rand(0, 41)];
}
for ($x = 0; $x < $numberOffset; $x++) {
$result .= rand(0, 9);
}
return $result;
}
}
if (!function_exists('isDisplayableImage')) {
/**
* @param string $mime
*
* @return bool
*/
function isDisplayableImage(?string $mime): bool
{
return in_array($mime, [
'image/apng',
'image/bmp',
'image/gif',
'image/x-icon',
'image/jpeg',
'image/png',
'image/tiff',
'image/webp',
]);
}
}
if (!function_exists('isEmbeddable')) {
/**
* @param ?string $mime
*
* @return bool
*/
function isEmbeddable(?string $mime): bool
{
return in_array($mime, [
'image/apng',
'image/bmp',
'image/gif',
'image/x-icon',
'image/jpeg',
'image/png',
'image/tiff',
'image/webp',
'video/mp4',
'video/ogg',
'video/webm',
]);
}
/**
* @param string $mime
* @return bool
*/
function isDisplayableImage(string $mime): bool
{
return in_array($mime, [
'image/apng',
'image/bmp',
'image/gif',
'image/x-icon',
'image/jpeg',
'image/png',
'image/svg',
'image/svg+xml',
'image/tiff',
'image/webp',
]);
}
}
if (!function_exists('stringToBytes')) {
/**
* @param $str
*
* @return float
*/
function stringToBytes(string $str): float
{
$val = trim($str);
if (is_numeric($val)) {
return (float) $val;
}
/**
* @param $str
* @return float
*/
function stringToBytes(string $str): float
{
$val = trim($str);
if (is_numeric($val)) {
return (float)$val;
}
$last = strtolower($val[strlen($val) - 1]);
$val = substr($val, 0, -1);
$last = strtolower($val[strlen($val) - 1]);
$val = substr($val, 0, -1);
$val = (float) $val;
switch ($last) {
case 't':
$val *= 1024;
// no break
case 'g':
$val *= 1024;
// no break
case 'm':
$val *= 1024;
// no break
case 'k':
$val *= 1024;
}
return $val;
}
$val = (float)$val;
switch ($last) {
case 'g':
$val *= 1024;
case 'm':
$val *= 1024;
case 'k':
$val *= 1024;
}
return $val;
}
}
if (!function_exists('removeDirectory')) {
/**
* Remove a directory and it's content.
*
* @param $path
*/
function removeDirectory($path)
{
cleanDirectory($path, true);
rmdir($path);
}
/**
* Remove a directory and it's content
* @param $path
*/
function removeDirectory($path)
{
$files = glob($path . '/*');
foreach ($files as $file) {
is_dir($file) ? removeDirectory($file) : unlink($file);
}
rmdir($path);
return;
}
}
if (!function_exists('cleanDirectory')) {
/**
* Removes all directory contents.
*
* @param $path
* @param bool $all
*/
function cleanDirectory($path, $all = false)
{
$directoryIterator = new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS);
$iteratorIterator = new RecursiveIteratorIterator($directoryIterator, RecursiveIteratorIterator::CHILD_FIRST);
foreach ($iteratorIterator as $file) {
if ($all || $file->getFilename() !== '.gitkeep') {
$file->isDir() ? rmdir($file) : unlink($file);
}
}
}
}
if (!function_exists('resolve')) {
/**
* Resolve a service from de DI container.
*
* @param string $service
*
* @return mixed
*/
function resolve(string $service)
{
global $app;
return $app->getContainer()->get($service);
}
}
if (!function_exists('make')) {
/**
* Resolve a service from de DI container.
*
* @param string $class
* @param array $params
* @return mixed
*/
function make(string $class, array $params = [])
{
global $app;
return $app->getContainer()->make($class, $params);
}
}
if (!function_exists('view')) {
/**
* Render a view to the response body.
*
* @return \App\Web\View
*/
function view()
{
return resolve('view');
}
/**
* Removes all directory contents
* @param $path
*/
function cleanDirectory($path)
{
$directoryIterator = new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS);
$iteratorIterator = new RecursiveIteratorIterator($directoryIterator, RecursiveIteratorIterator::CHILD_FIRST);
foreach ($iteratorIterator as $file) {
if ($file->getFilename() !== '.gitkeep') {
$file->isDir() ? rmdir($file) : unlink($file);
}
}
}
}
if (!function_exists('redirect')) {
/**
* Set the redirect response.
*
* @param Response $response
* @param string $url
* @param int $status
*
* @return Response
*/
function redirect(Response $response, string $url, $status = 302)
{
return $response
->withHeader('Location', $url)
->withStatus($status);
}
/**
* Set the redirect response
* @param \Slim\Http\Response $response
* @param string $path
* @param array $args
* @param null $status
* @return \Slim\Http\Response
*/
function redirect(\Slim\Http\Response $response, string $path, $args = [], $status = null)
{
if (substr($path, 0, 1) === '/' || substr($path, 0, 3) === '../' || substr($path, 0, 2) === './') {
$url = urlFor($path);
} else {
$url = route($path, $args);
}
return $response->withRedirect($url, $status);
}
}
if (!function_exists('asset')) {
/**
* Get the asset link with timestamp.
*
* @param string $path
*
* @return string
*/
function asset(string $path): string
{
return urlFor($path, '?'.filemtime(realpath(BASE_DIR.$path)));
}
/**
* Get the asset link with timestamp
* @param string $path
* @return string
*/
function asset(string $path): string
{
return urlFor($path, '?' . filemtime(realpath(BASE_DIR . $path)));
}
}
if (!function_exists('urlFor')) {
/**
* Generate the app url given a path.
*
* @param string $path
* @param string $append
*
* @return string
*/
function urlFor(string $path = '', string $append = ''): string
{
$baseUrl = resolve('config')['base_url'];
return $baseUrl.$path.$append;
}
/**
* Generate the app url given a path
* @param string $path
* @param string $append
* @return string
*/
function urlFor(string $path, string $append = ''): string
{
global $app;
$baseUrl = $app->getContainer()->get('settings')['base_url'];
return $baseUrl . $path . $append;
}
}
if (!function_exists('route')) {
/**
* Generate the app url given a path.
*
* @param string $path
* @param array $args
* @param string $append
*
* @return string
*/
function route(string $path, array $args = [], string $append = ''): string
{
global $app;
$uri = $app->getRouteCollector()->getRouteParser()->relativeUrlFor($path, $args);
return urlFor($uri, $append);
}
}
if (!function_exists('param')) {
/**
* Get a parameter from the request.
*
* @param Request $request
* @param string $name
* @param null $default
*
* @return mixed
*/
function param(Request $request, string $name, $default = null)
{
if ($request->getMethod() === 'GET') {
$params = $request->getQueryParams();
} else {
$params = $request->getParsedBody();
}
return $params[$name] ?? $default;
}
}
if (!function_exists('json')) {
/**
* Return a json response.
*
* @param Response $response
* @param $data
* @param int $status
* @param int $options
*
* @return Response
*/
function json(Response $response, $data, int $status = 200, $options = 0): Response
{
$response->getBody()->write(json_encode($data, $options));
return $response
->withStatus($status)
->withHeader('Content-Type', 'application/json');
}
/**
* Generate the app url given a path
* @param string $path
* @param array $args
* @param string $append
* @return string
*/
function route(string $path, array $args = [], string $append = ''): string
{
global $app;
$uri = $app->getContainer()->get('router')->relativePathFor($path, $args);
return urlFor($uri, $append);
}
}
if (!function_exists('lang')) {
/**
* @param string $key
* @param array $args
*
* @return string
*/
function lang(string $key, $args = []): string
{
return resolve('lang')->get($key, $args);
}
/**
* @param string $key
* @param array $args
* @return string
*/
function lang(string $key, $args = []): string
{
global $app;
return $app->getContainer()->get('lang')->get($key, $args);
}
}
if (!function_exists('isBot')) {
/**
* @param string $userAgent
* @return boolean
*/
function isBot(string $userAgent)
{
$bots = [
'TelegramBot',
'facebookexternalhit/',
'Discordbot/',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:38.0) Gecko/20100101 Firefox/38.0', // The discord service bot?
'Facebot',
'curl/',
'wget/',
];
foreach ($bots as $bot) {
if (stripos($userAgent, $bot) !== false) {
return true;
}
}
return false;
}
}
if (!function_exists('mime2font')) {
/**
* Convert get the icon from the file mimetype.
*
* @param $mime
*
* @return mixed|string
*/
function mime2font($mime)
{
$classes = [
'image' => 'fa-file-image',
'audio' => 'fa-file-audio',
'video' => 'fa-file-video',
'application/pdf' => 'fa-file-pdf',
'application/msword' => 'fa-file-word',
'application/vnd.ms-word' => 'fa-file-word',
'application/vnd.oasis.opendocument.text' => 'fa-file-word',
'application/vnd.openxmlformats-officedocument.wordprocessingml' => 'fa-file-word',
'application/vnd.ms-excel' => 'fa-file-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml' => 'fa-file-excel',
'application/vnd.oasis.opendocument.spreadsheet' => 'fa-file-excel',
'application/vnd.ms-powerpoint' => 'fa-file-powerpoint',
'application/vnd.openxmlformats-officedocument.presentationml' => 'fa-file-powerpoint',
'application/vnd.oasis.opendocument.presentation' => 'fa-file-powerpoint',
'text/plain' => 'fa-file-alt',
'text/html' => 'fa-file-code',
'text/x-php' => 'fa-file-code',
'application/json' => 'fa-file-code',
'application/gzip' => 'fa-file-archive',
'application/zip' => 'fa-file-archive',
'application/octet-stream' => 'fa-file-alt',
];
/**
* Convert get the icon from the file mimetype
* @param $mime
* @return mixed|string
*/
function mime2font($mime)
{
$classes = [
'image' => 'fa-file-image',
'audio' => 'fa-file-audio',
'video' => 'fa-file-video',
'application/pdf' => 'fa-file-pdf',
'application/msword' => 'fa-file-word',
'application/vnd.ms-word' => 'fa-file-word',
'application/vnd.oasis.opendocument.text' => 'fa-file-word',
'application/vnd.openxmlformats-officedocument.wordprocessingml' => 'fa-file-word',
'application/vnd.ms-excel' => 'fa-file-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml' => 'fa-file-excel',
'application/vnd.oasis.opendocument.spreadsheet' => 'fa-file-excel',
'application/vnd.ms-powerpoint' => 'fa-file-powerpoint',
'application/vnd.openxmlformats-officedocument.presentationml' => 'fa-file-powerpoint',
'application/vnd.oasis.opendocument.presentation' => 'fa-file-powerpoint',
'text/plain' => 'fa-file-alt',
'text/html' => 'fa-file-code',
'text/x-php' => 'fa-file-code',
'application/json' => 'fa-file-code',
'application/gzip' => 'fa-file-archive',
'application/zip' => 'fa-file-archive',
'application/octet-stream' => 'fa-file-alt',
];
foreach ($classes as $fullMime => $class) {
if (strpos($mime, $fullMime) === 0) {
return $class;
}
}
return 'fa-file';
}
foreach ($classes as $fullMime => $class) {
if (strpos($mime, $fullMime) === 0) {
return $class;
}
}
return 'fa-file';
}
}
if (!function_exists('dd')) {
/**
* Dumps all the given vars and halt the execution.
*/
function dd()
{
array_map(function ($x) {
echo '<pre>';
print_r($x);
echo '</pre>';
}, func_get_args());
die();
}
/**
* Dumps all the given vars and halt the execution.
*/
function dd()
{
array_map(function ($x) {
echo '<pre>';
print_r($x);
echo '</pre>';
}, func_get_args());
die();
}
}
if (!function_exists('queryParams')) {
/**
* Get the query parameters of the current request.
*
* @param array $replace
*
* @return string
*/
function queryParams(array $replace = [])
{
$request = ServerRequestCreatorFactory::determineServerRequestCreator()->createServerRequestFromGlobals();
/**
* Get the query parameters of the current request.
* @param array $replace
* @return string
* @throws \Interop\Container\Exception\ContainerException
*/
function queryParams(array $replace = [])
{
global $container;
/** @var \Slim\Http\Request $request */
$request = $container->get('request');
$params = array_replace_recursive($request->getQueryParams(), $replace);
$params = array_replace_recursive($request->getQueryParams(), $replace);
return !empty($params) ? '?'.http_build_query($params) : '';
}
}
if (!function_exists('inPath')) {
/**
* Check if uri start with a path.
*
* @param string $uri
* @param string $path
*
* @return bool
*/
function inPath(string $uri, string $path): bool
{
$path = parse_url(urlFor($path), PHP_URL_PATH);
return substr($uri, 0, strlen($path)) === $path;
}
return !empty($params) ? '?' . http_build_query($params) : '';
}
}
if (!function_exists('glob_recursive')) {
/**
* Does not support flag GLOB_BRACE.
*
* @param $pattern
* @param int $flags
*
* @return array|false
*/
function glob_recursive($pattern, $flags = 0)
{
$files = glob($pattern, $flags);
foreach (glob(dirname($pattern).'/*', GLOB_ONLYDIR | GLOB_NOSORT) as $dir) {
$files = array_merge($files, glob_recursive($dir.'/'.basename($pattern), $flags));
}
return $files;
}
}
if (!function_exists('dsnFromConfig')) {
/**
* Return the database DSN from config.
*
* @param array $config
*
* @return string
*/
function dsnFromConfig(array $config): string
{
$dsn = $config['db']['dsn'];
if ($config['db']['connection'] === 'sqlite') {
if (getcwd() !== BASE_DIR) { // if in installer, change the working dir to the app dir
chdir(BASE_DIR);
}
if (file_exists($config['db']['dsn'])) {
$dsn = realpath($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));
}
}
if (!function_exists('must_be_escaped')) {
/**
* Return the system no-reply mail.
*
* @param $mime
* @return bool
*/
function must_be_escaped($mime): bool
{
$mimes = [
'text/htm',
'image/svg',
];
foreach ($mimes as $m) {
if (stripos($mime, $m) !== false) {
return true;
}
}
return false;
}
}
if (!function_exists('isSecure')) {
/**
* @return bool
*/
function isSecure(): bool
{
return (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|| (!empty($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] === 443);
}
}
if (!function_exists('glue')) {
/**
* @param mixed ...$pieces
* @return string
*/
function glue(...$pieces): string
{
return '/'.implode('/', $pieces);
}
}
/**
* Does not support flag GLOB_BRACE
* @param $pattern
* @param int $flags
* @return array|false
*/
function glob_recursive($pattern, $flags = 0)
{
$files = glob($pattern, $flags);
foreach (glob(dirname($pattern) . '/*', GLOB_ONLYDIR | GLOB_NOSORT) as $dir) {
$files = array_merge($files, glob_recursive($dir . '/' . basename($pattern), $flags));
}
return $files;
}
}

111
app/routes.php Executable file → Normal file
View file

@ -1,93 +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');
$group->get('/home/switchView', [DashboardController::class, 'switchView'])->setName('switchView');
$app->group('', function () {
$this->get('/home[/page/{page}]', DashboardController::class . ':home')->setName('home');
$group->group('', function (RouteCollectorProxy $group) {
$group->get('/system/deleteOrphanFiles', [AdminController::class, 'deleteOrphanFiles'])->setName('system.deleteOrphanFiles');
$group->get('/system/recalculateUserQuota', [AdminController::class, 'recalculateUserQuota'])->setName('system.recalculateUserQuota');
$this->group('', function () {
$this->get('/home/switchView', DashboardController::class . ':switchView')->setName('switchView');
$group->get('/system/themes', [AdminController::class, 'getThemes'])->setName('theme');
$this->get('/system/deleteOrphanFiles', AdminController::class . ':deleteOrphanFiles')->setName('system.deleteOrphanFiles');
$group->post('/system/settings/save', [SettingController::class, 'saveSettings'])->setName('settings.save');
$this->get('/system/themes', ThemeController::class . ':getThemes')->setName('theme');
$this->post('/system/theme/apply', ThemeController::class . ':applyTheme')->setName('theme.apply');
$group->post('/system/upgrade', [UpgradeController::class, 'upgrade'])->setName('system.upgrade');
$group->get('/system/checkForUpdates', [UpgradeController::class, 'checkForUpdates'])->setName('system.checkForUpdates');
$group->get('/system/changelog', [UpgradeController::class, 'changelog'])->setName('system.changelog');
$this->post('/system/lang/apply', AdminController::class . ':applyLang')->setName('lang.apply');
$group->get('/system', [AdminController::class, 'system'])->setName('system');
$this->post('/system/upgrade', UpgradeController::class . ':upgrade')->setName('system.upgrade');
$this->get('/system/checkForUpdates', UpgradeController::class . ':checkForUpdates')->setName('system.checkForUpdates');
$group->get('/users[/page/{page}]', [UserController::class, 'index'])->setName('user.index');
})->add(AdminMiddleware::class);
$this->get('/system', AdminController::class . ':system')->setName('system');
$group->group('/user', function (RouteCollectorProxy $group) {
$group->get('/create', [UserController::class, 'create'])->setName('user.create');
$group->post('/create', [UserController::class, 'store'])->setName('user.store');
$group->get('/{id}/edit', [UserController::class, 'edit'])->setName('user.edit');
$group->post('/{id}', [UserController::class, 'update'])->setName('user.update');
$group->get('/{id}/delete', [UserController::class, 'delete'])->setName('user.delete');
$group->get('/{id}/clear', [UserController::class, 'clearUserMedia'])->setName('user.clear');
})->add(AdminMiddleware::class);
$this->get('/users[/page/{page}]', UserController::class . ':index')->setName('user.index');
})->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');
$group->get('/user/{id}/config/kde_script', [ClientController::class, 'getKDEScript'])->setName('kde_config.script');
$this->group('/user', function () {
$group->get('/user/{id}/export', [ExportController::class, 'downloadData'])->setName('export.data');
$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->post('/upload/{id}/publish', [MediaController::class, 'togglePublish'])->setName('upload.publish');
$group->post('/upload/{id}/unpublish', [MediaController::class, 'togglePublish'])->setName('upload.unpublish');
$group->post('/upload/{id}/vanity', [MediaController::class, 'createVanity'])->setName('upload.vanity');
$group->get('/upload/{id}/raw', [MediaController::class, 'getRawById'])->add(AdminMiddleware::class)->setName('upload.raw');
$group->map(['GET', 'POST'], '/upload/{id}/delete', [MediaController::class, 'delete'])->setName('upload.delete');
$this->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');
$this->post('/upload/{id}/publish', UploadController::class . ':togglePublish')->setName('upload.publish');
$this->post('/upload/{id}/unpublish', UploadController::class . ':togglePublish')->setName('upload.unpublish');
$this->get('/upload/{id}/raw', UploadController::class . ':getRawById')->add(AdminMiddleware::class)->setName('upload.raw');
$this->post('/upload/{id}/delete', UploadController::class . ':delete')->setName('upload.delete');
$group->post('/tag/add', [TagController::class, 'addTag'])->setName('tag.add');
$group->post('/tag/remove', [TagController::class, 'removeTag'])->setName('tag.remove');
})->add(App\Middleware\CheckForMaintenanceMiddleware::class)->add(AuthMiddleware::class);
$app->get('/', [DashboardController::class, 'redirects'])->setName('root');
$app->get('/register', [RegisterController::class, 'registerForm'])->setName('register.show');
$app->post('/register', [RegisterController::class, 'register'])->setName('register');
$app->get('/activate/{activateToken}', [RegisterController::class, 'activateUser'])->setName('activate');
$app->get('/recover', [PasswordRecoveryController::class, 'recover'])->setName('recover');
$app->post('/recover/mail', [PasswordRecoveryController::class, 'recoverMail'])->setName('recover.mail');
$app->get('/recover/password/{resetToken}', [PasswordRecoveryController::class, 'recoverPasswordForm'])->setName('recover.password.view');
$app->post('/recover/password/{resetToken}', [PasswordRecoveryController::class, 'recoverPassword'])->setName('recover.password');
$app->get('/login', [LoginController::class, 'show'])->setName('login.show');
$app->post('/login', [LoginController::class, 'login'])->setName('login');
$app->map(['GET', 'POST'], '/logout', [LoginController::class, 'logout'])->setName('logout');
$app->get('/', DashboardController::class . ':redirects')->setName('root');
$app->get('/login', LoginController::class . ':show')->setName('login.show');
$app->post('/login', LoginController::class . ':login')->setName('login');
$app->map(['GET', 'POST'], '/logout', LoginController::class . ':logout')->setName('logout');
$app->post('/upload', [UploadController::class, 'uploadEndpoint'])->setName('upload');
$app->post('/upload', UploadController::class . ':upload')->setName('upload');
$app->get('/user/{token}/config/screencloud', [ClientController::class, 'getScreenCloudConfig'])->setName('config.screencloud')->add(CheckForMaintenanceMiddleware::class);
$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

@ -1,7 +1,7 @@
#!/usr/bin/env php
<?php
((PHP_MAJOR_VERSION >= 7 && PHP_MINOR_VERSION >= 3) || PHP_MAJOR_VERSION > 7) ?: die('Sorry, PHP 7.3 or above is required to run XBackBone.');
if (PHP_SAPI !== 'cli') {
(PHP_MAJOR_VERSION >= 7 && PHP_MINOR_VERSION >= 1) ?: die('Sorry, PHP 7.1 or above is required to run XBackBone.');
if (php_sapi_name() !== 'cli') {
die();
}

View file

@ -1,45 +1,97 @@
#!/usr/bin/env php
<?php
((PHP_MAJOR_VERSION >= 7 && PHP_MINOR_VERSION >= 3) || PHP_MAJOR_VERSION > 7) ?: die('Sorry, PHP 7.3 or above is required to run XBackBone.');
if (PHP_SAPI !== 'cli') {
die();
(PHP_MAJOR_VERSION >= 7 && PHP_MINOR_VERSION >= 1) ?: die('Sorry, PHP 7.1 or above is required to run XBackBone.');
if (php_sapi_name() !== 'cli') {
die();
}
use App\Database\Migrator;
use DI\ContainerBuilder;
use App\Database\DB;
require __DIR__.'/../vendor/autoload.php';
require __DIR__ . '/../vendor/autoload.php';
define('BASE_DIR', realpath(__DIR__.'/../').DIRECTORY_SEPARATOR);
$config = include __DIR__.'/../config.php';
$config = include __DIR__ . '/../config.php';
if (!$config) {
die('config.php not found. Please create a new one.');
die('config.php not found. Please create a new one.');
}
chdir(BASE_DIR);
chdir(__DIR__ . '/../');
$builder = new ContainerBuilder();
$builder->addDefinitions(BASE_DIR.'bootstrap/container.php');
DB::setDsn($config['db']['connection'] . ':' . $config['db']['dsn'], $config['db']['username'], $config['db']['password']);
$container = $builder->build();
$container->set('config', $config);
$firstMigrate = false;
if (!file_exists($config['db']['dsn']) && DB::driver() === 'sqlite') {
touch($config['db']['dsn']);
$firstMigrate = true;
}
$db = $container->get('database');
try {
DB::doQuery('SELECT 1 FROM `migrations` LIMIT 1');
} catch (PDOException $exception) {
$firstMigrate = true;
}
$migrator = new Migrator($db, 'resources/schemas');
$migrator->migrate();
$migrator->reSyncQuotas($container->get('storage'));
echo 'Connected.' . PHP_EOL;
if ($firstMigrate) {
echo 'Creating migrations table...' . PHP_EOL;
DB::raw()->exec(file_get_contents('resources/schemas/migrations.sql'));
}
$files = glob('resources/schemas/' . DB::driver() . '/*.sql');
$names = array_map(function ($path) {
return basename($path);
}, $files);
$in = str_repeat('?, ', count($names) - 1) . '?';
$inMigrationsTable = DB::doQuery("SELECT * FROM `migrations` WHERE `name` IN ($in)", $names)->fetchAll();
foreach ($files as $file) {
$continue = false;
$exists = false;
foreach ($inMigrationsTable as $migration) {
if (basename($file) === $migration->name && $migration->migrated) {
$continue = true;
break;
} else if (basename($file) === $migration->name && !$migration->migrated) {
$exists = true;
break;
}
}
if ($continue) continue;
$sql = file_get_contents($file);
try {
DB::raw()->exec($sql);
if (!$exists) {
DB::doQuery('INSERT INTO `migrations` VALUES (?,?)', [basename($file), 1]);
} else {
DB::doQuery('UPDATE `migrations` SET `migrated`=? WHERE `name`=?', [1, basename($file)]);
}
echo "Migrated '$file'" . PHP_EOL;
} catch (PDOException $exception) {
if (!$exists) {
DB::doQuery('INSERT INTO `migrations` VALUES (?,?)', [basename($file), 0]);
}
echo "Error migrating '$file' (" . $exception->getMessage() . ')' . PHP_EOL;
echo $exception->getTraceAsString() . PHP_EOL;
}
}
if (isset($argv[1]) && $argv[1] === '--install') {
$db->query("INSERT INTO `users` (`email`, `username`, `password`, `is_admin`, `user_code`) VALUES ('admin@example.com', 'admin', ?, 1, ?)", [password_hash('admin', PASSWORD_DEFAULT), humanRandomString(5)]);
DB::doQuery("INSERT INTO `users` (`email`, `username`, `password`, `is_admin`, `user_code`) VALUES ('admin@example.com', 'admin', ?, 1, ?)", [password_hash('admin', PASSWORD_DEFAULT), humanRandomString(5)]);
}
if (file_exists(__DIR__.'/../install') && (!isset($config['debug']) || !$config['debug'])) {
removeDirectory(__DIR__.'/../install');
if (file_exists(__DIR__ . '/../install')) {
removeDirectory(__DIR__ . '/../install');
}
echo 'If you are upgrading from a previous version, please run a "php bin' . DIRECTORY_SEPARATOR . 'clean".'.PHP_EOL;
echo 'Done.'.PHP_EOL;
exit(0);
echo 'If you are upgrading from a previous version, please run a "php bin\clean".' . PHP_EOL;
echo 'Done.' . PHP_EOL;
exit(0);

36
bin/theme Normal file
View file

@ -0,0 +1,36 @@
#!/usr/bin/env php
<?php
(PHP_MAJOR_VERSION >= 7 && PHP_MINOR_VERSION >= 1) ?: die('Sorry, PHP 7.1 or above is required to run XBackBone.');
if (php_sapi_name() !== 'cli') {
die();
}
require __DIR__ . '/../vendor/autoload.php';
chdir(__DIR__ . '/../');
$json = json_decode(file_get_contents('https://bootswatch.com/api/4.json'));
if (!isset($argv[1])) {
echo 'Usage: php bin\\theme <theme-name|"default">' . PHP_EOL;
echo 'Here a list of available Bootswatch themes:' . PHP_EOL;
}
if (isset($argv[1]) && strtolower($argv[1]) === 'default') {
file_put_contents('static/bootstrap/css/bootstrap.min.css', file_get_contents('https://bootswatch.com/_vendor/bootstrap/dist/css/bootstrap.min.css'));
echo "Reverted to default theme." . PHP_EOL;
} else {
foreach ($json->themes as $theme) {
if (isset($argv[1]) && strtolower($argv[1]) === strtolower($theme->name)) {
file_put_contents('static/bootstrap/css/bootstrap.min.css', file_get_contents($theme->cssMin));
echo "Installed theme {$theme->name}." . PHP_EOL;
break;
}
if (!isset($argv[1])) {
echo " - {$theme->name} ({$theme->description})[Preview: {$theme->preview}]" . PHP_EOL;
}
}
}
exit(0);

View file

@ -1,121 +1,225 @@
<?php
use App\Exceptions\Handlers\AppErrorHandler;
use App\Exceptions\Handlers\Renderers\HtmlErrorRenderer;
use App\Factories\ViewFactory;
use App\Middleware\InjectMiddleware;
use App\Middleware\LangMiddleware;
use App\Middleware\RememberMiddleware;
use App\Database\DB;
use App\Exceptions\MaintenanceException;
use App\Exceptions\UnauthorizedException;
use App\Web\Lang;
use App\Web\Session;
use App\Web\View;
use DI\Bridge\Slim\Bridge;
use DI\ContainerBuilder;
use Psr\Container\ContainerInterface as Container;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use function DI\factory;
use function DI\get;
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_FILE) && is_dir(BASE_DIR.'install/')) {
header('Location: ./install/');
exit();
}
if (!file_exists(CONFIG_FILE) && !is_dir(BASE_DIR.'install/')) {
exit('Cannot find the config file.');
if (!file_exists('config.php') && is_dir('install/')) {
header('Location: ./install/');
exit();
} 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' => isSecure() ? 'https://'.$_SERVER['HTTP_HOST'] : 'http://'.$_SERVER['HTTP_HOST'],
'debug' => false,
'maintenance' => false,
'db' => [
'connection' => 'sqlite',
'dsn' => BASE_DIR.implode(DIRECTORY_SEPARATOR, ['resources', 'database', 'xbackbone.db']),
'username' => null,
'password' => null,
],
'storage' => [
'driver' => 'local',
'path' => realpath(__DIR__.'/').DIRECTORY_SEPARATOR.'storage',
],
'ldap' => [
'enabled' => false,
'host' => null,
'port' => null,
'base_domain' => null,
'user_domain' => null,
],
], require CONFIG_FILE);
'app_name' => 'XBackBone',
'base_url' => isset($_SERVER['HTTPS']) ? 'https://' . $_SERVER['HTTP_HOST'] : 'http://' . $_SERVER['HTTP_HOST'],
'displayErrorDetails' => false,
'maintenance' => false,
'db' => [
'connection' => 'sqlite',
'dsn' => BASE_DIR . 'resources/database/xbackbone.db',
'username' => null,
'password' => null,
],
'storage' => [
'driver' => 'local',
'path' => realpath(__DIR__ . '/') . DIRECTORY_SEPARATOR . 'storage',
],
], require BASE_DIR . 'config.php');
$builder = new ContainerBuilder();
if (!$config['debug']) {
$builder->enableCompilation(BASE_DIR.'/resources/cache/di');
$builder->writeProxiesToFile(true, BASE_DIR.'/resources/cache/di');
if (!$config['displayErrorDetails']) {
$config['routerCacheFile'] = BASE_DIR . 'resources/cache/routes.cache.php';
}
$builder->addDefinitions([
Session::class => factory(function () {
return new Session('xbackbone_session', BASE_DIR.'resources/sessions');
}),
'session' => get(Session::class),
View::class => factory(function (Container $container) {
return ViewFactory::createAppInstance($container);
}),
'view' => get(View::class),
]);
$container = new Container(['settings' => $config]);
$builder->addDefinitions(__DIR__.'/container.php');
$container['config'] = function ($container) use ($config) {
return $config;
};
global $app;
$app = Bridge::create($builder->build());
$app->getContainer()->set('config', $config);
$app->setBasePath(parse_url($config['base_url'], PHP_URL_PATH) ?: '');
$container['logger'] = function ($container) {
$logger = new Logger('app');
if (!$config['debug']) {
$app->getRouteCollector()->setCacheFile(BASE_DIR.'resources/cache/routes.cache.php');
}
$streamHandler = new RotatingFileHandler(BASE_DIR . 'logs/log.txt', 10, Logger::DEBUG);
$app->add(InjectMiddleware::class);
$app->add(LangMiddleware::class);
$app->add(RememberMiddleware::class);
$lineFormatter = new LineFormatter("[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n", "Y-m-d H:i:s");
$lineFormatter->includeStacktraces(true);
$streamHandler->setFormatter($lineFormatter);
$logger->pushHandler($streamHandler);
return $logger;
};
$container['session'] = function ($container) {
return new Session('xbackbone_session', BASE_DIR . 'resources/sessions');
};
$container['database'] = function ($container) use (&$config) {
$dsn = $config['db']['connection'] === 'sqlite' ? BASE_DIR . $config['db']['dsn'] : $config['db']['dsn'];
return new DB($config['db']['connection'] . ':' . $dsn, $config['db']['username'], $config['db']['password']);
};
$container['storage'] = function ($container) use (&$config) {
switch ($config['storage']['driver']) {
case 'local':
return new Filesystem(new Local($config['storage']['path']));
case 's3':
$client = new S3Client([
'credentials' => [
'key' => $config['storage']['key'],
'secret' => $config['storage']['secret'],
],
'region' => $config['storage']['region'],
'version' => 'latest',
]);
return new Filesystem(new AwsS3Adapter($client, $config['storage']['bucket'], $config['storage']['path']));
case 'dropbox':
$client = new DropboxClient($config['storage']['token']);
return new Filesystem(new DropboxAdapter($client), ['case_sensitive' => false]);
case 'ftp':
return new Filesystem(new FtpAdapter([
'host' => $config['storage']['host'],
'username' => $config['storage']['username'],
'password' => $config['storage']['password'],
'port' => $config['storage']['port'],
'root' => $config['storage']['path'],
'passive' => $config['storage']['passive'],
'ssl' => $config['storage']['ssl'],
'timeout' => 30,
]));
case 'google-cloud':
$client = new StorageClient([
'projectId' => $config['storage']['project_id'],
'keyFilePath' => $config['storage']['key_path'],
]);
return new Filesystem(new GoogleStorageAdapter($client, $client->bucket($config['storage']['bucket'])));
default:
throw new InvalidArgumentException('The driver specified is not supported.');
}
};
$container['lang'] = function ($container) use (&$config) {
if (isset($config['lang'])) {
return Lang::build($config['lang'], BASE_DIR . 'resources/lang/');
}
return Lang::build(Lang::recognize(), BASE_DIR . 'resources/lang/');
};
$container['view'] = function ($container) use (&$config) {
$view = new Twig(BASE_DIR . 'resources/templates', [
'cache' => BASE_DIR . 'resources/cache',
'autoescape' => 'html',
'debug' => $config['displayErrorDetails'],
'auto_reload' => $config['displayErrorDetails'],
]);
// Instantiate and add Slim specific extension
$router = $container->get('router');
$uri = Uri::createFromEnvironment(new Environment($_SERVER));
$view->addExtension(new Slim\Views\TwigExtension($router, $uri));
$view->getEnvironment()->addGlobal('config', $config);
$view->getEnvironment()->addGlobal('request', $container->get('request'));
$view->getEnvironment()->addGlobal('alerts', $container->get('session')->getAlert());
$view->getEnvironment()->addGlobal('session', $container->get('session')->all());
$view->getEnvironment()->addGlobal('current_lang', $container->get('lang')->getLang());
$view->getEnvironment()->addGlobal('PLATFORM_VERSION', PLATFORM_VERSION);
$view->getEnvironment()->addFunction(new TwigFunction('route', 'route'));
$view->getEnvironment()->addFunction(new TwigFunction('lang', 'lang'));
$view->getEnvironment()->addFunction(new TwigFunction('urlFor', 'urlFor'));
$view->getEnvironment()->addFunction(new TwigFunction('asset', 'asset'));
$view->getEnvironment()->addFunction(new TwigFunction('mime2font', 'mime2font'));
$view->getEnvironment()->addFunction(new TwigFunction('queryParams', 'queryParams'));
$view->getEnvironment()->addFunction(new TwigFunction('isDisplayableImage', 'isDisplayableImage'));
return $view;
};
$container['phpErrorHandler'] = function ($container) {
return function (Request $request, Response $response, Throwable $error) use (&$container) {
$container->logger->critical('Fatal runtime error during app execution', ['exception' => $error]);
return $container->view->render($response->withStatus(500), 'errors/500.twig', ['exception' => $error]);
};
};
$container['errorHandler'] = function ($container) {
return function (Request $request, Response $response, Exception $exception) use (&$container) {
if ($exception instanceof MaintenanceException) {
return $container->view->render($response->withStatus(503), 'errors/maintenance.twig');
}
if ($exception instanceof UnauthorizedException) {
return $container->view->render($response->withStatus(403), 'errors/403.twig');
}
$container->logger->critical('Fatal exception during app execution', ['exception' => $exception]);
return $container->view->render($response->withStatus(500), 'errors/500.twig', ['exception' => $exception]);
};
};
$container['notAllowedHandler'] = function ($container) {
return function (Request $request, Response $response, $methods) use (&$container) {
return $container->view->render($response->withStatus(405)->withHeader('Allow', implode(', ', $methods)), 'errors/405.twig');
};
};
$container['notFoundHandler'] = function ($container) {
return function (Request $request, Response $response) use (&$container) {
$response->withStatus(404)->withHeader('Content-Type', 'text/html');
return $container->view->render($response, 'errors/404.twig');
};
};
$app = new App($container);
// Permanently redirect paths with a trailing slash to their non-trailing counterpart
$app->add(function (Request $request, RequestHandler $handler) use (&$app, &$config) {
$uri = $request->getUri();
$path = $uri->getPath();
$app->add(function (Request $request, Response $response, callable $next) {
$uri = $request->getUri();
$path = $uri->getPath();
if ($path !== $app->getBasePath().'/' && substr($path, -1) === '/') {
// permanently redirect paths with a trailing slash
// to their non-trailing counterpart
$uri = $uri->withPath(substr($path, 0, -1));
if ($path !== '/' && substr($path, -1) === '/') {
$uri = $uri->withPath(substr($path, 0, -1));
if ($request->getMethod() === 'GET') {
return $app->getResponseFactory()
->createResponse(301)
->withHeader('Location', (string) $uri);
} else {
$request = $request->withUri($uri);
}
}
if ($request->getMethod() === 'GET') {
return $response->withRedirect((string)$uri, 301);
} else {
return $next($request->withUri($uri), $response);
}
}
return $handler->handle($request);
return $next($request, $response);
});
$app->addRoutingMiddleware();
// Configure the error handler
$errorHandler = new AppErrorHandler($app->getCallableResolver(), $app->getResponseFactory());
$errorHandler->registerErrorRenderer('text/html', HtmlErrorRenderer::class);
// Add Error Middleware
$errorMiddleware = $app->addErrorMiddleware($config['debug'], false, true);
$errorMiddleware->setDefaultErrorHandler($errorHandler);
// Load the application routes
require BASE_DIR.'app/routes.php';
require BASE_DIR . 'app/routes.php';
return $app;
return $app;

View file

@ -1,113 +0,0 @@
<?php
use App\Database\DB;
use App\Web\Lang;
use Aws\S3\S3Client;
use League\Flysystem\Cached\CachedAdapter;
use League\Flysystem\Cached\Storage\Adapter;
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\AzureBlobStorage\AzureBlobStorageAdapter;
use MicrosoftAzure\Storage\Blob\BlobRestProxy;
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),
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');
$driver = $config['storage']['driver'];
if ($driver === 'local') {
return new Filesystem(new Local($config['storage']['path']));
} elseif ($driver === 's3') {
$client = new S3Client([
'credentials' => [
'key' => $config['storage']['key'],
'secret' => $config['storage']['secret'],
],
'region' => $config['storage']['region'],
'endpoint' => $config['storage']['endpoint'],
'version' => 'latest',
'use_path_style_endpoint' => $config['storage']['use_path_style_endpoint'] ?? false,
'@http' => ['stream' => true],
]);
$adapter = new AwsS3Adapter($client, $config['storage']['bucket'], $config['storage']['path']);
} elseif ($driver === 'dropbox') {
$client = new DropboxClient($config['storage']['token']);
$adapter = new DropboxAdapter($client);
} elseif ($driver === 'ftp') {
$adapter = 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,
]);
} elseif ($driver === 'google-cloud') {
$client = new StorageClient([
'projectId' => $config['storage']['project_id'],
'keyFilePath' => $config['storage']['key_path'],
]);
$adapter = new GoogleStorageAdapter($client, $client->bucket($config['storage']['bucket']));
} elseif ($driver === 'azure') {
$client = BlobRestProxy::createBlobService(
sprintf(
'DefaultEndpointsProtocol=https;AccountName=%s;AccountKey=%s;',
$config['storage']['account_name'],
$config['storage']['account_key']
)
);
$adapter = new AzureBlobStorageAdapter($client, $config['storage']['container_name']);
} else {
throw new InvalidArgumentException('The driver specified is not supported.');
}
$cache = new Adapter(new Local(BASE_DIR.'resources/cache/fs'), 'file', 300); // 5min
return new Filesystem(new CachedAdapter($adapter, $cache));
}),
'storage' => get(Filesystem::class),
Lang::class => factory(function () {
return Lang::build(Lang::recognize(), BASE_DIR.'resources/lang/');
}),
'lang' => get(Lang::class),
];

View file

@ -1,40 +1,26 @@
{
"name": "sergix44/xbackbone",
"license": "AGPL-3.0-only",
"version": "3.7.0",
"version": "2.6.6",
"description": "A lightweight ShareX PHP backend",
"type": "project",
"require": {
"php": ">=7.3",
"ext-filter": "*",
"ext-gd": "*",
"ext-intl": "*",
"ext-json": "*",
"ext-pdo": "*",
"ext-zip": "*",
"erusev/parsedown": "^1.7",
"intervention/image": "^2.6",
"league/flysystem": "^1.1.4",
"league/flysystem-aws-s3-v3": "^1.0",
"league/flysystem-cached-adapter": "^1.1",
"maennchen/zipstream-php": "^2.0",
"php": ">=7.1",
"slim/slim": "^3.0",
"slim/twig-view": "^2.4",
"league/flysystem": "^1.0.45",
"monolog/monolog": "^1.23",
"php-di/slim-bridge": "^3.0",
"sapphirecat/slim4-http-interop-adapter": "^1.0",
"slim/psr7": "^1.5",
"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.14"
},
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true,
"platform": {
"php": "7.3.33"
}
"ext-intl": "*",
"ext-json": "*",
"ext-gd": "*",
"ext-pdo": "*",
"ext-zip": "*"
},
"prefer-stable": true,
"minimum-stability": "dev",
"autoload": {
"files": [
"app/helpers.php"
@ -43,17 +29,14 @@
"App\\": "app/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
"extra": {
"violinist": {
"allow_updates_beyond_constraint": 1,
"blacklist": [],
"update_with_dependencies": 1
}
},
"minimum-stability": "dev",
"prefer-stable": true,
"require-dev": {
"roave/security-advisories": "dev-latest",
"phpstan/phpstan": "^0.11.5",
"phpunit/phpunit": "^9.0",
"symfony/dom-crawler": "^4.4"
"phpstan/phpstan": "^0.11.5"
}
}

5445
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,15 +1,14 @@
<?php
return [
'base_url' => 'https://localhost', // no trailing slash
'db' => [
'connection' => 'sqlite',
'dsn' => realpath(__DIR__).'/resources/database/xbackbone.db',
'username' => null,
'password' => null,
],
'storage' => [
'driver' => 'local',
'path' => realpath(__DIR__).'/storage',
],
'base_url' => 'http://localhost',
'db' => [
'connection' => 'sqlite',
'dsn' => 'resources/database/xbackbone.db',
'username' => null,
'password' => null,
],
'storage' => [
'driver' => 'local',
'path' => './storage',
],
];

View file

@ -1 +0,0 @@
xbackbone.app

View file

@ -1,8 +0,0 @@
remote_theme: pmarsceill/just-the-docs
logo: "img/xbackbone.png"
title: "XBackBone"
aux_links:
"Star me on GitHub":
- "//github.com/SergiX44/XBackBone"
footer_content: "Copyright &copy; 2020 Sergio Brighenti. Distributed under <a href=\"https://github.com/SergiX44/XBackBone/blob/master/LICENSE\">AGPL v3.0 license.</a>"
ga_tracking: UA-55470316-7

View file

@ -1 +0,0 @@
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-1835087833720665" crossorigin="anonymous"></script>

View file

@ -1 +0,0 @@
google.com, pub-1835087833720665, DIRECT, f08c47fec0942fa0

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,451 +0,0 @@
---
layout: default
title: Changelog
nav_order: 9
---
# Changelog
## [3.7.0] - 2024-01-14
### Added
- Added support for vanity urls.
- Added KDE integration for linux script.
### Changed
- Updated translations.
- File preview is now clickable to open the file.
### Fixed
- Fixes for LDAP authentication.
## [3.6.3] - 2023-05-27
### Fixed
- Fix LDAP for php >= 8.1
## [3.6.2] - 2023-05-24
### Changed
- Support for PHP 8.2
### Removed
- Azure blob storage driver
## [3.6.1] - 2022-11-27
### Changed
- Upgraded dependencies
- Updated translations
### Fixed
- Fixed error in export data (#499)
- Fixed issues with reverse proxies (#495)
- Fixed duplicated twitter tags (#496)
## [3.6.0] - 2022-06-20
### Changed
- Improved embedding on discord of large videos.
- Releases are now compressed for faster downloads
- Updated translations
### Fixed
- Fixed deprecation notices on php >= 8
- Fixed embed UA for Discord.
- Fixed error with post_max_size = 0
### Removed
- Support for php 7.2
## [3.5.1] - 2021-10-22
### Changed
- Fixed embed UA for Discord.
- Updated translations.
## [3.5.0] - 2021-09-05
### Added
- Support for theme-park.dev themes.
- Updated translations.
### Fixed
- Wrong css when reapplying the default theme.
### Removed
- Dropped theme cli command.
## [3.4.1] - 2021-08-11
### Added
- Toggle to disable embeds.
### Changed
- Raw url copying now contains also the file extension.
## [3.4.0] - 2021-08-01
### Added
- Added image support for OG for Discord only.
### Changed
- Updated translations.
- Dropped support for PHP 7.1
### Fixed
- Fixed possible XSS and CSRF attacks.
## [3.3.5] - 2021-04-25
### Fixed
- Removed OG integration for discord.
### Changed
- Updated translations.
## [3.3.4] - 2021-03-07
### Added
- Login failed logging.
- User identifier option for LDAP configurations.
### Fixed
- Fixed open graph meta tags for Discord.
- Fixed custom html tags are not displayed back in the admin setting.
- Fixed python plugin for newer version of Screencloud.
- Fixed accented chars in email subject.
- Fixed error on PHP 8.
## [3.3.3] - 2020-11-13
### Fixed
- Fixed issue with responsive menu on mobile.
## [3.3.2] - 2020-11-12
### Fixed
- Fixed switch not works for the first time for normal users.
## [3.3.1] - 2020-11-12
### Fixed
- Formatting error on the check for updates.
- Fixed default view for normal users.
## [3.3.0] - 2020-11-12
### Added
- Enabled PHP 8 support.
- Added Screencloud client support (https://screencloud.net).
- OpenGraph image tag (issue #269).
- Start adding unit tests.
### Changed
- The list mode is now available also for non-admin accounts (issue #226).
### Fixed
- Linux script strange response code in headless mode.
### Removed
- Dropped Telegram share button.
## [3.2.0] - 2020-09-05
### Added
- Added support to use Azure Blob Storage account as storage location.
- Support for other S3-compatible storage endpoint.
- Line number when showing text files.
### Fixed
- S3 driver file streaming not working properly.
- Fixed Slack image preview.
## [3.1.4] - 2020-04-13
### Changed
- Now the migrate command resync the system quota for each user.
### Fixed
- Fixed error with the migrate command.
## [3.1.3] - 2020-04-13
### Changed
- Added changelog page.
- Updated translations.
## [3.1.2] - 2020-04-12
### Changed
- Improved installer storage checks.
### Fixed
- Fixed upload table lost when updating very old instances.
## [3.1.1] - 2020-04-11
### Fixed
- Fixed error during a fresh installation with sqlite.
## [3.1] - 2020-04-10
### Added
- 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.
### Changed
- The theme is now re-applied after every system update.
- Updated system settings page.
- Updated translations.
- Improved grid layout.
### Fixed
- Fixed bug html files raws are rendered in a browser.
- Fixes and improvements.
## [3.0.2] - 2019-12-04
### Changed
- Updated translations.
### Fixed
- Fixed error with migrate command.
## [3.0.1] - 2019-11-25
### Changed
- Small installer update.
### Fixed
- Fixed error with older mysql versions.
- Fixed config is compiled with the di container.
## [3.0] - 2019-11-23
### Added
- 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)).
- The linux script can be used on headless systems.
- Raw URL now accept file extensions.
- Implemented SameSite XSS protection.
### Changed
- Upgraded from Slim3 to Slim 4.
- Replaced videojs player with Plyr.
- Improved installer.
- Improved thumbnail generation.
- Small fixes and improvements.
## [2.6.6] - 2019-10-23
### Added
- Ability to choose between releases and prereleases with the web updater.
### Changed
- Updated translations.
## [2.6.5] - 2019-09-17
### Changed
- Changed color to some buttons to address visibility with some themes.
### Fixed
- Fixed error after orphaned files removal #74.
- Fixed update password not correctly removed from log files (#74).
## [2.6.4] - 2019-09-15
### Added
- Filter on displayable images.
### Changed
- The generated random strings are now more human readable.
### Fixed
- Fixed during upload error on php compiled for 32 bit.
- Fixed icons on the installer page.
## [2.6.3] - 2019-09-14
### Fixed
- Fixed #67.
- Fixed bad preload statement.
- Fixed wrong redirect after install in subdirs.
## [2.6.2] - 2019-09-06
### Added
- Added method for cache busting when updating/change theme.
- Added russian translation from [Weblate](https://hosted.weblate.org/projects/xbackbone/xbackbone/).
### Changed
- Changed background default color.
- Use the Font Awesome web font for better performances.
## [2.6.1] - 2019-09-04
### Added
- Added alert if required extensions are not loaded.
### Changed
- Improved shell commands.
- Updated translations.
### Fixed
- Fixed bad redirects on the web installer (#62).
- Fixed login page with dark themes.
## [2.6] - 2019-08-20
### Added
- Added support to use AWS S3, Google Cloud Storage, Dropbox and FTP(s) accounts as storage location.
- Added german and norwegian translations from [Weblate](https://hosted.weblate.org/projects/xbackbone/xbackbone/).
- Added ability to force system language.
### Changed
- Improved lang detection.
### Fixed
- Fixed missing icon.
## [2.5.3] - 2019-05-12
### Changed
- Improved exception stacktrace logging.
### Fixed
- Fixed bad css loading on Firefox (#35).
- Fixed wrong style for publish/unpublish button.
## [2.5.2] - 2019-05-09
### Added
- Added preloading for some resources to improve performances.
- Added check for block execution on EOL and unsupported PHP versions.
### Changed
- Improved session handling.
- Other minor improvements.
### Fixed
- Fixed telegram share not working.
- Fix for big text file now are not rendered in the browser.
## [2.5.1] - 2019-04-10
### Changed
- Improved HTTP partial content implementation for large files.
### Fixed
- Fixed bad redirect if the theme folder is not writable. (#27)
## [2.5] - 2019-02-10
### Added
- Added partial content implementation (stream seeking on chromium based browsers).
- **[BETA]** Added self update feature.
- Added project favicon.
### Changed
- Updated project license to [AGPL v3.0](https://choosealicense.com/licenses/agpl-3.0/) (now releases ships with the new license).
- Improved video.js alignment with large videos.
- Optimized output zip release size.
- Templates cleanup and optimizations.
- Improved error handling.
## [2.4.1] - 2019-01-24
### Fixed
- Fixed error message when the file is too large. (#15)
- Fixed button alignment.
## [2.4] - 2019-01-22
### Added
- Added function to remove orphaned files.
- Multiple uploads sorting methods.
- Switch between tab and gallery mode using an admin account.
- Search in uploads.
### Changed
- Updated js dependencies.
- Internal refactoring and improvements
## [2.3.1] - 2018-12-09
### Added
- Added checks during the installation wizard.
- cURL and Wget can now directly download the file.
### Fixed
- Fixed english language.
- Fixed forced background with dark themes.
## [2.3] - 2018-11-30
### Added
- Added overlay on user gallery images.
- Added linux script to allow uploads from linux screenshot tools.
- Enable audio player with video.js.
- Font Awesome icon match the single file mime-type.
### Changed
- Improved image scaling in user gallery.
- Video and audio now starts with volume at 50%.
- Minor layout fixes.
### Fixed
- Fixed IT translation.
## [2.2] - 2018-11-20
### Added
- Added multi-language support.
### Fixed
- Improved routing.
- Minor improvements and bug fixes.
- Fixed HTTP/2 push is resetting the current session.
## [2.1] - 2018-11-20
### Added
- Added video.js support.
- Allow e-mail login.
- Support for ShareX deletion URL.
### Changed
- Improved theme style.
- Improved page redirecting.
### Fixed
- Fixed HTTP/2 push preload.
## [2.0] - 2018-11-13
### Added
- Added install wizard (using the CLI is no longer required).
- Added used space indicator per user.
- Allow discord bot to display the preview.
- Theme switcher on the web UI.
- MySQL support.
### Changed
- Migrated from Flight to Slim 3 framework.
- Improvements under the hood.
## [1.3] - 2018-10-14
### Added
- Added command to switch between bootswatch.com themes.
- Added popover to write the telegram message when sharing.
- Allow Facebook bots to display the preview.
### Changed
- Packaging improvements.
- Updated some dependencies.
## [1.2] - 2018-05-01
### Added
- Added auto config generator for ShareX.
- Show upload file size on the dashboard.
### Changed
- Previews are now scaled for better page load.
### Removed
- Removed HTTP2 push from the dashboard to improve loading time.
### Fixed
- Fixed insert for admin user (running `php bin\migrate --install`).
## [1.1] - 2018-04-28
### Added
- Added logging.
- Added share to Telegram.
### Changed
- Improved migrate system.
- Updated Bootstrap theme.
### Fixed
- Fixed back to top when click delete or publish/unpublish.
- Login redirect back to the requested page.
## [1.0] - 2018-04-28
### Added
- Initial version.

View file

@ -1,48 +0,0 @@
---
layout: default
title: Client Configuration
nav_order: 5
---
# Clients Configuration
## ShareX (Windows)
Once you are logged in, just go in your profile settings and download the ShareX config file for your account.
## Screencloud (Windows, Mac and Linux)
Once you are logged in, go in your profile account and click on the Screencloud button.
Now open Screencloud, open "Preferences" > "Online Services" tab > click "More Services" > and "Install from URL"
and paste the URL copied from XBackBone, and all should work out-of-the-box.
If for whatever reason you need to change the instance url or the token, just edit the settings of the XBackBone plugin.
## MagicCap (Mac and Linux)
MagicCap supports the same file format used by ShareX.
Just download the ShareX config file from your profile, and then on MagicCap open the Preferences > Uploader settings and choose ShareX.
Set the path to the file you have downloaded, and you are good to go!
## uPic (Mac)
This tool does not support plugins or custom configuration, but you can configure it manually:
In preferences, you should add "Custom" host and configure it as follows:
- **API URL:** Your instance upload url, like `http://example.com/upload`
- **Request method:** POST
- **File field:** file
- **URL Path:** ["url"]
- In "Other fields", in the body section, you should add the field `token`, with your upload token.
- In "Other fields", in the headers section, you should add the field `Content-Type`, with the value `application/x-www-form-urlencoded`.
## Bash Script (Linux, Mac, WSL)
XBackBone can generate a script that allows you to share an item from any tool, even headless servers:
+ 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,20 +0,0 @@
---
layout: default
title: Common Issues
nav_order: 7
---
# Common Issues
### Error 404 after installation
If you have Apache web server, check if it's reading the file `.htaccess` and the module `mod_rewrite` is enabled.
<hr>
### [Discord, Telegram, ...] is not showing the image/video preview of the link.
If you use Cloudflare, check if the setting that blocks access to bots is active. If enabled, the bots of the respective platforms will not be able to access to download the preview.
<hr>
### How to increase the upload max file size?
Increase the `post_max_size` and `upload_max_filesize` in your `php.ini`.

View file

@ -1,214 +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:
This configuration requires anonymous LDAP access
```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
'rdn_attribute' => 'uid=', // the attribute to identify the user
),
);
```
The following configuration snippet enables authenticated LDAP user lookups
```php
return array(
...
'ldap' => array(
'enabled' => true, // enable it
'schema' => 'ldap', // use 'ldap' or 'ldaps' Default is 'ldap'
'host' => 'ad.example.com', // set the ldap host
'port' => 389, // ldap port
'base_domain' => 'dc=example,dc=com', // the base_dn string
'search_filter' => '(&(objectClass=user)(sAMAccountName=????))', // ???? is replaced with user provided username
'rdn_attribute' => 'sAMAccountName', // the attribute to use as username
'service_account_dn' => 'cn=xbackbone,cn=Users,dc=example,dc=com', // LDAP Service Account Full DN
'service_account_password' => 'examplepassword',
),
);
```
Enabling LDAP over TLS. Make sure to update port number. Merge with your current LDAP configuration.
```php
return array(
...
'ldap' => array(
'schema' => 'ldaps', //defaults to 'ldap'
'port' => 636,
),
);
```
Enabling StartTLS upgrade. Merge with your current LDAP configuration.
```php
return array(
...
'ldap' => array(
...
'useStartTLS' => true, //defaults to false
),
);
```
The 'schema' => 'ldaps' and 'useStartTLS'=> true configuration directives are mutually exclusive. Do no use them together.
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',
),
);
```
For any filesystem S3-compatible, it's possible to specify an `endpoint` (for i.e. Minio)
```php
return array(
...
'storage' => array (
'driver' => 's3',
'endpoint' => 'my-custom-endpoint',
'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-cloud',
'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: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

View file

@ -1,57 +0,0 @@
---
layout: default
title: Home
nav_order: 1
---
<p class="text-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 }
<p class="text-center">
<a href="https://github.com/SergiX44/XBackBone/releases/latest" class="btn btn-green">Download</a>
<a href="https://github.com/SergiX44/XBackBone" class="btn btn-blue">GitHub</a>
<a href="sponsor.html" class="btn btn-purple" style="background-color: #e7af06; background-image: linear-gradient(#f7d12e, #e7af06);">Sponsor</a>
</p>
## Main Features
+ Multiple clients supported: ShareX, Screencloud, uPic, ...
+ Config generator for ShareX, Screencloud.
+ Low memory footprint.
+ Multiple backends support: Local storage, AWS S3, Google Cloud, Azure Blob Storage, 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.
### Demo GIF
![img](https://i.imgur.com/iV8Rirn.gif)
## 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>

View file

@ -1,68 +0,0 @@
---
layout: default
title: Installation
nav_order: 2
---
# Installation
### Prerequisites
XBackBone require PHP >= `7.3`, 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.
+ `php-zip` compressed files 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' => 'absolute/path/to/storage',
],
'db' => [
'connection' => 'sqlite', // current support for sqlite and mysql
'dsn' => 'absolute/path/to/resources/database/xbackbone.db', // if sqlite should be an absolute path
'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://fleet.linuxserver.io/image?name=linuxserver/xbackbone){: .btn .btn-purple }

View file

@ -1,21 +0,0 @@
---
layout: default
title: License & Credits
nav_order: 10
---
# 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,22 +0,0 @@
---
layout: default
title: Sponsorship
nav_order: 8
---
# Sponsor this project
You can give your contribution to this project thanks to a small sponsorship, through GitHub Sponsors, or as a single donation on PayPal.
All those who make the recurring donations, the names or logos will be inserted in this page, if they wish.
[<img src="https://www.gravatar.com/avatar/98b8d56f4a193e3f7154f236c16930b2?s=160" alt="SergiX44" height="100">](https://github.com/SergiX44) |
---|
[SergiX44](https://github.com/SergiX44) |
[GitHub Sponsors](https://github.com/sponsors/SergiX44) |
[PayPal for XBackBone](http://bit.ly/XBackBonePaypal) |
{: .text-center}
## Sponsors
+ [@philw95](https://github.com/philw95)

View file

@ -1,34 +0,0 @@
---
layout: default
title: Upgrading
nav_order: 4
---
# Upgrading
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,11 +1,9 @@
<?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';
((PHP_MAJOR_VERSION >= 7 && PHP_MINOR_VERSION >= 3) || PHP_MAJOR_VERSION > 7) ?: die('Sorry, PHP 7.3 or above is required to run XBackBone.');
require __DIR__.'/vendor/autoload.php';
define('BASE_DIR', realpath(__DIR__).DIRECTORY_SEPARATOR);
define('BASE_DIR', realpath(__DIR__) . DIRECTORY_SEPARATOR);
define('PLATFORM_VERSION', json_decode(file_get_contents('composer.json'))->version);
define('CONFIG_FILE', BASE_DIR.'config.php');
$app = require __DIR__.'/bootstrap/app.php';
$app = require_once __DIR__ . '/bootstrap/app.php';
$app->run();

View file

@ -1,248 +1,333 @@
<?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';
((PHP_MAJOR_VERSION >= 7 && PHP_MINOR_VERSION >= 3) || PHP_MAJOR_VERSION > 7) ?: die('Sorry, PHP 7.3 or above is required to run XBackBone.');
require __DIR__.'/../vendor/autoload.php';
use App\Database\Migrator;
use App\Factories\ViewFactory;
use App\Database\DB;
use App\Web\Session;
use App\Web\View;
use DI\Bridge\Slim\Bridge;
use DI\ContainerBuilder;
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 Psr\Container\ContainerInterface as Container;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use function DI\factory;
use function DI\get;
use function DI\value;
use Spatie\Dropbox\Client as DropboxClient;
use League\Flysystem\Filesystem;
use Slim\App;
use Slim\Container;
use Slim\Http\Environment;
use Slim\Http\Request;
use Slim\Http\Response;
use Slim\Http\Uri;
use Slim\Views\Twig;
use Spatie\FlysystemDropbox\DropboxAdapter;
use Superbalist\Flysystem\GoogleStorage\GoogleStorageAdapter;
define('PLATFORM_VERSION', json_decode(file_get_contents(__DIR__.'/../composer.json'))->version);
define('BASE_DIR', realpath(__DIR__.'/../').DIRECTORY_SEPARATOR);
define('PLATFORM_VERSION', json_decode(file_get_contents(__DIR__ . '/../composer.json'))->version);
// default config
$config = [
'base_url' => str_replace('/install/', '', (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http')."://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]"),
'debug' => true,
'db' => [
'connection' => 'sqlite',
'dsn' => BASE_DIR.implode(DIRECTORY_SEPARATOR, ['resources', 'database', 'xbackbone.db']),
'username' => null,
'password' => null,
],
'storage' => [
'driver' => 'local',
'path' => realpath(__DIR__.'/../').DIRECTORY_SEPARATOR.'storage',
],
'base_url' => str_replace('/install/', '', (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http') . "://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]"),
'displayErrorDetails' => true,
'db' => [
'connection' => 'sqlite',
'dsn' => realpath(__DIR__ . '/../') . implode(DIRECTORY_SEPARATOR, ['resources', 'database', 'xbackbone.db']),
'username' => null,
'password' => null,
],
'storage' => [
'driver' => 'local',
'path' => realpath(__DIR__ . '/../') . DIRECTORY_SEPARATOR . 'storage',
],
];
$installed = false;
if (file_exists(__DIR__.'/../config.php')) {
$installed = true;
$config = array_replace_recursive($config, require __DIR__.'/../config.php');
if (isset($config['storage_dir'])) { // if from older installations with no support of other than local driver
$config['storage']['driver'] = 'local';
$config['storage']['path'] = $config['storage_dir'];
unset($config['storage_dir']);
}
if ($config['storage']['driver'] === 'local' && !is_dir($config['storage']['path'])) { // if installed with local driver, and the storage dir don't exists
$realPath = realpath(BASE_DIR.$config['storage']['path']);
if (is_dir($realPath) && is_writable($realPath)) { // and was a path relative to the upper folder
$config['storage']['path'] = $realPath; // update the config
}
}
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),
Session::class => factory(function () {
return new Session('xbackbone_session');
}),
'session' => get(Session::class),
]);
$builder->addDefinitions(__DIR__.'/../bootstrap/container.php');
$container['session'] = function ($container) {
return new Session('xbackbone_session');
};
$app = Bridge::create($builder->build());
$app->setBasePath(parse_url($config['base_url'].'/install', PHP_URL_PATH));
$app->addRoutingMiddleware();
$container['view'] = function ($container) use (&$config) {
$view = new Twig([__DIR__ . '/templates', __DIR__ . '/../resources/templates'], [
'cache' => false,
'autoescape' => 'html',
'debug' => $config['displayErrorDetails'],
'auto_reload' => $config['displayErrorDetails'],
]);
$app->get('/', function (Response $response, View $view, Session $session) {
if (!extension_loaded('gd')) {
$session->alert('The required "gd" extension is not loaded.', 'danger');
}
// Instantiate and add Slim specific extension
$router = $container->get('router');
$uri = Uri::createFromEnvironment(new Environment($_SERVER));
$view->addExtension(new Slim\Views\TwigExtension($router, $uri));
if (!extension_loaded('intl')) {
$session->alert('The required "intl" extension is not loaded.', 'danger');
}
$view->getEnvironment()->addGlobal('config', $config);
$view->getEnvironment()->addGlobal('request', $container->get('request'));
$view->getEnvironment()->addGlobal('alerts', $container->get('session')->getAlert());
$view->getEnvironment()->addGlobal('session', $container->get('session')->all());
$view->getEnvironment()->addGlobal('PLATFORM_VERSION', PLATFORM_VERSION);
return $view;
};
if (!extension_loaded('json')) {
$session->alert('The required "json" extension is not loaded.', 'danger');
}
$container['storage'] = function ($container) use (&$config) {
if (!extension_loaded('fileinfo')) {
$session->alert('The required "fileinfo" extension is not loaded.', 'danger');
}
switch ($config['storage']['driver']) {
case 'local':
return new Filesystem(new Local($config['storage']['path']));
case 's3':
$client = new S3Client([
'credentials' => [
'key' => $config['storage']['key'],
'secret' => $config['storage']['secret'],
],
'region' => $config['storage']['region'],
'version' => 'latest',
]);
if (!extension_loaded('zip')) {
$session->alert('The required "zip" extension is not loaded.', 'danger');
}
return new Filesystem(new AwsS3Adapter($client, $config['storage']['bucket'], $config['storage']['path']));
case 'dropbox':
$client = new DropboxClient($config['storage']['token']);
return new Filesystem(new DropboxAdapter($client), ['case_sensitive' => false]);
case 'ftp':
return new Filesystem(new FtpAdapter([
'host' => $config['storage']['host'],
'username' => $config['storage']['username'],
'password' => $config['storage']['password'],
'port' => $config['storage']['port'],
'root' => $config['storage']['path'],
'passive' => $config['storage']['passive'],
'ssl' => $config['storage']['ssl'],
'timeout' => 30,
]));
case 'google-cloud':
$client = new StorageClient([
'projectId' => $config['storage']['project_id'],
'keyFilePath' => $config['storage']['key_path'],
]);
return new Filesystem(new GoogleStorageAdapter($client, $client->bucket($config['storage']['bucket'])));
default:
throw new InvalidArgumentException('The driver specified is not supported.');
}
};
if (!is_writable(__DIR__.'/../resources/cache')) {
$session->alert('The cache folder is not writable ('.__DIR__.'/../resources/cache'.')', 'danger');
}
function migrate($config) {
$firstMigrate = false;
if ($config['db']['connection'] === 'sqlite' && !file_exists(__DIR__ . '/../' . $config['db']['dsn'])) {
touch(__DIR__ . '/../' . $config['db']['dsn']);
$firstMigrate = true;
}
if (!is_writable(__DIR__.'/../resources/database')) {
$session->alert('The database folder is not writable ('.__DIR__.'/../resources/database'.')', 'danger');
}
try {
DB::doQuery('SELECT 1 FROM `migrations` LIMIT 1');
} catch (PDOException $exception) {
$firstMigrate = true;
}
if (!is_writable(__DIR__.'/../resources/sessions')) {
$session->alert('The sessions folder is not writable ('.__DIR__.'/../resources/sessions'.')', 'danger');
}
if ($firstMigrate) {
DB::raw()->exec(file_get_contents(__DIR__ . '/../resources/schemas/migrations.sql'));
}
$installed = file_exists(__DIR__.'/../config.php');
$files = glob(__DIR__ . '/../resources/schemas/' . DB::driver() . '/*.sql');
return $view->render($response, 'install.twig', [
'installed' => $installed,
]);
})->setName('install');
$names = array_map(function ($path) {
return basename($path);
}, $files);
$app->post('/', function (Request $request, Response $response, \DI\Container $container, Session $session) use (&$config, &$installed) {
// disable debug in production
unset($config['debug']);
$in = str_repeat('?, ', count($names) - 1) . '?';
// Check if there is a previous installation, if not, setup the config file
if (!$installed) {
// 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');
$inMigrationsTable = DB::doQuery("SELECT * FROM `migrations` WHERE `name` IN ($in)", $names)->fetchAll();
// 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']['endpoint'] = !empty(param($request, 'storage_endpoint')) ? param($request, 'storage_endpoint') : null;
$config['storage']['bucket'] = param($request, 'storage_bucket');
$config['storage']['path'] = param($request, 'storage_path');
break;
case 'dropbox':
$config['storage']['token'] = param($request, 'storage_token');
break;
case 'ftp':
if (!extension_loaded('ftp')) {
$session->alert('The "ftp" extension is not loaded.', 'danger');
return redirect($response, urlFor('/'));
}
$config['storage']['host'] = param($request, 'storage_host');
$config['storage']['username'] = param($request, 'storage_username');
$config['storage']['password'] = param($request, 'storage_password');
$config['storage']['port'] = param($request, 'storage_port');
$config['storage']['path'] = param($request, 'storage_path');
$config['storage']['passive'] = param($request, 'storage_passive') === '1';
$config['storage']['ssl'] = param($request, 'storage_ssl') === '1';
break;
case 'google-cloud':
$config['storage']['project_id'] = param($request, 'storage_project_id');
$config['storage']['key_path'] = param($request, 'storage_key_path');
$config['storage']['bucket'] = param($request, 'storage_bucket');
break;
case 'azure':
$config['storage']['account_name'] = param($request, 'storage_account_name');
$config['storage']['account_key'] = param($request, 'storage_account_key');
$config['storage']['container_name'] = param($request, 'storage_container_name');
break;
case 'local':
default:
$config['storage']['path'] = param($request, 'storage_path');
break;
}
$container->set('config', value($config));
}
foreach ($files as $file) {
$storage = $container->get('storage');
// check if the storage is valid
$storageTestFile = 'storage_test.txt';
try {
try {
$success = $storage->write($storageTestFile, 'TEST_FILE');
} catch (FileExistsException $fileExistsException) {
$success = $storage->update($storageTestFile, 'TEST_FILE');
}
$continue = false;
$exists = false;
if (!$success) {
throw new Exception('The storage is not writable.');
}
$storage->readAndDelete($storageTestFile);
} catch (Exception $e) {
$session->alert("Storage setup error: {$e->getMessage()} [{$e->getCode()}]", 'danger');
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;
return redirect($response, urlFor('/install'));
}
$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;
}
}
}
// Get the db instance and run migrations
$db = $container->get('database');
try {
$migrator = new Migrator($db, __DIR__.'/../resources/schemas');
$migrator->migrate();
$migrator->reSyncQuotas($storage);
} catch (PDOException $e) {
$session->alert("Cannot connect to the database: {$e->getMessage()} [{$e->getCode()}]", 'danger');
$app = new App($container);
return redirect($response, urlFor('/install'));
}
$app->get('/', function (Request $request, Response $response) {
// if not installed, create the default admin account
if (!$installed) {
$db->query("INSERT INTO `users` (`email`, `username`, `password`, `is_admin`, `user_code`) VALUES (?, 'admin', ?, 1, ?)", [param($request, 'email'), password_hash(param($request, 'password'), PASSWORD_DEFAULT), humanRandomString(5)]);
}
if (!extension_loaded('gd')) {
$this->session->alert('The required "gd" extension is not loaded.', 'danger');
}
// re-apply the previous theme if is present
$css = $db->query('SELECT `value` FROM `settings` WHERE `key` = \'css\'')->fetch()->value ?? null;
if ($css && strpos($css, '|') !== false) {
$container->make(\App\Web\Theme::class)->applyTheme($css);
}
if (!extension_loaded('intl')) {
$this->session->alert('The required "intl" extension is not loaded.', 'danger');
}
// if is upgrading and existing installation, put it out maintenance
if ($installed) {
unset($config['maintenance']);
if (!extension_loaded('json')) {
$this->session->alert('The required "json" extension is not loaded.', 'danger');
}
// remove old config from old versions
unset($config['lang']);
unset($config['displayErrorDetails']);
}
if (!is_writable(__DIR__ . '/../resources/cache')) {
$this->session->alert('The cache folder is not writable (' . __DIR__ . '/../resources/cache' . ')', 'danger');
}
// 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');
if (!is_writable(__DIR__ . '/../resources/database')) {
$this->session->alert('The database folder is not writable (' . __DIR__ . '/../resources/database' . ')', 'danger');
}
return redirect($response, '/install');
}
if (!is_writable(__DIR__ . '/../resources/sessions')) {
$this->session->alert('The sessions folder is not writable (' . __DIR__ . '/../resources/sessions' . ')', 'danger');
}
// post install cleanup
cleanDirectory(__DIR__.'/../resources/cache');
cleanDirectory(__DIR__.'/../resources/sessions');
$installed = file_exists(__DIR__ . '/../config.php');
removeDirectory(__DIR__.'/../install');
// Installed successfully, destroy the installer session
$session->destroy();
return redirect($response, urlFor('/?afterInstall=true'));
return $this->view->render($response, 'install.twig', [
'installed' => $installed,
]);
});
$app->run();
$app->post('/', function (Request $request, Response $response) use (&$config) {
// Check if there is a previous installation, if not, setup the config file
$installed = true;
if (!file_exists(__DIR__ . '/../config.php')) {
$installed = false;
// config file setup
$config['base_url'] = $request->getParam('base_url');
$config['storage']['driver'] = $request->getParam('storage_driver');
unset($config['displayErrorDetails']);
$config['db']['connection'] = $request->getParam('connection');
$config['db']['dsn'] = $request->getParam('dsn');
$config['db']['username'] = $request->getParam('db_user');
$config['db']['password'] = $request->getParam('db_password');
// setup storage configuration
switch ($config['storage']['driver']) {
case 's3':
$config['storage']['key'] = $request->getParam('storage_key');
$config['storage']['secret'] = $request->getParam('storage_secret');
$config['storage']['region'] = $request->getParam('storage_region');
$config['storage']['bucket'] = $request->getParam('storage_bucket');
$config['storage']['path'] = $request->getParam('storage_path');
break;
case 'dropbox':
$config['storage']['token'] = $request->getParam('storage_token');
break;
case 'ftp':
$config['storage']['host'] = $request->getParam('storage_host');
$config['storage']['username'] = $request->getParam('storage_username');
$config['storage']['password'] = $request->getParam('storage_password');
$config['storage']['port'] = $request->getParam('storage_port');
$config['storage']['path'] = $request->getParam('storage_path');
$config['storage']['passive'] = $request->getParam('storage_passive') === '1';
$config['storage']['ssl'] = $request->getParam('storage_ssl') === '1';
break;
case 'google-cloud':
$config['storage']['project_id'] = $request->getParam('storage_project_id');
$config['storage']['key_path'] = $request->getParam('storage_key_path');
$config['storage']['bucket'] = $request->getParam('storage_bucket');
break;
case 'local':
default:
$config['storage']['path'] = $request->getParam('storage_path');
break;
}
// check if the storage is valid
$storageTestFile = 'storage_test.xbackbone.txt';
try {
try {
$success = $this->storage->write($storageTestFile, 'XBACKBONE_TEST_FILE');
} catch (FileExistsException $fileExistsException) {
$success = $this->storage->update($storageTestFile, 'XBACKBONE_TEST_FILE');
}
if (!$success) {
throw new Exception('The storage is not writable.');
}
$this->storage->readAndDelete($storageTestFile);
} catch (Exception $e) {
$this->session->alert("Storage setup error: {$e->getMessage()} [{$e->getCode()}]", 'danger');
return redirect($response, '/install');
}
$ret = file_put_contents(__DIR__ . '/../config.php', '<?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
// update the config
if ($installed && isset($config['storage_dir'])) {
$config['storage']['driver'] = 'local';
$config['storage']['path'] = $config['storage_dir'];
unset($config['storage_dir']);
}
// Build the dns string and run the migrations
try {
$dsn = $config['db']['connection'] === 'sqlite' ? __DIR__ . '/../' . $config['db']['dsn'] : $config['db']['dsn'];
DB::setDsn($config['db']['connection'] . ':' . $dsn, $config['db']['username'], $config['db']['password']);
migrate($config);
} catch (PDOException $e) {
$this->session->alert("Cannot connect to the database: {$e->getMessage()} [{$e->getCode()}]", 'danger');
return redirect($response, '/install');
}
// if not installed, create the default admin account
if (!$installed) {
DB::doQuery("INSERT INTO `users` (`email`, `username`, `password`, `is_admin`, `user_code`) VALUES (?, 'admin', ?, 1, ?)", [$request->getParam('email'), password_hash($request->getParam('password'), PASSWORD_DEFAULT), humanRandomString(5)]);
}
// post install cleanup
cleanDirectory(__DIR__ . '/../resources/cache');
cleanDirectory(__DIR__ . '/../resources/sessions');
removeDirectory(__DIR__ . '/../install');
// if is upgrading and existing installation, put it out maintenance
if ($installed) {
unset($config['maintenance']);
$ret = file_put_contents(__DIR__ . '/../config.php', '<?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 $response->withRedirect("{$config['base_url']}/?afterInstall=true");
});
$app->run();

View file

@ -6,25 +6,25 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="XBackBone Installer">
<link href="../static/bootstrap/css/bootstrap.min.css?{{ 'now'|date('U') }}" rel="stylesheet">
<link href="../static/fontawesome/css/all.min.css?{{ 'now'|date('U') }}" rel="stylesheet">
<link href="../static/app/app.css?{{ 'now'|date('U') }}" rel="stylesheet">
<link href="../static/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<link href="../static/fontawesome/css/all.min.css" rel="stylesheet">
<link href="../static/app/app.css" rel="stylesheet">
<script src="../static/jquery/jquery.min.js?{{ 'now'|date('U') }}"></script>
<script src="../static/bootstrap/js/bootstrap.bundle.min.js?{{ 'now'|date('U') }}"></script>
<script src="installer.js?{{ 'now'|date('U') }}"></script>
<script src="../static/jquery/jquery.min.js"></script>
<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' %}
</div>
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card mt-3 shadow-sm">
<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>
@ -71,9 +71,8 @@
<select id="storage_driver" name="storage_driver" class="form-control" required>
<option value="local">Local Storage</option>
<option value="ftp">FTP/FTPS</option>
<option value="s3">Amazon S3 (or compatible)</option>
<option value="s3">Amazon AWS S3</option>
<option value="google-cloud">Google Cloud Storage</option>
<option value="azure">Azure Blob Storage</option>
<option value="dropbox">Dropbox</option>
</select>
</div>
@ -102,12 +101,6 @@
<input type="text" class="form-control hook-storage-input" id="storage_region" name="storage_region" placeholder="your-region" autocomplete="off">
</div>
</div>
<div class="form-group row hook-storage">
<label for="storage_region" class="col-sm-3 col-form-label">AWS S3 endpoint</label>
<div class="col-sm-9">
<input type="text" class="form-control hook-storage-input" id="storage_endpoint" name="storage_endpoint" placeholder="optional (for S3-compatible services)" autocomplete="off">
</div>
</div>
<div class="form-group row hook-storage">
<label for="storage_token" class="col-sm-3 col-form-label">Dropbox token</label>
<div class="col-sm-9">
@ -168,24 +161,6 @@
<input type="text" class="form-control hook-storage-input" id="storage_key_path" name="storage_key_path" placeholder="/path/to/service-account.json" autocomplete="off">
</div>
</div>
<div class="form-group row hook-storage">
<label for="storage_account_name" class="col-sm-3 col-form-label">Azure storage account name</label>
<div class="col-sm-9">
<input type="text" class="form-control hook-storage-input" id="storage_account_name" name="storage_account_name" placeholder="your-storage-account-name" autocomplete="off">
</div>
</div>
<div class="form-group row hook-storage">
<label for="storage_account_key" class="col-sm-3 col-form-label">Azure storage account key</label>
<div class="col-sm-9">
<input type="text" class="form-control hook-storage-input" id="storage_account_key" name="storage_account_key" placeholder="Account (Access) Key" autocomplete="off">
</div>
</div>
<div class="form-group row hook-storage">
<label for="storage_container_name" class="col-sm-3 col-form-label">Azure storage container name</label>
<div class="col-sm-9">
<input type="text" class="form-control hook-storage-input" id="storage_container_name" name="storage_container_name" placeholder="your-blob-container-name" autocomplete="off">
</div>
</div>
<div class="form-group row hook-storage">
<label for="storage_bucket" class="col-sm-3 col-form-label">Storage bucket</label>
<div class="col-sm-9">
@ -209,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>
@ -218,8 +193,8 @@
{% 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">
<i class="fas fa-sync fa-fw"></i> Finalize update
<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>
</div>
@ -230,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">

View file

@ -28,10 +28,6 @@ location /logs {
return 403;
}
location CHANGELOG.md {
return 403;
}
index index.html index.htm index.php;
charset utf-8;

5304
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,26 +1,23 @@
{
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.2",
"bootstrap": "^4.6.1",
"bootstrap4-toggle": "^3.6.1",
"clipboard": "^2.0.10",
"dropzone": "^5.9.3",
"highlightjs": "^9.16.2",
"highlightjs-line-numbers.js": "^2.8.0",
"jquery": "^3.6.0",
"plyr": "^3.6.12",
"popper.js": "^1.16.1",
"tooltip.js": "^1.3.3"
"@fortawesome/fontawesome-free": "^5.10.2",
"bootstrap": "^4.3.1",
"clipboard": "^2.0.4",
"highlightjs": "^9.12.0",
"jquery": "^3.4.1",
"popper.js": "^1.15.0",
"tooltip.js": "^1.3.2",
"video.js": "^7.6.0"
},
"devDependencies": {
"grunt": "^1.5.3",
"grunt-contrib-compress": "^2.0.0",
"grunt": "^1.0.4",
"grunt-contrib-copy": "^1.0.0",
"grunt-contrib-cssmin": "^3.0.0",
"grunt-contrib-jshint": "^3.1.1",
"grunt-contrib-jshint": "^2.1.0",
"grunt-contrib-uglify": "^4.0.1",
"grunt-contrib-watch": "^1.1.0",
"grunt-shell": "^3.0.1",
"grunt-zip": "^0.18.2",
"load-grunt-tasks": "^4.0.0"
}
}

View file

@ -1,33 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="tests/bootstrap.php"
colors="true"
backupGlobals="false"
backupStaticAttributes="false"
stopOnFailure="false"
cacheResult="false"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true">
<testsuites>
<testsuite name="Feature">
<directory suffix="Test.php">tests/Feature</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">app</directory>
<exclude>
<file>app/routes.php</file>
</exclude>
</whitelist>
</filter>
<logging>
<log type="coverage-html" target="./coverage"
lowUpperBound="70"
highLowerBound="90"/>
</logging>
<php>
<server name="HTTP_HOST" value="localhost"/>
<server name="HTTPS" value="false"/>
</php>
</phpunit>

View file

@ -1,165 +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_account' => '%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 - إنشاء حساب جديد',
'php_info' => 'معلومات الPHP',
'enforce_language' => 'فرض اللغة',
'image_embeds' => 'تضمين الصور',
'token_not_found' => 'لم يتم العثور على الرمز المميز المحدد.',
'clean_orphaned_uploads' => 'تنظيف التحميلات المعزولة',
'show_all_tags' => 'إظهار جميع العلامات',
'cannot_demote' => 'لا يمكنك تخفيض رتبتك.',
'recaptcha_secret_key' => 'المفتاح السري reCAPTCHA',
'recaptcha_failed' => 'فشل reCAPTCHA',
'recaptcha_enabled' => 'تم تفعيل reCAPTCHA',
'default_user_quota' => 'الحصة الافتراضية للمستخدم',
'invalid_quota' => 'قيم غير صالحة كحصة مستخدم افتراضية.',
'copy_url_behavior' => 'نسخ وضع الرابط',
'token' => 'الرمز المميز',
'vanity_url' => 'رابط مخصص',
'orphaned_files' => 'الملفات المعزولة',
'copied' => 'تم النسخ إلى الحافظة!',
'client_config' => 'إعدادات العميل',
'deleted_orphans' => 'تم حذف %d من الملفات المعزولة بنجاح.',
'maintenance_in_progress' => 'المنصة تحت الصيانة، حاول مرة أخرى لاحقاً…',
'default_lang_behavior' => 'سيحاول XBackBone مطابقة لغة المتصفح افتراضيا (الاحتياطي هو الإنجليزية).',
'no_upload_token' => 'ليس لديك رمز مميز شخصي للتحميل. (قم بإنشاء واحد وحاول مرة أخرى.)',
'donate_text' => 'إذا كنت تحب XBackBone، ففكر في التبرع لدعم التطوير!',
'custom_head_html' => 'محتوى رأس HTML مخصص',
'custom_head_html_hint' => 'ستتم إضافة هذا المحتوى عند علامة <head> في كل صفحة.',
'custom_head_set' => 'تم تطبيق رأس HTML المخصص.',
'max_user_quota' => 'الحد الأقصى لحصة المستخدم',
'quota_enabled' => 'تفعيل حصة المستخدم',
'recalculate_user_quota' => 'إعادة حساب حصة المستخدم من القرص',
'mail.activate_text' => 'مرحبًا %s!<br>شكرًا لك على إنشاء حسابك على %s (<a href="%s">%s</a>)، انقر على الرابط التالي لتفعيله:<br><br><a href="%s">%s</a>',
'quota_recalculated' => 'تمت إعادة حساب حصة المستخدم من القرص بنجاح.',
'only_recaptcha_v3' => 'يتم دعم reCAPTCHA v3 فقط.',
'recaptcha_site_key' => 'مفتاح موقع reCAPTCHA',
'ldap_cant_connect' => 'لا يمكن الاتصال بخادم مصادقة LDAP.',
'upload_max_file_size' => 'الحد الأقصى لحجم الملف حاليًا هو %s.',
'zip_ext_not_loaded' => 'لم يتم تحميل الامتداد "zip" المطلوب',
'no_tags' => 'لم تتم إضافة أي علامات',
'changelog' => 'سجل التغيير',
'show_changelog' => 'عرض سجل التغيير',
'auto_tagging' => 'تحميل العلامات تلقائيًا',
'recaptcha_keys_required' => 'جميع مفاتيح reCAPTCHA مطلوبة.',
'user_create_password' => 'إذا تركت فارغة، فقد تحتاج إلى إرسال إعلام إلى عنوان البريد الإلكتروني للمستخدم.',
'mail.recover_text' => 'مرحبًا %s،<br>لقد تم طلب إعادة تعيين كلمة المرور لحسابك. لإكمال الإجراء انقر على الرابط التالي:<br><br><a href="%s">%s</a><br><br>إذا لم تكن أنت من طلب إعادة تعيين كلمة المرور، فما عليك سوى تجاهل هذا البريد الإلكتروني.',
'mail.new_account_text_with_reset' => 'مرحبًا %s!<br>تم إنشاء حساب جديد لك على %s (<a href="%s">%s</a>)، انقر على الرابط التالي لتعيين كلمة مرور وتنشيطها:<br ><br><a href="%s">%s</a>',
'mail.new_account_text_with_pw' => 'مرحبًا %s!<br>تم إنشاء حساب جديد لك على %s (<a href="%s">%s</a>)، باستخدام بيانات الاعتماد التالية:<br><br>اسم المستخدم: %s <br>كلمة المرور: %s<br><br>انقر على الرابط التالي للانتقال إلى صفحة تسجيل الدخول:<br><a href="%s">%s</a>',
];

View file

@ -6,7 +6,7 @@ return [
'no' => 'Не',
'send' => 'Изпрати',
'no_media' => 'Не бяха намерени файлове.',
'login.username' => 'Потребителско име или е-поща',
'login.username' => 'Потребителско име или E-Mail',
'password' => 'Парола',
'login' => 'Влезте',
'username' => 'Потребителско име',
@ -19,7 +19,7 @@ return [
'pager.previous' => 'Предишно',
'copy_link' => 'Копирай линк',
'public.telegram' => 'Сподели по Telegram',
'public.delete_text' => 'Наистина ли искате да изтриете този елемент? Няма да можете да го възстановите',
'public.delete_text' => 'Сигурни ли сте, че искате да изтриете това? Ще изчезне завинаги!',
'preview' => 'Предварителен преглед',
'filename' => 'Файлово име',
'size' => 'Размер',
@ -34,7 +34,7 @@ return [
'files' => 'Файлове',
'orphaned_files' => 'Загубени файлове',
'theme' => 'Тема',
'click_to_load' => 'Щракнете за зареждане…',
'click_to_load' => 'Натиснете за да заредите...',
'apply' => 'Приложи',
'save' => 'Запомни',
'used' => 'Използвано',
@ -58,107 +58,15 @@ return [
'installed' => 'Инсталацията завърши успешно!',
'welcome' => 'Добре дошли, %s!',
'goodbye' => 'Довиждане!',
'email_required' => зисква се имейл адрес.',
'email_taken' => 'Имейл адресът вече се използва.',
'email_required' => мейлът е необходим.',
'email_taken' => 'Имейлът вече се използва.',
'username_required' => 'Потребителското име е необходимо.',
'username_taken' => 'Потребителското име вече е заето.',
'password_required' => 'Паролата е задължителна.',
'user_created' => 'Потребител „%s“ бе създаден!',
'user_updated' => 'Потребител „%s“ бе обновен!',
'password_required' => 'Паролата е необходима.',
'user_created' => 'Потребител "%s" бе създаден!',
'user_updated' => 'Потребител "%s" бе обновен!',
'profile_updated' => 'Профилът бе обновен успешно!',
'user_deleted' => 'Потребителят бе изтрит.',
'cannot_delete' => 'Не можете да изтриете себе си.',
'gallery' => 'Галерия',
'image_embeds' => 'Вграждане на изображения',
'bad_login' => 'Грешни идентификационни данни.',
'account_disabled' => 'Вашият акаунт е деактивиран.',
'copied' => 'Копирано в клипборда!',
'none' => 'Нито един',
'open' => 'Отвори',
'token_not_found' => 'Посоченият токен не е намерен.',
'cannot_demote' => 'Не можете да се понижавате.',
'cannot_write_file' => 'Пътят на дестинацията не е за запис.',
'deleted_orphans' => 'Успешно изтрити %d осиротели файла.',
'switch_to' => 'Преминат към',
'table' => 'Таблица',
'time' => 'Време',
'name' => 'Име',
'maintenance' => 'Поддръжка',
'clean_orphaned_uploads' => 'Изчистване на остарели качвания',
'path_not_writable' => 'Изходният път не е за запис.',
'already_latest_version' => 'Вече имате най-новата версия.',
'new_version_available' => 'Налична е нова версия %s!',
'cannot_retrieve_file' => 'Файлът не може да бъде извлечен.',
'upgrade' => 'Подобряване',
'updates' => 'Надстройки',
'auto_set' => 'Задайте автоматично',
'default_lang_behavior' => 'XBackBone ще се опита да съвпадне с езика на браузъра по подразбиране (резервният вариант е английски).',
'donation' => 'Дарение',
'donate_text' => 'Ако харесвате XBackBone, помислете за дарение в подкрепа на развитието!',
'custom_head_html' => 'Персонализирано съдържание на HTML Head',
'custom_head_html_hint' => 'Това съдържание ще бъде добавено към маркера <head> на всяка страница.',
'custom_head_set' => 'Приложена е персонализирана HTML Head.',
'remember_me' => 'Помни ме',
'please_wait' => 'Моля изчакай…',
'dont_close' => 'Не затваряйте този раздел до приключване.',
'copy_url_behavior' => 'Режим на копиране на URL',
'dotted_search' => 'Търсене…',
'order_by' => 'Подредени по…',
'file_size_no_match' => 'Изтегленият файл не съответства на правилния размер на файла.',
'check_for_updates' => 'Провери за актуализации',
'no_account' => 'Нямате акаунт?',
'default_user_quota' => 'Потребителска квота по подразбиране',
'register' => 'Регистрация',
'max_user_quota' => 'Максимална потребителска квота',
'invalid_quota' => 'Невалидни стойности като потребителска квота по подразбиране.',
'account_activated' => 'Акаунтът е активиран, сега можете да влезете!',
'quota_enabled' => 'Активиране на потребителска квота',
'password_restored' => 'Нулиране на паролата.',
'quota_recalculated' => 'Потребителската квота е преизчислена от диска успешно.',
'delete_selected' => 'Изтрий избраното',
'delete_all' => 'Изтриване на всички',
'account_media_deleted' => 'Всички медии в акаунта са изтрити.',
'danger_zone' => 'Опасна зона',
'recaptcha_failed' => 'reCAPTCHA неуспешно',
'recaptcha_enabled' => 'reCAPTCHA е активирана',
'recaptcha_keys_required' => 'Всички ключове reCAPTCHA са задължителни.',
'recaptcha_site_key' => 'Ключ на сайта reCAPTCHA',
'send_notification' => 'Изпращане на известие по имейл',
'mail.new_account' => '%s - Създаване на нов акаунт',
'user_create_password' => 'Ако се остави празно, може да искате да изпратите известие до имейл адреса на потребителя.',
'ldap_cant_connect' => 'Не може да се свърже към LDAP сървъра за удостоверяване.',
'upload_max_file_size' => 'Максималният размер на файла в момента е %s.',
'no_tags' => 'Няма добавени тагове',
'auto_tagging' => 'Маркиране за автоматично качване',
'zip_ext_not_loaded' => 'Необходимото разширение "zip" не е заредено',
'changelog' => 'Регистър на промените',
'show_changelog' => 'Показване на регистъра на промените',
'register_success' => 'Акаунтът е създаден, изпратен е имейл за потвърждение.',
'recover_email_sent' => 'Ако е налице, имейл за възстановяване е изпратен до посочения акаунт.',
'mail.recover_password' => '%s - Възстановяване на парола',
'php_info' => 'PHP информация',
'system_settings' => 'Системни настройки',
'upload' => 'Качване',
'prerelease_channel' => 'Предварителен канал',
'settings_saved' => 'Системните настройки са запазени!',
'export_data' => 'Експортиране на данни',
'cancel' => 'Отмяна',
'no_upload_token' => 'Нямате личен токен за качване. (Генерирайте един и опитайте отново.)',
'maintenance_in_progress' => 'Платформата е в процес на поддръжка, опитайте отново по-късно…',
'drop_to_upload' => 'Щракнете или пуснете вашите файлове тук, за да ги качите.',
'register_enabled' => 'Регистрациите са активирани',
'hide_by_default' => 'Скриване на медиите по подразбиране',
'password_recovery' => 'Възстановяване на парола',
'mail.activate_text' => 'Здравейте %s!<br>благодаря ви, че създадохте своя акаунт в %s (<a href="%s">%s</a>), щракнете върху следната връзка, за да го активирате:<br><br>< a href="%s">%s</a>',
'mail.activate_account' => '%s - Активиране на акаунт',
'mail.recover_text' => 'Здравейте %s,<br>за вашия акаунт бе поискано нулиране на паролата. За да завършите процедурата, щракнете върху следната връзка:<br><br><a href="%s">%s</a><br><br>Ако не вие сте поискали нулирането на паролата, просто игнорирайте този имейл.',
'password_match' => 'Паролата и повторната парола трябва да са еднакви.',
'recalculate_user_quota' => 'Преизчисляване на потребителската квота от диска',
'password_repeat' => 'Повтори паролата',
'recaptcha_secret_key' => 'Секретен ключ на reCAPTCHA',
'mail.new_account_text_with_reset' => 'Здравейте %s!<br>за вас беше създаден нов акаунт на %s (<a href="%s">%s</a>), щракнете върху следната връзка, за да зададете парола и да я активирате:<br ><br><a href="%s">%s</a>',
'used_space' => 'Използвано пространство',
'clear_account' => 'Изчистване на акаунта',
'only_recaptcha_v3' => 'Поддържа се само reCAPTCHA v3.',
'mail.new_account_text_with_pw' => 'Здравейте %s!<br>за вас бе създаден нов акаунт на %s (<a href="%s">%s</a>) със следните идентификационни данни:<br><br>Потребителско име: %s <br>Парола: %s<br><br>Щракнете върху следната връзка, за да отидете на страницата за вход:<br><a href="%s">%s</a>',
];

View file

@ -1,163 +0,0 @@
<?php
return [
'no_media' => 'Sense medis.',
'raw' => 'Mostra raw',
'lang' => 'Anglès',
'enforce_language' => 'Aplicar idioma',
'no' => 'No',
'send' => 'Envia',
'login.username' => 'Nom d\'usuari o Email',
'password' => 'Contrasenya',
'login' => 'Registre',
'username' => 'Nom Usuari',
'home' => 'Casa',
'copy_link' => 'Copiar Link',
'public.telegram' => 'Compartir a Telegram',
'public.delete_text' => 'Segur que vols esborrar l\'element? No el podràs recuperar',
'preview' => 'Vista prèvia',
'filename' => 'Nom Arxiu',
'size' => 'Mida',
'public' => 'Public',
'owner' => 'Propietari',
'date' => 'Data',
'upload' => 'Puja',
'delete' => 'Esborra',
'hide' => 'Oculta',
'files' => 'Arxius',
'orphaned_files' => 'Arxius orfes',
'theme' => 'Tema',
'click_to_load' => 'Click per pujar…',
'apply' => 'Aplicar',
'save' => 'Guardar',
'used' => 'Usat',
'php_info' => 'Informació PHP',
'system_settings' => 'Opcions de Sistema',
'user.create' => 'Crear Usuari',
'user.edit' => 'Editar Usuari',
'is_active' => 'Està Actiu',
'is_admin' => 'és administrador',
'your_profile' => 'el teu perfil',
'token' => 'Token',
'copy' => 'Copiar',
'copied' => 'Copiat al portaretalls!',
'client_config' => 'Configuració Client',
'user_code' => 'Codi Usuari',
'active' => 'Actiu',
'reg_date' => 'Data Registre',
'none' => 'Cap',
'open' => 'Obert',
'confirm' => 'Confirmació',
'confirm_string' => 'Estàs segur?',
'installed' => 'Instal·lació satisfactòria!',
'bad_login' => 'Credencials Errònies.',
'account_disabled' => 'Compte Deshabilitat.',
'goodbye' => 'Adéu!',
'token_not_found' => 'Token no trobat.',
'email_taken' => 'Compte de correu en ús.',
'username_required' => 'Nom usuari Requerit.',
'username_taken' => 'Nom d\'usuari ja en ús.',
'password_required' => 'Contrasenya requerida.',
'user_created' => 'Usuari "%s" creat!',
'user_updated' => 'Usuari "%s" actualitzat!',
'profile_updated' => 'Perfil Actualitzat!',
'user_deleted' => 'Usuari esborrat.',
'cannot_delete' => 'No pots esborrar.-te.',
'cannot_demote' => 'No et pots degradar.',
'cannot_write_file' => 'No es pot escriure al path.',
'deleted_orphans' => '%d Fitxers orfes esborrats.',
'dotted_search' => 'Cerca…',
'order_by' => 'Endreça per.…',
'time' => 'Temps',
'name' => 'Nom',
'maintenance' => 'Manteniment',
'clean_orphaned_uploads' => 'Neteja càrregues orfes',
'path_not_writable' => 'No es pot escriure al path de sortida.',
'already_latest_version' => 'Ja estàs a la darrera versió.',
'new_version_available' => 'Nova versió %s disponible!',
'cannot_retrieve_file' => 'No es pot recuperar el fitxer.',
'file_size_no_match' => 'El fitxer baixat no coincideix amb la mida correcta del fitxer.',
'upgrade' => 'Actualitza',
'maintenance_in_progress' => 'En manteniment, torna-ho a provar més tard…',
'cancel' => 'Cancel·la',
'auto_set' => 'Estableix automàticament',
'default_lang_behavior' => 'XBackBone intentarà fer coincidir l\'idioma del navegador de manera predeterminada (la alternativa és l\'anglès).',
'prerelease_channel' => 'Canal de preestrena',
'drop_to_upload' => 'Feu clic o deixeu anar els vostres fitxers aquí per carregar-los.',
'donation' => 'Donatiu',
'custom_head_html' => 'Contingut de capçalera HTML personalitzat',
'custom_head_set' => 'Capçalera HTML personalitzada aplicada.',
'remember_me' => 'Recorda\'m',
'please_wait' => 'Esperi…',
'dont_close' => 'No tanquis la pestanya.',
'register_enabled' => 'Registres habilitats',
'hide_by_default' => 'Amaga medis per defecte',
'copy_url_behavior' => 'Mode copia URL',
'password_recovery' => 'Recupera password',
'no_account' => 'No tens compte?',
'register' => 'Registre',
'register_success' => 'S\'ha creat el compte, s\'ha enviat un correu electrònic de confirmació.',
'default_user_quota' => 'Quota d\'usuari per defecte',
'max_user_quota' => 'Quota màxima d\'usuari',
'invalid_quota' => 'Valors no vàlids com a quota d\'usuari predeterminada.',
'mail.activate_account' => '%s - Activació del compte',
'mail.recover_password' => '%s - Recuperació de la contrasenya',
'recover_email_sent' => 'Si existeix, s\'enviarà un correu electrònic de recuperació al compte especificat.',
'account_activated' => 'Compte activat, ara pots iniciar sessió!',
'quota_enabled' => 'Activa la quota d\'usuari',
'password_repeat' => 'Repeteix Contrasenya',
'password_match' => 'Les contrasenyes han de coincidir.',
'password_restored' => 'Contrasenya restablerta.',
'recalculate_user_quota' => 'Torneu a calcular la quota d\'usuari del disc',
'quota_recalculated' => 'La quota d\'usuari s\'ha recalculat des del disc correctament.',
'used_space' => 'Espai Utilitzat',
'delete_selected' => 'Esborra seleccionat',
'delete_all' => 'Esborra Tot',
'clear_account' => 'Esborra el compte',
'account_media_deleted' => 'S\'han suprimit tots els mitjans del compte.',
'danger_zone' => 'Zona Perillosa',
'recaptcha_failed' => 'reCAPTCHA Erroni',
'recaptcha_keys_required' => 'Calen totes les claus reCAPTCHA.',
'only_recaptcha_v3' => 'Només s\'admet reCAPTCHA v3.',
'recaptcha_site_key' => 'Clau del lloc reCAPTCHA',
'recaptcha_secret_key' => 'Clau secreta reCAPTCHA',
'send_notification' => 'Envia notificació per e-mail',
'mail.new_account' => '%s - Creació nou Compte',
'users' => 'Usuaris',
'system' => 'Sistema',
'mail.new_account_text_with_pw' => 'Hola, %s!<br>s\'ha creat un compte nou a %s (<a href="%s">%s</a>), amb les credencials següents:<br><br>Nom d\'usuari: %s <br>Contrasenya: %s<br><br>Feu clic a l\'enllaç següent per anar a la pàgina d\'inici de sessió:<br><a href="%s">%s</a>',
'user_create_password' => 'Si es deixa buit, és possible que vulgueu enviar una notificació a l\'adreça de correu electrònic de l\'usuari.',
'ldap_cant_connect' => 'No es pot connectar al servidor d\'autenticació LDAP.',
'upload_max_file_size' => 'La mida màxima del fitxer és actualment %s.',
'no_tags' => 'Sense Etiquetes',
'auto_tagging' => 'Etiquetatge de càrrega automàtica',
'zip_ext_not_loaded' => 'L\'extensió "zip" necessària no està carregada',
'changelog' => 'Codi de canvis',
'show_changelog' => 'Mostra canvis',
'image_embeds' => 'Insereix imatges',
'yes' => 'Sí',
'profile' => 'Perfil',
'pager.previous' => 'Anterior',
'logout' => 'Tancar Sessió',
'pager.next' => 'Següent',
'publish' => 'Publica',
'download' => 'Descarrega',
'update' => 'Actualitza',
'admin' => 'Admin',
'edit' => 'Edita',
'welcome' => 'Benvolgut, %s!',
'email_required' => 'Adreça email requerida.',
'gallery' => 'Galeria',
'switch_to' => 'Canvia a',
'no_upload_token' => 'No tens cap testimoni de càrrega personal. (Genereu-ne un i torneu-ho a provar.)',
'table' => 'Taula',
'check_for_updates' => 'Comprova actualitzacions',
'updates' => 'Actualitzacions',
'donate_text' => 'Si t\'agrada XBackBone, considera una donació per donar suport al desenvolupament!',
'settings_saved' => 'Opcions de sistema gravades!',
'export_data' => 'Exporta dades',
'custom_head_html_hint' => 'Aquest contingut s\'afegirà a l\'etiqueta <head> de cada pàgina.',
'mail.activate_text' => 'Hola, %s!<br>gràcies per crear el vostre compte a %s (<a href="%s">%s</a>), feu clic a l\'enllaç següent per activar-lo:<br><br>< a href="%s">%s</a>',
'mail.recover_text' => 'Hola %s,<br>s\'ha sol·licitat un restabliment de la contrasenya per al vostre compte. Per completar el procediment, feu clic a l\'enllaç següent:<br><br><a href="%s">%s</a><br><br>Si no heu estat vosaltres qui heu sol·licitat la restabliment de la contrasenya, simplement ignora aquest correu electrònic.',
'recaptcha_enabled' => 'reCAPTCHA Habilitat',
'mail.new_account_text_with_reset' => 'Hola, %s!<br>s\'ha creat un compte nou a %s (<a href="%s">%s</a>), feu clic a l\'enllaç següent per establir una contrasenya i activar-la:<br ><br><a href="%s">%s</a>',
];

View file

@ -1,163 +0,0 @@
<?php
return [
'prerelease_channel' => 'Kanál předběžného vydání',
'default_lang_behavior' => 'XBackBone se pokusí ve výchozím nastavení použít jazyk prohlížeče (záloha je angličtina).',
'auto_set' => 'Nastavit automaticky',
'cancel' => 'Zrušit',
'maintenance_in_progress' => 'Platforma je v údržbě, zkuste to znovu později…',
'updates' => 'Aktualizace',
'upgrade' => 'Aktualizovat',
'check_for_updates' => 'Zkontrolovat aktualizace',
'file_size_no_match' => 'Stažený soubor nemá správnou velikost.',
'cannot_retrieve_file' => 'Nemohu získat soubor.',
'new_version_available' => 'Je dostupná nová verze %s!',
'already_latest_version' => 'Již máte nejnovější verzi.',
'path_not_writable' => 'Výstupová cesta není zapisovatelná.',
'clean_orphaned_uploads' => 'Vymazat samostatné nahrávky',
'maintenance' => 'Údržba',
'name' => 'Jméno',
'time' => 'Čas',
'order_by' => 'Řadit podle…',
'dotted_search' => 'Hledat…',
'table' => 'Tabulka',
'gallery' => 'Galerie',
'switch_to' => 'Přepnout na',
'deleted_orphans' => 'Úspěšně odstraněno %d samostatných souborů.',
'cannot_write_file' => 'Cílová cesta není zapisovatelná.',
'cannot_demote' => 'Nemůžete sami sobě snížit role.',
'cannot_delete' => 'Nemůžete odstranit sami sebe.',
'user_deleted' => 'Uživatel odstraněn.',
'profile_updated' => 'Profil úspěšně aktualizován!',
'user_updated' => 'Uživatel "%s" aktualizován!',
'user_created' => 'Uživatel "%s" vytvořen!',
'password_required' => 'Je vyžadováno heslo.',
'username_taken' => 'Uživatelské jméno je již zabrané.',
'username_required' => 'Je vyžadováno uživatelské jméno.',
'email_taken' => 'Daná e-mailová adresa se již používá.',
'email_required' => 'Je vyžadována e-mailová adresa.',
'token_not_found' => 'Zadaný token nenalezen.',
'goodbye' => 'Mějte se!',
'welcome' => 'Vítejte, %s!',
'account_disabled' => 'Váš účet je pozastaven.',
'bad_login' => 'Špatné údaje.',
'installed' => 'Instalace úspěšně dokončena!',
'confirm_string' => 'Jste si jisti?',
'confirm' => 'Potvrzení',
'open' => 'Otevřený',
'none' => 'Žádný',
'reg_date' => 'Datum registrace',
'admin' => 'Admin',
'active' => 'Aktivní',
'user_code' => 'Uživatelský kód',
'client_config' => 'Konfigurace klienta',
'edit' => 'Upravit',
'update' => 'Aktualizovat',
'copied' => 'Zkopírováno do schránky!',
'copy' => 'Zkopírovat',
'token' => 'Token',
'your_profile' => 'Váš profil',
'is_admin' => 'Je administrátor',
'is_active' => 'Je aktivní',
'user.edit' => 'Upravit uživatele',
'user.create' => 'Vytvořit uživatele',
'system_settings' => 'Systémová nastavení',
'php_info' => 'Informace o PHP',
'used' => 'Použito',
'save' => 'Uložit',
'apply' => 'Použít',
'click_to_load' => 'Klikněte pro načtení…',
'theme' => 'Téma',
'orphaned_files' => 'Samotné soubory',
'files' => 'Soubory',
'hide' => 'Skrýt',
'publish' => 'Zveřejnit',
'delete' => 'Odstranit',
'upload' => 'Nahrát',
'download' => 'Stáhnout',
'raw' => 'Zobrazit základní',
'date' => 'Datum',
'owner' => 'Majitel',
'public' => 'Veřejné',
'size' => 'Velikost',
'filename' => 'Název souboru',
'preview' => 'Náhled',
'public.delete_text' => 'Opravdu chcete odstranit tuto položku? Nebudete ji moci později obnovit',
'public.telegram' => 'Sdílet na Telegramu',
'copy_link' => 'Zkopírovat odkaz',
'pager.previous' => 'Předchozí',
'pager.next' => 'Další',
'logout' => 'Odhlásit se',
'profile' => 'Profil',
'system' => 'Systém',
'users' => 'Uživatelé',
'home' => 'Domů',
'username' => 'Uživatelské jméno',
'login' => 'Přihlásit se',
'password' => 'Heslo',
'login.username' => 'Uživatelské jméno nebo e-mail',
'no_media' => 'Nenalezena žádná média.',
'send' => 'Poslat',
'no' => 'Ne',
'yes' => 'Ano',
'enforce_language' => 'Vynutit jazyk',
'lang' => 'Čeština',
'show_changelog' => 'Zobrazit seznam změn',
'changelog' => 'Seznam změn',
'zip_ext_not_loaded' => 'Vyžadované "zip" rozšíření není načteno',
'auto_tagging' => 'Automatické štítkování nahrání',
'no_tags' => 'Nebyly přidány žádné štítky',
'upload_max_file_size' => 'Maximální velikost souboru je momentálně %s.',
'ldap_cant_connect' => 'Nemohu se připojit k ověřovacímu serveru LDAP.',
'user_create_password' => 'Pokud bude ponecháno prázdné, budete možná chtít poslat oznámení na uživatelovu e-mailovou adresu.',
'mail.new_account_text_with_pw' => 'Zdravíme, %s!<br>Byl pro vás vytvořen nový účet na %s (<a href="%s">%s</a>) s následujícími údaji:<br><br>Uživatelské jméno: %s<br>Heslo: %s<br><br>Klikněte na následující odkaz pro vstup na přihlašovací stránku:<br><a href="%s">%s</a>',
'mail.new_account_text_with_reset' => 'Zdravíme, %s!<br>Byl pro vás vytvořen nový účet na %s (<a href="%s">%s</a>). Klikněte na následující odkaz pro nastavení hesla a aktivaci účtu:<br><br><a href="%s">%s</a>',
'mail.new_account' => '%s - tvorba nového účtu',
'send_notification' => 'Poslat e-mailové oznámení',
'recaptcha_secret_key' => 'reCAPTCHA Tajný klíč',
'recaptcha_site_key' => 'reCAPTCHA Webový Klíč',
'only_recaptcha_v3' => 'Je podporována pouze reCAPTCHA v3.',
'recaptcha_keys_required' => 'Jsou vyžadovány všechny klíče reCAPTCHA.',
'recaptcha_enabled' => 'reCAPTCHA povolena',
'recaptcha_failed' => 'reCAPTCHA selhala',
'danger_zone' => 'Nebezpečná oblast',
'account_media_deleted' => 'Všechna média na účtu byla odstraněna.',
'clear_account' => 'Promazat účet',
'delete_all' => 'Odstranit vše',
'delete_selected' => 'Odstranit vybrané',
'used_space' => 'Využitý prostor',
'quota_recalculated' => 'Uživatelská kvóta úspěšně přepočítána z disku.',
'recalculate_user_quota' => 'Přepočítat uživatelskou kvótu z disku',
'password_restored' => 'Heslo obnoveno.',
'password_match' => 'Pole Heslo a Heslo znovu se musí shodovat.',
'password_repeat' => 'Heslo znovu',
'quota_enabled' => 'Povolit uživatelskou kvótu',
'account_activated' => 'Účet aktivován, nyní se můžete přihlásit!',
'recover_email_sent' => 'Pokud existuje, byl odeslán obnovovací e-mail na zadaný účet.',
'mail.recover_password' => '%s - obnova hesla',
'mail.recover_text' => 'Zdravíme, %s,<br>u vašeho účtu bylo zažádáno obnovení hesla. Pro dokončení akce klikněte na následující odkaz:<br><br><a href="%s">%s</a><br><br>Pokud jste o obnovení hesla nezažádali vy, jednoduše ignorujte tento e-mail.',
'mail.activate_account' => '%s - aktivace účtu',
'mail.activate_text' => 'Zdravíme, %s!<br>Děkujeme za vytvoření účtu na %s (<a href="%s">%s</a>), klikněte na následující odkaz pro jeho aktivaci:<br><br><a href="%s">%s</a>',
'invalid_quota' => 'Neplatné hodnoty jako výchozí uživatelská kvóta.',
'max_user_quota' => 'Maximální uživatelská kvóta',
'default_user_quota' => 'Výchozí uživatelská kvóta',
'register_success' => 'Účet byl vytvořen a potvrzovací e-mail odeslán.',
'register' => 'Zaregistrovat se',
'no_account' => 'Nemáte účet?',
'password_recovery' => 'Obnovit heslo',
'export_data' => 'Exportovat data',
'settings_saved' => 'Nastavení systému uložena!',
'copy_url_behavior' => 'Režim kopírování URL',
'hide_by_default' => 'Skrýt média ve výchozím nastavení',
'register_enabled' => 'Registrace povoleny',
'dont_close' => 'Nezavírejte tuto kartu před dokončením.',
'please_wait' => 'Čekejte prosím…',
'remember_me' => 'Pamatovat si mě',
'custom_head_set' => 'Vlastní hlavička HTML použita.',
'custom_head_html_hint' => 'Tento obsah bude přidán do tagu <head> na každé stránce.',
'custom_head_html' => 'Vlastní obsah hlavičky HTML',
'donate_text' => 'Pokud se vám líbí XBackBone, zvažte příspěvek na podporu vývoje!',
'donation' => 'Přispět',
'drop_to_upload' => 'Klikněte nebo sem přetáhněte soubory pro nahrání.',
'no_upload_token' => 'Nemáte osobní nahrávací token. (Vygenerujte si jej a zkuste to znovu.)',
'image_embeds' => 'Vložené obrázky',
];

View file

@ -1,164 +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å, at du vil slette dette element? Du vil ikke være i stand til at gendanne den',
'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',
'user_updated' => 'Bruger »%s« er opdateret!',
'user_created' => 'Bruger »%s« oprettet!',
'email_taken' => 'E-mail-adressen er allerede i brug.',
'email_required' => 'E-mail er påkrævet.',
'mail.new_account' => '%s - Oprettelse af ny konto',
'send_notification' => 'Send e-mail notifikation',
'recaptcha_enabled' => 'reCAPTCHA slået til',
'recaptcha_failed' => 'reCAPTCHA fejlede',
'delete_all' => 'Slet alt',
'delete_selected' => 'Slet valgte',
'used_space' => 'Brugt plads',
'password_repeat' => 'Gentag adgangskode',
'account_activated' => 'Konto aktiveret, du kan nu logge ind!',
'register' => 'Tilmeld',
'no_account' => 'Har du ikke en konto?',
'password_recovery' => 'Gendan adgangskode',
'export_data' => 'Eksporter data',
'settings_saved' => 'Systemindstillinger gemt!',
'copy_url_behavior' => 'Kopier URL tilstand',
'hide_by_default' => 'Skjul medie som standard',
'register_enabled' => 'Registreringer slået til',
'please_wait' => 'Vent venligst…',
'remember_me' => 'Husk mig',
'custom_head_html' => 'Tilpasset HTML Head indhold',
'donate_text' => 'Hvis du kan lide XBackBone, kan du overveje en donation for at støtte udviklingen!',
'donation' => 'Donering',
'drop_to_upload' => 'Klik eller slip dine filer her for at uploade.',
'auto_set' => 'Sæt automatisk',
'cancel' => 'Annuller',
'maintenance_in_progress' => 'Platformen er under vedligeholdelse, prøv igen senere…',
'updates' => 'Opdateringer',
'upgrade' => 'Opgrader',
'check_for_updates' => 'Søg efter opdateringer',
'file_size_no_match' => 'Den downloadede fil matcher ikke den korrekte filstørrelse.',
'cannot_retrieve_file' => 'Filen kan ikke hentes.',
'new_version_available' => 'Ny version %s tilgængelig!',
'already_latest_version' => 'Du har allerede den seneste version.',
'maintenance' => 'Vedligeholdelse',
'name' => 'Navn',
'time' => 'Tid',
'order_by' => 'Sorter efter…',
'dotted_search' => 'Søg…',
'table' => 'Tabel',
'gallery' => 'Galleri',
'switch_to' => 'Skift til',
'cannot_write_file' => 'Destinationsstien er ikke skrivbar.',
'cannot_demote' => 'Du kan ikke degradere dig selv.',
'cannot_delete' => 'Du kan ikke slette dig selv.',
'user_deleted' => 'Bruger slettet.',
'profile_updated' => 'Profilen blev opdateret!',
'password_required' => 'Adgangskoden er påkrævet.',
'username_taken' => 'Brugernavnet er allerede i brug.',
'username_required' => 'Brugernavnet er påkrævet.',
'goodbye' => 'Farvel!',
'welcome' => 'Velkommen, %s!',
'account_disabled' => 'Din konto er deaktiveret.',
'bad_login' => 'Forkerte loginoplysninger.',
'installed' => 'Installationen blev gennemført!',
'confirm_string' => 'Er du sikker?',
'confirm' => 'Bekræftelse',
'open' => 'Åbn',
'none' => 'Ingen',
'reg_date' => 'Registreringsdato',
'admin' => 'Admin',
'active' => 'Aktiv',
'user_code' => 'Bruger kode',
'client_config' => 'Klientkonfiguration',
'edit' => 'Rediger',
'update' => 'Opdater',
'copied' => 'Kopieret til udklipsholderen!',
'copy' => 'Kopier',
'token' => 'Nøgle',
'system_settings' => 'Systemindstillinger',
'php_info' => 'PHP Informationer',
'upload' => 'Overfør',
'enforce_language' => 'Håndhæve sproget',
'deleted_orphans' => 'Slettet %d forældreløse filer.',
'default_lang_behavior' => 'XBackBone vil prøve at matche browsersproget som standard (tilbagefaldet er Engelsk).',
'prerelease_channel' => 'Forhåndsudgivelse Kanal',
'token_not_found' => 'Den angivne token blev ikke fundet.',
'clean_orphaned_uploads' => 'Fjern forældreløse overførsler',
'custom_head_set' => 'Brugerdefineret HTML-hoved anvendt.',
'dont_close' => 'Luk ikke denne fane, før den er færdig.',
'default_user_quota' => 'Standard bruger plads',
'max_user_quota' => 'Max bruger plads',
'invalid_quota' => 'Ugyldige værdier som standard bruger plads.',
'mail.activate_account' => '%s - Aktivering af konto',
'mail.recover_password' => '%s - Gendan adgangskode',
'password_restored' => 'Nulstil kodeord.',
'recalculate_user_quota' => 'Genberegn bruger plads fra disk',
'quota_recalculated' => 'Bruger pladsen genberegnet fra disken med succes.',
'account_media_deleted' => 'Alle medier på kontoen er blevet slettet.',
'danger_zone' => 'Farligt område',
'recaptcha_keys_required' => 'Alle reCAPTCHA nøgler er påkrævet.',
'recaptcha_site_key' => 'reCAPTCHA webstedsnøgle',
'recaptcha_secret_key' => 'reCAPTCHA hemmelig nøgle',
'mail.new_account_text_with_reset' => 'Hej %s!<br>en ny konto blev oprettet til dig den %s (<a href="%s">%s</a>), klik på følgende link for at indstille en adgangskode og aktivere den:<br><br><a href="%s">%s</a>',
'user_create_password' => 'Hvis den efterlades tom, vil det være en god idé selv at sende en meddelelse til brugerens e-mailadresse.',
'ldap_cant_connect' => 'Kan ikke oprette forbindelse til LDAP-godkendelsesserveren.',
'upload_max_file_size' => 'Den maksimale filstørrelse er i øjeblikket %s.',
'no_tags' => 'Ingen tags tilføjet',
'auto_tagging' => 'Automatisk upload tagging',
'zip_ext_not_loaded' => 'Den nødvendige "zip"-udvidelse er ikke indlæst',
'changelog' => 'Ændringslog',
'show_changelog' => 'Vis ændringslog',
'no_upload_token' => 'Du har ikke et personligt upload token. (Generer en og prøv igen.)',
'custom_head_html_hint' => 'Dette indhold vil blive tilføjet ved <head> tagget på hver side.',
'register_success' => 'Kontoen er oprettet, en bekræftelses-e-mail er blevet sendt.',
'quota_enabled' => 'Aktiver brugerkvote',
'mail.activate_text' => 'Hej %s!<br>tak fordi du oprettede din konto på %s (<a href="%s">%s</a>), klik på følgende link for at aktivere den:<br><br><a href="%s">%s</a>',
'mail.recover_text' => 'Hej %s,<br>der er blevet anmodet om en nulstilling af adgangskoden til din konto. For at fuldføre proceduren, klik på følgende link:<br><br><a href="%s">%s</a><br><br>Hvis det ikke var dig, der anmodede om nulstilling af adgangskoden, skal du blot ignorere denne e-mail.',
'recover_email_sent' => 'Hvis tilstede, blev der sendt en gendannelses-e-mail til den angivne konto.',
'password_match' => 'Adgangskode og den gentaget adgangskode skal være ens.',
'clear_account' => 'Ryd Konto',
'only_recaptcha_v3' => 'Kun reCAPTCHA v3 understøttes.',
'image_embeds' => 'Integrer billeder',
'mail.new_account_text_with_pw' => 'Hej %s!<br>en ny konto blev oprettet til dig den %s (<a href="%s">%s</a>), med følgende oplysninger:<br><br>Brugernavn: %s<br>Adgangskode: %s<br><br>Klik på følgende link for at gå til login-siden:<br><a href="%s">%s</a>',
'path_not_writable' => 'Outputstien er ikke skrivbar.',
];

View file

@ -1,27 +1,27 @@
<?php
return [
'lang' => 'Deutsch',
'lang' => 'German',
'yes' => 'Ja',
'no' => 'Nein',
'send' => 'Senden',
'no_media' => 'Datei nicht gefunden.',
'login.username' => 'Benutzername oder E-Mail',
'password' => 'Passwort',
'login' => 'Anmelden',
'login' => 'Login',
'username' => 'Benutzername',
'home' => 'Startseite',
'users' => 'Benutzer',
'system' => 'System',
'profile' => 'Profil',
'logout' => 'Abmelden',
'logout' => 'Ausloggen',
'pager.next' => 'Nächste',
'pager.previous' => 'Zurück',
'copy_link' => 'Kopiere Link',
'public.telegram' => 'Teile auf Telegram',
'public.delete_text' => 'Möchtest du das wirklich löschen? Es wird dann für immer weg sein',
'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 Dateien',
'size' => 'Größe',
'public' => 'Öffentlich',
'owner' => 'Besitzer',
'date' => 'Datum',
@ -33,10 +33,10 @@ return [
'files' => 'Datei(n)',
'orphaned_files' => 'verwaiste Dateien',
'theme' => 'Design',
'click_to_load' => 'Zum Laden klicken ',
'click_to_load' => 'Klick um zu laden...',
'apply' => 'Sichern',
'save' => 'Speichern',
'used' => 'Benutzt',
'used' => 'Benutzen',
'system_info' => 'System Informationen',
'user.create' => 'Benutzer erstellen',
'user.edit' => 'Benutzer bearbeiten',
@ -53,8 +53,8 @@ return [
'admin' => 'Administrator',
'reg_date' => 'Registrierungsdatum',
'none' => 'keine',
'open' => 'Öffnen',
'confirm' => 'Bestätigen',
'open' => 'Offen',
'confirm' => 'Bestätigung',
'confirm_string' => 'Bist du sicher?',
'installed' => 'Installation erfolgreich abgeschlossen!',
'bad_login' => 'Falsche Anmeldeinformationen.',
@ -62,13 +62,13 @@ return [
'welcome' => 'Willkommen, %s!',
'goodbye' => 'Auf Wiedersehen!',
'token_not_found' => 'Das angegebene Schlüssel wurde nicht gefunden.',
'email_required' => 'E-Mail-Adresse erforderlich.',
'email_taken' => 'Die E-Mail-Adresse wird bereits verwendet.',
'email_required' => 'Die E-Mail ist erforderlich.',
'email_taken' => 'Die E-Mail ist bereits vergeben.',
'username_required' => 'Der Benutzername ist erforderlich.',
'username_taken' => 'Der Benutzername ist bereits vergeben.',
'password_required' => 'Das Passwort ist erforderlich.',
'user_created' => 'Benutzer „%s“ wurde erstellt!',
'user_updated' => 'Benutzer „%s“ wurde aktualisiert!',
'user_created' => 'Benutzer "%s" wurde erstellt!',
'user_updated' => 'Benutzer "%s" wurde aktualisiert!',
'profile_updated' => 'Profil erfolgreich aktualisiert!',
'user_deleted' => 'Benutzer gelöscht.',
'cannot_delete' => 'Du kannst dich nicht selbst löschen.',
@ -77,94 +77,28 @@ return [
'switch_to' => 'Wechseln zu',
'gallery' => 'Galerie',
'table' => 'Tabelle',
'dotted_search' => 'Suche …',
'order_by' => 'Sortieren nach …',
'dotted_search' => 'Suche...',
'order_by' => 'Sortieren nach...',
'time' => 'Zeit',
'name' => 'Name',
'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.',
'check_for_updates' => 'Auf Aktualisierungen prüfen',
'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' => 'Aktualisierungen',
'maintenance_in_progress' => 'Plattform wird gewartet, versuchen Sie es später erneut ',
'updates' => 'Updates',
'maintenance_in_progress' => 'Die Seite macht Wartungsarbeiten, bitte versuche es später noch einmal.',
'cancel' => 'Abbruch',
'deleted_orphans' => '%d verwaiste Dateie(n) wurden erfolgreich gelöscht.',
'enforce_language' => 'Sprache erzwingen',
'enforce_language' => 'Name der Sprache',
'auto_set' => 'Automatisch einstellen',
'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' => 'Merken',
'please_wait' => 'Bitte warten …',
'dont_close' => 'Schließe diesen Tab erst wenn fertig.',
'php_info' => 'PHP Informationen',
'system_settings' => 'Systemeinstellungen',
'register_enabled' => 'Registrierungen aktiviert',
'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' => 'Haben Sie kein Konto?',
'register' => 'Registrieren',
'default_user_quota' => 'Standard Speicherplatz',
'invalid_quota' => 'Ungültiger Wert für den Speicherplatz.',
'register_success' => 'Das Konto wurde erstellt, eine Bestätigungs-E-Mail wurde gesendet.',
'mail.activate_account' => '%s Aktivierung des Kontos',
'mail.recover_password' => '%s Zurücksetzung des Passworts',
'recover_email_sent' => 'Falls vorhanden, wurde eine Wiederherstellungs-E-Mail an das angegebene Konto gesendet.',
'account_activated' => 'Konto wurde aktiviert, du kannst dich jetzt anmelden!',
'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 neu berechnen',
'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' => 'E-Mail-Benachrichtigung senden',
'mail.new_account' => '%s Erstellung von Konto',
'user_create_password' => 'Wenn das leer bleibt, wollen Sie vielleicht eine Benachrichtigung an die Benutzer per E-Mail senden.',
'no_tags' => 'Keine Tags hinzugefügt',
'show_all_tags' => 'Alle Tags anzeigen',
'upload_max_file_size' => 'Die maximale Dateigröße beträgt derzeit %s.',
'ldap_cant_connect' => 'Es kann keine Verbindung zum LDAP-Auth-Server hergestellt werden.',
'zip_ext_not_loaded' => 'Die zip-Erweiterung ist erforderlich',
'auto_tagging' => 'Automatische Markierung des Hochladen',
'mail.new_account_text_with_pw' => 'Hallo %s! <br>ein neues Konto wurde für Dich auf %s (<a href="%s">%s</a>) erstellt, mit den folgenden Anmeldeinformationen:<br><br>Benutzername: %s<br>Password: %s<br><br>Klicken Sie auf den folgenden Link, um zur Anmeldeseite zu gelangen:<br><a href="%s">%s</a>',
'mail.new_account_text_with_reset' => 'Hallo %s! <br>ein neues Konto wurde für dich auf %s erstellt (<a href="%s">%s</a>), klicken auf den folgenden Link, um ein Passwort festzulegen und es zu aktivieren:<br><br><a href="%s">%s</a>',
'mail.recover_text' => 'Hallo %s,<br>für Ihr Konto wurde eine Zurücksetzung des Passworts angefordert. Um das Verfahren abzuschließen, klicken Sie auf den folgenden Link:<br><br><a href="%s">%s</a><br><br>Wenn Sie keine Zurücksetzung Ihres Passworts angefordert haben, ignorieren Sie diese E-Mail einfach.',
'mail.activate_text' => 'Hallo %s! <br>Vielen Dank fürs Registrieren auf %s (\'a href="%s">%s</a>), klicken Sie auf den folgenden Link, um es zu aktivieren:<br><br>\'a href="%s">%s</a>',
'show_changelog' => 'Änderungsprotokoll ansehen',
'changelog' => 'Änderungsprotokoll',
'copied' => 'In Zwischenablage kopiert!',
'image_embeds' => 'Bilder einbetten',
'vanity_url' => 'Benutzerdefinierte URL',
];

View file

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

88
resources/lang/en.lang.php Executable file → Normal file
View file

@ -19,7 +19,7 @@ return [
'pager.previous' => 'Previous',
'copy_link' => 'Copy link',
'public.telegram' => 'Share on Telegram',
'public.delete_text' => 'Are you sure you want to delete this item? You will not be able to recover it',
'public.delete_text' => 'Are you sure you want to delete this item? It will be gone forever!',
'preview' => 'Preview',
'filename' => 'Filename',
'size' => 'Size',
@ -28,21 +28,17 @@ return [
'date' => 'Date',
'raw' => 'Show raw',
'download' => 'Download',
'upload' => 'Upload',
'delete' => 'Delete',
'confirm' => 'Confirm',
'vanity_url' => 'Custom URL',
'publish' => 'Publish',
'hide' => 'Hide',
'files' => 'Files',
'orphaned_files' => 'Orphaned Files',
'theme' => 'Theme',
'click_to_load' => 'Click to load',
'click_to_load' => 'Click to load...',
'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',
@ -50,7 +46,6 @@ return [
'your_profile' => 'Your Profile',
'token' => 'Token',
'copy' => 'Copy',
'copied' => 'Copied to clipboard!',
'update' => 'Update',
'edit' => 'Edit',
'client_config' => 'Client Configuration',
@ -60,6 +55,7 @@ return [
'reg_date' => 'Registration Date',
'none' => 'None',
'open' => 'Open',
'confirm' => 'Confirmation',
'confirm_string' => 'Are you sure?',
'installed' => 'Installation completed successfully!',
'bad_login' => 'Wrong credentials.',
@ -67,8 +63,8 @@ return [
'welcome' => 'Welcome, %s!',
'goodbye' => 'Goodbye!',
'token_not_found' => 'Token specified not found.',
'email_required' => 'E-mail address required.',
'email_taken' => 'The e-mail address is already in use.',
'email_required' => 'The email is required.',
'email_taken' => 'The email is already taken.',
'username_required' => 'The username is required.',
'username_taken' => 'The username is already taken.',
'password_required' => 'The password is required.',
@ -83,8 +79,8 @@ return [
'switch_to' => 'Switch to',
'gallery' => 'Gallery',
'table' => 'Table',
'dotted_search' => 'Search',
'order_by' => 'Order by',
'dotted_search' => 'Search...',
'order_by' => 'Order by...',
'time' => 'Time',
'name' => 'Name',
'maintenance' => 'Maintenance',
@ -97,69 +93,13 @@ return [
'check_for_updates' => 'Check for updates',
'upgrade' => 'Upgrade',
'updates' => 'Updates',
'maintenance_in_progress' => 'Platform under maintenance, try again later',
'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 e-mail 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!<br>thank you for creating your account on %s (<a href="%s">%s</a>), click on the following link to activate it:<br><br><a href="%s">%s</a>',
'mail.activate_account' => '%s - Account Activation',
'mail.recover_text' => 'Hi %s,<br>a password reset has been requested for your account. To complete the procedure click on the following link:<br><br><a href="%s">%s</a><br><br>If it wasn\'t you who requested the password reset, simply ignore this e-mail.',
'mail.recover_password' => '%s - Password Recovery',
'recover_email_sent' => 'If present, a recovery e-mail 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 E-mail Notification',
'mail.new_account' => '%s - New Account Creation',
'mail.new_account_text_with_reset' => 'Hi %s!<br>a new account was created for you on %s (<a href="%s">%s</a>), click on the following link to set a password and activate it:<br><br><a href="%s">%s</a>',
'mail.new_account_text_with_pw' => 'Hi %s!<br>a new account was created for you on %s (<a href="%s">%s</a>), with the following credentials:<br><br>Username: %s<br>Password: %s<br><br>Click on the following link to go to the login page:<br><a href="%s">%s</a>',
'user_create_password' => 'If leaved empty, you might want to send a notification to the user e-mail address.',
'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',
'show_all_tags' => 'Show all tags',
'auto_tagging' => 'Auto upload tagging',
'zip_ext_not_loaded' => 'The required "zip" extension is not loaded',
'changelog' => 'Changelog',
'show_changelog' => 'Show changelog',
'image_embeds' => 'Embed images'
];

View file

@ -1,11 +1,11 @@
<?php
return [
'lang' => 'Español',
'yes' => 'Sí',
'lang' => 'Spanish',
'yes' => 'Si',
'no' => 'No',
'send' => 'Enviar',
'no_media' => 'Contenido no encontrado.',
'login.username' => 'Nombre de usuario o correo electrónico',
'login.username' => 'Nombre de usuario o Correo Electrónico',
'password' => 'Contraseña',
'login' => 'Iniciar sesión',
'username' => 'Nombre de usuario',
@ -18,7 +18,7 @@ return [
'pager.previous' => 'Previo',
'copy_link' => 'Copiar enlace',
'public.telegram' => 'Compartir en Telegram',
'public.delete_text' => '¿Está seguro que desea borrar este elemento? No vas a poder recuperarlo',
'public.delete_text' => '¿Está seguro que desea borrar este elemento? ¡Será borrado para siempre!',
'preview' => 'Vista previa',
'filename' => 'Nombre del archivo',
'size' => 'Tamaño',
@ -33,14 +33,14 @@ return [
'files' => 'Archivos',
'orphaned_files' => 'Archivos huérfanos',
'theme' => 'Tema',
'click_to_load' => 'Clic para cargar',
'click_to_load' => 'Clic para cargar...',
'apply' => 'Aplicar',
'save' => 'Guardar',
'used' => 'Usado',
'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',
@ -54,7 +54,7 @@ return [
'reg_date' => 'Fecha de Registración',
'none' => 'Ninguno',
'open' => 'Abrir',
'confirm' => 'Confirmar',
'confirm' => 'Confirmación',
'confirm_string' => '¿Está seguro?',
'installed' => '¡Instalación completa!',
'bad_login' => 'Credenciales incorrectas.',
@ -62,13 +62,13 @@ return [
'welcome' => '¡Bienvenido, %s!',
'goodbye' => '¡Adiós!',
'token_not_found' => 'Ficha indicada No encontrada.',
'email_required' => 'El correo electrónico es requisito.',
'email_taken' => 'El correo electrónico ya está en uso.',
'email_required' => 'El Correo Electrónico es requisito.',
'email_taken' => 'El Correo Electrónico ya está en uso.',
'username_required' => 'El Nombre de Usuario es requisito.',
'username_taken' => 'El Nombre de Usuario ya está en uso.',
'password_required' => 'La Contraseña es requisito.',
'user_created' => '¡Usuario «%s» creado!',
'user_updated' => '¡Usuario «%s» actualizado!',
'user_created' => '¡Usuario "%s" creado!',
'user_updated' => '¡Usuario "%s" actualizado!',
'profile_updated' => '¡Perfil actualizado con éxito!',
'user_deleted' => 'Usuario borrado.',
'cannot_delete' => 'No puede borrarse a usted mismo.',
@ -77,8 +77,8 @@ return [
'switch_to' => 'Cambiar a',
'gallery' => 'Galería',
'table' => 'Tabla',
'dotted_search' => 'Búsqueda',
'order_by' => 'Ordenar por',
'dotted_search' => 'Búsqueda...',
'order_by' => 'Ordenar por...',
'time' => 'Tiempo',
'name' => 'Nombre',
'maintenance' => 'Mantenimiento',
@ -90,7 +90,7 @@ return [
'check_for_updates' => 'Buscar actualizaciones',
'upgrade' => 'Mejorar',
'updates' => 'Actualizaciones',
'maintenance_in_progress' => 'Plataforma en mantenimiento, inténtelo más tarde',
'maintenance_in_progress' => 'Plataforma en mantenimiento, inténtelo más tarde...',
'cancel' => 'Cancelar',
'enforce_language' => 'Afirmar lenguaje',
'auto_set' => 'Establecer automáticamente',
@ -99,72 +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',
'mail.new_account' => '% s Creación de nueva cuenta',
'send_notification' => 'Enviar notificación por correo electrónico',
'mail.recover_password' => '% s Recuperación de contraseña',
'mail.activate_account' => '%s Activación de la cuenta',
'register_success' => 'La cuenta ha sido creada, se ha enviado un correo electrónico de confirmación.',
'donate_text' => 'Si te gusta XBackBone, considera una donación para apoyar el desarrollo!',
'donation' => 'Donación',
'drop_to_upload' => 'Haz clic o arrastra tus archivos aquí para subirlos.',
'no_upload_token' => 'No tienes una clave personal de carga. (Genera uno e inténtalo de nuevo.)',
'prerelease_channel' => 'Canal de prelanzamiento',
'clean_orphaned_uploads' => 'Limpiar las cargas de los huéspedes',
'cannot_demote' => 'No puedes degradarte.',
'copied' => '¡Copiado en el portapapeles!',
'quota_recalculated' => 'Cuota de usuario recalculada desde el disco con éxito.',
'recalculate_user_quota' => 'Recalcular la cuota de usuarios desde el disco',
'quota_enabled' => 'Activar la cuota de usuarios',
'show_changelog' => 'Mostrar el registro de cambios',
'changelog' => 'Registro de cambios',
'zip_ext_not_loaded' => 'La extensión zip requerida no está cargada',
'auto_tagging' => 'Carga automática de etiquetas',
'no_tags' => 'No se han añadido etiquetas',
'upload_max_file_size' => 'El tamaño máximo del archivo es actualmente %s.',
'ldap_cant_connect' => 'No se puede conectar con el servidor de autentificación LDAP.',
'user_create_password' => 'Si se deja vacío, es posible que desee enviar una notificación a la dirección de correo electrónico del usuario.',
'mail.new_account_text_with_pw' => '¡Hola %s!<br>se ha creado una nueva cuenta para ti en %s (<a href="%s">%s</a>), haz clic en el siguiente enlace para establecer una contraseña y activarla:<br><br><a href="%s">%s</a>',
'mail.new_account_text_with_reset' => '¡Hola, %s!<br>Se creó una nueva cuenta para ti en %s (<a href="%s">%s</a>), haz clic en el siguiente enlace para establecer una contraseña y activarla:<br ><br><a href="%s">%s</a>',
'recaptcha_enabled' => 'reCAPTCHA activado',
'account_media_deleted' => 'Se han eliminado todos los archivos de la cuenta.',
'clear_account' => 'Limpiar cuenta',
'delete_selected' => 'Eliminar seleccionado',
'used_space' => 'Espacio usado',
'password_restored' => 'Restablecimiento de la contraseña.',
'password_match' => 'La contraseña y la contraseña repetida deben ser las mismas.',
'password_repeat' => 'Repite la contraseñá',
'account_activated' => 'Cuenta activada, ¡ahora puedes iniciar sesión!',
'recover_email_sent' => 'Si está presente, se ha enviado un correo electrónico de recuperación a la cuenta especificada.',
'mail.recover_text' => 'Hola %s,<br>se ha solicitado el restablecimiento de la contraseña de su cuenta. Para completar el procedimiento haga clic en el siguiente enlace:<br><br><a href="%s">%s</a><br>Si no fue usted quien solicitó el restablecimiento de la contraseña, simplemente ignore este correo electrónico.',
'mail.activate_text' => 'Hola %s!<br>gracias por crear tu cuenta en %s (<a href="%s">%s</a>), haz clic en el siguiente enlace para activarla:<br><br><a href="%s">%s</a>',
'invalid_quota' => 'Valores no válidos como cuota de usuario por defecto.',
'max_user_quota' => 'Cuota máxima del usuario',
'default_user_quota' => 'Cuota de usuarios por defecto',
'register' => 'Registrarse',
'no_account' => '¿No tienes cuenta?',
'password_recovery' => 'Recuperar contraseña',
'export_data' => 'Exportar datos',
'settings_saved' => '¡Configuración del sistema guardada!',
'copy_url_behavior' => 'Modo copiar URL',
'hide_by_default' => 'Ocultar media por defecto',
'register_enabled' => 'Registros abiertos',
'dont_close' => 'No cierres esta pestaña hasta que se haya completado.',
'please_wait' => 'Por favor, espera…',
'remember_me' => 'Recuerdame',
'custom_head_html_hint' => 'Este contenido se añadirá en la etiqueta <head> de cada página.',
'custom_head_html' => 'Contenido personalizado del encabezado HTML',
'image_embeds' => 'Incrustar imágenes',
'recaptcha_keys_required' => 'Todas las claves reCAPTCHA son requeridas.',
'recaptcha_secret_key' => 'Clave secreta de reCAPTCHA',
'recaptcha_site_key' => 'Clave del sitio reCAPTCHA',
'only_recaptcha_v3' => 'Solo reCAPTCHA v3 está soportado.',
'custom_head_set' => 'Cabecera HTML personalizada aplicada.',
'vanity_url' => 'URL personalizada',
'show_all_tags' => 'Mostrar todas las etiquetas',
];

View file

@ -1,29 +0,0 @@
<?php
return [
'lang' => 'انگلیسی',
'login.username' => 'نام کاربری یا صندوق پستی الکترونیک',
'enforce_language' => 'اجرا کردن زبان',
'yes' => 'بله',
'send' => 'ارسال',
'no_media' => 'رسانه ای یافت نشد .',
'home' => 'خانه',
'users' => 'کاربران',
'system' => 'سیستم',
'profile' => 'حساب کاربری',
'logout' => 'خارج شدن',
'public.telegram' => 'اشتراک گذاری در تلگرام',
'public.delete_text' => 'آیا مطمعن به حذف این آیتم هستید ؟ غیرقابل بازیابی بعد از حذف',
'preview' => 'پیش نمایش',
'size' => 'سایز',
'public' => 'عمومی',
'owner' => 'مالک',
'date' => 'تاریخ',
'no' => 'خیر',
'password' => 'کلمه عبور',
'login' => 'وارد شدن',
'username' => 'نام کاربری',
'pager.next' => 'بعدی',
'pager.previous' => 'قبلی',
'copy_link' => 'رونوشت از پیوند',
'filename' => 'نام فایل',
];

View file

@ -1,164 +0,0 @@
<?php
return [
'cannot_retrieve_file' => 'Tiedostoa ei voi palauttaa.',
'new_version_available' => 'Uusi versio %s saatavilla!',
'already_latest_version' => 'Sinulla on jo viimeisin versio.',
'path_not_writable' => 'Ei voida kirjoittaa kohteeseen.',
'clean_orphaned_uploads' => 'Siivoa hylätyt lataukset',
'maintenance' => 'Huoltokatko',
'name' => 'Nimi',
'time' => 'Aika',
'order_by' => 'Järjestä…',
'dotted_search' => 'Haku…',
'gallery' => 'Galleria',
'switch_to' => 'Vaihda',
'deleted_orphans' => '%d Unohdettua tiedostoa poistettu.',
'cannot_write_file' => 'Kohdepolku ei ole kirjoitettavissa.',
'token_not_found' => 'Määritettyä tunnusta ei löydy.',
'client_config' => 'Asiakasohjelman konfigurointi',
'home' => 'Koti',
'enforce_language' => 'Pakota kieli',
'cannot_demote' => 'Et voi alentaa itseäsi.',
'cannot_delete' => 'Et voi poistaa itseäsi.',
'user_deleted' => 'Käyttäjä poistettu.',
'profile_updated' => 'Profiilin päivitys onnistui!',
'user_updated' => 'Käyttäjä ”%s” on päivitetty!',
'user_created' => 'Käyttäjä ”%s” on luotu!',
'password_required' => 'Salasana vaaditaan.',
'username_taken' => 'Käyttäjänimi on jo käytössä.',
'username_required' => 'Käyttäjänimi vaaditaan.',
'email_taken' => 'Sähköpostiosoite on jo käytössä.',
'email_required' => 'Sähköposti vaaditaan.',
'goodbye' => 'Näkemiin!',
'welcome' => 'Tervetuloa %s!',
'account_disabled' => 'Sinun tilisi on jäädytetty.',
'bad_login' => 'Väärät kirjautumistiedot.',
'installed' => 'Asennus suoritettu loppuun onnistuneesti!',
'confirm_string' => 'Oletko varma?',
'confirm' => 'Vahvista',
'open' => 'Auki',
'none' => 'Ei mitään',
'reg_date' => 'Rekisteröitymis päivä',
'admin' => 'Järjestelmänvalvoja',
'active' => 'Aktiivinen',
'user_code' => 'Käyttäjän koodi',
'edit' => 'Muokkaa',
'update' => 'Päivitä',
'copied' => 'Kopioitu leikepöydälle!',
'copy' => 'Kopioi',
'token' => 'Tunnus',
'your_profile' => 'Sinun profiili',
'is_admin' => 'On järjestelmänvalvoja',
'is_active' => 'On aktiivinen',
'user.edit' => 'Muokkaa käyttäjää',
'user.create' => 'Luo käyttäjä',
'system_settings' => 'Järjestelmän asetukset',
'php_info' => 'PHP Informaatiot',
'used' => 'Käytetty',
'save' => 'Tallenna',
'apply' => 'Hyväksy',
'click_to_load' => 'Paina tästä ladataksesi.…',
'theme' => 'Teema',
'orphaned_files' => 'Unohdetut tiedostot',
'files' => 'Tiedostot',
'hide' => 'Piilota',
'publish' => 'Julkaise',
'delete' => 'Poista',
'upload' => 'Lähetä',
'download' => 'Lataa',
'raw' => 'Näytä alkuperäinen',
'date' => 'Päivämäärä',
'owner' => 'Omistaja',
'public' => 'Julkinen',
'size' => 'Koko',
'filename' => 'Tiedoston nimi',
'preview' => 'Esikatsele',
'public.delete_text' => 'Haluatko varmasti poistaa tämän tiedoston? Tiedostoa ei pysty palauttamaan',
'public.telegram' => 'Jaa Telegramissa',
'copy_link' => 'Kopioi linkki',
'pager.previous' => 'Edellinen',
'pager.next' => 'Seuraava',
'logout' => 'Kirjaudu ulos',
'profile' => 'Profiili',
'system' => 'Järjestelmä',
'users' => 'Käyttäjät',
'username' => 'Käyttäjänimi',
'login' => 'Kirjaudu',
'password' => 'Salasana',
'login.username' => 'Käyttäjänimi tai sähköpostiosoite',
'no_media' => 'Mediaa ei löytynyt.',
'send' => 'Lähetä',
'no' => 'Ei',
'yes' => 'Kyllä',
'lang' => 'Englanti',
'recaptcha_enabled' => 'reCAPTCHA Käytössä',
'cancel' => 'Peruuta',
'send_notification' => 'Lähetä sähköpostivahvistus',
'auto_set' => 'Määritä automaattisesti',
'drop_to_upload' => 'Lähetä tiedostosi painamalla tai tiputtamalla tähän.',
'account_activated' => 'Käyttäjä aktivoitu, voit nyt kirjautua sisään!',
'used_space' => 'Käytetty Tila',
'delete_selected' => 'Poista Valitut',
'delete_all' => 'Poista Kaikki',
'danger_zone' => 'Vaarallinen Alue',
'recaptcha_failed' => 'reCAPTCHA Epäonnistui',
'only_recaptcha_v3' => 'Vain reCAPTCHA v3 on tuettu.',
'upload_max_file_size' => 'Suurin tiedostokoko on tällä hetkellä %s.',
'changelog' => 'Muutosloki',
'show_changelog' => 'Näytä muutosloki',
'please_wait' => 'Odota…',
'hide_by_default' => 'Piilota media oletuksena',
'settings_saved' => 'Järjestelmäasetukset tallennettu!',
'export_data' => 'Vie dataa',
'dont_close' => 'Älä sulje tätä välilehteä ennen kuin on valmista.',
'donation' => 'Lahjoitus',
'donate_text' => 'Jos tykkäät XBackBone-sovelluksesta, harkitse lahjoittamista kannattaaksesi sovelluksen ohjelmointia/kehittämistä!',
'table' => 'Pöytä',
'check_for_updates' => 'Tarkista päivitykset',
'upgrade' => 'Päivitä',
'updates' => 'Päivitykset',
'maintenance_in_progress' => 'Alusta huollossa, yritä myöhemmin uudelleen…',
'password_recovery' => 'Palauta salasana',
'no_account' => 'Eikö sinulla ole käyttäjää?',
'register' => 'Rekisteröidy',
'register_success' => 'Käyttäjä on luotu, vahvistussähköposti on lähetetty.',
'mail.recover_password' => '%s - Salasanan Palautus',
'password_repeat' => 'Toista salasana',
'password_match' => 'Salasanan ja uudelleenkirjoitetun salasanan täytyy olla sama.',
'password_restored' => 'Salana nollattu.',
'image_embeds' => 'Kuvien upottaminen',
'recalculate_user_quota' => 'Käyttäjäkiintiön laskeminen uudelleen levyltä',
'quota_recalculated' => 'Käyttäjäkiintiö lasketaan uudelleen levyltä onnistuneesti.',
'recaptcha_secret_key' => 'reCAPTCHA:n salainen avain',
'recaptcha_keys_required' => 'Kaikki reCAPTCHA-näppäimet ovat pakollisia.',
'mail.new_account' => '%s - Uuden tilin luominen',
'no_tags' => 'Tunnisteita ei ole lisätty',
'auto_tagging' => 'Automaattisen latauksen taggaus',
'zip_ext_not_loaded' => 'Vaadittua "zip" -laajennusta ei ladata',
'remember_me' => 'Muista minut',
'copy_url_behavior' => 'Kopioi URL-osoite -tila',
'register_enabled' => 'Rekisteröinnit käytössä',
'no_upload_token' => 'Sinulla ei ole henkilökohtaista lataustunnusta. (Luo sellainen ja yritä uudelleen.)',
'custom_head_html_hint' => 'Tämä sisältö lisätään <head> jokaisen sivun tunnisteeseen.',
'custom_head_set' => 'Mukautettu HTML-pää käytössä.',
'file_size_no_match' => 'Ladattu tiedosto ei vastaa oikeaa tiedostokokoa.',
'prerelease_channel' => 'Ennakkojulkaisukanava',
'default_user_quota' => 'Käyttäjän oletuskiintiö',
'max_user_quota' => 'Käyttäjän enimmäiskiintiö',
'mail.activate_account' => '%s - Tilin aktivointi',
'recover_email_sent' => 'Jos sellainen on, tietylle tilille lähetettiin palautussähköpostiviesti.',
'clear_account' => 'Tyhjennä tili',
'user_create_password' => 'Jos sinut jätetään tyhjäksi, sinun kannattaa ehkä lähettää ilmoitus käyttäjän sähköpostiosoitteeseen.',
'default_lang_behavior' => 'XBackBone yrittää oletusarvoisesti vastata selaimen kieltä (vara on englanti).',
'mail.activate_text' => 'Hei %s! <br>kiitos, että loit tilisi %s(<a href="%s":lle">%s</a>), napsauta seuraavaa linkkiä aktivoidaksesi sen:<br><br><a href="%s">%s</a>',
'account_media_deleted' => 'Kaikki tilin mediat on poistettu.',
'mail.new_account_text_with_reset' => 'Hei %s! <br>sinulle luotiin uusi tili %s(<a href="%s">%s</a>), napsauta seuraavaa linkkiä asettaaksesi salasanan ja aktivoidaksesi sen:<br><br><a href="%s">%s</a>',
'mail.new_account_text_with_pw' => 'Hei %s! <br>sinulle luotiin uusi tili %s(<a href="%s">%s</a>), jolla on seuraavat tunnistetiedot:<br><br>Käyttäjänimi: %s<br>Salasana: %s<br><br>Napsauta seuraavaa linkkiä siirtyäksesi kirjautumissivulle:<br><a href="%s">%s</a>',
'ldap_cant_connect' => 'Yhteyden muodostaminen LDAP-todennuspalvelimeen ei onnistu.',
'recaptcha_site_key' => 'reCAPTCHA-sivuston avain',
'custom_head_html' => 'Mukautettu HTML Head -sisältö',
'invalid_quota' => 'Virheelliset arvot oletuskäyttäjäkiintiönä.',
'mail.recover_text' => 'Hei %s, <br>tilillesi on pyydetty salasanan palautusta. Suorita toimenpide napsauttamalla seuraavaa linkkiä:<br><br><a href="%s">%s</a><br><br>Jos et ollut sinä, joka pyysit salasanan vaihtamista, ohita tämä sähköposti.',
'quota_enabled' => 'Käyttäjäkiintiön ottaminen käyttöön',
'vanity_url' => 'Mukautettu URL',
];

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