Compare commits
No commits in common. "analysis-32MNoP" and "master" have entirely different histories.
analysis-3
...
master
284 changed files with 28107 additions and 22303 deletions
27
.github/workflows/test_suite.yml
vendored
Normal file
27
.github/workflows/test_suite.yml
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
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 +1,141 @@
|
|||
/.idea
|
||||
### Composer ###
|
||||
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
|
||||
|
|
8
.htaccess
Normal file
8
.htaccess
Normal file
|
@ -0,0 +1,8 @@
|
|||
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
Normal file
39
.travis.yml
Normal file
|
@ -0,0 +1,39 @@
|
|||
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
Normal file
189
Gruntfile.js
Normal file
|
@ -0,0 +1,189 @@
|
|||
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']);
|
||||
};
|
|
@ -1,4 +0,0 @@
|
|||
APP_KEY=base64:88Nwiwz8SgR2v7Spx27RDdj7uCYidIwKCmzQCs4l0V4=
|
||||
APP_ENV=production
|
||||
APP_TIMEZONE=UTC
|
||||
APP_URL=http://localhost
|
21
app/.gitignore
vendored
21
app/.gitignore
vendored
|
@ -1,21 +0,0 @@
|
|||
/.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
|
110
app/Controllers/AdminController.php
Normal file
110
app/Controllers/AdminController.php
Normal file
|
@ -0,0 +1,110 @@
|
|||
<?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'));
|
||||
}
|
||||
}
|
123
app/Controllers/Auth/AuthController.php
Normal file
123
app/Controllers/Auth/AuthController.php
Normal file
|
@ -0,0 +1,123 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
183
app/Controllers/Auth/LoginController.php
Normal file
183
app/Controllers/Auth/LoginController.php
Normal file
|
@ -0,0 +1,183 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
130
app/Controllers/Auth/PasswordRecoveryController.php
Normal file
130
app/Controllers/Auth/PasswordRecoveryController.php
Normal file
|
@ -0,0 +1,130 @@
|
|||
<?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'));
|
||||
}
|
||||
}
|
126
app/Controllers/Auth/RegisterController.php
Normal file
126
app/Controllers/Auth/RegisterController.php
Normal file
|
@ -0,0 +1,126 @@
|
|||
<?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'));
|
||||
}
|
||||
}
|
148
app/Controllers/ClientController.php
Normal file
148
app/Controllers/ClientController.php
Normal file
|
@ -0,0 +1,148 @@
|
|||
<?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,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
154
app/Controllers/Controller.php
Normal file
154
app/Controllers/Controller.php
Normal file
|
@ -0,0 +1,154 @@
|
|||
<?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');
|
||||
}
|
||||
}
|
97
app/Controllers/DashboardController.php
Normal file
97
app/Controllers/DashboardController.php
Normal file
|
@ -0,0 +1,97 @@
|
|||
<?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'));
|
||||
}
|
||||
}
|
49
app/Controllers/ExportController.php
Normal file
49
app/Controllers/ExportController.php
Normal file
|
@ -0,0 +1,49 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
549
app/Controllers/MediaController.php
Normal file
549
app/Controllers/MediaController.php
Normal file
|
@ -0,0 +1,549 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
74
app/Controllers/ProfileController.php
Normal file
74
app/Controllers/ProfileController.php
Normal file
|
@ -0,0 +1,74 @@
|
|||
<?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'));
|
||||
}
|
||||
}
|
115
app/Controllers/SettingController.php
Normal file
115
app/Controllers/SettingController.php
Normal file
|
@ -0,0 +1,115 @@
|
|||
<?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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
79
app/Controllers/TagController.php
Normal file
79
app/Controllers/TagController.php
Normal file
|
@ -0,0 +1,79 @@
|
|||
<?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'));
|
||||
}
|
||||
}
|
194
app/Controllers/UpgradeController.php
Normal file
194
app/Controllers/UpgradeController.php
Normal file
|
@ -0,0 +1,194 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
237
app/Controllers/UploadController.php
Normal file
237
app/Controllers/UploadController.php
Normal file
|
@ -0,0 +1,237 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
||||
}
|
317
app/Controllers/UserController.php
Normal file
317
app/Controllers/UserController.php
Normal file
|
@ -0,0 +1,317 @@
|
|||
<?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();
|
||||
}
|
||||
}
|
65
app/Database/DB.php
Normal file
65
app/Database/DB.php
Normal file
|
@ -0,0 +1,65 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
111
app/Database/Migrator.php
Normal file
111
app/Database/Migrator.php
Normal file
|
@ -0,0 +1,111 @@
|
|||
<?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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
387
app/Database/Repositories/MediaRepository.php
Normal file
387
app/Database/Repositories/MediaRepository.php
Normal file
|
@ -0,0 +1,387 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
106
app/Database/Repositories/TagRepository.php
Normal file
106
app/Database/Repositories/TagRepository.php
Normal file
|
@ -0,0 +1,106 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
181
app/Database/Repositories/UserRepository.php
Normal file
181
app/Database/Repositories/UserRepository.php
Normal file
|
@ -0,0 +1,181 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
27
app/Exceptions/Handlers/AppErrorHandler.php
Normal file
27
app/Exceptions/Handlers/AppErrorHandler.php
Normal file
|
@ -0,0 +1,27 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
50
app/Exceptions/Handlers/Renderers/HtmlErrorRenderer.php
Normal file
50
app/Exceptions/Handlers/Renderers/HtmlErrorRenderer.php
Normal file
|
@ -0,0 +1,50 @@
|
|||
<?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]);
|
||||
}
|
||||
}
|
13
app/Exceptions/UnderMaintenanceException.php
Normal file
13
app/Exceptions/UnderMaintenanceException.php
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?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! :)';
|
||||
}
|
30
app/Exceptions/ValidationException.php
Normal file
30
app/Exceptions/ValidationException.php
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
71
app/Factories/ViewFactory.php
Normal file
71
app/Factories/ViewFactory.php
Normal file
|
@ -0,0 +1,71 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
30
app/Middleware/AdminMiddleware.php
Normal file
30
app/Middleware/AdminMiddleware.php
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
35
app/Middleware/AuthMiddleware.php
Normal file
35
app/Middleware/AuthMiddleware.php
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
28
app/Middleware/CheckForMaintenanceMiddleware.php
Normal file
28
app/Middleware/CheckForMaintenanceMiddleware.php
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
23
app/Middleware/InjectMiddleware.php
Normal file
23
app/Middleware/InjectMiddleware.php
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
27
app/Middleware/LangMiddleware.php
Normal file
27
app/Middleware/LangMiddleware.php
Normal file
|
@ -0,0 +1,27 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
19
app/Middleware/Middleware.php
Normal file
19
app/Middleware/Middleware.php
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?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);
|
||||
}
|
41
app/Middleware/RememberMiddleware.php
Normal file
41
app/Middleware/RememberMiddleware.php
Normal file
|
@ -0,0 +1,41 @@
|
|||
<?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
Normal file
159
app/Web/Lang.php
Normal file
|
@ -0,0 +1,159 @@
|
|||
<?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
Normal file
152
app/Web/Mail.php
Normal file
|
@ -0,0 +1,152 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
161
app/Web/Session.php
Normal file
161
app/Web/Session.php
Normal file
|
@ -0,0 +1,161 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
67
app/Web/Theme.php
Normal file
67
app/Web/Theme.php
Normal file
|
@ -0,0 +1,67 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
55
app/Web/UA.php
Normal file
55
app/Web/UA.php
Normal file
|
@ -0,0 +1,55 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
66
app/Web/ValidationHelper.php
Normal file
66
app/Web/ValidationHelper.php
Normal file
|
@ -0,0 +1,66 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
69
app/Web/View.php
Normal file
69
app/Web/View.php
Normal file
|
@ -0,0 +1,69 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
<?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
2
app/bootstrap/cache/.gitignore
vendored
|
@ -1,2 +0,0 @@
|
|||
*
|
||||
!.gitignore
|
|
@ -1,35 +0,0 @@
|
|||
{
|
||||
"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
6609
app/composer.lock
generated
File diff suppressed because it is too large
Load diff
530
app/helpers.php
Normal file
530
app/helpers.php
Normal file
|
@ -0,0 +1,530 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
<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>
|
|
@ -1,16 +0,0 @@
|
|||
<?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());
|
|
@ -1,2 +0,0 @@
|
|||
User-agent: *
|
||||
Disallow:
|
93
app/routes.php
Executable file
93
app/routes.php
Executable file
|
@ -0,0 +1,93 @@
|
|||
<?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
3
app/storage/app/.gitignore
vendored
|
@ -1,3 +0,0 @@
|
|||
*
|
||||
!public/
|
||||
!.gitignore
|
2
app/storage/app/public/.gitignore
vendored
2
app/storage/app/public/.gitignore
vendored
|
@ -1,2 +0,0 @@
|
|||
*
|
||||
!.gitignore
|
9
app/storage/framework/.gitignore
vendored
9
app/storage/framework/.gitignore
vendored
|
@ -1,9 +0,0 @@
|
|||
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
3
app/storage/framework/cache/.gitignore
vendored
|
@ -1,3 +0,0 @@
|
|||
*
|
||||
!data/
|
||||
!.gitignore
|
2
app/storage/framework/cache/data/.gitignore
vendored
2
app/storage/framework/cache/data/.gitignore
vendored
|
@ -1,2 +0,0 @@
|
|||
*
|
||||
!.gitignore
|
2
app/storage/framework/sessions/.gitignore
vendored
2
app/storage/framework/sessions/.gitignore
vendored
|
@ -1,2 +0,0 @@
|
|||
*
|
||||
!.gitignore
|
2
app/storage/framework/testing/.gitignore
vendored
2
app/storage/framework/testing/.gitignore
vendored
|
@ -1,2 +0,0 @@
|
|||
*
|
||||
!.gitignore
|
2
app/storage/framework/views/.gitignore
vendored
2
app/storage/framework/views/.gitignore
vendored
|
@ -1,2 +0,0 @@
|
|||
*
|
||||
!.gitignore
|
2
app/storage/logs/.gitignore
vendored
2
app/storage/logs/.gitignore
vendored
|
@ -1,2 +0,0 @@
|
|||
*
|
||||
!.gitignore
|
12
app/xbb
12
app/xbb
|
@ -1,12 +0,0 @@
|
|||
#!/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
Normal file
28
bin/clean
Normal file
|
@ -0,0 +1,28 @@
|
|||
#!/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
Normal file
45
bin/migrate
Normal file
|
@ -0,0 +1,45 @@
|
|||
#!/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);
|
121
bootstrap/app.php
Normal file
121
bootstrap/app.php
Normal file
|
@ -0,0 +1,121 @@
|
|||
<?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;
|
113
bootstrap/container.php
Normal file
113
bootstrap/container.php
Normal file
|
@ -0,0 +1,113 @@
|
|||
<?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),
|
||||
];
|
59
composer.json
Normal file
59
composer.json
Normal file
|
@ -0,0 +1,59 @@
|
|||
{
|
||||
"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
Normal file
7169
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
15
config.example.php
Normal file
15
config.example.php
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?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',
|
||||
],
|
||||
];
|
|
@ -1,18 +0,0 @@
|
|||
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
|
|
@ -1,63 +0,0 @@
|
|||
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
11
core/.gitattributes
vendored
|
@ -1,11 +0,0 @@
|
|||
* 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
21
core/.gitignore
vendored
|
@ -1,21 +0,0 @@
|
|||
/.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,66 +0,0 @@
|
|||
<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).
|
|
@ -1,40 +0,0 @@
|
|||
<?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']),
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
<?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'];
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
<?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();
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
<?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();
|
||||
}
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
<?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();
|
||||
}
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
//
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
<?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']);
|
||||
}
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
<?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());
|
||||
}
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
<?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()
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
<?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';
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
<?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';
|
||||
}
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
<?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');
|
||||
}
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
<?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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
<?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
15
core/artisan
|
@ -1,15 +0,0 @@
|
|||
#!/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);
|
|
@ -1,17 +0,0 @@
|
|||
<?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
2
core/bootstrap/cache/.gitignore
vendored
|
@ -1,2 +0,0 @@
|
|||
*
|
||||
!.gitignore
|
|
@ -1,6 +0,0 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
App\Providers\AppServiceProvider::class,
|
||||
App\Providers\FortifyServiceProvider::class,
|
||||
];
|
|
@ -1,73 +0,0 @@
|
|||
{
|
||||
"name": "xbb/core",
|
||||
"description": "The xbackbone core",
|
||||
"keywords": [
|
||||
"xbb",
|
||||
"core"
|
||||
],
|
||||
"license": "AGPL-3.0-only",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"laravel/fortify": "^1.24",
|
||||
"laravel/framework": "^11.9",
|
||||
"laravel/pennant": "^1.11",
|
||||
"laravel/tinker": "^2.9",
|
||||
"livewire/livewire": "^3.5",
|
||||
"robsontenorio/mary": "^1.35",
|
||||
"sergix44/imagezen": "^0.2.2",
|
||||
"sqids/sqids": "^0.4.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.23",
|
||||
"laravel/pint": "^1.13",
|
||||
"laravel/sail": "^1.26",
|
||||
"mockery/mockery": "^1.6",
|
||||
"nunomaduro/collision": "^8.0",
|
||||
"pestphp/pest": "^2.35"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "app/",
|
||||
"Database\\Factories\\": "database/factories/",
|
||||
"Database\\Seeders\\": "database/seeders/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"post-autoload-dump": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||
"@php artisan package:discover --ansi"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
|
||||
],
|
||||
"post-root-package-install": [
|
||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||
],
|
||||
"post-create-project-cmd": [
|
||||
"@php artisan key:generate --ansi",
|
||||
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
|
||||
"@php artisan migrate --graceful --ansi"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"dont-discover": []
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true,
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true,
|
||||
"php-http/discovery": true
|
||||
}
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue