Compare commits
9 commits
Author | SHA1 | Date | |
---|---|---|---|
|
5a685429c5 | ||
|
db2629b422 | ||
|
2b37296225 | ||
|
1adede3a3c | ||
|
2cdf85ad21 | ||
|
6d6923eb09 | ||
|
c981498810 | ||
|
eb656afe06 | ||
|
58a47733f5 |
284 changed files with 22308 additions and 28107 deletions
27
.github/workflows/test_suite.yml
vendored
27
.github/workflows/test_suite.yml
vendored
|
@ -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
|
|
142
.gitignore
vendored
142
.gitignore
vendored
|
@ -1,141 +1 @@
|
||||||
### Composer ###
|
/.idea
|
||||||
composer.phar
|
|
||||||
/vendor/
|
|
||||||
|
|
||||||
### grunt ###
|
|
||||||
# Grunt usually compiles files inside this directory
|
|
||||||
dist/
|
|
||||||
release*.zip
|
|
||||||
|
|
||||||
# Grunt usually preprocesses files such as coffeescript, compass... inside the .tmp directory
|
|
||||||
.tmp/
|
|
||||||
|
|
||||||
### Node ###
|
|
||||||
# Logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
|
|
||||||
# Runtime data
|
|
||||||
pids
|
|
||||||
*.pid
|
|
||||||
*.seed
|
|
||||||
*.pid.lock
|
|
||||||
|
|
||||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
|
||||||
lib-cov
|
|
||||||
|
|
||||||
# Coverage directory used by tools like istanbul
|
|
||||||
coverage
|
|
||||||
|
|
||||||
# nyc test coverage
|
|
||||||
.nyc_output
|
|
||||||
|
|
||||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
|
||||||
.grunt
|
|
||||||
|
|
||||||
# Bower dependency directory (https://bower.io/)
|
|
||||||
bower_components
|
|
||||||
|
|
||||||
# node-waf configuration
|
|
||||||
.lock-wscript
|
|
||||||
|
|
||||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
|
||||||
build/Release
|
|
||||||
|
|
||||||
# Dependency directories
|
|
||||||
node_modules/
|
|
||||||
jspm_packages/
|
|
||||||
|
|
||||||
# Typescript v1 declaration files
|
|
||||||
typings/
|
|
||||||
|
|
||||||
# Optional npm cache directory
|
|
||||||
.npm
|
|
||||||
|
|
||||||
# Optional eslint cache
|
|
||||||
.eslintcache
|
|
||||||
|
|
||||||
# Optional REPL history
|
|
||||||
.node_repl_history
|
|
||||||
|
|
||||||
# Output of 'npm pack'
|
|
||||||
*.tgz
|
|
||||||
|
|
||||||
# Yarn Integrity file
|
|
||||||
.yarn-integrity
|
|
||||||
|
|
||||||
# dotenv environment variables file
|
|
||||||
.env
|
|
||||||
|
|
||||||
### PhpStorm ###
|
|
||||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
|
|
||||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
|
||||||
|
|
||||||
# User-specific stuff:
|
|
||||||
.idea/
|
|
||||||
.idea/**/workspace.xml
|
|
||||||
.idea/**/tasks.xml
|
|
||||||
.idea/dictionaries
|
|
||||||
|
|
||||||
# Sensitive or high-churn files:
|
|
||||||
.idea/**/dataSources/
|
|
||||||
.idea/**/dataSources.ids
|
|
||||||
.idea/**/dataSources.xml
|
|
||||||
.idea/**/dataSources.local.xml
|
|
||||||
.idea/**/sqlDataSources.xml
|
|
||||||
.idea/**/dynamic.xml
|
|
||||||
.idea/**/uiDesigner.xml
|
|
||||||
|
|
||||||
## File-based project format:
|
|
||||||
*.iws
|
|
||||||
|
|
||||||
## Plugin-specific files:
|
|
||||||
|
|
||||||
# IntelliJ
|
|
||||||
/out/
|
|
||||||
|
|
||||||
# mpeltonen/sbt-idea plugin
|
|
||||||
.idea_modules/
|
|
||||||
|
|
||||||
# JIRA plugin
|
|
||||||
atlassian-ide-plugin.xml
|
|
||||||
|
|
||||||
# Cursive Clojure plugin
|
|
||||||
.idea/replstate.xml
|
|
||||||
|
|
||||||
# Ruby plugin and RubyMine
|
|
||||||
/.rakeTasks
|
|
||||||
|
|
||||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
|
||||||
com_crashlytics_export_strings.xml
|
|
||||||
crashlytics.properties
|
|
||||||
crashlytics-build.properties
|
|
||||||
fabric.properties
|
|
||||||
|
|
||||||
### PhpStorm Patch ###
|
|
||||||
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
|
|
||||||
|
|
||||||
# *.iml
|
|
||||||
# modules.xml
|
|
||||||
# .idea/misc.xml
|
|
||||||
# *.ipr
|
|
||||||
|
|
||||||
# Sonarlint plugin
|
|
||||||
.idea/sonarlint
|
|
||||||
|
|
||||||
# Repo file
|
|
||||||
storage/
|
|
||||||
static/
|
|
||||||
install/installer.js
|
|
||||||
!resources/cache/.gitkeep
|
|
||||||
resources/cache/*
|
|
||||||
resources/database/*.db
|
|
||||||
resources/sessions/sess_*
|
|
||||||
logs/log-*.txt
|
|
||||||
config.php
|
|
||||||
release.zip
|
|
||||||
/.settings/
|
|
||||||
/.project
|
|
||||||
/.buildpath
|
|
|
@ -1,8 +0,0 @@
|
||||||
Options -Indexes +SymLinksIfOwnerMatch
|
|
||||||
<IfModule mod_rewrite.c>
|
|
||||||
RewriteEngine On
|
|
||||||
RewriteRule ^(app|bin|bootstrap|resources|storage|vendor|logs|CHANGELOG.md)(/.*|)$ - [NC,F]
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-f
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-d
|
|
||||||
RewriteRule ^(.*)$ index.php [QSA,L]
|
|
||||||
</IfModule>
|
|
39
.travis.yml
39
.travis.yml
|
@ -1,39 +0,0 @@
|
||||||
notifications:
|
|
||||||
on_success: never
|
|
||||||
on_failure: always
|
|
||||||
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- language: php
|
|
||||||
php:
|
|
||||||
- '7.1'
|
|
||||||
# Remove comment on if unit test ready
|
|
||||||
#before_script:
|
|
||||||
# - if find . -name "*.php" ! -path "./vendor/*" -exec php -l {} \; | grep "Fatal error"; then exit 1; fi
|
|
||||||
# - composer self-update
|
|
||||||
# - composer install --prefer-source --no-interaction --dev
|
|
||||||
script:
|
|
||||||
# Remove on if unit test ready
|
|
||||||
- if find . -name "*.php" ! -path "./vendor/*" -exec php -l {} \; | grep "Fatal error"; then exit 1; fi
|
|
||||||
- language: php
|
|
||||||
php:
|
|
||||||
- '7.2'
|
|
||||||
# Remove comment on if unit test ready
|
|
||||||
#before_script:
|
|
||||||
# - if find . -name "*.php" ! -path "./vendor/*" -exec php -l {} \; | grep "Fatal error"; then exit 1; fi
|
|
||||||
# - composer self-update
|
|
||||||
# - composer install --prefer-source --no-interaction --dev
|
|
||||||
script:
|
|
||||||
# Remove on if unit test ready
|
|
||||||
- if find . -name "*.php" ! -path "./vendor/*" -exec php -l {} \; | grep "Fatal error"; then exit 1; fi
|
|
||||||
- language: node_js
|
|
||||||
node_js:
|
|
||||||
- "lts/*"
|
|
||||||
before_script:
|
|
||||||
- npm install grunt-cli -g
|
|
||||||
script:
|
|
||||||
- grunt test
|
|
||||||
|
|
||||||
after_script:
|
|
||||||
- curl -H "Content-Type: application/json" --data '{"docker_tag": "master"}' -X POST https://registry.hub.docker.com/u/pe46dro/xbackbone-docker/trigger/505df075-f5b6-48bf-a22c-ce678f477929/
|
|
||||||
|
|
189
Gruntfile.js
189
Gruntfile.js
|
@ -1,189 +0,0 @@
|
||||||
module.exports = function (grunt) {
|
|
||||||
let version = grunt.file.readJSON('composer.json').version;
|
|
||||||
let releaseFilename = 'release-v' + version + '.zip';
|
|
||||||
grunt.initConfig({
|
|
||||||
pkg: grunt.file.readJSON('package.json'),
|
|
||||||
|
|
||||||
jshint: {
|
|
||||||
all: ['Gruntfile.js', 'src/js/app.js'],
|
|
||||||
options: {
|
|
||||||
'esversion': 6,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
cssmin: {
|
|
||||||
build: {
|
|
||||||
files: {
|
|
||||||
'static/app/app.css': [
|
|
||||||
'src/css/app.css'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
uglify: {
|
|
||||||
options: {
|
|
||||||
preserveComments: false,
|
|
||||||
compress: true
|
|
||||||
},
|
|
||||||
build: {
|
|
||||||
files: {
|
|
||||||
'static/app/app.js': [
|
|
||||||
'src/js/app.js'
|
|
||||||
],
|
|
||||||
'install/installer.js': [
|
|
||||||
'src/js/installer.js'
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
css: {
|
|
||||||
files: [
|
|
||||||
'src/css/app.css'
|
|
||||||
],
|
|
||||||
|
|
||||||
tasks: ['cssmin']
|
|
||||||
},
|
|
||||||
scripts: {
|
|
||||||
files: [
|
|
||||||
'src/js/app.js',
|
|
||||||
'src/js/installer.js',
|
|
||||||
],
|
|
||||||
|
|
||||||
tasks: ['uglify']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
copy: {
|
|
||||||
main: {
|
|
||||||
files: [
|
|
||||||
{
|
|
||||||
expand: true,
|
|
||||||
cwd: 'node_modules/@fortawesome/fontawesome-free',
|
|
||||||
src: ['css/all.min.css', 'webfonts/**/*'],
|
|
||||||
dest: 'static/fontawesome'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
expand: true,
|
|
||||||
cwd: 'node_modules/bootstrap/dist/css',
|
|
||||||
src: ['bootstrap.min.css'],
|
|
||||||
dest: 'static/bootstrap/css'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
expand: true,
|
|
||||||
cwd: 'node_modules/bootstrap/dist/js',
|
|
||||||
src: ['bootstrap.bundle.min.js'],
|
|
||||||
dest: 'static/bootstrap/js'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
expand: true,
|
|
||||||
cwd: 'node_modules/clipboard/dist',
|
|
||||||
src: ['clipboard.min.js'],
|
|
||||||
dest: 'static/clipboardjs'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
expand: true,
|
|
||||||
cwd: 'node_modules/plyr/dist',
|
|
||||||
src: ['plyr.min.js', 'plyr.css'],
|
|
||||||
dest: 'static/plyr'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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'}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
shell: {
|
|
||||||
phpstan: {
|
|
||||||
command: '"./vendor/bin/phpstan" --level=0 analyse app resources/lang bin install'
|
|
||||||
},
|
|
||||||
composer_no_dev: {
|
|
||||||
command: 'composer install --no-dev --prefer-dist'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
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: '/'
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
require('load-grunt-tasks')(grunt);
|
|
||||||
grunt.registerTask('default', ['jshint', 'cssmin', 'uglify', 'copy']);
|
|
||||||
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']);
|
|
||||||
};
|
|
4
app/.env.example
Normal file
4
app/.env.example
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
APP_KEY=base64:88Nwiwz8SgR2v7Spx27RDdj7uCYidIwKCmzQCs4l0V4=
|
||||||
|
APP_ENV=production
|
||||||
|
APP_TIMEZONE=UTC
|
||||||
|
APP_URL=http://localhost
|
21
app/.gitignore
vendored
Normal file
21
app/.gitignore
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
/.phpunit.cache
|
||||||
|
/node_modules
|
||||||
|
/public/build
|
||||||
|
/public/hot
|
||||||
|
/public/storage
|
||||||
|
/storage/*.key
|
||||||
|
/vendor
|
||||||
|
.env
|
||||||
|
.env.backup
|
||||||
|
.env.production
|
||||||
|
.phpactor.json
|
||||||
|
.phpunit.result.cache
|
||||||
|
Homestead.json
|
||||||
|
Homestead.yaml
|
||||||
|
auth.json
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
/.fleet
|
||||||
|
/.idea
|
||||||
|
/.vscode
|
||||||
|
/*.db
|
|
@ -1,110 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
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
|
|
||||||
);
|
|
||||||
|
|
||||||
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,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param Response $response
|
|
||||||
*
|
|
||||||
* @return Response
|
|
||||||
*/
|
|
||||||
public function deleteOrphanFiles(Response $response): Response
|
|
||||||
{
|
|
||||||
$orphans = $this->database->query('SELECT * FROM `uploads` WHERE `user_id` IS NULL')->fetchAll();
|
|
||||||
|
|
||||||
$filesystem = $this->storage;
|
|
||||||
$deleted = 0;
|
|
||||||
|
|
||||||
foreach ($orphans as $orphan) {
|
|
||||||
try {
|
|
||||||
$filesystem->delete($orphan->storage_path);
|
|
||||||
$deleted++;
|
|
||||||
} catch (FileNotFoundException $e) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->database->query('DELETE FROM `uploads` WHERE `user_id` IS NULL');
|
|
||||||
|
|
||||||
$this->session->alert(lang('deleted_orphans', [$deleted]));
|
|
||||||
|
|
||||||
return redirect($response, route('system'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param Response $response
|
|
||||||
*
|
|
||||||
* @return Response
|
|
||||||
*/
|
|
||||||
public function getThemes(Response $response): Response
|
|
||||||
{
|
|
||||||
$themes = make(Theme::class)->availableThemes();
|
|
||||||
|
|
||||||
$out = [];
|
|
||||||
|
|
||||||
foreach ($themes as $vendor => $list) {
|
|
||||||
$out["-- {$vendor} --"] = null;
|
|
||||||
foreach ($list as $name => $url) {
|
|
||||||
$out[$name] = "{$vendor}|{$url}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return json($response, $out);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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'));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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'));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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'));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,154 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
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\Filesystem;
|
|
||||||
use Monolog\Logger;
|
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @property Session session
|
|
||||||
* @property View view
|
|
||||||
* @property DB database
|
|
||||||
* @property Logger|null logger
|
|
||||||
* @property Filesystem|null storage
|
|
||||||
* @property Lang lang
|
|
||||||
* @property array config
|
|
||||||
*/
|
|
||||||
abstract class Controller
|
|
||||||
{
|
|
||||||
/** @var Container */
|
|
||||||
protected $container;
|
|
||||||
|
|
||||||
public function __construct(Container $container)
|
|
||||||
{
|
|
||||||
$this->container = $container;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param $name
|
|
||||||
*
|
|
||||||
* @return mixed|null
|
|
||||||
* @throws NotFoundException
|
|
||||||
*
|
|
||||||
* @throws DependencyException
|
|
||||||
*/
|
|
||||||
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 $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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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);
|
|
||||||
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,97 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
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
|
|
||||||
* @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);
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
$isAdmin = (bool) $this->session->get('admin', false);
|
|
||||||
|
|
||||||
/** @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);
|
|
||||||
|
|
||||||
$tags = make(TagRepository::class, [
|
|
||||||
'isAdmin' => $isAdmin,
|
|
||||||
'userId' => $this->session->get('user_id')
|
|
||||||
])->all();
|
|
||||||
|
|
||||||
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,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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'));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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'));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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'));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,194 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
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 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 Response $response
|
|
||||||
*
|
|
||||||
* @param Logger $logger
|
|
||||||
* @param Session $session
|
|
||||||
* @return Response
|
|
||||||
*/
|
|
||||||
public function upgrade(Response $response, Logger $logger, Session $session): 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'));
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$json = $this->getApiJson();
|
|
||||||
} catch (RuntimeException $e) {
|
|
||||||
$session->alert($e->getMessage(), 'danger');
|
|
||||||
|
|
||||||
return redirect($response, route('system'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (version_compare($json[0]->tag_name, PLATFORM_VERSION, '<=')) {
|
|
||||||
$session->alert(lang('already_latest_version'), 'warning');
|
|
||||||
|
|
||||||
return redirect($response, route('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'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filesize($tmpFile) !== $json[0]->assets[0]->size) {
|
|
||||||
$session->alert(lang('file_size_no_match'), 'danger');
|
|
||||||
|
|
||||||
return redirect($response, route('system'));
|
|
||||||
}
|
|
||||||
$logger->info('System update started.');
|
|
||||||
|
|
||||||
$config = require BASE_DIR.'config.php';
|
|
||||||
$config['maintenance'] = true;
|
|
||||||
|
|
||||||
file_put_contents(BASE_DIR.'config.php', '<?php'.PHP_EOL.'return '.var_export($config, true).';');
|
|
||||||
|
|
||||||
$currentFiles = array_merge(
|
|
||||||
glob_recursive(BASE_DIR.'app/*'),
|
|
||||||
glob_recursive(BASE_DIR.'bin/*'),
|
|
||||||
glob_recursive(BASE_DIR.'bootstrap/*'),
|
|
||||||
glob_recursive(BASE_DIR.'resources/templates/*'),
|
|
||||||
glob_recursive(BASE_DIR.'resources/lang/*'),
|
|
||||||
glob_recursive(BASE_DIR.'resources/schemas/*'),
|
|
||||||
glob_recursive(BASE_DIR.'static/*')
|
|
||||||
);
|
|
||||||
|
|
||||||
removeDirectory(BASE_DIR.'vendor/');
|
|
||||||
|
|
||||||
$updateZip = new ZipArchive();
|
|
||||||
$updateZip->open($tmpFile);
|
|
||||||
|
|
||||||
for ($i = 0; $i < $updateZip->numFiles; $i++) {
|
|
||||||
$nameIndex = $updateZip->getNameIndex($i);
|
|
||||||
|
|
||||||
$updateZip->extractTo(BASE_DIR, $nameIndex);
|
|
||||||
|
|
||||||
if (($key = array_search(rtrim(BASE_DIR.$nameIndex, '/'), $currentFiles)) !== false) {
|
|
||||||
unset($currentFiles[$key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$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'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param Request $request
|
|
||||||
* @param Response $response
|
|
||||||
*
|
|
||||||
* @return Response
|
|
||||||
*/
|
|
||||||
public function checkForUpdates(Request $request, Response $response): Response
|
|
||||||
{
|
|
||||||
$jsonResponse = [
|
|
||||||
'status' => 'OK',
|
|
||||||
'message' => lang('already_latest_version'),
|
|
||||||
'upgrade' => false,
|
|
||||||
];
|
|
||||||
|
|
||||||
$acceptPrerelease = param($request, 'prerelease', 'false') === 'true';
|
|
||||||
|
|
||||||
try {
|
|
||||||
$json = $this->getApiJson();
|
|
||||||
|
|
||||||
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]);
|
|
||||||
$jsonResponse['upgrade'] = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (version_compare($release->tag_name, PLATFORM_VERSION, '<=')) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} 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')),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function getApiJson()
|
|
||||||
{
|
|
||||||
$opts = [
|
|
||||||
'http' => [
|
|
||||||
'method' => 'GET',
|
|
||||||
'header' => [
|
|
||||||
'User-Agent: XBackBone-App',
|
|
||||||
'Accept: application/vnd.github.v3+json',
|
|
||||||
],
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
$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.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return json_decode($data);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,237 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
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')));
|
|
||||||
|
|
||||||
return view()->render($response, 'upload/web.twig', [
|
|
||||||
'max_file_size' => humanFileSize($maxFileSize),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param Request $request
|
|
||||||
* @param Response $response
|
|
||||||
* @return Response
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
public function uploadWeb(Request $request, Response $response): Response
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$file = $this->validateFile($request, $response);
|
|
||||||
|
|
||||||
$user = make(UserRepository::class)->get($request, $this->session->get('user_id'));
|
|
||||||
|
|
||||||
$this->validateUser($request, $response, $file, $user);
|
|
||||||
} catch (ValidationException $e) {
|
|
||||||
return $e->response();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$this->updateUserQuota($request, $user->id, $file->getSize())) {
|
|
||||||
$this->json['message'] = 'User disk quota exceeded.';
|
|
||||||
|
|
||||||
return json($response, $this->json, 507);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$response = $this->saveMedia($response, $file, $user);
|
|
||||||
$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;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param Request $request
|
|
||||||
* @param Response $response
|
|
||||||
*
|
|
||||||
* @return Response
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
public function uploadEndpoint(Request $request, Response $response): Response
|
|
||||||
{
|
|
||||||
if ($this->config['maintenance']) {
|
|
||||||
$this->json['message'] = 'Endpoint under maintenance.';
|
|
||||||
|
|
||||||
return json($response, $this->json, 503);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$file = $this->validateFile($request, $response);
|
|
||||||
|
|
||||||
if (param($request, 'token') === null) {
|
|
||||||
$this->json['message'] = 'Token not specified.';
|
|
||||||
|
|
||||||
return json($response, $this->json, 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = $this->database->query('SELECT * FROM `users` WHERE `token` = ? LIMIT 1', param($request, 'token'))->fetch();
|
|
||||||
|
|
||||||
$this->validateUser($request, $response, $file, $user);
|
|
||||||
} catch (ValidationException $e) {
|
|
||||||
return $e->response();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$this->updateUserQuota($request, $user->id, $file->getSize())) {
|
|
||||||
$this->json['message'] = 'User disk quota exceeded.';
|
|
||||||
|
|
||||||
return json($response, $this->json, 507);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$response = $this->saveMedia($response, $file, $user);
|
|
||||||
} catch (Exception $e) {
|
|
||||||
$this->updateUserQuota($request, $user->id, $file->getSize(), true);
|
|
||||||
throw $e;
|
|
||||||
}
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param Request $request
|
|
||||||
* @param Response $response
|
|
||||||
* @return UploadedFileInterface
|
|
||||||
* @throws ValidationException
|
|
||||||
*/
|
|
||||||
protected function validateFile(Request $request, Response $response)
|
|
||||||
{
|
|
||||||
$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?).';
|
|
||||||
|
|
||||||
throw new ValidationException(json($response, $this->json, 400));
|
|
||||||
}
|
|
||||||
|
|
||||||
$file = array_values($request->getUploadedFiles());
|
|
||||||
/** @var UploadedFileInterface|null $file */
|
|
||||||
$file = $file[0] ?? null;
|
|
||||||
|
|
||||||
if ($file === null) {
|
|
||||||
$this->json['message'] = 'Request without file attached.';
|
|
||||||
|
|
||||||
throw new ValidationException(json($response, $this->json, 400));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($file->getError() === UPLOAD_ERR_INI_SIZE) {
|
|
||||||
$this->json['message'] = 'File too large (upload_max_filesize too low?).';
|
|
||||||
|
|
||||||
throw new ValidationException(json($response, $this->json, 400));
|
|
||||||
}
|
|
||||||
|
|
||||||
return $file;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param Request $request
|
|
||||||
* @param Response $response
|
|
||||||
* @param UploadedFileInterface $file
|
|
||||||
* @param $user
|
|
||||||
* @return void
|
|
||||||
* @throws ValidationException
|
|
||||||
*/
|
|
||||||
protected function validateUser(Request $request, Response $response, UploadedFileInterface $file, $user)
|
|
||||||
{
|
|
||||||
if (!$user) {
|
|
||||||
$this->json['message'] = 'Token specified not found.';
|
|
||||||
|
|
||||||
throw new ValidationException(json($response, $this->json, 404));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$user->active) {
|
|
||||||
$this->json['message'] = 'Account disabled.';
|
|
||||||
|
|
||||||
throw new ValidationException(json($response, $this->json, 401));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param Response $response
|
|
||||||
* @param UploadedFileInterface $file
|
|
||||||
* @param $user
|
|
||||||
* @return Response
|
|
||||||
* @throws \League\Flysystem\FileExistsException
|
|
||||||
* @throws \League\Flysystem\FileNotFoundException
|
|
||||||
*/
|
|
||||||
protected function saveMedia(Response $response, UploadedFileInterface $file, $user)
|
|
||||||
{
|
|
||||||
do {
|
|
||||||
$code = humanRandomString();
|
|
||||||
} while ($this->database->query('SELECT COUNT(*) AS `count` FROM `uploads` WHERE `code` = ?', $code)->fetch()->count > 0);
|
|
||||||
|
|
||||||
$fileInfo = pathinfo($file->getClientFilename());
|
|
||||||
$storagePath = "$user->user_code/$code.$fileInfo[extension]";
|
|
||||||
|
|
||||||
$this->storage->writeStream($storagePath, $file->getStream()->detach());
|
|
||||||
|
|
||||||
$this->database->query('INSERT INTO `uploads`(`user_id`, `code`, `filename`, `storage_path`, `published`) VALUES (?, ?, ?, ?, ?)', [
|
|
||||||
$user->id,
|
|
||||||
$code,
|
|
||||||
$file->getClientFilename(),
|
|
||||||
$storagePath,
|
|
||||||
$user->hide_uploads == '1' ? 0 : 1,
|
|
||||||
]);
|
|
||||||
$mediaId = $this->database->getPdo()->lastInsertId();
|
|
||||||
|
|
||||||
if ($this->getSetting('auto_tagging') === 'on') {
|
|
||||||
$this->autoTag($mediaId, $storagePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
$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']}");
|
|
||||||
|
|
||||||
$this->logger->info("User $user->username uploaded new media.", [$mediaId]);
|
|
||||||
|
|
||||||
return json($response, $this->json, 201);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param $mediaId
|
|
||||||
* @param $storagePath
|
|
||||||
* @throws \League\Flysystem\FileNotFoundException
|
|
||||||
*/
|
|
||||||
protected function autoTag($mediaId, $storagePath)
|
|
||||||
{
|
|
||||||
$mime = $this->storage->getMimetype($storagePath);
|
|
||||||
|
|
||||||
[$type, $subtype] = explode('/', $mime);
|
|
||||||
|
|
||||||
/** @var TagRepository $query */
|
|
||||||
$query = make(TagRepository::class);
|
|
||||||
$query->addTag($type, $mediaId);
|
|
||||||
|
|
||||||
if ($type === 'application' || $subtype === 'gif') {
|
|
||||||
$query->addTag($subtype, $mediaId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,317 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
class UserController extends Controller
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
|
|
||||||
$users = $this->database->query('SELECT * FROM `users` LIMIT ? OFFSET ?', [self::PER_PAGE, $page * self::PER_PAGE])->fetchAll();
|
|
||||||
|
|
||||||
$pages = $this->database->query('SELECT COUNT(*) AS `count` FROM `users`')->fetch()->count / self::PER_PAGE;
|
|
||||||
|
|
||||||
return view()->render(
|
|
||||||
$response,
|
|
||||||
'user/index.twig',
|
|
||||||
[
|
|
||||||
'users' => $users,
|
|
||||||
'next' => $page < floor($pages),
|
|
||||||
'previous' => $page >= 1,
|
|
||||||
'current_page' => ++$page,
|
|
||||||
'quota_enabled' => $this->getSetting('quota_enabled'),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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
|
|
||||||
* @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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($maxUserQuota !== '-1') {
|
|
||||||
$maxUserQuota = stringToBytes($maxUserQuota);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
if ($validator->fails()) {
|
|
||||||
return redirect($response, route('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 (param($request, 'send_notification') !== null) {
|
|
||||||
$resetToken = null;
|
|
||||||
if (empty(param($request, 'password'))) {
|
|
||||||
$resetToken = bin2hex(random_bytes(16));
|
|
||||||
|
|
||||||
$this->database->query('UPDATE `users` SET `reset_token`=? WHERE `id` = ?', [
|
|
||||||
$resetToken,
|
|
||||||
$this->database->getPdo()->lastInsertId(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
$this->sendCreateNotification($request, $resetToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
$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']))]);
|
|
||||||
|
|
||||||
return redirect($response, route('user.index'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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 view()->render($response, 'user/edit.twig', [
|
|
||||||
'profile' => false,
|
|
||||||
'user' => $user,
|
|
||||||
'quota_enabled' => $this->getSetting('quota_enabled', 'off'),
|
|
||||||
'max_disk_quota' => $user->max_disk_quota > 0 ? humanFileSize($user->max_disk_quota, 0, true) : -1,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param Request $request
|
|
||||||
* @param Response $response
|
|
||||||
* @param int $id
|
|
||||||
*
|
|
||||||
* @return Response
|
|
||||||
*/
|
|
||||||
public function update(Request $request, Response $response, int $id): Response
|
|
||||||
{
|
|
||||||
$user = make(UserRepository::class)->get($request, $id);
|
|
||||||
$user->max_disk_quota = -1;
|
|
||||||
|
|
||||||
/** @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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($maxUserQuota !== '-1') {
|
|
||||||
$user->max_disk_quota = stringToBytes($maxUserQuota);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
if ($validator->fails()) {
|
|
||||||
return redirect($response, route('user.edit', ['id' => $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 ($user->id === $this->session->get('user_id')) {
|
|
||||||
$this->setSessionQuotaInfo($user->current_disk_quota, $user->max_disk_quota);
|
|
||||||
}
|
|
||||||
|
|
||||||
$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'])),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return redirect($response, route('user.index'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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 ($user->id === $this->session->get('user_id')) {
|
|
||||||
$this->session->alert(lang('cannot_delete'), 'danger');
|
|
||||||
|
|
||||||
return redirect($response, route('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.");
|
|
||||||
|
|
||||||
return redirect($response, route('user.index'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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);
|
|
||||||
|
|
||||||
$medias = $this->database->query('SELECT * FROM `uploads` WHERE `user_id` = ?', $user->id);
|
|
||||||
|
|
||||||
foreach ($medias as $media) {
|
|
||||||
try {
|
|
||||||
$this->storage->delete($media->storage_path);
|
|
||||||
} catch (FileNotFoundException $e) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$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,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->session->alert(lang('account_media_deleted'), 'success');
|
|
||||||
return redirect($response, route('user.edit', ['id' => $id]));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param Request $request
|
|
||||||
* @param Response $response
|
|
||||||
* @param int $id
|
|
||||||
*
|
|
||||||
* @return Response
|
|
||||||
*/
|
|
||||||
public function refreshToken(Request $request, Response $response, int $id): Response
|
|
||||||
{
|
|
||||||
$query = make(UserRepository::class);
|
|
||||||
$user = $query->get($request, $id, true);
|
|
||||||
|
|
||||||
$this->logger->info('User '.$this->session->get('username')." refreshed token of user $user->id.");
|
|
||||||
|
|
||||||
$response->getBody()->write($query->refreshToken($user->id));
|
|
||||||
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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]),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Database;
|
|
||||||
|
|
||||||
use PDO;
|
|
||||||
|
|
||||||
class DB
|
|
||||||
{
|
|
||||||
/** @var DB */
|
|
||||||
protected static $instance;
|
|
||||||
|
|
||||||
/** @var PDO */
|
|
||||||
protected $pdo;
|
|
||||||
|
|
||||||
/** @var string */
|
|
||||||
protected $currentDriver;
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
$this->currentDriver = $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
|
|
||||||
if ($this->currentDriver === 'sqlite') {
|
|
||||||
$this->pdo->exec('PRAGMA foreign_keys = ON');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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]);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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! :)';
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
throw new HttpUnauthorizedException($request);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $handler->handle($request);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
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'));
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
return redirect((new ResponseFactory())->createResponse(), route('login.show'));
|
|
||||||
}
|
|
||||||
|
|
||||||
return $handler->handle($request);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $handler->handle($request);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
abstract class Middleware extends Controller
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @param Request $request
|
|
||||||
* @param RequestHandler $handler
|
|
||||||
*
|
|
||||||
* @return Response
|
|
||||||
*/
|
|
||||||
abstract public function __invoke(Request $request, RequestHandler $handler);
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
159
app/Web/Lang.php
159
app/Web/Lang.php
|
@ -1,159 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Web;
|
|
||||||
|
|
||||||
class Lang
|
|
||||||
{
|
|
||||||
const DEFAULT_LANG = 'en';
|
|
||||||
const LANG_PATH = __DIR__.'../../resources/lang/';
|
|
||||||
|
|
||||||
/** @var string */
|
|
||||||
protected static $langPath = self::LANG_PATH;
|
|
||||||
|
|
||||||
/** @var string */
|
|
||||||
protected static $lang;
|
|
||||||
|
|
||||||
/** @var Lang */
|
|
||||||
protected static $instance;
|
|
||||||
|
|
||||||
/** @var array */
|
|
||||||
protected $cache = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return Lang
|
|
||||||
*/
|
|
||||||
public static function getInstance(): self
|
|
||||||
{
|
|
||||||
if (self::$instance === null) {
|
|
||||||
self::$instance = new self();
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
if ($langPath !== null) {
|
|
||||||
self::$langPath = $langPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
self::$instance = new self();
|
|
||||||
|
|
||||||
return self::$instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public static function getLang(): string
|
|
||||||
{
|
|
||||||
return self::$lang;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param $lang
|
|
||||||
*/
|
|
||||||
public static function setLang($lang)
|
|
||||||
{
|
|
||||||
self::$lang = $lang;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public static function getList()
|
|
||||||
{
|
|
||||||
$languages = [];
|
|
||||||
|
|
||||||
$default = count(include self::$langPath.self::DEFAULT_LANG.'.lang.php') - 1;
|
|
||||||
|
|
||||||
foreach (glob(self::$langPath.'*.lang.php') as $file) {
|
|
||||||
$dict = include $file;
|
|
||||||
|
|
||||||
if (!is_array($dict) || !isset($dict['lang'])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$count = count($dict) - 1;
|
|
||||||
$percent = min(round(($count / $default) * 100), 100);
|
|
||||||
|
|
||||||
$languages[str_replace('.lang.php', '', basename($file))] = "[{$percent}%] ".$dict['lang'];
|
|
||||||
}
|
|
||||||
|
|
||||||
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 $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($key, $transDict)) {
|
|
||||||
$string = @vsprintf($transDict[$key], $args);
|
|
||||||
if ($string !== false) {
|
|
||||||
return $string;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($lang !== self::DEFAULT_LANG) {
|
|
||||||
return $this->getString($key, self::DEFAULT_LANG, $args);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $key;
|
|
||||||
}
|
|
||||||
}
|
|
152
app/Web/Mail.php
152
app/Web/Mail.php
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,161 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
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']
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$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(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!$started) {
|
|
||||||
throw new Exception("Cannot start the HTTP session. The session path '{$path}' is not writable.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getId()
|
|
||||||
{
|
|
||||||
return session_id();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Destroy the current session.
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function destroy(): bool
|
|
||||||
{
|
|
||||||
return session_destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
* @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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
17
app/bootstrap/app.php
Normal file
17
app/bootstrap/app.php
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Application;
|
||||||
|
|
||||||
|
if (!defined('APP_ROOT')) {
|
||||||
|
define('APP_ROOT', dirname(__DIR__));
|
||||||
|
putenv('APP_ROOT='.APP_ROOT);
|
||||||
|
}
|
||||||
|
putenv('COMPOSER_VENDOR_DIR='.APP_ROOT.'/vendor');
|
||||||
|
|
||||||
|
/** @var Application $app */
|
||||||
|
$app = require APP_ROOT.'/vendor/xbb/core/bootstrap/app.php';
|
||||||
|
|
||||||
|
return $app->usePublicPath(APP_ROOT.'/public')
|
||||||
|
->useEnvironmentPath(APP_ROOT)
|
||||||
|
->useStoragePath(APP_ROOT.'/storage')
|
||||||
|
->useBootstrapPath(__DIR__);
|
2
app/bootstrap/cache/.gitignore
vendored
Normal file
2
app/bootstrap/cache/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
*
|
||||||
|
!.gitignore
|
35
app/composer.json
Normal file
35
app/composer.json
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
{
|
||||||
|
"name": "xbb/app",
|
||||||
|
"description": "The standalone xbackbone application",
|
||||||
|
"license": "AGPL-3.0-only",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Sergio Brighenti",
|
||||||
|
"email": "sergio@brighenti.me"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"repositories": [
|
||||||
|
{
|
||||||
|
"type": "path",
|
||||||
|
"url": "../core/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"xbb/core": "dev-next as 1.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"post-autoload-dump": [
|
||||||
|
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||||
|
"@php xbb package:discover --ansi",
|
||||||
|
"@php xbb vendor:publish --tag=app --force --ansi",
|
||||||
|
"@php xbb vendor:publish --tag=app-img --ansi"
|
||||||
|
],
|
||||||
|
"post-create-project-cmd": [
|
||||||
|
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
|
||||||
|
"@php xbb key:generate --ansi",
|
||||||
|
"@php -r \"file_exists('xbb.db') || touch('xbb.db');\"",
|
||||||
|
"@php xbb migrate --graceful --ansi"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
6609
app/composer.lock
generated
Normal file
6609
app/composer.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
530
app/helpers.php
530
app/helpers.php
|
@ -1,530 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
use Psr\Http\Message\ResponseInterface as Response;
|
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
|
||||||
use Slim\Factory\ServerRequestCreatorFactory;
|
|
||||||
|
|
||||||
if (!defined('HUMAN_RANDOM_CHARS')) {
|
|
||||||
define('HUMAN_RANDOM_CHARS', 'bcdfghjklmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZaeiouAEIOU');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!function_exists('humanFileSize')) {
|
|
||||||
/**
|
|
||||||
* Generate a human readable file size.
|
|
||||||
*
|
|
||||||
* @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];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!function_exists('stringToBytes')) {
|
|
||||||
/**
|
|
||||||
* @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);
|
|
||||||
|
|
||||||
$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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!function_exists('removeDirectory')) {
|
|
||||||
/**
|
|
||||||
* Remove a directory and it's content.
|
|
||||||
*
|
|
||||||
* @param $path
|
|
||||||
*/
|
|
||||||
function removeDirectory($path)
|
|
||||||
{
|
|
||||||
cleanDirectory($path, true);
|
|
||||||
rmdir($path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!function_exists('lang')) {
|
|
||||||
/**
|
|
||||||
* @param string $key
|
|
||||||
* @param array $args
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
function lang(string $key, $args = []): string
|
|
||||||
{
|
|
||||||
return resolve('lang')->get($key, $args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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',
|
|
||||||
];
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
$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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
21
app/public/.htaccess
Normal file
21
app/public/.htaccess
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<IfModule mod_rewrite.c>
|
||||||
|
<IfModule mod_negotiation.c>
|
||||||
|
Options -MultiViews -Indexes
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
RewriteEngine On
|
||||||
|
|
||||||
|
# Handle Authorization Header
|
||||||
|
RewriteCond %{HTTP:Authorization} .
|
||||||
|
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
|
||||||
|
|
||||||
|
# Redirect Trailing Slashes If Not A Folder...
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteCond %{REQUEST_URI} (.+)/$
|
||||||
|
RewriteRule ^ %1 [L,R=301]
|
||||||
|
|
||||||
|
# Send Requests To Front Controller...
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteRule ^ index.php [L]
|
||||||
|
</IfModule>
|
16
app/public/index.php
Normal file
16
app/public/index.php
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
define('LARAVEL_START', microtime(true));
|
||||||
|
|
||||||
|
// Determine if the application is in maintenance mode...
|
||||||
|
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
|
||||||
|
require $maintenance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the Composer autoloader...
|
||||||
|
require __DIR__.'/../vendor/autoload.php';
|
||||||
|
|
||||||
|
// Bootstrap Laravel and handle the request...
|
||||||
|
(require_once __DIR__.'/../bootstrap/app.php')->handleRequest(Request::capture());
|
2
app/public/robots.txt
Normal file
2
app/public/robots.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
|
@ -1,93 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
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\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');
|
|
||||||
|
|
||||||
$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');
|
|
||||||
|
|
||||||
$group->get('/system/themes', [AdminController::class, 'getThemes'])->setName('theme');
|
|
||||||
|
|
||||||
$group->post('/system/settings/save', [SettingController::class, 'saveSettings'])->setName('settings.save');
|
|
||||||
|
|
||||||
$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');
|
|
||||||
|
|
||||||
$group->get('/system', [AdminController::class, 'system'])->setName('system');
|
|
||||||
|
|
||||||
$group->get('/users[/page/{page}]', [UserController::class, 'index'])->setName('user.index');
|
|
||||||
})->add(AdminMiddleware::class);
|
|
||||||
|
|
||||||
$group->group('/user', function (RouteCollectorProxy $group) {
|
|
||||||
$group->get('/create', [UserController::class, 'create'])->setName('user.create');
|
|
||||||
$group->post('/create', [UserController::class, 'store'])->setName('user.store');
|
|
||||||
$group->get('/{id}/edit', [UserController::class, 'edit'])->setName('user.edit');
|
|
||||||
$group->post('/{id}', [UserController::class, 'update'])->setName('user.update');
|
|
||||||
$group->get('/{id}/delete', [UserController::class, 'delete'])->setName('user.delete');
|
|
||||||
$group->get('/{id}/clear', [UserController::class, 'clearUserMedia'])->setName('user.clear');
|
|
||||||
})->add(AdminMiddleware::class);
|
|
||||||
|
|
||||||
$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');
|
|
||||||
|
|
||||||
$group->get('/user/{id}/export', [ExportController::class, 'downloadData'])->setName('export.data');
|
|
||||||
|
|
||||||
$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');
|
|
||||||
|
|
||||||
$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->post('/upload', [UploadController::class, 'uploadEndpoint'])->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');
|
|
3
app/storage/app/.gitignore
vendored
Normal file
3
app/storage/app/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
*
|
||||||
|
!public/
|
||||||
|
!.gitignore
|
2
app/storage/app/public/.gitignore
vendored
Normal file
2
app/storage/app/public/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
*
|
||||||
|
!.gitignore
|
9
app/storage/framework/.gitignore
vendored
Normal file
9
app/storage/framework/.gitignore
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
compiled.php
|
||||||
|
config.php
|
||||||
|
down
|
||||||
|
events.scanned.php
|
||||||
|
maintenance.php
|
||||||
|
routes.php
|
||||||
|
routes.scanned.php
|
||||||
|
schedule-*
|
||||||
|
services.json
|
3
app/storage/framework/cache/.gitignore
vendored
Normal file
3
app/storage/framework/cache/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
*
|
||||||
|
!data/
|
||||||
|
!.gitignore
|
2
app/storage/framework/cache/data/.gitignore
vendored
Normal file
2
app/storage/framework/cache/data/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
*
|
||||||
|
!.gitignore
|
2
app/storage/framework/sessions/.gitignore
vendored
Normal file
2
app/storage/framework/sessions/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
*
|
||||||
|
!.gitignore
|
2
app/storage/framework/testing/.gitignore
vendored
Normal file
2
app/storage/framework/testing/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
*
|
||||||
|
!.gitignore
|
2
app/storage/framework/views/.gitignore
vendored
Normal file
2
app/storage/framework/views/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
*
|
||||||
|
!.gitignore
|
2
app/storage/logs/.gitignore
vendored
Normal file
2
app/storage/logs/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
*
|
||||||
|
!.gitignore
|
12
app/xbb
Normal file
12
app/xbb
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Symfony\Component\Console\Input\ArgvInput;
|
||||||
|
|
||||||
|
define('LARAVEL_START', microtime(true));
|
||||||
|
|
||||||
|
// Register the Composer autoloader...
|
||||||
|
require __DIR__.'/vendor/autoload.php';
|
||||||
|
|
||||||
|
// Bootstrap Laravel and handle the command...
|
||||||
|
exit((require_once __DIR__.'/bootstrap/app.php')->handleCommand(new ArgvInput));
|
28
bin/clean
28
bin/clean
|
@ -1,28 +0,0 @@
|
||||||
#!/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();
|
|
||||||
}
|
|
||||||
|
|
||||||
require __DIR__ . '/../vendor/autoload.php';
|
|
||||||
|
|
||||||
$action = isset($argv[1]) ? $argv[1] : 'all';
|
|
||||||
|
|
||||||
switch ($action) {
|
|
||||||
case 'cache':
|
|
||||||
cleanDirectory(__DIR__ . '/../resources/cache');
|
|
||||||
break;
|
|
||||||
case 'sessions':
|
|
||||||
cleanDirectory(__DIR__ . '/../resources/sessions');
|
|
||||||
break;
|
|
||||||
case 'all':
|
|
||||||
cleanDirectory(__DIR__ . '/../resources/cache');
|
|
||||||
cleanDirectory(__DIR__ . '/../resources/sessions');
|
|
||||||
break;
|
|
||||||
case 'help':
|
|
||||||
default:
|
|
||||||
echo 'Usage: php ' . $argv[0] . ' <cache|sessions|all|help>' . PHP_EOL;
|
|
||||||
}
|
|
||||||
|
|
||||||
exit(0);
|
|
45
bin/migrate
45
bin/migrate
|
@ -1,45 +0,0 @@
|
||||||
#!/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();
|
|
||||||
}
|
|
||||||
|
|
||||||
use App\Database\Migrator;
|
|
||||||
use DI\ContainerBuilder;
|
|
||||||
|
|
||||||
require __DIR__.'/../vendor/autoload.php';
|
|
||||||
|
|
||||||
define('BASE_DIR', realpath(__DIR__.'/../').DIRECTORY_SEPARATOR);
|
|
||||||
|
|
||||||
$config = include __DIR__.'/../config.php';
|
|
||||||
|
|
||||||
if (!$config) {
|
|
||||||
die('config.php not found. Please create a new one.');
|
|
||||||
}
|
|
||||||
|
|
||||||
chdir(BASE_DIR);
|
|
||||||
|
|
||||||
$builder = new ContainerBuilder();
|
|
||||||
$builder->addDefinitions(BASE_DIR.'bootstrap/container.php');
|
|
||||||
|
|
||||||
$container = $builder->build();
|
|
||||||
$container->set('config', $config);
|
|
||||||
|
|
||||||
$db = $container->get('database');
|
|
||||||
|
|
||||||
$migrator = new Migrator($db, 'resources/schemas');
|
|
||||||
$migrator->migrate();
|
|
||||||
$migrator->reSyncQuotas($container->get('storage'));
|
|
||||||
|
|
||||||
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)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file_exists(__DIR__.'/../install') && (!isset($config['debug']) || !$config['debug'])) {
|
|
||||||
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);
|
|
|
@ -1,121 +0,0 @@
|
||||||
<?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\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;
|
|
||||||
|
|
||||||
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.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
$builder = new ContainerBuilder();
|
|
||||||
|
|
||||||
if (!$config['debug']) {
|
|
||||||
$builder->enableCompilation(BASE_DIR.'/resources/cache/di');
|
|
||||||
$builder->writeProxiesToFile(true, BASE_DIR.'/resources/cache/di');
|
|
||||||
}
|
|
||||||
|
|
||||||
$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),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$builder->addDefinitions(__DIR__.'/container.php');
|
|
||||||
|
|
||||||
global $app;
|
|
||||||
$app = Bridge::create($builder->build());
|
|
||||||
$app->getContainer()->set('config', $config);
|
|
||||||
$app->setBasePath(parse_url($config['base_url'], PHP_URL_PATH) ?: '');
|
|
||||||
|
|
||||||
if (!$config['debug']) {
|
|
||||||
$app->getRouteCollector()->setCacheFile(BASE_DIR.'resources/cache/routes.cache.php');
|
|
||||||
}
|
|
||||||
|
|
||||||
$app->add(InjectMiddleware::class);
|
|
||||||
$app->add(LangMiddleware::class);
|
|
||||||
$app->add(RememberMiddleware::class);
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
|
|
||||||
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 ($request->getMethod() === 'GET') {
|
|
||||||
return $app->getResponseFactory()
|
|
||||||
->createResponse(301)
|
|
||||||
->withHeader('Location', (string) $uri);
|
|
||||||
} else {
|
|
||||||
$request = $request->withUri($uri);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $handler->handle($request);
|
|
||||||
});
|
|
||||||
|
|
||||||
$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';
|
|
||||||
|
|
||||||
return $app;
|
|
|
@ -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),
|
|
||||||
];
|
|
|
@ -1,59 +0,0 @@
|
||||||
{
|
|
||||||
"name": "sergix44/xbackbone",
|
|
||||||
"license": "AGPL-3.0-only",
|
|
||||||
"version": "3.7.0",
|
|
||||||
"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",
|
|
||||||
"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",
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"autoload": {
|
|
||||||
"files": [
|
|
||||||
"app/helpers.php"
|
|
||||||
],
|
|
||||||
"psr-4": {
|
|
||||||
"App\\": "app/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"autoload-dev": {
|
|
||||||
"psr-4": {
|
|
||||||
"Tests\\": "tests/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
7169
composer.lock
generated
7169
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,15 +0,0 @@
|
||||||
<?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',
|
|
||||||
],
|
|
||||||
];
|
|
18
core/.editorconfig
Normal file
18
core/.editorconfig
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[*.{yml,yaml}]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[docker-compose.yml]
|
||||||
|
indent_size = 4
|
63
core/.env.example
Normal file
63
core/.env.example
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
APP_ENV=local
|
||||||
|
APP_KEY=
|
||||||
|
APP_DEBUG=true
|
||||||
|
APP_TIMEZONE=UTC
|
||||||
|
APP_URL=http://localhost
|
||||||
|
|
||||||
|
APP_LOCALE=en
|
||||||
|
APP_FALLBACK_LOCALE=en
|
||||||
|
APP_FAKER_LOCALE=en_US
|
||||||
|
|
||||||
|
APP_MAINTENANCE_DRIVER=file
|
||||||
|
# APP_MAINTENANCE_STORE=database
|
||||||
|
|
||||||
|
BCRYPT_ROUNDS=12
|
||||||
|
|
||||||
|
LOG_CHANNEL=stack
|
||||||
|
LOG_STACK=single
|
||||||
|
LOG_DEPRECATIONS_CHANNEL=null
|
||||||
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
|
DB_CONNECTION=sqlite
|
||||||
|
# DB_HOST=127.0.0.1
|
||||||
|
# DB_PORT=3306
|
||||||
|
# DB_DATABASE=laravel
|
||||||
|
# DB_USERNAME=root
|
||||||
|
# DB_PASSWORD=
|
||||||
|
|
||||||
|
SESSION_DRIVER=database
|
||||||
|
SESSION_LIFETIME=120
|
||||||
|
SESSION_ENCRYPT=false
|
||||||
|
SESSION_PATH=/
|
||||||
|
SESSION_DOMAIN=null
|
||||||
|
|
||||||
|
BROADCAST_CONNECTION=log
|
||||||
|
FILESYSTEM_DISK=local
|
||||||
|
QUEUE_CONNECTION=database
|
||||||
|
|
||||||
|
CACHE_STORE=database
|
||||||
|
CACHE_PREFIX=
|
||||||
|
|
||||||
|
MEMCACHED_HOST=127.0.0.1
|
||||||
|
|
||||||
|
REDIS_CLIENT=phpredis
|
||||||
|
REDIS_HOST=127.0.0.1
|
||||||
|
REDIS_PASSWORD=null
|
||||||
|
REDIS_PORT=6379
|
||||||
|
|
||||||
|
MAIL_MAILER=log
|
||||||
|
MAIL_HOST=127.0.0.1
|
||||||
|
MAIL_PORT=2525
|
||||||
|
MAIL_USERNAME=null
|
||||||
|
MAIL_PASSWORD=null
|
||||||
|
MAIL_ENCRYPTION=null
|
||||||
|
MAIL_FROM_ADDRESS="hello@example.com"
|
||||||
|
MAIL_FROM_NAME="${APP_NAME}"
|
||||||
|
|
||||||
|
AWS_ACCESS_KEY_ID=
|
||||||
|
AWS_SECRET_ACCESS_KEY=
|
||||||
|
AWS_DEFAULT_REGION=us-east-1
|
||||||
|
AWS_BUCKET=
|
||||||
|
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||||
|
|
||||||
|
VITE_APP_NAME="${APP_NAME}"
|
11
core/.gitattributes
vendored
Normal file
11
core/.gitattributes
vendored
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
*.blade.php diff=html
|
||||||
|
*.css diff=css
|
||||||
|
*.html diff=html
|
||||||
|
*.md diff=markdown
|
||||||
|
*.php diff=php
|
||||||
|
|
||||||
|
/.github export-ignore
|
||||||
|
CHANGELOG.md export-ignore
|
||||||
|
.styleci.yml export-ignore
|
21
core/.gitignore
vendored
Normal file
21
core/.gitignore
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
/.phpunit.cache
|
||||||
|
/node_modules
|
||||||
|
/public/build
|
||||||
|
/public/hot
|
||||||
|
/public/storage
|
||||||
|
/storage/*.key
|
||||||
|
/vendor
|
||||||
|
.env
|
||||||
|
.env.backup
|
||||||
|
.env.production
|
||||||
|
.phpactor.json
|
||||||
|
.phpunit.result.cache
|
||||||
|
Homestead.json
|
||||||
|
Homestead.yaml
|
||||||
|
auth.json
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
/.fleet
|
||||||
|
/.idea
|
||||||
|
/.vscode
|
||||||
|
/*.db
|
66
core/README.md
Normal file
66
core/README.md
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
|
||||||
|
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
|
||||||
|
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
|
||||||
|
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## About Laravel
|
||||||
|
|
||||||
|
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
|
||||||
|
|
||||||
|
- [Simple, fast routing engine](https://laravel.com/docs/routing).
|
||||||
|
- [Powerful dependency injection container](https://laravel.com/docs/container).
|
||||||
|
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
|
||||||
|
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
|
||||||
|
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
|
||||||
|
- [Robust background job processing](https://laravel.com/docs/queues).
|
||||||
|
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
|
||||||
|
|
||||||
|
Laravel is accessible, powerful, and provides tools required for large, robust applications.
|
||||||
|
|
||||||
|
## Learning Laravel
|
||||||
|
|
||||||
|
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
|
||||||
|
|
||||||
|
You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch.
|
||||||
|
|
||||||
|
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
|
||||||
|
|
||||||
|
## Laravel Sponsors
|
||||||
|
|
||||||
|
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
|
||||||
|
|
||||||
|
### Premium Partners
|
||||||
|
|
||||||
|
- **[Vehikl](https://vehikl.com/)**
|
||||||
|
- **[Tighten Co.](https://tighten.co)**
|
||||||
|
- **[WebReinvent](https://webreinvent.com/)**
|
||||||
|
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
|
||||||
|
- **[64 Robots](https://64robots.com)**
|
||||||
|
- **[Curotec](https://www.curotec.com/services/technologies/laravel/)**
|
||||||
|
- **[Cyber-Duck](https://cyber-duck.co.uk)**
|
||||||
|
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
|
||||||
|
- **[Jump24](https://jump24.co.uk)**
|
||||||
|
- **[Redberry](https://redberry.international/laravel/)**
|
||||||
|
- **[Active Logic](https://activelogic.com)**
|
||||||
|
- **[byte5](https://byte5.de)**
|
||||||
|
- **[OP.GG](https://op.gg)**
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
|
||||||
|
|
||||||
|
## Code of Conduct
|
||||||
|
|
||||||
|
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
|
||||||
|
|
||||||
|
## Security Vulnerabilities
|
||||||
|
|
||||||
|
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
|
40
core/app/Actions/Fortify/CreateNewUser.php
Normal file
40
core/app/Actions/Fortify/CreateNewUser.php
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Actions\Fortify;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Laravel\Fortify\Contracts\CreatesNewUsers;
|
||||||
|
|
||||||
|
class CreateNewUser implements CreatesNewUsers
|
||||||
|
{
|
||||||
|
use PasswordValidationRules;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate and create a newly registered user.
|
||||||
|
*
|
||||||
|
* @param array<string, string> $input
|
||||||
|
*/
|
||||||
|
public function create(array $input): User
|
||||||
|
{
|
||||||
|
Validator::make($input, [
|
||||||
|
'name' => ['required', 'string', 'max:255'],
|
||||||
|
'email' => [
|
||||||
|
'required',
|
||||||
|
'string',
|
||||||
|
'email',
|
||||||
|
'max:255',
|
||||||
|
Rule::unique(User::class),
|
||||||
|
],
|
||||||
|
'password' => $this->passwordRules(),
|
||||||
|
])->validate();
|
||||||
|
|
||||||
|
return User::create([
|
||||||
|
'name' => $input['name'],
|
||||||
|
'email' => $input['email'],
|
||||||
|
'password' => Hash::make($input['password']),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
18
core/app/Actions/Fortify/PasswordValidationRules.php
Normal file
18
core/app/Actions/Fortify/PasswordValidationRules.php
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Actions\Fortify;
|
||||||
|
|
||||||
|
use Illuminate\Validation\Rules\Password;
|
||||||
|
|
||||||
|
trait PasswordValidationRules
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the validation rules used to validate passwords.
|
||||||
|
*
|
||||||
|
* @return array<int, \Illuminate\Contracts\Validation\Rule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
protected function passwordRules(): array
|
||||||
|
{
|
||||||
|
return ['required', 'string', Password::default(), 'confirmed'];
|
||||||
|
}
|
||||||
|
}
|
29
core/app/Actions/Fortify/ResetUserPassword.php
Normal file
29
core/app/Actions/Fortify/ResetUserPassword.php
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Actions\Fortify;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
use Laravel\Fortify\Contracts\ResetsUserPasswords;
|
||||||
|
|
||||||
|
class ResetUserPassword implements ResetsUserPasswords
|
||||||
|
{
|
||||||
|
use PasswordValidationRules;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate and reset the user's forgotten password.
|
||||||
|
*
|
||||||
|
* @param array<string, string> $input
|
||||||
|
*/
|
||||||
|
public function reset(User $user, array $input): void
|
||||||
|
{
|
||||||
|
Validator::make($input, [
|
||||||
|
'password' => $this->passwordRules(),
|
||||||
|
])->validate();
|
||||||
|
|
||||||
|
$user->forceFill([
|
||||||
|
'password' => Hash::make($input['password']),
|
||||||
|
])->save();
|
||||||
|
}
|
||||||
|
}
|
32
core/app/Actions/Fortify/UpdateUserPassword.php
Normal file
32
core/app/Actions/Fortify/UpdateUserPassword.php
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Actions\Fortify;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
use Laravel\Fortify\Contracts\UpdatesUserPasswords;
|
||||||
|
|
||||||
|
class UpdateUserPassword implements UpdatesUserPasswords
|
||||||
|
{
|
||||||
|
use PasswordValidationRules;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate and update the user's password.
|
||||||
|
*
|
||||||
|
* @param array<string, string> $input
|
||||||
|
*/
|
||||||
|
public function update(User $user, array $input): void
|
||||||
|
{
|
||||||
|
Validator::make($input, [
|
||||||
|
'current_password' => ['required', 'string', 'current_password:web'],
|
||||||
|
'password' => $this->passwordRules(),
|
||||||
|
], [
|
||||||
|
'current_password.current_password' => __('The provided password does not match your current password.'),
|
||||||
|
])->validateWithBag('updatePassword');
|
||||||
|
|
||||||
|
$user->forceFill([
|
||||||
|
'password' => Hash::make($input['password']),
|
||||||
|
])->save();
|
||||||
|
}
|
||||||
|
}
|
58
core/app/Actions/Fortify/UpdateUserProfileInformation.php
Normal file
58
core/app/Actions/Fortify/UpdateUserProfileInformation.php
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Actions\Fortify;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
|
||||||
|
|
||||||
|
class UpdateUserProfileInformation implements UpdatesUserProfileInformation
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Validate and update the given user's profile information.
|
||||||
|
*
|
||||||
|
* @param array<string, string> $input
|
||||||
|
*/
|
||||||
|
public function update(User $user, array $input): void
|
||||||
|
{
|
||||||
|
Validator::make($input, [
|
||||||
|
'name' => ['required', 'string', 'max:255'],
|
||||||
|
|
||||||
|
'email' => [
|
||||||
|
'required',
|
||||||
|
'string',
|
||||||
|
'email',
|
||||||
|
'max:255',
|
||||||
|
Rule::unique('users')->ignore($user->id),
|
||||||
|
],
|
||||||
|
])->validateWithBag('updateProfileInformation');
|
||||||
|
|
||||||
|
if ($input['email'] !== $user->email &&
|
||||||
|
$user instanceof MustVerifyEmail) {
|
||||||
|
$this->updateVerifiedUser($user, $input);
|
||||||
|
} else {
|
||||||
|
$user->forceFill([
|
||||||
|
'name' => $input['name'],
|
||||||
|
'email' => $input['email'],
|
||||||
|
])->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the given verified user's profile information.
|
||||||
|
*
|
||||||
|
* @param array<string, string> $input
|
||||||
|
*/
|
||||||
|
protected function updateVerifiedUser(User $user, array $input): void
|
||||||
|
{
|
||||||
|
$user->forceFill([
|
||||||
|
'name' => $input['name'],
|
||||||
|
'email' => $input['email'],
|
||||||
|
'email_verified_at' => null,
|
||||||
|
])->save();
|
||||||
|
|
||||||
|
$user->sendEmailVerificationNotification();
|
||||||
|
}
|
||||||
|
}
|
20
core/app/Features/AlphabetForIds.php
Normal file
20
core/app/Features/AlphabetForIds.php
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Features;
|
||||||
|
|
||||||
|
use Illuminate\Support\Lottery;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Sqids\Sqids;
|
||||||
|
|
||||||
|
class AlphabetForIds
|
||||||
|
{
|
||||||
|
public string $name = 'id-alphabet';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the feature's initial value.
|
||||||
|
*/
|
||||||
|
public function resolve(mixed $scope): mixed
|
||||||
|
{
|
||||||
|
return str_shuffle(Sqids::DEFAULT_ALPHABET);
|
||||||
|
}
|
||||||
|
}
|
18
core/app/Features/SignUp.php
Normal file
18
core/app/Features/SignUp.php
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Features;
|
||||||
|
|
||||||
|
use Illuminate\Support\Lottery;
|
||||||
|
|
||||||
|
class SignUp
|
||||||
|
{
|
||||||
|
public string $name = 'signup';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the feature's initial value.
|
||||||
|
*/
|
||||||
|
public function resolve(mixed $scope): mixed
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
8
core/app/Http/Controllers/Controller.php
Normal file
8
core/app/Http/Controllers/Controller.php
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
abstract class Controller
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
30
core/app/Livewire/Auth/Login.php
Normal file
30
core/app/Livewire/Auth/Login.php
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Auth;
|
||||||
|
|
||||||
|
use App\Livewire\Forms\LoginForm;
|
||||||
|
use Laravel\Fortify\Fortify;
|
||||||
|
use Livewire\Component;
|
||||||
|
use Mary\Traits\Toast;
|
||||||
|
|
||||||
|
class Login extends Component
|
||||||
|
{
|
||||||
|
use Toast;
|
||||||
|
|
||||||
|
public LoginForm $form;
|
||||||
|
|
||||||
|
public function authenticate()
|
||||||
|
{
|
||||||
|
$this->validate();
|
||||||
|
|
||||||
|
$this->form->authenticate();
|
||||||
|
|
||||||
|
return redirect()->intended(Fortify::redirects('dashboard'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.auth.login')
|
||||||
|
->layout('components.layouts.auth', ['title' => 'Login']);
|
||||||
|
}
|
||||||
|
}
|
72
core/app/Livewire/Forms/LoginForm.php
Normal file
72
core/app/Livewire/Forms/LoginForm.php
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Forms;
|
||||||
|
|
||||||
|
use Illuminate\Auth\Events\Lockout;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\RateLimiter;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Livewire\Attributes\Validate;
|
||||||
|
use Livewire\Form;
|
||||||
|
|
||||||
|
class LoginForm extends Form
|
||||||
|
{
|
||||||
|
#[Validate('required|string|email')]
|
||||||
|
public string $email = '';
|
||||||
|
|
||||||
|
#[Validate('required|string')]
|
||||||
|
public string $password = '';
|
||||||
|
|
||||||
|
#[Validate('boolean')]
|
||||||
|
public bool $remember = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to authenticate the request's credentials.
|
||||||
|
*
|
||||||
|
* @throws \Illuminate\Validation\ValidationException
|
||||||
|
*/
|
||||||
|
public function authenticate(): void
|
||||||
|
{
|
||||||
|
$this->ensureIsNotRateLimited();
|
||||||
|
|
||||||
|
if (!Auth::attempt($this->only(['email', 'password']), $this->remember)) {
|
||||||
|
RateLimiter::hit($this->throttleKey());
|
||||||
|
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'form.email' => trans('auth.failed'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
RateLimiter::clear($this->throttleKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the authentication request is not rate limited.
|
||||||
|
*/
|
||||||
|
protected function ensureIsNotRateLimited(): void
|
||||||
|
{
|
||||||
|
if (!RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event(new Lockout(request()));
|
||||||
|
|
||||||
|
$seconds = RateLimiter::availableIn($this->throttleKey());
|
||||||
|
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'form.email' => trans('auth.throttle', [
|
||||||
|
'seconds' => $seconds,
|
||||||
|
'minutes' => ceil($seconds / 60),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the authentication rate limiting throttle key.
|
||||||
|
*/
|
||||||
|
protected function throttleKey(): string
|
||||||
|
{
|
||||||
|
return Str::transliterate(Str::lower($this->email).'|'.request()->ip());
|
||||||
|
}
|
||||||
|
}
|
69
core/app/Livewire/Welcome.php
Normal file
69
core/app/Livewire/Welcome.php
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire;
|
||||||
|
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Livewire\Component;
|
||||||
|
use Mary\Traits\Toast;
|
||||||
|
|
||||||
|
class Welcome extends Component
|
||||||
|
{
|
||||||
|
use Toast;
|
||||||
|
|
||||||
|
public string $search = '';
|
||||||
|
|
||||||
|
public bool $drawer = false;
|
||||||
|
|
||||||
|
public array $sortBy = ['column' => 'name', 'direction' => 'asc'];
|
||||||
|
|
||||||
|
// Clear filters
|
||||||
|
public function clear(): void
|
||||||
|
{
|
||||||
|
$this->reset();
|
||||||
|
$this->success('Filters cleared.', position: 'toast-bottom');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete action
|
||||||
|
public function delete($id): void
|
||||||
|
{
|
||||||
|
$this->warning("Will delete #$id", 'It is fake.', position: 'toast-bottom');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Table headers
|
||||||
|
public function headers(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
['key' => 'id', 'label' => '#', 'class' => 'w-1'],
|
||||||
|
['key' => 'name', 'label' => 'Name', 'class' => 'w-64'],
|
||||||
|
['key' => 'age', 'label' => 'Age', 'class' => 'w-20'],
|
||||||
|
['key' => 'email', 'label' => 'E-mail', 'sortable' => false],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For demo purpose, this is a static collection.
|
||||||
|
*
|
||||||
|
* On real projects you do it with Eloquent collections.
|
||||||
|
* Please, refer to maryUI docs to see the eloquent examples.
|
||||||
|
*/
|
||||||
|
public function users(): Collection
|
||||||
|
{
|
||||||
|
return collect([
|
||||||
|
['id' => 1, 'name' => 'Mary', 'email' => 'mary@mary-ui.com', 'age' => 23],
|
||||||
|
['id' => 2, 'name' => 'Giovanna', 'email' => 'giovanna@mary-ui.com', 'age' => 7],
|
||||||
|
['id' => 3, 'name' => 'Marina', 'email' => 'marina@mary-ui.com', 'age' => 5],
|
||||||
|
])
|
||||||
|
->sortBy([[...array_values($this->sortBy)]])
|
||||||
|
->when($this->search, function (Collection $collection) {
|
||||||
|
return $collection->filter(fn(array $item) => str($item['name'])->contains($this->search, true));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.welcome', [
|
||||||
|
'users' => $this->users(),
|
||||||
|
'headers' => $this->headers()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
13
core/app/Models/Properties/ResourceType.php
Normal file
13
core/app/Models/Properties/ResourceType.php
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models\Properties;
|
||||||
|
|
||||||
|
enum ResourceType: string
|
||||||
|
{
|
||||||
|
case IMAGE = 'IMAGE';
|
||||||
|
case VIDEO = 'VIDEO';
|
||||||
|
case AUDIO = 'AUDIO';
|
||||||
|
case PDF = 'PDF';
|
||||||
|
case FILE = 'FILE';
|
||||||
|
case LINK = 'LINK';
|
||||||
|
}
|
49
core/app/Models/Resource.php
Normal file
49
core/app/Models/Resource.php
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Models\Properties\ResourceType;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class Resource extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'type',
|
||||||
|
'user_id',
|
||||||
|
'code',
|
||||||
|
'hidden',
|
||||||
|
'target',
|
||||||
|
'filename',
|
||||||
|
'size',
|
||||||
|
'mime',
|
||||||
|
'views',
|
||||||
|
'downloads',
|
||||||
|
'password',
|
||||||
|
'published_at',
|
||||||
|
'expires_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $hidden = [
|
||||||
|
'password',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => ResourceType::class,
|
||||||
|
'hidden' => 'boolean',
|
||||||
|
'published_at' => 'datetime',
|
||||||
|
'expires_at' => 'datetime',
|
||||||
|
'password' => 'hashed',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
}
|
65
core/app/Models/User.php
Normal file
65
core/app/Models/User.php
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
|
||||||
|
class User extends Authenticatable
|
||||||
|
{
|
||||||
|
use HasFactory, Notifiable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that are mass assignable.
|
||||||
|
*
|
||||||
|
* @var array<int, string>
|
||||||
|
*/
|
||||||
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
|
'email',
|
||||||
|
'password',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $appends = [
|
||||||
|
'avatar',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that should be hidden for serialization.
|
||||||
|
*
|
||||||
|
* @var array<int, string>
|
||||||
|
*/
|
||||||
|
protected $hidden = [
|
||||||
|
'password',
|
||||||
|
'remember_token',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the attributes that should be cast.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'email_verified_at' => 'datetime',
|
||||||
|
'password' => 'hashed',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return HasMany
|
||||||
|
*/
|
||||||
|
public function resources(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Resource::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAvatarAttribute(): string
|
||||||
|
{
|
||||||
|
return 'https://www.gravatar.com/avatar/'.hash('sha256', strtolower($this->email)).'?d=robohash&r=x';
|
||||||
|
}
|
||||||
|
}
|
48
core/app/Providers/AppServiceProvider.php
Normal file
48
core/app/Providers/AppServiceProvider.php
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Auth\Notifications\ResetPassword;
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use Laravel\Pennant\Feature;
|
||||||
|
use Sqids\Sqids;
|
||||||
|
|
||||||
|
class AppServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Register any application services.
|
||||||
|
*/
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
$this->app->bind(Sqids::class, function () {
|
||||||
|
return new Sqids(Feature::value('id-alphabet'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bootstrap any application services.
|
||||||
|
*/
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
if (!$this->app->runningInConsole()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$group = [
|
||||||
|
base_path('public/build/') => public_path('build'),
|
||||||
|
base_path('public/vendor/') => public_path('vendor'),
|
||||||
|
base_path('public/.htaccess') => public_path('.htaccess'),
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($this->app->environment('local')) {
|
||||||
|
$group[base_path('public/hot')] = public_path('hot');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->publishes($group, 'app');
|
||||||
|
|
||||||
|
$this->publishes([
|
||||||
|
base_path('public/favicon.ico') => public_path('favicon.ico'),
|
||||||
|
base_path('public/img/') => public_path('img'),
|
||||||
|
], 'app-img');
|
||||||
|
}
|
||||||
|
}
|
51
core/app/Providers/FortifyServiceProvider.php
Normal file
51
core/app/Providers/FortifyServiceProvider.php
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use App\Actions\Fortify\CreateNewUser;
|
||||||
|
use App\Actions\Fortify\ResetUserPassword;
|
||||||
|
use App\Actions\Fortify\UpdateUserPassword;
|
||||||
|
use App\Actions\Fortify\UpdateUserProfileInformation;
|
||||||
|
use App\Livewire\Auth\Login;
|
||||||
|
use Illuminate\Cache\RateLimiting\Limit;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\RateLimiter;
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Laravel\Fortify\Fortify;
|
||||||
|
|
||||||
|
class FortifyServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Register any application services.
|
||||||
|
*/
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bootstrap any application services.
|
||||||
|
*/
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
Fortify::createUsersUsing(CreateNewUser::class);
|
||||||
|
Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class);
|
||||||
|
Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);
|
||||||
|
Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
|
||||||
|
|
||||||
|
RateLimiter::for('login', function (Request $request) {
|
||||||
|
$throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())).'|'.$request->ip());
|
||||||
|
|
||||||
|
return Limit::perMinute(5)->by($throttleKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
RateLimiter::for('two-factor', function (Request $request) {
|
||||||
|
return Limit::perMinute(5)->by($request->session()->get('login.id'));
|
||||||
|
});
|
||||||
|
|
||||||
|
Fortify::loginView(static function () {
|
||||||
|
return app()->call(Login::class);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
47
core/app/View/Components/AppBrand.php
Normal file
47
core/app/View/Components/AppBrand.php
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\View\Components;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Contracts\View\View;
|
||||||
|
use Illuminate\View\Component;
|
||||||
|
|
||||||
|
class AppBrand extends Component
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create a new component instance.
|
||||||
|
*/
|
||||||
|
public function __construct(public ?bool $onTop = false)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the view / contents that represent the component.
|
||||||
|
*/
|
||||||
|
public function render(): View|Closure|string
|
||||||
|
{
|
||||||
|
return <<<'HTML'
|
||||||
|
<a href="/" wire:navigate>
|
||||||
|
<!-- Hidden when collapsed -->
|
||||||
|
<div {{ $attributes->class(["hidden-when-collapsed"]) }}>
|
||||||
|
<div class="flex items-center {{ $onTop ? 'flex-col justify-center' : 'gap-2 btn btn-link no-underline hover:no-underline flex-nowrap' }}">
|
||||||
|
<div class="avatar">
|
||||||
|
<div class="{{ $onTop ? 'w-24' : 'w-12' }}">
|
||||||
|
<img src="{{ asset('img/android-chrome-192x192.png') }}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="font-bold text-3xl bg-gradient-to-r from-blue-500 to-green-400 bg-clip-text text-transparent ">
|
||||||
|
{{ config('app.name') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Display when collapsed -->
|
||||||
|
<div class="display-when-collapsed hidden mx-5 mt-4 lg:mb-6 h-[28px]">
|
||||||
|
<x-icon name="s-square-3-stack-3d" class="w-6 -mb-1 text-purple-500" />
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
HTML;
|
||||||
|
}
|
||||||
|
}
|
15
core/artisan
Normal file
15
core/artisan
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Symfony\Component\Console\Input\ArgvInput;
|
||||||
|
|
||||||
|
define('LARAVEL_START', microtime(true));
|
||||||
|
|
||||||
|
// Register the Composer autoloader...
|
||||||
|
require __DIR__.'/vendor/autoload.php';
|
||||||
|
|
||||||
|
// Bootstrap Laravel and handle the command...
|
||||||
|
$status = (require_once __DIR__.'/bootstrap/app.php')
|
||||||
|
->handleCommand(new ArgvInput);
|
||||||
|
|
||||||
|
exit($status);
|
17
core/bootstrap/app.php
Normal file
17
core/bootstrap/app.php
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Application;
|
||||||
|
use Illuminate\Foundation\Configuration\Exceptions;
|
||||||
|
use Illuminate\Foundation\Configuration\Middleware;
|
||||||
|
|
||||||
|
return Application::configure(basePath: dirname(__DIR__))
|
||||||
|
->withRouting(
|
||||||
|
web: __DIR__.'/../routes/web.php',
|
||||||
|
health: '/up',
|
||||||
|
)
|
||||||
|
->withMiddleware(function (Middleware $middleware) {
|
||||||
|
//
|
||||||
|
})
|
||||||
|
->withExceptions(function (Exceptions $exceptions) {
|
||||||
|
//
|
||||||
|
})->create();
|
2
core/bootstrap/cache/.gitignore
vendored
Normal file
2
core/bootstrap/cache/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
*
|
||||||
|
!.gitignore
|
6
core/bootstrap/providers.php
Normal file
6
core/bootstrap/providers.php
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
App\Providers\AppServiceProvider::class,
|
||||||
|
App\Providers\FortifyServiceProvider::class,
|
||||||
|
];
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue