Compare commits

...

9 commits
master ... next

Author SHA1 Message Date
Sergio Brighenti
5a685429c5 wip navbar 2024-08-26 12:16:35 +02:00
Sergio Brighenti
db2629b422 updated and fixed navbar 2024-08-26 10:49:23 +02:00
Sergio Brighenti
2b37296225 wip layout 2024-08-26 01:20:57 +02:00
Sergio Brighenti
1adede3a3c wip on database 2024-08-26 00:24:12 +02:00
Sergio Brighenti
2cdf85ad21 add pennant to manage settings 2024-08-25 11:35:44 +02:00
Sergio Brighenti
6d6923eb09 added tests
login working as expected
2024-08-25 01:29:02 +02:00
Sergio Brighenti
c981498810 Added login form 2024-08-24 13:22:35 +02:00
Sergio Brighenti
eb656afe06 fix branch name 2024-08-24 11:02:08 +02:00
Sergio Brighenti
58a47733f5 first monorepo structure 2024-08-24 10:01:44 +02:00
284 changed files with 22308 additions and 28107 deletions

View file

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

142
.gitignore vendored
View file

@ -1,141 +1 @@
### 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
/.idea

View file

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

View file

@ -1,39 +0,0 @@
notifications:
on_success: never
on_failure: always
matrix:
include:
- language: php
php:
- '7.1'
# Remove comment on if unit test ready
#before_script:
# - if find . -name "*.php" ! -path "./vendor/*" -exec php -l {} \; | grep "Fatal error"; then exit 1; fi
# - composer self-update
# - composer install --prefer-source --no-interaction --dev
script:
# Remove on if unit test ready
- if find . -name "*.php" ! -path "./vendor/*" -exec php -l {} \; | grep "Fatal error"; then exit 1; fi
- language: php
php:
- '7.2'
# Remove comment on if unit test ready
#before_script:
# - if find . -name "*.php" ! -path "./vendor/*" -exec php -l {} \; | grep "Fatal error"; then exit 1; fi
# - composer self-update
# - composer install --prefer-source --no-interaction --dev
script:
# Remove on if unit test ready
- if find . -name "*.php" ! -path "./vendor/*" -exec php -l {} \; | grep "Fatal error"; then exit 1; fi
- language: node_js
node_js:
- "lts/*"
before_script:
- npm install grunt-cli -g
script:
- grunt test
after_script:
- curl -H "Content-Type: application/json" --data '{"docker_tag": "master"}' -X POST https://registry.hub.docker.com/u/pe46dro/xbackbone-docker/trigger/505df075-f5b6-48bf-a22c-ce678f477929/

View file

@ -1,189 +0,0 @@
module.exports = function (grunt) {
let version = grunt.file.readJSON('composer.json').version;
let releaseFilename = 'release-v' + version + '.zip';
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
jshint: {
all: ['Gruntfile.js', 'src/js/app.js'],
options: {
'esversion': 6,
}
},
cssmin: {
build: {
files: {
'static/app/app.css': [
'src/css/app.css'
]
}
}
},
uglify: {
options: {
preserveComments: false,
compress: true
},
build: {
files: {
'static/app/app.js': [
'src/js/app.js'
],
'install/installer.js': [
'src/js/installer.js'
],
}
}
},
watch: {
css: {
files: [
'src/css/app.css'
],
tasks: ['cssmin']
},
scripts: {
files: [
'src/js/app.js',
'src/js/installer.js',
],
tasks: ['uglify']
}
},
copy: {
main: {
files: [
{
expand: true,
cwd: 'node_modules/@fortawesome/fontawesome-free',
src: ['css/all.min.css', 'webfonts/**/*'],
dest: 'static/fontawesome'
},
{
expand: true,
cwd: 'node_modules/bootstrap/dist/css',
src: ['bootstrap.min.css'],
dest: 'static/bootstrap/css'
},
{
expand: true,
cwd: 'node_modules/bootstrap/dist/js',
src: ['bootstrap.bundle.min.js'],
dest: 'static/bootstrap/js'
},
{
expand: true,
cwd: 'node_modules/clipboard/dist',
src: ['clipboard.min.js'],
dest: 'static/clipboardjs'
},
{
expand: true,
cwd: 'node_modules/plyr/dist',
src: ['plyr.min.js', 'plyr.css'],
dest: 'static/plyr'
},
{
expand: true,
cwd: 'node_modules/highlightjs',
src: ['styles/**/*', 'highlight.pack.min.js'],
dest: 'static/highlightjs'
}, {
expand: true,
cwd: 'node_modules/highlightjs-line-numbers.js/dist',
src: ['highlightjs-line-numbers.min.js'],
dest: 'static/highlightjs'
},
{
expand: true,
cwd: 'node_modules/dropzone/dist/min',
src: ['dropzone.min.css', 'dropzone.min.js'],
dest: 'static/dropzone'
},
{
expand: true,
cwd: 'node_modules/bootstrap4-toggle/css',
src: ['bootstrap4-toggle.min.css'],
dest: 'static/bootstrap/css'
},
{
expand: true,
cwd: 'node_modules/bootstrap4-toggle/js',
src: ['bootstrap4-toggle.min.js'],
dest: 'static/bootstrap/js'
},
{
expand: true,
cwd: 'src/images',
src: ['**/*'],
dest: 'static/images'
},
{expand: true, cwd: 'node_modules/jquery/dist', src: ['jquery.min.js'], dest: 'static/jquery'}
],
},
},
shell: {
phpstan: {
command: '"./vendor/bin/phpstan" --level=0 analyse app resources/lang bin install'
},
composer_no_dev: {
command: 'composer install --no-dev --prefer-dist'
}
},
compress: {
main: {
options: {
archive: releaseFilename,
mode: 'zip',
level: 9,
},
files: [{
expand: true,
cwd: './',
src: [
'app/**/*',
'bin/**/*',
'bootstrap/**/*',
'install/**/*',
'logs/**/',
'resources/cache',
'resources/sessions',
'resources/database',
'resources/lang/**/*',
'resources/templates/**/*',
'resources/schemas/**/*',
'resources/lang/**/*',
'resources/uploaders/**/*',
'static/**/*',
'vendor/**/*',
'.htaccess',
'config.example.php',
'index.php',
'composer.json',
'composer.lock',
'LICENSE',
'favicon.ico',
'CHANGELOG.md'
],
dest: '/'
}]
}
},
});
require('load-grunt-tasks')(grunt);
grunt.registerTask('default', ['jshint', 'cssmin', 'uglify', 'copy']);
grunt.registerTask('test', ['jshint']);
grunt.registerTask('phpstan', ['shell:phpstan']);
grunt.registerTask('composer_no_dev', ['shell:composer_no_dev']);
grunt.registerTask('build-release', ['default', 'composer_no_dev', 'compress']);
};

4
app/.env.example Normal file
View file

@ -0,0 +1,4 @@
APP_KEY=base64:88Nwiwz8SgR2v7Spx27RDdj7uCYidIwKCmzQCs4l0V4=
APP_ENV=production
APP_TIMEZONE=UTC
APP_URL=http://localhost

21
app/.gitignore vendored Normal file
View file

@ -0,0 +1,21 @@
/.phpunit.cache
/node_modules
/public/build
/public/hot
/public/storage
/storage/*.key
/vendor
.env
.env.backup
.env.production
.phpactor.json
.phpunit.result.cache
Homestead.json
Homestead.yaml
auth.json
npm-debug.log
yarn-error.log
/.fleet
/.idea
/.vscode
/*.db

View file

@ -1,110 +0,0 @@
<?php
namespace App\Controllers;
use App\Database\Migrator;
use App\Web\Theme;
use League\Flysystem\FileNotFoundException;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
class AdminController extends Controller
{
/**
* @param Request $request
* @param Response $response
*
* @return Response
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*
*/
public function system(Request $request, Response $response): Response
{
$settings = [];
foreach ($this->database->query('SELECT `key`, `value` FROM `settings`') as $setting) {
$settings[$setting->key] = $setting->value;
}
$settings['default_user_quota'] = humanFileSize(
$this->getSetting('default_user_quota', stringToBytes('1G')),
0,
true
);
return view()->render($response, 'dashboard/system.twig', [
'usersCount' => $this->database->query('SELECT COUNT(*) AS `count` FROM `users`')->fetch()->count,
'mediasCount' => $this->database->query('SELECT COUNT(*) AS `count` FROM `uploads`')->fetch()->count,
'orphanFilesCount' => $this->database->query('SELECT COUNT(*) AS `count` FROM `uploads` WHERE `user_id` IS NULL')->fetch()->count,
'totalSize' => humanFileSize($this->database->query('SELECT SUM(`current_disk_quota`) AS `sum` FROM `users`')->fetch()->sum ?? 0),
'post_max_size' => ini_get('post_max_size'),
'upload_max_filesize' => ini_get('upload_max_filesize'),
'installed_lang' => $this->lang->getList(),
'forced_lang' => $request->getAttribute('forced_lang'),
'php_version' => PHP_VERSION,
'max_memory' => ini_get('memory_limit'),
'settings' => $settings,
]);
}
/**
* @param Response $response
*
* @return Response
*/
public function deleteOrphanFiles(Response $response): Response
{
$orphans = $this->database->query('SELECT * FROM `uploads` WHERE `user_id` IS NULL')->fetchAll();
$filesystem = $this->storage;
$deleted = 0;
foreach ($orphans as $orphan) {
try {
$filesystem->delete($orphan->storage_path);
$deleted++;
} catch (FileNotFoundException $e) {
}
}
$this->database->query('DELETE FROM `uploads` WHERE `user_id` IS NULL');
$this->session->alert(lang('deleted_orphans', [$deleted]));
return redirect($response, route('system'));
}
/**
* @param Response $response
*
* @return Response
*/
public function getThemes(Response $response): Response
{
$themes = make(Theme::class)->availableThemes();
$out = [];
foreach ($themes as $vendor => $list) {
$out["-- {$vendor} --"] = null;
foreach ($list as $name => $url) {
$out[$name] = "{$vendor}|{$url}";
}
}
return json($response, $out);
}
/**
* @param Response $response
* @return Response
*/
public function recalculateUserQuota(Response $response): Response
{
$migrator = new Migrator($this->database, null);
$migrator->reSyncQuotas($this->storage);
$this->session->alert(lang('quota_recalculated'));
return redirect($response, route('system'));
}
}

View file

@ -1,123 +0,0 @@
<?php
namespace App\Controllers\Auth;
use App\Controllers\Controller;
use App\Web\Session;
use App\Web\ValidationHelper;
use Psr\Http\Message\ServerRequestInterface as Request;
abstract class AuthController extends Controller
{
protected function checkRecaptcha(ValidationHelper $validator, Request $request)
{
$validator->callIf($this->getSetting('recaptcha_enabled') === 'on', function (Session $session) use (&$request) {
$recaptcha = json_decode(file_get_contents('https://www.google.com/recaptcha/api/siteverify?secret='.$this->getSetting('recaptcha_secret_key').'&response='.param($request, 'recaptcha_token')));
if ($recaptcha->success && $recaptcha->score < 0.5) {
$session->alert(lang('recaptcha_failed'), 'danger');
return false;
}
return true;
});
return $validator;
}
/**
* Connects to LDAP server and logs in with service account (if configured)
* @return \LDAP\Connection|resource|false
*/
public function ldapConnect()
{
if (!extension_loaded('ldap')) {
$this->logger->error('The LDAP extension is not loaded.');
return false;
}
// Building LDAP URI
$ldapSchema=(@is_string($this->config['ldap']['schema'])) ?
strtolower($this->config['ldap']['schema']) : 'ldap';
$ldapURI="$ldapSchema://".$this->config['ldap']['host'].':'.$this->config['ldap']['port'];
// Connecting to LDAP server
$this->logger->debug("Connecting to $ldapURI");
$server = ldap_connect($ldapURI);
if ($server) {
ldap_set_option($server, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($server, LDAP_OPT_REFERRALS, 0);
ldap_set_option($server, LDAP_OPT_NETWORK_TIMEOUT, 10);
} else {
$this->logger->error('LDAP-URI was not parseable');
return false;
}
// Upgrade to StartTLS
$useStartTLS = @is_bool($this->config['ldap']['useStartTLS']) ? $this->config['ldap']['useStartTLS'] : false;
if (($useStartTLS === true) && (ldap_start_tls($server) === false)) {
$this->logger->debug(ldap_error($server));
$this->logger->error("Failed to establish secure LDAP swith StartTLS");
return false;
}
// Authenticating LDAP service account (if configured)
$serviceAccountFQDN= (@is_string($this->config['ldap']['service_account_dn'])) ?
$this->config['ldap']['service_account_dn'] : null;
if (is_string($serviceAccountFQDN)) {
if (ldap_bind($server, $serviceAccountFQDN, $this->config['ldap']['service_account_password']) === false) {
$this->logger->debug(ldap_error($server));
$this->logger->error("Bind with service account ($serviceAccountFQDN) failed.");
return false;
}
}
return $server;
}
/**
* Returns User's LDAP DN
* @param string $username
* @param \LDAP\Connection|resource $server LDAP Server Resource
* @return string|null
*/
protected function getLdapRdn(string $username, $server)
{
//Dynamic LDAP User Binding
if (@is_string($this->config['ldap']['search_filter'])) {
//Replace ???? with username
$searchFilter = str_replace('????', ldap_escape($username, '', LDAP_ESCAPE_FILTER), $this->config['ldap']['search_filter']);
$ldapAddributes = array('dn');
$this->logger->debug("LDAP Search filter: $searchFilter");
$ldapSearchResp = ldap_search(
$server,
$this->config['ldap']['base_domain'],
$searchFilter,
$ldapAddributes
);
if (!$ldapSearchResp) {
$this->logger->debug(ldap_error($server));
$this->logger->error("User LDAP search for user $username failed");
return null;
}
if (ldap_count_entries($server, $ldapSearchResp) !== 1) {
$this->logger->notice("LDAP search for $username not found or had multiple entries");
return null;
}
$ldapEntry = ldap_first_entry($server, $ldapSearchResp);
//Returns full DN
$bindString = ldap_get_dn($server, $ldapEntry);
} else {
// Static LDAP Binding
$bindString = ($this->config['ldap']['rdn_attribute'] ?? 'uid=').addslashes($username);
if ($this->config['ldap']['user_domain'] !== null) {
$bindString .= ','.$this->config['ldap']['user_domain'];
}
if ($this->config['ldap']['base_domain'] !== null) {
$bindString .= ','.$this->config['ldap']['base_domain'];
}
//returns partial DN
}
return $bindString;
}
}

View file

@ -1,183 +0,0 @@
<?php
namespace App\Controllers\Auth;
use App\Database\Repositories\UserRepository;
use App\Web\ValidationHelper;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
class LoginController extends AuthController
{
/**
* @param Response $response
*
* @return Response
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*
* @throws \Twig\Error\LoaderError
*/
public function show(Response $response): Response
{
if ($this->session->get('logged', false)) {
return redirect($response, route('home'));
}
return view()->render($response, 'auth/login.twig', [
'register_enabled' => $this->getSetting('register_enabled', 'off'),
'recaptcha_site_key' => $this->getSetting('recaptcha_enabled') === 'on' ? $this->getSetting('recaptcha_site_key') : null,
]);
}
/**
* @param Request $request
* @param Response $response
*
* @return Response
* @throws \Exception
*
*/
public function login(Request $request, Response $response): Response
{
/** @var ValidationHelper $validator */
$validator = make(ValidationHelper::class);
if ($this->checkRecaptcha($validator, $request)->fails()) {
return redirect($response, route('login'));
}
$username = param($request, 'username');
$password = param($request, 'password');
$user = $this->database->query('SELECT `id`, `email`, `username`, `password`,`is_admin`, `active`, `current_disk_quota`, `max_disk_quota`, `ldap`, `copy_raw` FROM `users` WHERE `username` = ? OR `email` = ? LIMIT 1', [$username, $username])->fetch();
if ($this->config['ldap']['enabled'] && (!$user || $user->ldap ?? true)) {
$user = $this->ldapLogin($request, $username, param($request, 'password'), $user);
}
$validator
->alertIf(!$user || !password_verify($password, $user->password), 'bad_login')
->alertIf(isset($this->config['maintenance']) && $this->config['maintenance'] && !($user->is_admin ?? true), 'maintenance_in_progress', 'info')
->alertIf(!($user->active ?? false), 'account_disabled');
if ($validator->fails()) {
if (!empty($request->getHeaderLine('X-Forwarded-For'))) {
$ip = $request->getHeaderLine('X-Forwarded-For');
} else {
$ip = $request->getServerParams()['REMOTE_ADDR'] ?? null;
}
$this->logger->info("Login failed with username='{$username}', ip={$ip}.");
return redirect($response, route('login'));
}
$this->session->set('logged', true)
->set('user_id', $user->id)
->set('username', $user->username)
->set('admin', $user->is_admin)
->set('copy_raw', $user->copy_raw);
$this->setSessionQuotaInfo($user->current_disk_quota, $user->max_disk_quota);
$this->session->alert(lang('welcome', [$user->username]), 'info');
$this->logger->info("User $user->username logged in.");
if (param($request, 'remember') === 'on') {
$this->refreshRememberCookie($user->id);
}
if ($this->session->has('redirectTo')) {
return redirect($response, $this->session->get('redirectTo'));
}
return redirect($response, route('home'));
}
/**
* @param Request $request
* @param Response $response
*
* @return Response
*/
public function logout(Request $request, Response $response): Response
{
$this->session->clear();
$this->session->set('logged', false);
$this->session->alert(lang('goodbye'), 'warning');
if (!empty($request->getCookieParams()['remember'])) {
setcookie('remember', null, 0, '', '', false, true);
}
return redirect($response, route('login.show'));
}
/**
* @param Request $request
* @param string $username
* @param string $password
* @param $dbUser
* @return bool|null
* @throws \Slim\Exception\HttpNotFoundException
* @throws \Slim\Exception\HttpUnauthorizedException
*/
protected function ldapLogin(Request $request, string $username, string $password, $dbUser)
{
// Build LDAP connection
$server = $this->ldapConnect();
if (!$server) {
$this->session->alert(lang('ldap_cant_connect'), 'warning');
return $dbUser;
}
//Get LDAP user's (R)DN
$userDN=$this->getLdapRdn($username, $server);
if (!is_string($userDN)) {
return null;
}
//Bind as user to validate password
if (@ldap_bind($server, $userDN, $password)) {
$this->logger->debug("$userDN authenticated against LDAP sucessfully");
} else {
$this->logger->debug("$userDN authenticated against LDAP unsucessfully");
if ($dbUser && !$dbUser->ldap) {
return $dbUser;
}
return null;
}
if (!$dbUser) {
$email = $username;
if (!filter_var($username, FILTER_VALIDATE_EMAIL)) {
if (@is_string($this->config['ldap']['search_filter'])) {
$search = ldap_read(
$server,
$userDN,
'objectClass=*',
array('mail',$this->config['ldap']['rdn_attribute'])
);
} else {
$search = ldap_search($server, $this->config['ldap']['base_domain'], ($this->config['ldap']['rdn_attribute'] ?? 'uid=').addslashes($username), ['mail']);
}
$entry = ldap_first_entry($server, $search);
$email = @ldap_get_values($server, $entry, 'mail')[0] ?? platform_mail($username.rand(0, 100)); // if the mail is not set, generate a placeholder
}
/** @var UserRepository $userQuery */
$userQuery = make(UserRepository::class);
$userQuery->create($email, $username, $password, 0, 1, (int) $this->getSetting('default_user_quota', -1), null, 1);
return $userQuery->get($request, $this->database->getPdo()->lastInsertId());
}
if ($server) {
ldap_close($server);
}
if (!password_verify($password, $dbUser->password)) {
$userQuery = make(UserRepository::class);
$userQuery->update($dbUser->id, $dbUser->email, $username, $password, $dbUser->is_admin, $dbUser->active, $dbUser->max_disk_quota, $dbUser->ldap);
return $userQuery->get($request, $dbUser->id);
}
return $dbUser;
}
}

View file

@ -1,130 +0,0 @@
<?php
namespace App\Controllers\Auth;
use App\Web\Mail;
use App\Web\ValidationHelper;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Exception\HttpNotFoundException;
class PasswordRecoveryController extends AuthController
{
/**
* @param Response $response
* @return Response
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*/
public function recover(Response $response): Response
{
return view()->render($response, 'auth/recover_mail.twig', [
'recaptcha_site_key' => $this->getSetting('recaptcha_enabled') === 'on' ? $this->getSetting('recaptcha_site_key') : null,
]);
}
/**
* @param Request $request
* @param Response $response
* @return Response
* @throws \Exception
*/
public function recoverMail(Request $request, Response $response): Response
{
if ($this->session->get('logged', false)) {
return redirect($response, route('home'));
}
if ($this->checkRecaptcha(make(ValidationHelper::class), $request)->fails()) {
return redirect($response, route('recover'));
}
$user = $this->database->query('SELECT `id`, `username` FROM `users` WHERE `email` = ? AND NOT `ldap` LIMIT 1', param($request, 'email'))->fetch();
if (!isset($user->id)) {
$this->session->alert(lang('recover_email_sent'), 'success');
return redirect($response, route('recover'));
}
$resetToken = bin2hex(random_bytes(16));
$this->database->query('UPDATE `users` SET `reset_token`=? WHERE `id` = ?', [
$resetToken,
$user->id,
]);
Mail::make()
->from(platform_mail(), $this->config['app_name'])
->to(param($request, 'email'))
->subject(lang('mail.recover_password', [$this->config['app_name']]))
->message(lang('mail.recover_text', [
$user->username,
route('recover.password', ['resetToken' => $resetToken]),
route('recover.password', ['resetToken' => $resetToken]),
]))
->send();
$this->session->alert(lang('recover_email_sent'), 'success');
return redirect($response, route('recover'));
}
/**
* @param Request $request
* @param Response $response
* @param string $resetToken
* @return Response
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
* @throws HttpNotFoundException
*/
public function recoverPasswordForm(Request $request, Response $response, string $resetToken): Response
{
$user = $this->database->query('SELECT `id` FROM `users` WHERE `reset_token` = ? LIMIT 1', $resetToken)->fetch();
if (!$user) {
throw new HttpNotFoundException($request);
}
return view()->render($response, 'auth/recover_password.twig', [
'reset_token' => $resetToken
]);
}
/**
* @param Request $request
* @param Response $response
* @param string $resetToken
* @return Response
* @throws HttpNotFoundException
*/
public function recoverPassword(Request $request, Response $response, string $resetToken): Response
{
$user = $this->database->query('SELECT `id` FROM `users` WHERE `reset_token` = ? LIMIT 1', $resetToken)->fetch();
if (!$user) {
throw new HttpNotFoundException($request);
}
/** @var ValidationHelper $validator */
$validator = make(ValidationHelper::class)
->alertIf(empty(param($request, 'password')), 'password_required')
->alertIf(param($request, 'password') !== param($request, 'password_repeat'), 'password_match');
if ($validator->fails()) {
return redirect($response, route('recover.password', ['resetToken' => $resetToken]));
}
$this->database->query('UPDATE `users` SET `password`=?, `reset_token`=? WHERE `id` = ?', [
password_hash(param($request, 'password'), PASSWORD_DEFAULT),
null,
$user->id,
]);
$this->session->alert(lang('password_restored'), 'success');
return redirect($response, route('login.show'));
}
}

View file

@ -1,126 +0,0 @@
<?php
namespace App\Controllers\Auth;
use App\Controllers\Controller;
use App\Database\Repositories\UserRepository;
use App\Web\Mail;
use App\Web\ValidationHelper;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Exception\HttpNotFoundException;
class RegisterController extends AuthController
{
/**
* @param Request $request
* @param Response $response
* @return Response
* @throws HttpNotFoundException
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*/
public function registerForm(Request $request, Response $response): Response
{
if ($this->session->get('logged', false)) {
return redirect($response, route('home'));
}
if ($this->getSetting('register_enabled', 'off') === 'off') {
throw new HttpNotFoundException($request);
}
return view()->render($response, 'auth/register.twig', [
'recaptcha_site_key' => $this->getSetting('recaptcha_enabled') === 'on' ? $this->getSetting('recaptcha_site_key') : null,
]);
}
/**
* @param Request $request
* @param Response $response
* @return Response
* @throws HttpNotFoundException
* @throws \Exception
*/
public function register(Request $request, Response $response): Response
{
if ($this->session->get('logged', false)) {
return redirect($response, route('home'));
}
if ($this->getSetting('register_enabled', 'off') === 'off') {
throw new HttpNotFoundException($request);
}
if ($this->checkRecaptcha(make(ValidationHelper::class), $request)->fails()) {
return redirect($response, route('register.show'));
}
$validator = $this->getUserCreateValidator($request)->alertIf(empty(param($request, 'password')), 'password_required');
if ($validator->fails()) {
return redirect($response, route('register.show'));
}
$activateToken = bin2hex(random_bytes(16));
make(UserRepository::class)->create(
param($request, 'email'),
param($request, 'username'),
param($request, 'password'),
0,
0,
(int) $this->getSetting('default_user_quota', -1),
$activateToken
);
Mail::make()
->from(platform_mail(), $this->config['app_name'])
->to(param($request, 'email'))
->subject(lang('mail.activate_account', [$this->config['app_name']]))
->message(lang('mail.activate_text', [
param($request, 'username'),
$this->config['app_name'],
$this->config['base_url'],
$this->config['base_url'],
route('activate', ['activateToken' => $activateToken]),
route('activate', ['activateToken' => $activateToken]),
]))
->send();
$this->session->alert(lang('register_success', [param($request, 'username')]), 'success');
$this->logger->info('New user registered.', [array_diff_key($request->getParsedBody(), array_flip(['password']))]);
return redirect($response, route('login.show'));
}
/**
* @param Response $response
* @param string $activateToken
* @return Response
*/
public function activateUser(Response $response, string $activateToken): Response
{
if ($this->session->get('logged', false)) {
return redirect($response, route('home'));
}
$userId = $this->database->query('SELECT `id` FROM `users` WHERE `activate_token` = ? LIMIT 1', $activateToken)->fetch()->id ?? null;
if ($userId === null) {
$this->session->alert(lang('account_not_found'), 'warning');
return redirect($response, route('login.show'));
}
$this->database->query('UPDATE `users` SET `activate_token`=?, `active`=? WHERE `id` = ?', [
null,
1,
$userId,
]);
$this->session->alert(lang('account_activated'), 'success');
return redirect($response, route('login.show'));
}
}

View file

@ -1,148 +0,0 @@
<?php
namespace App\Controllers;
use App\Database\Repositories\UserRepository;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Exception\HttpNotFoundException;
use ZipStream\Option\Archive;
use ZipStream\ZipStream;
class ClientController extends Controller
{
/**
* @param Request $request
* @param Response $response
* @param int $id
*
* @return Response
*/
public function getShareXConfig(Request $request, Response $response, int $id): Response
{
$user = make(UserRepository::class)->get($request, $id, true);
if (!$user->token) {
$this->session->alert(lang('no_upload_token'), 'danger');
return redirect($response, $request->getHeaderLine('Referer'));
}
$json = [
'DestinationType' => 'ImageUploader, TextUploader, FileUploader',
'RequestURL' => route('upload'),
'FileFormName' => 'upload',
'Arguments' => [
'file' => '$filename$',
'text' => '$input$',
'token' => $user->token,
],
'URL' => '$json:url$',
'ThumbnailURL' => '$json:url$/raw',
'DeletionURL' => '$json:url$/delete/'.$user->token,
];
return json($response, $json, 200, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)
->withHeader('Content-Disposition', 'attachment;filename="'.$user->username.'-ShareX.sxcu"');
}
/**
* @param Request $request
* @param string|null $token
* @return Response
* @throws \ZipStream\Exception\FileNotFoundException
* @throws \ZipStream\Exception\FileNotReadableException
* @throws \ZipStream\Exception\OverflowException
* @throws HttpNotFoundException
*/
public function getScreenCloudConfig(Request $request, string $token): Response
{
$user = $this->database->query('SELECT * FROM `users` WHERE `token` = ? LIMIT 1', $token)->fetch();
if (!$user) {
throw new HttpNotFoundException($request);
}
$config = [
'token' => $token,
'host' => route('root'),
];
ob_end_clean();
$options = new Archive();
$options->setSendHttpHeaders(true);
$zip = new ZipStream($user->username.'-screencloud.zip', $options);
$zip->addFileFromPath('main.py', BASE_DIR.'resources/uploaders/screencloud/main.py');
$zip->addFileFromPath('icon.png', BASE_DIR.'static/images/favicon-32x32.png');
$zip->addFileFromPath('metadata.xml', BASE_DIR.'resources/uploaders/screencloud/metadata.xml');
$zip->addFileFromPath('settings.ui', BASE_DIR.'resources/uploaders/screencloud/settings.ui');
$zip->addFile('config.json', json_encode($config, JSON_UNESCAPED_SLASHES));
$zip->finish();
exit(0);
}
/**
* @param Request $request
* @param Response $response
* @param int $id
*
* @return Response
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*/
public function getBashScript(Request $request, Response $response, int $id): Response
{
$user = make(UserRepository::class)->get($request, $id, true);
if (!$user->token) {
$this->session->alert(lang('no_upload_token'), 'danger');
return redirect($response, $request->getHeaderLine('Referer'));
}
return view()->render(
$response->withHeader('Content-Disposition', 'attachment;filename="xbackbone_uploader_'.$user->username.'.sh"'),
'scripts/xbackbone_uploader.sh.twig',
[
'username' => $user->username,
'upload_url' => route('upload'),
'token' => $user->token,
]
);
}
/**
* @param Request $request
* @param Response $response
* @param int $id
*
* @return Response
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*/
public function getKDEScript(Request $request, Response $response, int $id): Response
{
$user = make(UserRepository::class)->get($request, $id, true);
if (!$user->token) {
$this->session->alert(lang('no_upload_token'), 'danger');
return redirect($response, $request->getHeaderLine('Referer'));
}
return view()->render(
$response->withHeader('Content-Disposition', 'attachment;filename="xbackbone_uploader_'.$user->username.'.sh"'),
'scripts/xbackbone_kde_uploader.sh.twig',
[
'username' => $user->username,
'upload_url' => route('upload'),
'token' => $user->token,
]
);
}
}

View file

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

View file

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

View file

@ -1,49 +0,0 @@
<?php
namespace App\Controllers;
use App\Database\Repositories\UserRepository;
use League\Flysystem\FileNotFoundException;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use ZipStream\Option\Archive;
use ZipStream\ZipStream;
class ExportController extends Controller
{
/**
* @param Request $request
* @param Response $response
* @param int|null $id
* @return Response
* @throws \ZipStream\Exception\OverflowException
*/
public function downloadData(Request $request, Response $response, int $id): Response
{
$user = make(UserRepository::class)->get($request, $id, true);
$medias = $this->database->query('SELECT `uploads`.`filename`, `uploads`.`storage_path` FROM `uploads` WHERE `user_id` = ?', $user->id);
$this->logger->info("User $user->id, $user->username, exporting data...");
set_time_limit(0);
ob_end_clean();
$options = new Archive();
$options->setSendHttpHeaders(true);
$zip = new ZipStream($user->username.'-'.time().'-export.zip', $options);
$filesystem = $this->storage;
foreach ($medias as $media) {
try {
$zip->addFileFromStream($media->filename, $filesystem->readStream($media->storage_path));
} catch (FileNotFoundException $e) {
$this->logger->error('Cannot export file', ['exception' => $e]);
}
}
$zip->finish();
exit(0);
}
}

View file

@ -1,549 +0,0 @@
<?php
namespace App\Controllers;
use App\Database\Repositories\UserRepository;
use App\Web\UA;
use Intervention\Image\Constraint;
use Intervention\Image\ImageManagerStatic as Image;
use League\Flysystem\FileNotFoundException;
use League\Flysystem\Filesystem;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Exception\HttpBadRequestException;
use Slim\Exception\HttpNotFoundException;
use Slim\Exception\HttpUnauthorizedException;
use Slim\Psr7\Stream;
class MediaController extends Controller
{
/**
* @param Request $request
* @param Response $response
* @param string $userCode
* @param string $mediaCode
* @param string|null $token
*
* @return Response
* @throws HttpNotFoundException
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
* @throws FileNotFoundException
*
*/
public function show(
Request $request,
Response $response,
string $userCode,
string $mediaCode,
string $token = null
): Response {
$media = $this->getMedia($userCode, $mediaCode, true);
if (!$media || (!$media->published && $this->session->get('user_id') !== $media->user_id && !$this->session->get(
'admin',
false
))) {
throw new HttpNotFoundException($request);
}
$filesystem = $this->storage;
$userAgent = $request->getHeaderLine('User-Agent');
$mime = $filesystem->getMimetype($media->storage_path);
try {
$media->mimetype = $mime;
$media->extension = pathinfo($media->filename, PATHINFO_EXTENSION);
$size = $filesystem->getSize($media->storage_path);
$type = explode('/', $media->mimetype)[0];
if ($type === 'image' && !isDisplayableImage($media->mimetype)) {
$type = 'application';
$media->mimetype = 'application/octet-stream';
}
if ($type === 'text') {
if ($size <= (500 * 1024)) { // less than 500 KB
$media->text = $filesystem->read($media->storage_path);
} else {
$type = 'application';
$media->mimetype = 'application/octet-stream';
}
}
$media->size = humanFileSize($size);
} catch (FileNotFoundException $e) {
throw new HttpNotFoundException($request);
}
if (
UA::isBot($userAgent) &&
!(
// embed if enabled
(UA::embedsLinks($userAgent) &&
isEmbeddable($mime) &&
$this->getSetting('image_embeds') === 'on') ||
// if the file is too large to be displayed as non embedded
(UA::embedsLinks($userAgent) &&
isEmbeddable($mime) &&
$size >= (8 * 1024 * 1024))
)
) {
return $this->streamMedia($request, $response, $filesystem, $media);
}
return view()->render($response, 'upload/public.twig', [
'delete_token' => $token,
'media' => $media,
'type' => $type,
'url' => urlFor(glue($userCode, $mediaCode)),
'copy_raw' => $this->session->get('copy_raw', false),
]);
}
/**
* @param Request $request
* @param Response $response
* @param int $id
*
* @return Response
* @throws HttpNotFoundException
*
* @throws FileNotFoundException
*/
public function getRawById(Request $request, Response $response, int $id): Response
{
$media = $this->database->query('SELECT * FROM `uploads` WHERE `id` = ? LIMIT 1', $id)->fetch();
if (!$media) {
throw new HttpNotFoundException($request);
}
return $this->streamMedia($request, $response, $this->storage, $media);
}
/**
* @param Request $request
* @param Response $response
* @param string $userCode
* @param string $mediaCode
* @param string|null $ext
*
* @return Response
* @throws HttpBadRequestException
* @throws HttpNotFoundException
*
* @throws FileNotFoundException
*/
public function getRaw(
Request $request,
Response $response,
string $userCode,
string $mediaCode,
?string $ext = null
): Response {
$media = $this->getMedia($userCode, $mediaCode, false);
if (!$media || (!$media->published && $this->session->get('user_id') !== $media->user_id && !$this->session->get(
'admin',
false
))) {
throw new HttpNotFoundException($request);
}
if ($ext !== null && pathinfo($media->filename, PATHINFO_EXTENSION) !== $ext) {
throw new HttpBadRequestException($request);
}
if (must_be_escaped($this->storage->getMimetype($media->storage_path))) {
$response = $this->streamMedia($request, $response, $this->storage, $media);
return $response->withHeader('Content-Type', 'text/plain');
}
return $this->streamMedia($request, $response, $this->storage, $media);
}
/**
* @param Request $request
* @param Response $response
* @param string $userCode
* @param string $mediaCode
*
* @return Response
* @throws HttpNotFoundException
*
* @throws FileNotFoundException
*/
public function download(Request $request, Response $response, string $userCode, string $mediaCode): Response
{
$media = $this->getMedia($userCode, $mediaCode, false);
if (!$media || (!$media->published && $this->session->get('user_id') !== $media->user_id && !$this->session->get(
'admin',
false
))) {
throw new HttpNotFoundException($request);
}
return $this->streamMedia($request, $response, $this->storage, $media, 'attachment');
}
/**
* @param Request $request
* @param Response $response
* @param string $vanity
* @param string $id
*
* @return Response
* @throws HttpNotFoundException
* @throws HttpBadRequestException
*/
public function createVanity(Request $request, Response $response, int $id): Response
{
$media = $this->database->query('SELECT * FROM `uploads` WHERE `id` = ? LIMIT 1', $id)->fetch();
$vanity = param($request, 'vanity');
$vanity = preg_replace('/[^a-z0-9]+/', '-', strtolower($vanity));
//handle collisions
$collision = $this->database->query('SELECT * FROM `uploads` WHERE `code` = ? AND `id` != ? LIMIT 1', [$vanity, $id])->fetch();
if (!$media) {
throw new HttpNotFoundException($request);
}
if ($vanity === '' || $collision) {
throw new HttpBadRequestException($request);
}
$this->database->query('UPDATE `uploads` SET `code` = ? WHERE `id` = ?', [$vanity, $media->id]);
$media->code = $vanity;
$response->getBody()->write(json_encode($media));
$this->logger->info('User '.$this->session->get('username').' created a vanity link for media '.$media->id);
return $response;
}
/**
* @param Request $request
* @param Response $response
* @param int $id
*
* @return Response
* @throws HttpNotFoundException
*
*/
public function togglePublish(Request $request, Response $response, int $id): Response
{
if ($this->session->get('admin')) {
$media = $this->database->query('SELECT * FROM `uploads` WHERE `id` = ? LIMIT 1', $id)->fetch();
} else {
$media = $this->database->query(
'SELECT * FROM `uploads` WHERE `id` = ? AND `user_id` = ? LIMIT 1',
[$id, $this->session->get('user_id')]
)->fetch();
}
if (!$media) {
throw new HttpNotFoundException($request);
}
$this->database->query(
'UPDATE `uploads` SET `published`=? WHERE `id`=?',
[$media->published ? 0 : 1, $media->id]
);
return $response;
}
/**
* @param Request $request
* @param Response $response
* @param int $id
*
* @return Response
* @throws HttpNotFoundException
* @throws HttpUnauthorizedException
*/
public function delete(Request $request, Response $response, int $id): Response
{
$media = $this->database->query('SELECT * FROM `uploads` WHERE `id` = ? LIMIT 1', $id)->fetch();
if (!$media) {
throw new HttpNotFoundException($request);
}
if (!$this->session->get('admin', false) && $media->user_id !== $this->session->get('user_id')) {
throw new HttpUnauthorizedException($request);
}
$this->deleteMedia($request, $media->storage_path, $id, $media->user_id);
$this->logger->info('User '.$this->session->get('username').' deleted a media.', [$id]);
if ($media->user_id === $this->session->get('user_id')) {
$user = make(UserRepository::class)->get($request, $media->user_id, true);
$this->setSessionQuotaInfo($user->current_disk_quota, $user->max_disk_quota);
}
if ($request->getMethod() === 'GET') {
return redirect($response, route('home'));
}
return $response;
}
/**
* @param Request $request
* @param Response $response
* @param string $userCode
* @param string $mediaCode
* @param string $token
*
* @return Response
* @throws HttpUnauthorizedException
*
* @throws HttpNotFoundException
*/
public function deleteByToken(
Request $request,
Response $response,
string $userCode,
string $mediaCode,
string $token
): Response {
$media = $this->getMedia($userCode, $mediaCode, false);
if (!$media) {
throw new HttpNotFoundException($request);
}
$user = $this->database->query('SELECT `id`, `active` FROM `users` WHERE `token` = ? LIMIT 1', $token)->fetch();
if (!$user) {
$this->session->alert(lang('token_not_found'), 'danger');
return redirect($response, $request->getHeaderLine('Referer'));
}
if (!$user->active) {
$this->session->alert(lang('account_disabled'), 'danger');
return redirect($response, $request->getHeaderLine('Referer'));
}
if ($this->session->get('admin', false) || $user->id === $media->user_id) {
$this->deleteMedia($request, $media->storage_path, $media->mediaId, $user->id);
$this->logger->info('User '.$user->username.' deleted a media via token.', [$media->mediaId]);
} else {
throw new HttpUnauthorizedException($request);
}
return redirect($response, route('home'));
}
/**
* @param Request $request
* @param string $storagePath
* @param int $id
*
* @param int $userId
* @return void
* @throws HttpNotFoundException
*/
protected function deleteMedia(Request $request, string $storagePath, int $id, int $userId)
{
try {
$size = $this->storage->getSize($storagePath);
$this->storage->delete($storagePath);
$this->updateUserQuota($request, $userId, $size, true);
} catch (FileNotFoundException $e) {
throw new HttpNotFoundException($request);
} finally {
$this->database->query('DELETE FROM `uploads` WHERE `id` = ?', $id);
$this->database->query('DELETE FROM `tags` WHERE `tags`.`id` NOT IN (SELECT `uploads_tags`.`tag_id` FROM `uploads_tags`)');
}
}
/**
* @param $userCode
* @param $mediaCode
*
* @param bool $withTags
* @return mixed
*/
protected function getMedia($userCode, $mediaCode, $withTags = false)
{
$mediaCode = pathinfo($mediaCode)['filename'];
$media = $this->database->query(
'SELECT `uploads`.*, `users`.*, `users`.`id` AS `userId`, `uploads`.`id` AS `mediaId` FROM `uploads` INNER JOIN `users` ON `uploads`.`user_id` = `users`.`id` WHERE `user_code` = ? AND `uploads`.`code` = ? LIMIT 1',
[
$userCode,
$mediaCode,
]
)->fetch();
if (!$withTags || !$media) {
return $media;
}
$media->tags = [];
foreach ($this->database->query(
'SELECT `tags`.`id`, `tags`.`name` FROM `uploads_tags` INNER JOIN `tags` ON `uploads_tags`.`tag_id` = `tags`.`id` WHERE `uploads_tags`.`upload_id` = ?',
$media->mediaId
) as $tag) {
$media->tags[$tag->id] = $tag->name;
}
return $media;
}
/**
* @param Request $request
* @param Response $response
* @param Filesystem $storage
* @param $media
* @param string $disposition
*
* @return Response
* @throws FileNotFoundException
*
*/
protected function streamMedia(
Request $request,
Response $response,
Filesystem $storage,
$media,
string $disposition = 'inline'
): Response {
set_time_limit(0);
$this->session->close();
$mime = $storage->getMimetype($media->storage_path);
if ((param($request, 'width') !== null || param($request, 'height') !== null) && explode(
'/',
$mime
)[0] === 'image') {
return $this->makeThumbnail(
$storage,
$media,
param($request, 'width'),
param($request, 'height'),
$disposition
);
}
$stream = new Stream($storage->readStream($media->storage_path));
if (!in_array(explode('/', $mime)[0], ['image', 'video', 'audio']) || $disposition === 'attachment') {
return $response->withHeader('Content-Type', $mime)
->withHeader('Content-Disposition', $disposition.'; filename="'.$media->filename.'"')
->withHeader('Content-Length', $stream->getSize())
->withBody($stream);
}
if (isset($request->getServerParams()['HTTP_RANGE'])) {
return $this->handlePartialRequest(
$response,
$stream,
$request->getServerParams()['HTTP_RANGE'],
$disposition,
$media,
$mime
);
}
return $response->withHeader('Content-Type', $mime)
->withHeader('Content-Length', $stream->getSize())
->withHeader('Accept-Ranges', 'bytes')
->withBody($stream);
}
/**
* @param Filesystem $storage
* @param $media
* @param null $width
* @param null $height
* @param string $disposition
*
* @return Response
* @throws FileNotFoundException
*
*/
protected function makeThumbnail(
Filesystem $storage,
$media,
$width = null,
$height = null,
string $disposition = 'inline'
) {
return Image::make($storage->readStream($media->storage_path))
->resize($width, $height, function (Constraint $constraint) {
$constraint->aspectRatio();
})
->resizeCanvas($width, $height, 'center')
->psrResponse('png')
->withHeader(
'Content-Disposition',
$disposition.';filename="scaled-'.pathinfo($media->filename, PATHINFO_FILENAME).'.png"'
);
}
/**
* @param Response $response
* @param Stream $stream
* @param string $range
* @param string $disposition
* @param $media
* @param $mime
*
* @return Response
*/
protected function handlePartialRequest(
Response $response,
Stream $stream,
string $range,
string $disposition,
$media,
$mime
) {
$end = $stream->getSize() - 1;
[, $range] = explode('=', $range, 2);
if (strpos($range, ',') !== false) {
return $response->withHeader('Content-Type', $mime)
->withHeader('Content-Disposition', $disposition.'; filename="'.$media->filename.'"')
->withHeader('Content-Length', $stream->getSize())
->withHeader('Accept-Ranges', 'bytes')
->withHeader('Content-Range', "0,{$stream->getSize()}")
->withStatus(416)
->withBody($stream);
}
if ($range === '-') {
$start = $stream->getSize() - (int) substr($range, 1);
} else {
$range = explode('-', $range);
$start = (int) $range[0];
$end = (isset($range[1]) && is_numeric($range[1])) ? (int) $range[1] : $stream->getSize();
}
if ($end > $stream->getSize() - 1) {
$end = $stream->getSize() - 1;
}
$stream->seek($start);
header("Content-Type: $mime");
header('Content-Length: '.($end - $start + 1));
header('Accept-Ranges: bytes');
header("Content-Range: bytes $start-$end/{$stream->getSize()}");
http_response_code(206);
ob_end_clean();
fpassthru($stream->detach());
exit(0);
}
}

View file

@ -1,74 +0,0 @@
<?php
namespace App\Controllers;
use App\Database\Repositories\UserRepository;
use App\Web\ValidationHelper;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
class ProfileController extends Controller
{
/**
* @param Request $request
* @param Response $response
*
* @return Response
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*/
public function profile(Request $request, Response $response): Response
{
$user = make(UserRepository::class)->get($request, $this->session->get('user_id'), true);
return view()->render($response, 'user/edit.twig', [
'profile' => true,
'user' => $user,
]);
}
/**
* @param Request $request
* @param Response $response
* @param int $id
*
* @return Response
*/
public function profileEdit(Request $request, Response $response, int $id): Response
{
$user = make(UserRepository::class)->get($request, $id, true);
/** @var ValidationHelper $validator */
$validator = make(ValidationHelper::class)
->alertIf(!filter_var(param($request, 'email'), FILTER_VALIDATE_EMAIL), 'email_required')
->alertIf($this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `email` = ? AND `email` <> ?', [param($request, 'email'), $user->email])->fetch()->count != 0, 'email_taken');
if ($validator->fails()) {
return redirect($response, route('profile'));
}
if (param($request, 'password') !== null && !empty(param($request, 'password'))) {
$this->database->query('UPDATE `users` SET `email`=?, `password`=?, `hide_uploads`=?, `copy_raw`=? WHERE `id` = ?', [
param($request, 'email'),
password_hash(param($request, 'password'), PASSWORD_DEFAULT),
param($request, 'hide_uploads') !== null ? 1 : 0,
param($request, 'copy_raw') !== null ? 1 : 0,
$user->id,
]);
} else {
$this->database->query('UPDATE `users` SET `email`=?, `hide_uploads`=?, `copy_raw`=? WHERE `id` = ?', [
param($request, 'email'),
param($request, 'hide_uploads') !== null ? 1 : 0,
param($request, 'copy_raw') !== null ? 1 : 0,
$user->id,
]);
}
$this->session->set('copy_raw', param($request, 'copy_raw') !== null ? 1 : 0)->alert(lang('profile_updated'), 'success');
$this->logger->info('User '.$this->session->get('username')." updated profile of $user->id.");
return redirect($response, route('profile'));
}
}

View file

@ -1,115 +0,0 @@
<?php
namespace App\Controllers;
use App\Database\Repositories\UserRepository;
use App\Web\Theme;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Exception\HttpBadRequestException;
use Slim\Exception\HttpInternalServerErrorException;
class SettingController extends Controller
{
/**
* @param Request $request
* @param Response $response
*
* @return Response
* @throws HttpInternalServerErrorException
*/
public function saveSettings(Request $request, Response $response): Response
{
if (!preg_match('/[0-9]+[K|M|G|T]/i', param($request, 'default_user_quota', '1G'))) {
$this->session->alert(lang('invalid_quota', 'danger'));
return redirect($response, route('system'));
}
if (param($request, 'recaptcha_enabled', 'off') === 'on' && (empty(param($request, 'recaptcha_site_key')) || empty(param($request, 'recaptcha_secret_key')))) {
$this->session->alert(lang('recaptcha_keys_required', 'danger'));
return redirect($response, route('system'));
}
// registrations
$this->updateSetting('register_enabled', param($request, 'register_enabled', 'off'));
$this->updateSetting('auto_tagging', param($request, 'auto_tagging', 'off'));
// quota
$this->updateSetting('quota_enabled', param($request, 'quota_enabled', 'off'));
$this->updateSetting('default_user_quota', stringToBytes(param($request, 'default_user_quota', '1G')));
$user = make(UserRepository::class)->get($request, $this->session->get('user_id'));
$this->setSessionQuotaInfo($user->current_disk_quota, $user->max_disk_quota);
$this->updateSetting('custom_head', param($request, 'custom_head'));
$this->updateSetting('recaptcha_enabled', param($request, 'recaptcha_enabled', 'off'));
$this->updateSetting('recaptcha_site_key', param($request, 'recaptcha_site_key'));
$this->updateSetting('recaptcha_secret_key', param($request, 'recaptcha_secret_key'));
$this->updateSetting('image_embeds', param($request, 'image_embeds'));
$this->applyTheme($request);
$this->applyLang($request);
$this->logger->info("User $user->username updated the system settings.");
$this->session->alert(lang('settings_saved'));
return redirect($response, route('system'));
}
/**
* @param Request $request
*/
public function applyLang(Request $request)
{
if (param($request, 'lang') !== 'auto') {
$this->updateSetting('lang', param($request, 'lang'));
} else {
$this->database->query('DELETE FROM `settings` WHERE `key` = \'lang\'');
}
}
/**
* @param Request $request
* @throws HttpInternalServerErrorException
*/
public function applyTheme(Request $request)
{
$css = param($request, 'css');
if ($css === null) {
return;
}
if (!is_writable(BASE_DIR.'static/bootstrap/css/bootstrap.min.css')) {
$this->session->alert(lang('cannot_write_file'), 'danger');
throw new HttpInternalServerErrorException($request);
}
make(Theme::class)->applyTheme($css);
// if is default, remove setting
if ($css !== Theme::default()) {
$this->updateSetting('css', $css);
} else {
$this->database->query('DELETE FROM `settings` WHERE `key` = \'css\'');
}
}
/**
* @param $key
* @param null $value
*/
private function updateSetting($key, $value = null)
{
if (!$this->database->query('SELECT `value` FROM `settings` WHERE `key` = '.$this->database->getPdo()->quote($key))->fetch()) {
$this->database->query(
'INSERT INTO `settings`(`key`, `value`) VALUES ('.$this->database->getPdo()->quote($key).', ?)',
$value
);
} else {
$this->database->query(
'UPDATE `settings` SET `value`=? WHERE `key` = '.$this->database->getPdo()->quote($key),
$value
);
}
}
}

View file

@ -1,79 +0,0 @@
<?php
namespace App\Controllers;
use App\Database\Repositories\TagRepository;
use App\Web\ValidationHelper;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Exception\HttpBadRequestException;
use Slim\Exception\HttpNotFoundException;
class TagController extends Controller
{
/**
* @param Request $request
* @param Response $response
* @return Response
* @throws HttpBadRequestException
*/
public function addTag(Request $request, Response $response): Response
{
$validator = $this->validateTag($request)->failIf(empty(param($request, 'tag')));
if ($validator->fails()) {
throw new HttpBadRequestException($request);
}
[$id, $limit] = make(TagRepository::class)->addTag(param($request, 'tag'), param($request, 'mediaId'));
$this->logger->info("Tag added $id.");
return json($response, [
'limitReached' => $limit,
'tagId' => $id,
'href' => queryParams(['tag' => $id]),
]);
}
/**
* @param Request $request
* @param Response $response
* @return Response
* @throws HttpBadRequestException
* @throws HttpNotFoundException
*/
public function removeTag(Request $request, Response $response): Response
{
$validator = $this->validateTag($request);
if ($validator->fails()) {
throw new HttpBadRequestException($request);
}
$result = make(TagRepository::class)->removeTag(param($request, 'tagId'), param($request, 'mediaId'));
if ($result === null) {
throw new HttpNotFoundException($request);
}
$this->logger->info("Tag removed ".param($request, 'tagId').', from media '.param($request, 'mediaId'));
return json($response, [
'deleted' => $result,
]);
}
/**
* @param Request $request
* @return ValidationHelper
*/
protected function validateTag(Request $request)
{
return make(ValidationHelper::class)
->failIf(empty(param($request, 'mediaId')))
->failIf($this->database->query('SELECT COUNT(*) AS `count` FROM `uploads` WHERE `id` = ?', param($request, 'mediaId'))->fetch()->count == 0)
->failIf(!$this->session->get('admin', false) && $this->database->query('SELECT `user_id` FROM `uploads` WHERE `id` = ? LIMIT 1', param($request, 'mediaId'))->fetch()->user_id != $this->session->get('user_id'));
}
}

View file

@ -1,194 +0,0 @@
<?php
namespace App\Controllers;
use App\Web\Session;
use Monolog\Logger;
use Parsedown;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use RuntimeException;
use ZipArchive;
use function glob_recursive;
use function redirect;
use function removeDirectory;
use function route;
use function urlFor;
class UpgradeController extends Controller
{
const GITHUB_SOURCE_API = 'https://api.github.com/repos/SergiX44/XBackBone/releases';
/**
* @param Response $response
*
* @param Logger $logger
* @param Session $session
* @return Response
*/
public function upgrade(Response $response, Logger $logger, Session $session): Response
{
if (!extension_loaded('zip')) {
$session->alert(lang('zip_ext_not_loaded'), 'danger');
return redirect($response, route('system'));
}
if (!is_writable(BASE_DIR)) {
$session->alert(lang('path_not_writable', BASE_DIR), 'warning');
return redirect($response, route('system'));
}
try {
$json = $this->getApiJson();
} catch (RuntimeException $e) {
$session->alert($e->getMessage(), 'danger');
return redirect($response, route('system'));
}
if (version_compare($json[0]->tag_name, PLATFORM_VERSION, '<=')) {
$session->alert(lang('already_latest_version'), 'warning');
return redirect($response, route('system'));
}
$tmpFile = sys_get_temp_dir().DIRECTORY_SEPARATOR.'xbackbone_update.zip';
if (file_put_contents($tmpFile, file_get_contents($json[0]->assets[0]->browser_download_url)) === false) {
$session->alert(lang('cannot_retrieve_file'), 'danger');
return redirect($response, route('system'));
}
if (filesize($tmpFile) !== $json[0]->assets[0]->size) {
$session->alert(lang('file_size_no_match'), 'danger');
return redirect($response, route('system'));
}
$logger->info('System update started.');
$config = require BASE_DIR.'config.php';
$config['maintenance'] = true;
file_put_contents(BASE_DIR.'config.php', '<?php'.PHP_EOL.'return '.var_export($config, true).';');
$currentFiles = array_merge(
glob_recursive(BASE_DIR.'app/*'),
glob_recursive(BASE_DIR.'bin/*'),
glob_recursive(BASE_DIR.'bootstrap/*'),
glob_recursive(BASE_DIR.'resources/templates/*'),
glob_recursive(BASE_DIR.'resources/lang/*'),
glob_recursive(BASE_DIR.'resources/schemas/*'),
glob_recursive(BASE_DIR.'static/*')
);
removeDirectory(BASE_DIR.'vendor/');
$updateZip = new ZipArchive();
$updateZip->open($tmpFile);
for ($i = 0; $i < $updateZip->numFiles; $i++) {
$nameIndex = $updateZip->getNameIndex($i);
$updateZip->extractTo(BASE_DIR, $nameIndex);
if (($key = array_search(rtrim(BASE_DIR.$nameIndex, '/'), $currentFiles)) !== false) {
unset($currentFiles[$key]);
}
}
$updateZip->close();
unlink($tmpFile);
foreach ($currentFiles as $extraneous) {
if (is_dir($extraneous)) {
removeDirectory($extraneous);
} else {
unlink($extraneous);
}
}
$logger->info('System update completed.');
return redirect($response, urlFor('/install'));
}
/**
* @param Request $request
* @param Response $response
*
* @return Response
*/
public function checkForUpdates(Request $request, Response $response): Response
{
$jsonResponse = [
'status' => 'OK',
'message' => lang('already_latest_version'),
'upgrade' => false,
];
$acceptPrerelease = param($request, 'prerelease', 'false') === 'true';
try {
$json = $this->getApiJson();
foreach ($json as $release) {
if (
$release->prerelease === $acceptPrerelease &&
version_compare($release->tag_name, PLATFORM_VERSION, '>') &&
version_compare($release->tag_name, '4.0.0', '<')
) {
$jsonResponse['message'] = lang('new_version_available', [$release->tag_name]);
$jsonResponse['upgrade'] = true;
break;
}
if (version_compare($release->tag_name, PLATFORM_VERSION, '<=')) {
break;
}
}
} catch (RuntimeException $e) {
$jsonResponse['status'] = 'ERROR';
$jsonResponse['message'] = $e->getMessage();
}
return json($response, $jsonResponse);
}
/**
* @param Request $request
* @param Response $response
* @return Response
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*/
public function changelog(Request $request, Response $response): Response
{
return view()->render($response, 'dashboard/changelog.twig', [
'content' => Parsedown::instance()->text(file_get_contents('CHANGELOG.md')),
]);
}
protected function getApiJson()
{
$opts = [
'http' => [
'method' => 'GET',
'header' => [
'User-Agent: XBackBone-App',
'Accept: application/vnd.github.v3+json',
],
],
];
$data = @file_get_contents(self::GITHUB_SOURCE_API, false, stream_context_create($opts));
if ($data === false) {
throw new RuntimeException('Cannot contact the Github API. Try again.');
}
return json_decode($data);
}
}

View file

@ -1,237 +0,0 @@
<?php
namespace App\Controllers;
use App\Database\Repositories\TagRepository;
use App\Database\Repositories\UserRepository;
use App\Exceptions\ValidationException;
use Exception;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\UploadedFileInterface;
class UploadController extends Controller
{
private $json = [
'message' => null,
'version' => PLATFORM_VERSION,
];
/**
* @param Response $response
*
* @return Response
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*/
public function uploadWebPage(Response $response): Response
{
$maxFileSize = min(stringToBytes(ini_get('post_max_size')), stringToBytes(ini_get('upload_max_filesize')));
return view()->render($response, 'upload/web.twig', [
'max_file_size' => humanFileSize($maxFileSize),
]);
}
/**
* @param Request $request
* @param Response $response
* @return Response
* @throws Exception
*/
public function uploadWeb(Request $request, Response $response): Response
{
try {
$file = $this->validateFile($request, $response);
$user = make(UserRepository::class)->get($request, $this->session->get('user_id'));
$this->validateUser($request, $response, $file, $user);
} catch (ValidationException $e) {
return $e->response();
}
if (!$this->updateUserQuota($request, $user->id, $file->getSize())) {
$this->json['message'] = 'User disk quota exceeded.';
return json($response, $this->json, 507);
}
try {
$response = $this->saveMedia($response, $file, $user);
$this->setSessionQuotaInfo($user->current_disk_quota + $file->getSize(), $user->max_disk_quota);
} catch (Exception $e) {
$this->updateUserQuota($request, $user->id, $file->getSize(), true);
throw $e;
}
return $response;
}
/**
* @param Request $request
* @param Response $response
*
* @return Response
* @throws Exception
*/
public function uploadEndpoint(Request $request, Response $response): Response
{
if ($this->config['maintenance']) {
$this->json['message'] = 'Endpoint under maintenance.';
return json($response, $this->json, 503);
}
try {
$file = $this->validateFile($request, $response);
if (param($request, 'token') === null) {
$this->json['message'] = 'Token not specified.';
return json($response, $this->json, 400);
}
$user = $this->database->query('SELECT * FROM `users` WHERE `token` = ? LIMIT 1', param($request, 'token'))->fetch();
$this->validateUser($request, $response, $file, $user);
} catch (ValidationException $e) {
return $e->response();
}
if (!$this->updateUserQuota($request, $user->id, $file->getSize())) {
$this->json['message'] = 'User disk quota exceeded.';
return json($response, $this->json, 507);
}
try {
$response = $this->saveMedia($response, $file, $user);
} catch (Exception $e) {
$this->updateUserQuota($request, $user->id, $file->getSize(), true);
throw $e;
}
return $response;
}
/**
* @param Request $request
* @param Response $response
* @return UploadedFileInterface
* @throws ValidationException
*/
protected function validateFile(Request $request, Response $response)
{
$iniValue = ini_get('post_max_size');
$maxPostSize = $iniValue === '0' ? INF : stringToBytes($iniValue);
if ($request->getServerParams()['CONTENT_LENGTH'] > $maxPostSize) {
$this->json['message'] = 'File too large (post_max_size too low?).';
throw new ValidationException(json($response, $this->json, 400));
}
$file = array_values($request->getUploadedFiles());
/** @var UploadedFileInterface|null $file */
$file = $file[0] ?? null;
if ($file === null) {
$this->json['message'] = 'Request without file attached.';
throw new ValidationException(json($response, $this->json, 400));
}
if ($file->getError() === UPLOAD_ERR_INI_SIZE) {
$this->json['message'] = 'File too large (upload_max_filesize too low?).';
throw new ValidationException(json($response, $this->json, 400));
}
return $file;
}
/**
* @param Request $request
* @param Response $response
* @param UploadedFileInterface $file
* @param $user
* @return void
* @throws ValidationException
*/
protected function validateUser(Request $request, Response $response, UploadedFileInterface $file, $user)
{
if (!$user) {
$this->json['message'] = 'Token specified not found.';
throw new ValidationException(json($response, $this->json, 404));
}
if (!$user->active) {
$this->json['message'] = 'Account disabled.';
throw new ValidationException(json($response, $this->json, 401));
}
}
/**
* @param Response $response
* @param UploadedFileInterface $file
* @param $user
* @return Response
* @throws \League\Flysystem\FileExistsException
* @throws \League\Flysystem\FileNotFoundException
*/
protected function saveMedia(Response $response, UploadedFileInterface $file, $user)
{
do {
$code = humanRandomString();
} while ($this->database->query('SELECT COUNT(*) AS `count` FROM `uploads` WHERE `code` = ?', $code)->fetch()->count > 0);
$fileInfo = pathinfo($file->getClientFilename());
$storagePath = "$user->user_code/$code.$fileInfo[extension]";
$this->storage->writeStream($storagePath, $file->getStream()->detach());
$this->database->query('INSERT INTO `uploads`(`user_id`, `code`, `filename`, `storage_path`, `published`) VALUES (?, ?, ?, ?, ?)', [
$user->id,
$code,
$file->getClientFilename(),
$storagePath,
$user->hide_uploads == '1' ? 0 : 1,
]);
$mediaId = $this->database->getPdo()->lastInsertId();
if ($this->getSetting('auto_tagging') === 'on') {
$this->autoTag($mediaId, $storagePath);
}
$this->json['message'] = 'OK';
$this->json['url'] = urlFor("/{$user->user_code}/{$code}.{$fileInfo['extension']}");
$this->json['raw_url'] = urlFor("/{$user->user_code}/{$code}/raw.{$fileInfo['extension']}");
$this->logger->info("User $user->username uploaded new media.", [$mediaId]);
return json($response, $this->json, 201);
}
/**
* @param $mediaId
* @param $storagePath
* @throws \League\Flysystem\FileNotFoundException
*/
protected function autoTag($mediaId, $storagePath)
{
$mime = $this->storage->getMimetype($storagePath);
[$type, $subtype] = explode('/', $mime);
/** @var TagRepository $query */
$query = make(TagRepository::class);
$query->addTag($type, $mediaId);
if ($type === 'application' || $subtype === 'gif') {
$query->addTag($subtype, $mediaId);
}
}
}

View file

@ -1,317 +0,0 @@
<?php
namespace App\Controllers;
use App\Database\Repositories\UserRepository;
use App\Web\Mail;
use App\Web\ValidationHelper;
use League\Flysystem\FileNotFoundException;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
class UserController extends Controller
{
const PER_PAGE = 15;
/**
* @param Response $response
* @param int|null $page
*
* @return Response
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*
* @throws \Twig\Error\LoaderError
*/
public function index(Response $response, int $page = 0): Response
{
$page = max(0, --$page);
$users = $this->database->query('SELECT * FROM `users` LIMIT ? OFFSET ?', [self::PER_PAGE, $page * self::PER_PAGE])->fetchAll();
$pages = $this->database->query('SELECT COUNT(*) AS `count` FROM `users`')->fetch()->count / self::PER_PAGE;
return view()->render(
$response,
'user/index.twig',
[
'users' => $users,
'next' => $page < floor($pages),
'previous' => $page >= 1,
'current_page' => ++$page,
'quota_enabled' => $this->getSetting('quota_enabled'),
]
);
}
/**
* @param Response $response
*
* @return Response
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*
* @throws \Twig\Error\LoaderError
*/
public function create(Response $response): Response
{
return view()->render($response, 'user/create.twig', [
'default_user_quota' => humanFileSize($this->getSetting('default_user_quota'), 0, true),
'quota_enabled' => $this->getSetting('quota_enabled', 'off'),
]);
}
/**
* @param Request $request
* @param Response $response
*
* @return Response
* @throws \Exception
*/
public function store(Request $request, Response $response): Response
{
$maxUserQuota = -1;
$validator = $this->getUserCreateValidator($request)
->callIf($this->getSetting('quota_enabled') === 'on', function ($session) use (&$maxUserQuota, &$request) {
$maxUserQuota = param($request, 'max_user_quota', humanFileSize($this->getSetting('default_user_quota'), 0, true));
if (!preg_match('/(^[0-9]+[B|K|M|G|T]$)|(^\-1$)/i', $maxUserQuota)) {
$session->alert(lang('invalid_quota', 'danger'));
return false;
}
if ($maxUserQuota !== '-1') {
$maxUserQuota = stringToBytes($maxUserQuota);
}
return true;
});
if ($validator->fails()) {
return redirect($response, route('user.create'));
}
make(UserRepository::class)->create(
param($request, 'email'),
param($request, 'username'),
param($request, 'password'),
param($request, 'is_admin') !== null ? 1 : 0,
param($request, 'is_active') !== null ? 1 : 0,
$maxUserQuota,
false,
param($request, 'hide_uploads') !== null ? 1 : 0,
param($request, 'copy_raw') !== null ? 1 : 0
);
if (param($request, 'send_notification') !== null) {
$resetToken = null;
if (empty(param($request, 'password'))) {
$resetToken = bin2hex(random_bytes(16));
$this->database->query('UPDATE `users` SET `reset_token`=? WHERE `id` = ?', [
$resetToken,
$this->database->getPdo()->lastInsertId(),
]);
}
$this->sendCreateNotification($request, $resetToken);
}
$this->session->alert(lang('user_created', [param($request, 'username')]), 'success');
$this->logger->info('User '.$this->session->get('username').' created a new user.', [array_diff_key($request->getParsedBody(), array_flip(['password']))]);
return redirect($response, route('user.index'));
}
/**
* @param Request $request
* @param Response $response
* @param int $id
*
* @return Response
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*/
public function edit(Request $request, Response $response, int $id): Response
{
$user = make(UserRepository::class)->get($request, $id);
return view()->render($response, 'user/edit.twig', [
'profile' => false,
'user' => $user,
'quota_enabled' => $this->getSetting('quota_enabled', 'off'),
'max_disk_quota' => $user->max_disk_quota > 0 ? humanFileSize($user->max_disk_quota, 0, true) : -1,
]);
}
/**
* @param Request $request
* @param Response $response
* @param int $id
*
* @return Response
*/
public function update(Request $request, Response $response, int $id): Response
{
$user = make(UserRepository::class)->get($request, $id);
$user->max_disk_quota = -1;
/** @var ValidationHelper $validator */
$validator = make(ValidationHelper::class)
->alertIf(!filter_var(param($request, 'email'), FILTER_VALIDATE_EMAIL), 'email_required')
->alertIf(empty(param($request, 'username')), 'username_required')
->alertIf($this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `email` = ? AND `email` <> ?', [param($request, 'email'), $user->email])->fetch()->count != 0, 'email_taken')
->alertIf($this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `username` = ? AND `username` <> ?', [param($request, 'username'), $user->username])->fetch()->count != 0, 'username_taken')
->alertIf($user->id === $this->session->get('user_id') && param($request, 'is_admin') === null, 'cannot_demote')
->callIf($this->getSetting('quota_enabled') === 'on', function ($session) use (&$user, &$request) {
$maxUserQuota = param($request, 'max_user_quota', humanFileSize($this->getSetting('default_user_quota'), 0, true));
if (!preg_match('/(^[0-9]+[B|K|M|G|T]$)|(^\-1$)/i', $maxUserQuota)) {
$session->alert(lang('invalid_quota', 'danger'));
return false;
}
if ($maxUserQuota !== '-1') {
$user->max_disk_quota = stringToBytes($maxUserQuota);
}
return true;
});
if ($validator->fails()) {
return redirect($response, route('user.edit', ['id' => $id]));
}
make(UserRepository::class)->update(
$user->id,
param($request, 'email'),
param($request, 'username'),
param($request, 'password'),
param($request, 'is_admin') !== null ? 1 : 0,
param($request, 'is_active') !== null ? 1 : 0,
$user->max_disk_quota,
param($request, 'ldap') !== null ? 1 : 0,
param($request, 'hide_uploads') !== null ? 1 : 0,
param($request, 'copy_raw') !== null ? 1 : 0
);
if ($user->id === $this->session->get('user_id')) {
$this->setSessionQuotaInfo($user->current_disk_quota, $user->max_disk_quota);
}
$this->session->alert(lang('user_updated', [param($request, 'username')]), 'success');
$this->logger->info('User '.$this->session->get('username')." updated $user->id.", [
array_diff_key((array) $user, array_flip(['password'])),
array_diff_key($request->getParsedBody(), array_flip(['password'])),
]);
return redirect($response, route('user.index'));
}
/**
* @param Request $request
* @param Response $response
* @param int $id
*
* @return Response
*/
public function delete(Request $request, Response $response, int $id): Response
{
$user = make(UserRepository::class)->get($request, $id);
if ($user->id === $this->session->get('user_id')) {
$this->session->alert(lang('cannot_delete'), 'danger');
return redirect($response, route('user.index'));
}
$this->database->query('DELETE FROM `users` WHERE `id` = ?', $user->id);
$this->session->alert(lang('user_deleted'), 'success');
$this->logger->info('User '.$this->session->get('username')." deleted $user->id.");
return redirect($response, route('user.index'));
}
/**
* @param Request $request
* @param Response $response
* @param int $id
* @return Response
*/
public function clearUserMedia(Request $request, Response $response, int $id): Response
{
$user = make(UserRepository::class)->get($request, $id, true);
$medias = $this->database->query('SELECT * FROM `uploads` WHERE `user_id` = ?', $user->id);
foreach ($medias as $media) {
try {
$this->storage->delete($media->storage_path);
} catch (FileNotFoundException $e) {
}
}
$this->database->query('DELETE FROM `uploads` WHERE `user_id` = ?', $user->id);
$this->database->query('UPDATE `users` SET `current_disk_quota`=? WHERE `id` = ?', [
0,
$user->id,
]);
$this->session->alert(lang('account_media_deleted'), 'success');
return redirect($response, route('user.edit', ['id' => $id]));
}
/**
* @param Request $request
* @param Response $response
* @param int $id
*
* @return Response
*/
public function refreshToken(Request $request, Response $response, int $id): Response
{
$query = make(UserRepository::class);
$user = $query->get($request, $id, true);
$this->logger->info('User '.$this->session->get('username')." refreshed token of user $user->id.");
$response->getBody()->write($query->refreshToken($user->id));
return $response;
}
/**
* @param $request
* @param null $resetToken
*/
private function sendCreateNotification($request, $resetToken = null)
{
if ($resetToken === null && !empty(param($request, 'password'))) {
$message = lang('mail.new_account_text_with_pw', [
param($request, 'username'),
$this->config['app_name'],
$this->config['base_url'],
$this->config['base_url'],
param($request, 'username'),
param($request, 'password'),
route('login.show'),
route('login.show'),
]);
} else {
$message = lang('mail.new_account_text_with_reset', [
param($request, 'username'),
$this->config['app_name'],
$this->config['base_url'],
$this->config['base_url'],
route('recover.password', ['resetToken' => $resetToken]),
route('recover.password', ['resetToken' => $resetToken]),
]);
}
Mail::make()
->from(platform_mail(), $this->config['app_name'])
->to(param($request, 'email'))
->subject(lang('mail.new_account', [$this->config['app_name']]))
->message($message)
->send();
}
}

View file

@ -1,65 +0,0 @@
<?php
namespace App\Database;
use PDO;
class DB
{
/** @var DB */
protected static $instance;
/** @var PDO */
protected $pdo;
/** @var string */
protected $currentDriver;
public function __construct(string $dsn, string $username = null, string $password = null)
{
$this->pdo = new PDO($dsn, $username, $password);
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ);
$this->currentDriver = $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
if ($this->currentDriver === 'sqlite') {
$this->pdo->exec('PRAGMA foreign_keys = ON');
}
}
public function query(string $query, $parameters = [])
{
if (!is_array($parameters)) {
$parameters = [$parameters];
}
$query = $this->pdo->prepare($query);
foreach ($parameters as $index => $parameter) {
$query->bindValue($index + 1, $parameter, is_int($parameter) ? PDO::PARAM_INT : PDO::PARAM_STR);
}
$query->execute();
return $query;
}
/**
* Get the PDO instance.
*
* @return PDO
*/
public function getPdo(): PDO
{
return $this->pdo;
}
/**
* Get the current PDO driver.
*
* @return string
*/
public function getCurrentDriver(): string
{
return $this->currentDriver;
}
}

View file

@ -1,111 +0,0 @@
<?php
namespace App\Database;
use League\Flysystem\FileNotFoundException;
use League\Flysystem\Filesystem;
use PDOException;
class Migrator
{
/**
* @var DB
*/
private $db;
/**
* @var string
*/
private $schemaPath;
/**
* Migrator constructor.
*
* @param DB $db
* @param string|null $schemaPath
*/
public function __construct(DB $db, ?string $schemaPath)
{
$this->db = $db;
$this->schemaPath = $schemaPath;
}
public function migrate(): void
{
$this->db->getPdo()->exec(file_get_contents($this->schemaPath.DIRECTORY_SEPARATOR.'migrations.sql'));
$files = glob($this->schemaPath.'/'.$this->db->getCurrentDriver().'/*.sql');
$names = array_map(static function ($path) {
return basename($path);
}, $files);
$in = str_repeat('?, ', count($names) - 1).'?';
$inMigrationsTable = $this->db->query("SELECT * FROM `migrations` WHERE `name` IN ($in)", $names)->fetchAll();
foreach ($files as $file) {
$continue = false;
$exists = false;
foreach ($inMigrationsTable as $migration) {
if (basename($file) === $migration->name && $migration->migrated) {
$continue = true;
break;
}
if (basename($file) === $migration->name && !$migration->migrated) {
$exists = true;
break;
}
}
if ($continue) {
continue;
}
$sql = file_get_contents($file);
try {
$this->db->getPdo()->exec($sql);
if (!$exists) {
$this->db->query('INSERT INTO `migrations` VALUES (?,?)', [basename($file), 1]);
} else {
$this->db->query('UPDATE `migrations` SET `migrated`=? WHERE `name`=?', [1, basename($file)]);
}
} catch (PDOException $exception) {
if (!$exists) {
$this->db->query('INSERT INTO `migrations` VALUES (?,?)', [basename($file), 0]);
}
throw $exception;
}
}
}
/**
* @param Filesystem $filesystem
*/
public function reSyncQuotas(Filesystem $filesystem)
{
$uploads = $this->db->query('SELECT `id`,`user_id`, `storage_path` FROM `uploads`')->fetchAll();
$usersQuotas = [];
foreach ($uploads as $upload) {
if (!array_key_exists($upload->user_id, $usersQuotas)) {
$usersQuotas[$upload->user_id] = 0;
}
try {
$usersQuotas[$upload->user_id] += $filesystem->getSize($upload->storage_path);
} catch (FileNotFoundException $e) {
$this->db->query('DELETE FROM `uploads` WHERE `id` = ?', $upload->id);
}
}
foreach ($usersQuotas as $userId => $quota) {
$this->db->query('UPDATE `users` SET `current_disk_quota`=? WHERE `id` = ?', [
$quota,
$userId,
]);
}
}
}

View file

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

View file

@ -1,106 +0,0 @@
<?php
namespace App\Database\Repositories;
use App\Database\DB;
use PDO;
class TagRepository
{
public const PER_MEDIA_LIMIT = 10;
/**
* @var DB
*/
private $db;
/**
* @var null|bool
*/
private $isAdmin;
/**
* @var null|int|string
*/
private $userId;
public function __construct(DB $db, $isAdmin = null, $userId = null)
{
$this->db = $db;
$this->isAdmin = $isAdmin;
$this->userId = $userId;
}
/**
* @return array
*/
public function all()
{
if ($this->isAdmin) {
return $this->db->query('SELECT * FROM `tags` ORDER BY `name`')->fetchAll();
}
return $this->db->query('SELECT DISTINCT `tags`.* FROM `tags` INNER JOIN `uploads_tags` ON `tags`.`id` = `uploads_tags`.`tag_id` INNER JOIN `uploads` ON `uploads`.`id` = `uploads_tags`.`upload_id` WHERE `uploads`.`user_id` = ? ORDER BY `tags`.`name`', $this->userId)->fetchAll();
}
/**
* @param string $tagName
* @param $mediaId
* @return array [id, limit]
*/
public function addTag(string $tagName, $mediaId)
{
$tag = $this->db->query('SELECT * FROM `tags` WHERE `name` = ? LIMIT 1', $tagName)->fetch();
$connectedIds = $this->db->query('SELECT `tag_id` FROM `uploads_tags` WHERE `upload_id` = ?', $mediaId)->fetchAll(PDO::FETCH_COLUMN, 0);
if (!$tag && count($connectedIds) < self::PER_MEDIA_LIMIT) {
$this->db->query('INSERT INTO `tags`(`name`) VALUES (?)', strtolower($tagName));
$tagId = $this->db->getPdo()->lastInsertId();
$this->db->query('INSERT INTO `uploads_tags`(`upload_id`, `tag_id`) VALUES (?, ?)', [
$mediaId,
$tagId,
]);
return [$tagId, false];
}
if (count($connectedIds) >= self::PER_MEDIA_LIMIT || in_array($tag->id, $connectedIds)) {
return [null, true];
}
$this->db->query('INSERT INTO `uploads_tags`(`upload_id`, `tag_id`) VALUES (?, ?)', [
$mediaId,
$tag->id,
]);
return [$tag->id, false];
}
/**
* @param $tagId
* @param $mediaId
* @return bool
*/
public function removeTag($tagId, $mediaId)
{
$tag = $this->db->query('SELECT * FROM `tags` WHERE `id` = ? LIMIT 1', $tagId)->fetch();
if ($tag) {
$this->db->query('DELETE FROM `uploads_tags` WHERE `upload_id` = ? AND `tag_id` = ?', [
$mediaId,
$tag->id,
]);
if ($this->db->query('SELECT COUNT(*) AS `count` FROM `uploads_tags` WHERE `tag_id` = ?', $tag->id)->fetch()->count == 0) {
$this->db->query('DELETE FROM `tags` WHERE `id` = ? ', $tag->id);
return true;
}
return false;
}
return null;
}
}

View file

@ -1,181 +0,0 @@
<?php
namespace App\Database\Repositories;
use App\Database\DB;
use App\Web\Session;
use InvalidArgumentException;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Exception\HttpNotFoundException;
use Slim\Exception\HttpUnauthorizedException;
class UserRepository
{
/**
* @var DB
*/
private $database;
/**
* @var Session
*/
private $session;
/**
* UserQuery constructor.
* @param DB $db
* @param Session|null $session
*/
public function __construct(DB $db, ?Session $session)
{
$this->database = $db;
$this->session = $session;
}
/**
* @param DB $db
* @param Session|null $session
* @return UserRepository
*/
public static function make(DB $db, Session $session = null)
{
return new self($db, $session);
}
/**
* @param Request $request
* @param $id
* @param bool $authorize
* @return mixed
* @throws HttpNotFoundException
* @throws HttpUnauthorizedException
*/
public function get(Request $request, $id, $authorize = false)
{
$user = $this->database->query('SELECT * FROM `users` WHERE `id` = ? LIMIT 1', $id)->fetch();
if (!$user) {
throw new HttpNotFoundException($request);
}
if ($authorize) {
if ($this->session === null) {
throw new InvalidArgumentException('The session is null.');
}
if ($user->id !== $this->session->get('user_id') && !$this->session->get('admin', false)) {
throw new HttpUnauthorizedException($request);
}
}
return $user;
}
/**
* @param string $email
* @param string $username
* @param string|null $password
* @param int $isAdmin
* @param int $isActive
* @param int $maxUserQuota
* @param string|null $activateToken
* @param int $ldap
* @param int $hideUploads
* @param int $copyRaw
* @return bool|\PDOStatement|string
*/
public function create(string $email, string $username, string $password = null, int $isAdmin = 0, int $isActive = 0, int $maxUserQuota = -1, string $activateToken = null, int $ldap = 0, int $hideUploads = 0, int $copyRaw = 0)
{
do {
$userCode = humanRandomString(5);
} while ($this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `user_code` = ?', $userCode)->fetch()->count > 0);
$token = $this->generateUserUploadToken();
return $this->database->query('INSERT INTO `users`(`email`, `username`, `password`, `is_admin`, `active`, `user_code`, `token`, `max_disk_quota`, `activate_token`, `ldap`, `hide_uploads`, `copy_raw`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [
$email,
$username,
$password !== null ? password_hash($password, PASSWORD_DEFAULT) : null,
$isAdmin,
$isActive,
$userCode,
$token,
$maxUserQuota,
$activateToken,
$ldap,
$hideUploads,
$copyRaw,
]);
}
/**
* @param $id
* @param string $email
* @param string $username
* @param string|null $password
* @param int $isAdmin
* @param int $isActive
* @param int $maxUserQuota
* @param int $ldap
* @param int $hideUploads
* @param int $copyRaw
* @return bool|\PDOStatement|string
*/
public function update($id, string $email, string $username, string $password = null, int $isAdmin = 0, int $isActive = 0, int $maxUserQuota = -1, int $ldap = 0, int $hideUploads = 0, int $copyRaw = 0)
{
if (!empty($password)) {
return $this->database->query('UPDATE `users` SET `email`=?, `username`=?, `password`=?, `is_admin`=?, `active`=?, `max_disk_quota`=?, `ldap`=?, `hide_uploads`=?, `copy_raw`=? WHERE `id` = ?', [
$email,
$username,
password_hash($password, PASSWORD_DEFAULT),
$isAdmin,
$isActive,
$maxUserQuota,
$ldap,
$hideUploads,
$copyRaw,
$id,
]);
}
return $this->database->query('UPDATE `users` SET `email`=?, `username`=?, `is_admin`=?, `active`=?, `max_disk_quota`=?, `ldap`=?, `hide_uploads`=?, `copy_raw`=? WHERE `id` = ?', [
$email,
$username,
$isAdmin,
$isActive,
$maxUserQuota,
$ldap,
$hideUploads,
$copyRaw,
$id,
]);
}
/**
* @param $id
* @return string
*/
public function refreshToken($id)
{
$token = $this->generateUserUploadToken();
$this->database->query('UPDATE `users` SET `token`=? WHERE `id` = ?', [
$token,
$id,
]);
return $token;
}
/**
* @return string
*/
protected function generateUserUploadToken(): string
{
do {
$token = 'token_'.md5(uniqid('', true));
} while ($this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `token` = ?', $token)->fetch()->count > 0);
return $token;
}
}

View file

@ -1,27 +0,0 @@
<?php
namespace App\Exceptions\Handlers;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Handlers\ErrorHandler;
use Throwable;
class AppErrorHandler extends ErrorHandler
{
protected function logError(string $error): void
{
resolve('logger')->error($error);
}
public function __invoke(ServerRequestInterface $request, Throwable $exception, bool $displayErrorDetails, bool $logErrors, bool $logErrorDetails): ResponseInterface
{
$response = parent::__invoke($request, $exception, $displayErrorDetails, $logErrors, $logErrorDetails);
if ($response->getStatusCode() !== 404) {
$this->writeToErrorLog();
}
return $response;
}
}

View file

@ -1,50 +0,0 @@
<?php
namespace App\Exceptions\Handlers\Renderers;
use App\Exceptions\UnderMaintenanceException;
use Slim\Exception\HttpBadRequestException;
use Slim\Exception\HttpForbiddenException;
use Slim\Exception\HttpMethodNotAllowedException;
use Slim\Exception\HttpNotFoundException;
use Slim\Exception\HttpUnauthorizedException;
use Slim\Interfaces\ErrorRendererInterface;
use Throwable;
class HtmlErrorRenderer implements ErrorRendererInterface
{
/**
* @param Throwable $exception
* @param bool $displayErrorDetails
*
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*
* @return string
*/
public function __invoke(Throwable $exception, bool $displayErrorDetails): string
{
if ($exception instanceof UnderMaintenanceException) {
return view()->string('errors/maintenance.twig');
}
if ($exception instanceof HttpUnauthorizedException || $exception instanceof HttpForbiddenException) {
return view()->string('errors/403.twig');
}
if ($exception instanceof HttpMethodNotAllowedException) {
return view()->string('errors/405.twig');
}
if ($exception instanceof HttpNotFoundException) {
return view()->string('errors/404.twig');
}
if ($exception instanceof HttpBadRequestException) {
return view()->string('errors/400.twig');
}
return view()->string('errors/500.twig', ['exception' => $displayErrorDetails ? $exception : null]);
}
}

View file

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

View file

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

View file

@ -1,71 +0,0 @@
<?php
namespace App\Factories;
use App\Web\View;
use Psr\Container\ContainerInterface as Container;
use Slim\Factory\ServerRequestCreatorFactory;
use Twig\Environment;
use Twig\Loader\FilesystemLoader;
use Twig\TwigFunction;
class ViewFactory
{
public static function createAppInstance(Container $container)
{
$config = $container->get('config');
$loader = new FilesystemLoader(BASE_DIR.'resources/templates');
$twig = new Environment($loader, [
'cache' => BASE_DIR.'resources/cache/twig',
'autoescape' => 'html',
'debug' => $config['debug'],
'auto_reload' => $config['debug'],
]);
$request = ServerRequestCreatorFactory::determineServerRequestCreator()->createServerRequestFromGlobals();
$twig->addGlobal('config', $config);
$twig->addGlobal('request', $request);
$twig->addGlobal('session', $container->get('session'));
$twig->addGlobal('current_lang', $container->get('lang')->getLang());
$twig->addGlobal('maxUploadSize', stringToBytes(ini_get('post_max_size')));
$twig->addGlobal('PLATFORM_VERSION', PLATFORM_VERSION);
$twig->addFunction(new TwigFunction('route', 'route'));
$twig->addFunction(new TwigFunction('lang', 'lang'));
$twig->addFunction(new TwigFunction('urlFor', 'urlFor'));
$twig->addFunction(new TwigFunction('asset', 'asset'));
$twig->addFunction(new TwigFunction('mime2font', 'mime2font'));
$twig->addFunction(new TwigFunction('queryParams', 'queryParams'));
$twig->addFunction(new TwigFunction('isDisplayableImage', 'isDisplayableImage'));
$twig->addFunction(new TwigFunction('inPath', 'inPath'));
$twig->addFunction(new TwigFunction('humanFileSize', 'humanFileSize'));
$twig->addFunction(new TwigFunction('param', 'param'));
$twig->addFunction(new TwigFunction('glue', 'glue'));
return new View($twig);
}
public static function createInstallerInstance(Container $container)
{
$config = $container->get('config');
$loader = new FilesystemLoader([BASE_DIR.'install/templates', BASE_DIR.'resources/templates']);
$twig = new Environment($loader, [
'cache' => false,
'autoescape' => 'html',
'debug' => $config['debug'],
'auto_reload' => $config['debug'],
]);
$request = ServerRequestCreatorFactory::determineServerRequestCreator()->createServerRequestFromGlobals();
$twig->addGlobal('config', $config);
$twig->addGlobal('request', $request);
$twig->addGlobal('session', $container->get('session'));
$twig->addGlobal('PLATFORM_VERSION', PLATFORM_VERSION);
return new View($twig);
}
}

View file

@ -1,30 +0,0 @@
<?php
namespace App\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Exception\HttpUnauthorizedException;
class AdminMiddleware extends Middleware
{
/**
* @param Request $request
* @param RequestHandler $handler
*
* @throws HttpUnauthorizedException
*
* @return ResponseInterface
*/
public function __invoke(Request $request, RequestHandler $handler): ResponseInterface
{
if (!$this->database->query('SELECT `id`, `is_admin` FROM `users` WHERE `id` = ? LIMIT 1', [$this->session->get('user_id')])->fetch()->is_admin) {
$this->session->set('admin', false);
throw new HttpUnauthorizedException($request);
}
return $handler->handle($request);
}
}

View file

@ -1,35 +0,0 @@
<?php
namespace App\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Psr7\Factory\ResponseFactory;
class AuthMiddleware extends Middleware
{
/**
* @param Request $request
* @param RequestHandler $handler
*
* @return ResponseInterface
*/
public function __invoke(Request $request, RequestHandler $handler): ResponseInterface
{
if (!$this->session->get('logged', false)) {
$this->session->set('redirectTo', (string) $request->getUri()->getPath());
return redirect((new ResponseFactory())->createResponse(), route('login.show'));
}
if (!$this->database->query('SELECT `id`, `active` FROM `users` WHERE `id` = ? LIMIT 1', [$this->session->get('user_id')])->fetch()->active) {
$this->session->alert(lang('account_disabled'), 'danger');
$this->session->set('logged', false);
return redirect((new ResponseFactory())->createResponse(), route('login.show'));
}
return $handler->handle($request);
}
}

View file

@ -1,28 +0,0 @@
<?php
namespace App\Middleware;
use App\Exceptions\UnderMaintenanceException;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
class CheckForMaintenanceMiddleware extends Middleware
{
/**
* @param Request $request
* @param RequestHandler $handler
*
* @throws UnderMaintenanceException
*
* @return Response
*/
public function __invoke(Request $request, RequestHandler $handler): Response
{
if ($this->config['maintenance'] && !$this->database->query('SELECT `id`, `is_admin` FROM `users` WHERE `id` = ? LIMIT 1', [$this->session->get('user_id')])->fetch()->is_admin) {
throw new UnderMaintenanceException($request);
}
return $handler->handle($request);
}
}

View file

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

View file

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

View file

@ -1,19 +0,0 @@
<?php
namespace App\Middleware;
use App\Controllers\Controller;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
abstract class Middleware extends Controller
{
/**
* @param Request $request
* @param RequestHandler $handler
*
* @return Response
*/
abstract public function __invoke(Request $request, RequestHandler $handler);
}

View file

@ -1,41 +0,0 @@
<?php
namespace App\Middleware;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
class RememberMiddleware extends Middleware
{
/**
* @param Request $request
* @param RequestHandler $handler
*
* @return Response
* @throws \Exception
*/
public function __invoke(Request $request, RequestHandler $handler)
{
if (!$this->session->get('logged', false) && !empty($request->getCookieParams()['remember'])) {
[$selector, $token] = explode(':', $request->getCookieParams()['remember']);
$user = $this->database->query(
'SELECT `id`, `username`,`is_admin`, `active`, `remember_token`, `current_disk_quota`, `max_disk_quota`, `copy_raw` FROM `users` WHERE `remember_selector` = ? AND `remember_expire` > ? LIMIT 1',
[$selector, date('Y-m-d\TH:i:s', time())]
)->fetch();
if ($user && password_verify($token, $user->remember_token) && $user->active) {
$this->session->set('logged', true)
->set('user_id', $user->id)
->set('username', $user->username)
->set('admin', $user->is_admin)
->set('copy_raw', $user->copy_raw);
$this->setSessionQuotaInfo($user->current_disk_quota, $user->max_disk_quota);
$this->refreshRememberCookie($user->id);
}
}
return $handler->handle($request);
}
}

View file

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

View file

@ -1,152 +0,0 @@
<?php
namespace App\Web;
use InvalidArgumentException;
class Mail
{
/**
* @var bool
*/
private static $testing = false;
protected $fromMail = 'no-reply@example.com';
protected $fromName;
protected $to;
protected $subject;
protected $message;
protected $additionalHeaders = '';
protected $headers = '';
/**
* @return Mail
*/
public static function make()
{
return new self();
}
/**
* This will skip the email send
*/
public static function fake()
{
self::$testing = true;
}
/**
* @param $mail
* @param $name
* @return $this
*/
public function from(string $mail, string $name)
{
$this->fromMail = $mail;
$this->fromName = $name;
return $this;
}
/**
* @param $mail
* @return $this
*/
public function to(string $mail)
{
if (!filter_var($mail, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Mail not valid.');
}
$this->to = "<$mail>";
return $this;
}
/**
* @param $text
* @return $this
*/
public function subject(string $text)
{
$this->subject = $text;
return $this;
}
/**
* @param $text
* @return $this
*/
public function message(string $text)
{
$this->message = $text;
return $this;
}
/**
* @param $header
* @return $this
*/
public function addHeader(string $header)
{
$this->additionalHeaders .= "$header\r\n";
return $this;
}
/**
* @param $header
* @return $this
*/
protected function addRequiredHeader(string $header)
{
$this->headers .= "$header\r\n";
return $this;
}
/**
* Set headers before send
*/
protected function setHeaders()
{
if ($this->fromName === null) {
$this->addRequiredHeader("From: $this->fromMail");
} else {
$this->addRequiredHeader("From: $this->fromName <$this->fromMail>");
}
$this->addRequiredHeader('X-Mailer: PHP/'.phpversion())
->addRequiredHeader('MIME-Version: 1.0')
->addRequiredHeader('Content-Type: text/html; charset=utf-8');
}
/**
* @return int
*/
public function send()
{
if ($this->to === null) {
throw new InvalidArgumentException('Target email cannot be null.');
}
if ($this->subject === null) {
throw new InvalidArgumentException('Subject cannot be null.');
}
if ($this->message === null) {
throw new InvalidArgumentException('Message cannot be null.');
}
$this->setHeaders();
$this->headers .= $this->additionalHeaders;
$message = html_entity_decode($this->message);
if (self::$testing) {
return 1;
}
return (int) mail($this->to, $this->subject, "<html><body>$message</body></html>", $this->headers);
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,69 +0,0 @@
<?php
namespace App\Web;
use Psr\Http\Message\ResponseInterface as Response;
use Twig\Environment;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Error\SyntaxError;
class View
{
/**
* @var Environment
*/
private $twig;
/**
* View constructor.
*
* @param Environment $twig
*/
public function __construct(Environment $twig)
{
$this->twig = $twig;
}
/**
* @param Response $response
* @param string $view
* @param array|null $parameters
*
* @throws LoaderError
* @throws RuntimeError
* @throws SyntaxError
*
* @return Response
*/
public function render(Response $response, string $view, ?array $parameters = [])
{
$body = $this->twig->render($view, $parameters);
$response->getBody()->write($body);
return $response;
}
/**
* @param string $view
* @param array|null $parameters
*
* @throws LoaderError
* @throws RuntimeError
* @throws SyntaxError
*
* @return string
*/
public function string(string $view, ?array $parameters = [])
{
return $this->twig->render($view, $parameters);
}
/**
* @return Environment
*/
public function getTwig(): Environment
{
return $this->twig;
}
}

17
app/bootstrap/app.php Normal file
View file

@ -0,0 +1,17 @@
<?php
use Illuminate\Foundation\Application;
if (!defined('APP_ROOT')) {
define('APP_ROOT', dirname(__DIR__));
putenv('APP_ROOT='.APP_ROOT);
}
putenv('COMPOSER_VENDOR_DIR='.APP_ROOT.'/vendor');
/** @var Application $app */
$app = require APP_ROOT.'/vendor/xbb/core/bootstrap/app.php';
return $app->usePublicPath(APP_ROOT.'/public')
->useEnvironmentPath(APP_ROOT)
->useStoragePath(APP_ROOT.'/storage')
->useBootstrapPath(__DIR__);

2
app/bootstrap/cache/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*
!.gitignore

35
app/composer.json Normal file
View file

@ -0,0 +1,35 @@
{
"name": "xbb/app",
"description": "The standalone xbackbone application",
"license": "AGPL-3.0-only",
"authors": [
{
"name": "Sergio Brighenti",
"email": "sergio@brighenti.me"
}
],
"minimum-stability": "stable",
"repositories": [
{
"type": "path",
"url": "../core/"
}
],
"require": {
"xbb/core": "dev-next as 1.0"
},
"scripts": {
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php xbb package:discover --ansi",
"@php xbb vendor:publish --tag=app --force --ansi",
"@php xbb vendor:publish --tag=app-img --ansi"
],
"post-create-project-cmd": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
"@php xbb key:generate --ansi",
"@php -r \"file_exists('xbb.db') || touch('xbb.db');\"",
"@php xbb migrate --graceful --ansi"
]
}
}

6609
app/composer.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

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

21
app/public/.htaccess Normal file
View file

@ -0,0 +1,21 @@
<IfModule mod_rewrite.c>
<IfModule mod_negotiation.c>
Options -MultiViews -Indexes
</IfModule>
RewriteEngine On
# Handle Authorization Header
RewriteCond %{HTTP:Authorization} .
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
# Redirect Trailing Slashes If Not A Folder...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} (.+)/$
RewriteRule ^ %1 [L,R=301]
# Send Requests To Front Controller...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L]
</IfModule>

16
app/public/index.php Normal file
View file

@ -0,0 +1,16 @@
<?php
use Illuminate\Http\Request;
define('LARAVEL_START', microtime(true));
// Determine if the application is in maintenance mode...
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
require $maintenance;
}
// Register the Composer autoloader...
require __DIR__.'/../vendor/autoload.php';
// Bootstrap Laravel and handle the request...
(require_once __DIR__.'/../bootstrap/app.php')->handleRequest(Request::capture());

2
app/public/robots.txt Normal file
View file

@ -0,0 +1,2 @@
User-agent: *
Disallow:

View file

@ -1,93 +0,0 @@
<?php
use App\Controllers\AdminController;
use App\Controllers\Auth\LoginController;
use App\Controllers\Auth\PasswordRecoveryController;
use App\Controllers\Auth\RegisterController;
use App\Controllers\ClientController;
use App\Controllers\DashboardController;
use App\Controllers\ExportController;
use App\Controllers\MediaController;
use App\Controllers\ProfileController;
use App\Controllers\SettingController;
use App\Controllers\TagController;
use App\Controllers\UpgradeController;
use App\Controllers\UploadController;
use App\Controllers\UserController;
use App\Middleware\AdminMiddleware;
use App\Middleware\AuthMiddleware;
use App\Middleware\CheckForMaintenanceMiddleware;
use Slim\Routing\RouteCollectorProxy;
global $app;
$app->group('', function (RouteCollectorProxy $group) {
$group->get('/home[/page/{page}]', [DashboardController::class, 'home'])->setName('home');
$group->get('/upload', [UploadController::class, 'uploadWebPage'])->setName('upload.web.show');
$group->post('/upload/web', [UploadController::class, 'uploadWeb'])->setName('upload.web');
$group->get('/home/switchView', [DashboardController::class, 'switchView'])->setName('switchView');
$group->group('', function (RouteCollectorProxy $group) {
$group->get('/system/deleteOrphanFiles', [AdminController::class, 'deleteOrphanFiles'])->setName('system.deleteOrphanFiles');
$group->get('/system/recalculateUserQuota', [AdminController::class, 'recalculateUserQuota'])->setName('system.recalculateUserQuota');
$group->get('/system/themes', [AdminController::class, 'getThemes'])->setName('theme');
$group->post('/system/settings/save', [SettingController::class, 'saveSettings'])->setName('settings.save');
$group->post('/system/upgrade', [UpgradeController::class, 'upgrade'])->setName('system.upgrade');
$group->get('/system/checkForUpdates', [UpgradeController::class, 'checkForUpdates'])->setName('system.checkForUpdates');
$group->get('/system/changelog', [UpgradeController::class, 'changelog'])->setName('system.changelog');
$group->get('/system', [AdminController::class, 'system'])->setName('system');
$group->get('/users[/page/{page}]', [UserController::class, 'index'])->setName('user.index');
})->add(AdminMiddleware::class);
$group->group('/user', function (RouteCollectorProxy $group) {
$group->get('/create', [UserController::class, 'create'])->setName('user.create');
$group->post('/create', [UserController::class, 'store'])->setName('user.store');
$group->get('/{id}/edit', [UserController::class, 'edit'])->setName('user.edit');
$group->post('/{id}', [UserController::class, 'update'])->setName('user.update');
$group->get('/{id}/delete', [UserController::class, 'delete'])->setName('user.delete');
$group->get('/{id}/clear', [UserController::class, 'clearUserMedia'])->setName('user.clear');
})->add(AdminMiddleware::class);
$group->get('/profile', [ProfileController::class, 'profile'])->setName('profile');
$group->post('/profile/{id}', [ProfileController::class, 'profileEdit'])->setName('profile.update');
$group->post('/user/{id}/refreshToken', [UserController::class, 'refreshToken'])->setName('refreshToken');
$group->get('/user/{id}/config/sharex', [ClientController::class, 'getShareXConfig'])->setName('config.sharex');
$group->get('/user/{id}/config/script', [ClientController::class, 'getBashScript'])->setName('config.script');
$group->get('/user/{id}/config/kde_script', [ClientController::class, 'getKDEScript'])->setName('kde_config.script');
$group->get('/user/{id}/export', [ExportController::class, 'downloadData'])->setName('export.data');
$group->post('/upload/{id}/publish', [MediaController::class, 'togglePublish'])->setName('upload.publish');
$group->post('/upload/{id}/unpublish', [MediaController::class, 'togglePublish'])->setName('upload.unpublish');
$group->post('/upload/{id}/vanity', [MediaController::class, 'createVanity'])->setName('upload.vanity');
$group->get('/upload/{id}/raw', [MediaController::class, 'getRawById'])->add(AdminMiddleware::class)->setName('upload.raw');
$group->map(['GET', 'POST'], '/upload/{id}/delete', [MediaController::class, 'delete'])->setName('upload.delete');
$group->post('/tag/add', [TagController::class, 'addTag'])->setName('tag.add');
$group->post('/tag/remove', [TagController::class, 'removeTag'])->setName('tag.remove');
})->add(App\Middleware\CheckForMaintenanceMiddleware::class)->add(AuthMiddleware::class);
$app->get('/', [DashboardController::class, 'redirects'])->setName('root');
$app->get('/register', [RegisterController::class, 'registerForm'])->setName('register.show');
$app->post('/register', [RegisterController::class, 'register'])->setName('register');
$app->get('/activate/{activateToken}', [RegisterController::class, 'activateUser'])->setName('activate');
$app->get('/recover', [PasswordRecoveryController::class, 'recover'])->setName('recover');
$app->post('/recover/mail', [PasswordRecoveryController::class, 'recoverMail'])->setName('recover.mail');
$app->get('/recover/password/{resetToken}', [PasswordRecoveryController::class, 'recoverPasswordForm'])->setName('recover.password.view');
$app->post('/recover/password/{resetToken}', [PasswordRecoveryController::class, 'recoverPassword'])->setName('recover.password');
$app->get('/login', [LoginController::class, 'show'])->setName('login.show');
$app->post('/login', [LoginController::class, 'login'])->setName('login');
$app->map(['GET', 'POST'], '/logout', [LoginController::class, 'logout'])->setName('logout');
$app->post('/upload', [UploadController::class, 'uploadEndpoint'])->setName('upload');
$app->get('/user/{token}/config/screencloud', [ClientController::class, 'getScreenCloudConfig'])->setName('config.screencloud')->add(CheckForMaintenanceMiddleware::class);
$app->get('/{userCode}/{mediaCode}', [MediaController::class, 'show'])->setName('public');
$app->get('/{userCode}/{mediaCode}/delete/{token}', [MediaController::class, 'show'])->setName('public.delete.show')->add(CheckForMaintenanceMiddleware::class);
$app->post('/{userCode}/{mediaCode}/delete/{token}', [MediaController::class, 'deleteByToken'])->setName('public.delete')->add(CheckForMaintenanceMiddleware::class);
$app->get('/{userCode}/{mediaCode}/raw[.{ext}]', [MediaController::class, 'getRaw'])->setName('public.raw');
$app->get('/{userCode}/{mediaCode}/download', [MediaController::class, 'download'])->setName('public.download');

3
app/storage/app/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
*
!public/
!.gitignore

2
app/storage/app/public/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*
!.gitignore

9
app/storage/framework/.gitignore vendored Normal file
View file

@ -0,0 +1,9 @@
compiled.php
config.php
down
events.scanned.php
maintenance.php
routes.php
routes.scanned.php
schedule-*
services.json

View file

@ -0,0 +1,3 @@
*
!data/
!.gitignore

View file

@ -0,0 +1,2 @@
*
!.gitignore

View file

@ -0,0 +1,2 @@
*
!.gitignore

View file

@ -0,0 +1,2 @@
*
!.gitignore

View file

@ -0,0 +1,2 @@
*
!.gitignore

2
app/storage/logs/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*
!.gitignore

12
app/xbb Normal file
View file

@ -0,0 +1,12 @@
#!/usr/bin/env php
<?php
use Symfony\Component\Console\Input\ArgvInput;
define('LARAVEL_START', microtime(true));
// Register the Composer autoloader...
require __DIR__.'/vendor/autoload.php';
// Bootstrap Laravel and handle the command...
exit((require_once __DIR__.'/bootstrap/app.php')->handleCommand(new ArgvInput));

View file

@ -1,28 +0,0 @@
#!/usr/bin/env php
<?php
((PHP_MAJOR_VERSION >= 7 && PHP_MINOR_VERSION >= 3) || PHP_MAJOR_VERSION > 7) ?: die('Sorry, PHP 7.3 or above is required to run XBackBone.');
if (PHP_SAPI !== 'cli') {
die();
}
require __DIR__ . '/../vendor/autoload.php';
$action = isset($argv[1]) ? $argv[1] : 'all';
switch ($action) {
case 'cache':
cleanDirectory(__DIR__ . '/../resources/cache');
break;
case 'sessions':
cleanDirectory(__DIR__ . '/../resources/sessions');
break;
case 'all':
cleanDirectory(__DIR__ . '/../resources/cache');
cleanDirectory(__DIR__ . '/../resources/sessions');
break;
case 'help':
default:
echo 'Usage: php ' . $argv[0] . ' <cache|sessions|all|help>' . PHP_EOL;
}
exit(0);

View file

@ -1,45 +0,0 @@
#!/usr/bin/env php
<?php
((PHP_MAJOR_VERSION >= 7 && PHP_MINOR_VERSION >= 3) || PHP_MAJOR_VERSION > 7) ?: die('Sorry, PHP 7.3 or above is required to run XBackBone.');
if (PHP_SAPI !== 'cli') {
die();
}
use App\Database\Migrator;
use DI\ContainerBuilder;
require __DIR__.'/../vendor/autoload.php';
define('BASE_DIR', realpath(__DIR__.'/../').DIRECTORY_SEPARATOR);
$config = include __DIR__.'/../config.php';
if (!$config) {
die('config.php not found. Please create a new one.');
}
chdir(BASE_DIR);
$builder = new ContainerBuilder();
$builder->addDefinitions(BASE_DIR.'bootstrap/container.php');
$container = $builder->build();
$container->set('config', $config);
$db = $container->get('database');
$migrator = new Migrator($db, 'resources/schemas');
$migrator->migrate();
$migrator->reSyncQuotas($container->get('storage'));
if (isset($argv[1]) && $argv[1] === '--install') {
$db->query("INSERT INTO `users` (`email`, `username`, `password`, `is_admin`, `user_code`) VALUES ('admin@example.com', 'admin', ?, 1, ?)", [password_hash('admin', PASSWORD_DEFAULT), humanRandomString(5)]);
}
if (file_exists(__DIR__.'/../install') && (!isset($config['debug']) || !$config['debug'])) {
removeDirectory(__DIR__.'/../install');
}
echo 'If you are upgrading from a previous version, please run a "php bin' . DIRECTORY_SEPARATOR . 'clean".'.PHP_EOL;
echo 'Done.'.PHP_EOL;
exit(0);

View file

@ -1,121 +0,0 @@
<?php
use App\Exceptions\Handlers\AppErrorHandler;
use App\Exceptions\Handlers\Renderers\HtmlErrorRenderer;
use App\Factories\ViewFactory;
use App\Middleware\InjectMiddleware;
use App\Middleware\LangMiddleware;
use App\Middleware\RememberMiddleware;
use App\Web\Session;
use App\Web\View;
use DI\Bridge\Slim\Bridge;
use DI\ContainerBuilder;
use Psr\Container\ContainerInterface as Container;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use function DI\factory;
use function DI\get;
if (!file_exists(CONFIG_FILE) && is_dir(BASE_DIR.'install/')) {
header('Location: ./install/');
exit();
}
if (!file_exists(CONFIG_FILE) && !is_dir(BASE_DIR.'install/')) {
exit('Cannot find the config file.');
}
// Load the config
$config = array_replace_recursive([
'app_name' => 'XBackBone',
'base_url' => isSecure() ? 'https://'.$_SERVER['HTTP_HOST'] : 'http://'.$_SERVER['HTTP_HOST'],
'debug' => false,
'maintenance' => false,
'db' => [
'connection' => 'sqlite',
'dsn' => BASE_DIR.implode(DIRECTORY_SEPARATOR, ['resources', 'database', 'xbackbone.db']),
'username' => null,
'password' => null,
],
'storage' => [
'driver' => 'local',
'path' => realpath(__DIR__.'/').DIRECTORY_SEPARATOR.'storage',
],
'ldap' => [
'enabled' => false,
'host' => null,
'port' => null,
'base_domain' => null,
'user_domain' => null,
],
], require CONFIG_FILE);
$builder = new ContainerBuilder();
if (!$config['debug']) {
$builder->enableCompilation(BASE_DIR.'/resources/cache/di');
$builder->writeProxiesToFile(true, BASE_DIR.'/resources/cache/di');
}
$builder->addDefinitions([
Session::class => factory(function () {
return new Session('xbackbone_session', BASE_DIR.'resources/sessions');
}),
'session' => get(Session::class),
View::class => factory(function (Container $container) {
return ViewFactory::createAppInstance($container);
}),
'view' => get(View::class),
]);
$builder->addDefinitions(__DIR__.'/container.php');
global $app;
$app = Bridge::create($builder->build());
$app->getContainer()->set('config', $config);
$app->setBasePath(parse_url($config['base_url'], PHP_URL_PATH) ?: '');
if (!$config['debug']) {
$app->getRouteCollector()->setCacheFile(BASE_DIR.'resources/cache/routes.cache.php');
}
$app->add(InjectMiddleware::class);
$app->add(LangMiddleware::class);
$app->add(RememberMiddleware::class);
// Permanently redirect paths with a trailing slash to their non-trailing counterpart
$app->add(function (Request $request, RequestHandler $handler) use (&$app, &$config) {
$uri = $request->getUri();
$path = $uri->getPath();
if ($path !== $app->getBasePath().'/' && substr($path, -1) === '/') {
// permanently redirect paths with a trailing slash
// to their non-trailing counterpart
$uri = $uri->withPath(substr($path, 0, -1));
if ($request->getMethod() === 'GET') {
return $app->getResponseFactory()
->createResponse(301)
->withHeader('Location', (string) $uri);
} else {
$request = $request->withUri($uri);
}
}
return $handler->handle($request);
});
$app->addRoutingMiddleware();
// Configure the error handler
$errorHandler = new AppErrorHandler($app->getCallableResolver(), $app->getResponseFactory());
$errorHandler->registerErrorRenderer('text/html', HtmlErrorRenderer::class);
// Add Error Middleware
$errorMiddleware = $app->addErrorMiddleware($config['debug'], false, true);
$errorMiddleware->setDefaultErrorHandler($errorHandler);
// Load the application routes
require BASE_DIR.'app/routes.php';
return $app;

View file

@ -1,113 +0,0 @@
<?php
use App\Database\DB;
use App\Web\Lang;
use Aws\S3\S3Client;
use League\Flysystem\Cached\CachedAdapter;
use League\Flysystem\Cached\Storage\Adapter;
use function DI\factory;
use function DI\get;
use Google\Cloud\Storage\StorageClient;
use League\Flysystem\Adapter\Ftp as FtpAdapter;
use League\Flysystem\Adapter\Local;
use League\Flysystem\AwsS3v3\AwsS3Adapter;
use League\Flysystem\AzureBlobStorage\AzureBlobStorageAdapter;
use MicrosoftAzure\Storage\Blob\BlobRestProxy;
use League\Flysystem\Filesystem;
use Monolog\Formatter\LineFormatter;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Logger;
use Psr\Container\ContainerInterface as Container;
use Spatie\Dropbox\Client as DropboxClient;
use Spatie\FlysystemDropbox\DropboxAdapter;
use Superbalist\Flysystem\GoogleStorage\GoogleStorageAdapter;
return [
Logger::class => factory(function () {
$logger = new Logger('app');
$streamHandler = new RotatingFileHandler(BASE_DIR.'logs/log.txt', 10, Logger::DEBUG);
$lineFormatter = new LineFormatter("[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n", 'Y-m-d H:i:s');
$lineFormatter->includeStacktraces(true);
$streamHandler->setFormatter($lineFormatter);
$logger->pushHandler($streamHandler);
return $logger;
}),
'logger' => get(Logger::class),
DB::class => factory(function (Container $container) {
$config = $container->get('config');
return new DB(dsnFromConfig($config), $config['db']['username'], $config['db']['password']);
}),
'database' => get(DB::class),
Filesystem::class => factory(function (Container $container) {
$config = $container->get('config');
$driver = $config['storage']['driver'];
if ($driver === 'local') {
return new Filesystem(new Local($config['storage']['path']));
} elseif ($driver === 's3') {
$client = new S3Client([
'credentials' => [
'key' => $config['storage']['key'],
'secret' => $config['storage']['secret'],
],
'region' => $config['storage']['region'],
'endpoint' => $config['storage']['endpoint'],
'version' => 'latest',
'use_path_style_endpoint' => $config['storage']['use_path_style_endpoint'] ?? false,
'@http' => ['stream' => true],
]);
$adapter = new AwsS3Adapter($client, $config['storage']['bucket'], $config['storage']['path']);
} elseif ($driver === 'dropbox') {
$client = new DropboxClient($config['storage']['token']);
$adapter = new DropboxAdapter($client);
} elseif ($driver === 'ftp') {
$adapter = new FtpAdapter([
'host' => $config['storage']['host'],
'username' => $config['storage']['username'],
'password' => $config['storage']['password'],
'port' => $config['storage']['port'],
'root' => $config['storage']['path'],
'passive' => $config['storage']['passive'],
'ssl' => $config['storage']['ssl'],
'timeout' => 30,
]);
} elseif ($driver === 'google-cloud') {
$client = new StorageClient([
'projectId' => $config['storage']['project_id'],
'keyFilePath' => $config['storage']['key_path'],
]);
$adapter = new GoogleStorageAdapter($client, $client->bucket($config['storage']['bucket']));
} elseif ($driver === 'azure') {
$client = BlobRestProxy::createBlobService(
sprintf(
'DefaultEndpointsProtocol=https;AccountName=%s;AccountKey=%s;',
$config['storage']['account_name'],
$config['storage']['account_key']
)
);
$adapter = new AzureBlobStorageAdapter($client, $config['storage']['container_name']);
} else {
throw new InvalidArgumentException('The driver specified is not supported.');
}
$cache = new Adapter(new Local(BASE_DIR.'resources/cache/fs'), 'file', 300); // 5min
return new Filesystem(new CachedAdapter($adapter, $cache));
}),
'storage' => get(Filesystem::class),
Lang::class => factory(function () {
return Lang::build(Lang::recognize(), BASE_DIR.'resources/lang/');
}),
'lang' => get(Lang::class),
];

View file

@ -1,59 +0,0 @@
{
"name": "sergix44/xbackbone",
"license": "AGPL-3.0-only",
"version": "3.7.0",
"description": "A lightweight ShareX PHP backend",
"type": "project",
"require": {
"php": ">=7.3",
"ext-filter": "*",
"ext-gd": "*",
"ext-intl": "*",
"ext-json": "*",
"ext-pdo": "*",
"ext-zip": "*",
"erusev/parsedown": "^1.7",
"intervention/image": "^2.6",
"league/flysystem": "^1.1.4",
"league/flysystem-aws-s3-v3": "^1.0",
"league/flysystem-cached-adapter": "^1.1",
"maennchen/zipstream-php": "^2.0",
"monolog/monolog": "^1.23",
"php-di/slim-bridge": "^3.0",
"sapphirecat/slim4-http-interop-adapter": "^1.0",
"slim/psr7": "^1.5",
"slim/slim": "^4.0",
"spatie/flysystem-dropbox": "^1.0",
"superbalist/flysystem-google-storage": "^7.2",
"twig/twig": "^2.14"
},
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true,
"platform": {
"php": "7.3.33"
}
},
"autoload": {
"files": [
"app/helpers.php"
],
"psr-4": {
"App\\": "app/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"minimum-stability": "dev",
"prefer-stable": true,
"require-dev": {
"roave/security-advisories": "dev-latest",
"phpstan/phpstan": "^0.11.5",
"phpunit/phpunit": "^9.0",
"symfony/dom-crawler": "^4.4"
}
}

7169
composer.lock generated

File diff suppressed because it is too large Load diff

View file

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

18
core/.editorconfig Normal file
View file

@ -0,0 +1,18 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[docker-compose.yml]
indent_size = 4

63
core/.env.example Normal file
View file

@ -0,0 +1,63 @@
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_TIMEZONE=UTC
APP_URL=http://localhost
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
CACHE_STORE=database
CACHE_PREFIX=
MEMCACHED_HOST=127.0.0.1
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=log
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"

11
core/.gitattributes vendored Normal file
View file

@ -0,0 +1,11 @@
* text=auto eol=lf
*.blade.php diff=html
*.css diff=css
*.html diff=html
*.md diff=markdown
*.php diff=php
/.github export-ignore
CHANGELOG.md export-ignore
.styleci.yml export-ignore

21
core/.gitignore vendored Normal file
View file

@ -0,0 +1,21 @@
/.phpunit.cache
/node_modules
/public/build
/public/hot
/public/storage
/storage/*.key
/vendor
.env
.env.backup
.env.production
.phpactor.json
.phpunit.result.cache
Homestead.json
Homestead.yaml
auth.json
npm-debug.log
yarn-error.log
/.fleet
/.idea
/.vscode
/*.db

66
core/README.md Normal file
View file

@ -0,0 +1,66 @@
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
<p align="center">
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
</p>
## About Laravel
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
- [Simple, fast routing engine](https://laravel.com/docs/routing).
- [Powerful dependency injection container](https://laravel.com/docs/container).
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
- [Robust background job processing](https://laravel.com/docs/queues).
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
Laravel is accessible, powerful, and provides tools required for large, robust applications.
## Learning Laravel
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch.
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
## Laravel Sponsors
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
### Premium Partners
- **[Vehikl](https://vehikl.com/)**
- **[Tighten Co.](https://tighten.co)**
- **[WebReinvent](https://webreinvent.com/)**
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
- **[64 Robots](https://64robots.com)**
- **[Curotec](https://www.curotec.com/services/technologies/laravel/)**
- **[Cyber-Duck](https://cyber-duck.co.uk)**
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
- **[Jump24](https://jump24.co.uk)**
- **[Redberry](https://redberry.international/laravel/)**
- **[Active Logic](https://activelogic.com)**
- **[byte5](https://byte5.de)**
- **[OP.GG](https://op.gg)**
## Contributing
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
## Code of Conduct
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
## Security Vulnerabilities
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
## License
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).

View file

@ -0,0 +1,40 @@
<?php
namespace App\Actions\Fortify;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Laravel\Fortify\Contracts\CreatesNewUsers;
class CreateNewUser implements CreatesNewUsers
{
use PasswordValidationRules;
/**
* Validate and create a newly registered user.
*
* @param array<string, string> $input
*/
public function create(array $input): User
{
Validator::make($input, [
'name' => ['required', 'string', 'max:255'],
'email' => [
'required',
'string',
'email',
'max:255',
Rule::unique(User::class),
],
'password' => $this->passwordRules(),
])->validate();
return User::create([
'name' => $input['name'],
'email' => $input['email'],
'password' => Hash::make($input['password']),
]);
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace App\Actions\Fortify;
use Illuminate\Validation\Rules\Password;
trait PasswordValidationRules
{
/**
* Get the validation rules used to validate passwords.
*
* @return array<int, \Illuminate\Contracts\Validation\Rule|array<mixed>|string>
*/
protected function passwordRules(): array
{
return ['required', 'string', Password::default(), 'confirmed'];
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace App\Actions\Fortify;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Laravel\Fortify\Contracts\ResetsUserPasswords;
class ResetUserPassword implements ResetsUserPasswords
{
use PasswordValidationRules;
/**
* Validate and reset the user's forgotten password.
*
* @param array<string, string> $input
*/
public function reset(User $user, array $input): void
{
Validator::make($input, [
'password' => $this->passwordRules(),
])->validate();
$user->forceFill([
'password' => Hash::make($input['password']),
])->save();
}
}

View file

@ -0,0 +1,32 @@
<?php
namespace App\Actions\Fortify;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Laravel\Fortify\Contracts\UpdatesUserPasswords;
class UpdateUserPassword implements UpdatesUserPasswords
{
use PasswordValidationRules;
/**
* Validate and update the user's password.
*
* @param array<string, string> $input
*/
public function update(User $user, array $input): void
{
Validator::make($input, [
'current_password' => ['required', 'string', 'current_password:web'],
'password' => $this->passwordRules(),
], [
'current_password.current_password' => __('The provided password does not match your current password.'),
])->validateWithBag('updatePassword');
$user->forceFill([
'password' => Hash::make($input['password']),
])->save();
}
}

View file

@ -0,0 +1,58 @@
<?php
namespace App\Actions\Fortify;
use App\Models\User;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
class UpdateUserProfileInformation implements UpdatesUserProfileInformation
{
/**
* Validate and update the given user's profile information.
*
* @param array<string, string> $input
*/
public function update(User $user, array $input): void
{
Validator::make($input, [
'name' => ['required', 'string', 'max:255'],
'email' => [
'required',
'string',
'email',
'max:255',
Rule::unique('users')->ignore($user->id),
],
])->validateWithBag('updateProfileInformation');
if ($input['email'] !== $user->email &&
$user instanceof MustVerifyEmail) {
$this->updateVerifiedUser($user, $input);
} else {
$user->forceFill([
'name' => $input['name'],
'email' => $input['email'],
])->save();
}
}
/**
* Update the given verified user's profile information.
*
* @param array<string, string> $input
*/
protected function updateVerifiedUser(User $user, array $input): void
{
$user->forceFill([
'name' => $input['name'],
'email' => $input['email'],
'email_verified_at' => null,
])->save();
$user->sendEmailVerificationNotification();
}
}

View file

@ -0,0 +1,20 @@
<?php
namespace App\Features;
use Illuminate\Support\Lottery;
use Illuminate\Support\Str;
use Sqids\Sqids;
class AlphabetForIds
{
public string $name = 'id-alphabet';
/**
* Resolve the feature's initial value.
*/
public function resolve(mixed $scope): mixed
{
return str_shuffle(Sqids::DEFAULT_ALPHABET);
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace App\Features;
use Illuminate\Support\Lottery;
class SignUp
{
public string $name = 'signup';
/**
* Resolve the feature's initial value.
*/
public function resolve(mixed $scope): mixed
{
return false;
}
}

View file

@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}

View file

@ -0,0 +1,30 @@
<?php
namespace App\Livewire\Auth;
use App\Livewire\Forms\LoginForm;
use Laravel\Fortify\Fortify;
use Livewire\Component;
use Mary\Traits\Toast;
class Login extends Component
{
use Toast;
public LoginForm $form;
public function authenticate()
{
$this->validate();
$this->form->authenticate();
return redirect()->intended(Fortify::redirects('dashboard'));
}
public function render()
{
return view('livewire.auth.login')
->layout('components.layouts.auth', ['title' => 'Login']);
}
}

View file

@ -0,0 +1,72 @@
<?php
namespace App\Livewire\Forms;
use Illuminate\Auth\Events\Lockout;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Livewire\Attributes\Validate;
use Livewire\Form;
class LoginForm extends Form
{
#[Validate('required|string|email')]
public string $email = '';
#[Validate('required|string')]
public string $password = '';
#[Validate('boolean')]
public bool $remember = false;
/**
* Attempt to authenticate the request's credentials.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function authenticate(): void
{
$this->ensureIsNotRateLimited();
if (!Auth::attempt($this->only(['email', 'password']), $this->remember)) {
RateLimiter::hit($this->throttleKey());
throw ValidationException::withMessages([
'form.email' => trans('auth.failed'),
]);
}
RateLimiter::clear($this->throttleKey());
}
/**
* Ensure the authentication request is not rate limited.
*/
protected function ensureIsNotRateLimited(): void
{
if (!RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
return;
}
event(new Lockout(request()));
$seconds = RateLimiter::availableIn($this->throttleKey());
throw ValidationException::withMessages([
'form.email' => trans('auth.throttle', [
'seconds' => $seconds,
'minutes' => ceil($seconds / 60),
]),
]);
}
/**
* Get the authentication rate limiting throttle key.
*/
protected function throttleKey(): string
{
return Str::transliterate(Str::lower($this->email).'|'.request()->ip());
}
}

View file

@ -0,0 +1,69 @@
<?php
namespace App\Livewire;
use Illuminate\Support\Collection;
use Livewire\Component;
use Mary\Traits\Toast;
class Welcome extends Component
{
use Toast;
public string $search = '';
public bool $drawer = false;
public array $sortBy = ['column' => 'name', 'direction' => 'asc'];
// Clear filters
public function clear(): void
{
$this->reset();
$this->success('Filters cleared.', position: 'toast-bottom');
}
// Delete action
public function delete($id): void
{
$this->warning("Will delete #$id", 'It is fake.', position: 'toast-bottom');
}
// Table headers
public function headers(): array
{
return [
['key' => 'id', 'label' => '#', 'class' => 'w-1'],
['key' => 'name', 'label' => 'Name', 'class' => 'w-64'],
['key' => 'age', 'label' => 'Age', 'class' => 'w-20'],
['key' => 'email', 'label' => 'E-mail', 'sortable' => false],
];
}
/**
* For demo purpose, this is a static collection.
*
* On real projects you do it with Eloquent collections.
* Please, refer to maryUI docs to see the eloquent examples.
*/
public function users(): Collection
{
return collect([
['id' => 1, 'name' => 'Mary', 'email' => 'mary@mary-ui.com', 'age' => 23],
['id' => 2, 'name' => 'Giovanna', 'email' => 'giovanna@mary-ui.com', 'age' => 7],
['id' => 3, 'name' => 'Marina', 'email' => 'marina@mary-ui.com', 'age' => 5],
])
->sortBy([[...array_values($this->sortBy)]])
->when($this->search, function (Collection $collection) {
return $collection->filter(fn(array $item) => str($item['name'])->contains($this->search, true));
});
}
public function render()
{
return view('livewire.welcome', [
'users' => $this->users(),
'headers' => $this->headers()
]);
}
}

View file

@ -0,0 +1,13 @@
<?php
namespace App\Models\Properties;
enum ResourceType: string
{
case IMAGE = 'IMAGE';
case VIDEO = 'VIDEO';
case AUDIO = 'AUDIO';
case PDF = 'PDF';
case FILE = 'FILE';
case LINK = 'LINK';
}

View file

@ -0,0 +1,49 @@
<?php
namespace App\Models;
use App\Models\Properties\ResourceType;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Resource extends Model
{
use HasFactory;
protected $fillable = [
'type',
'user_id',
'code',
'hidden',
'target',
'filename',
'size',
'mime',
'views',
'downloads',
'password',
'published_at',
'expires_at',
];
protected $hidden = [
'password',
];
protected function casts(): array
{
return [
'type' => ResourceType::class,
'hidden' => 'boolean',
'published_at' => 'datetime',
'expires_at' => 'datetime',
'password' => 'hashed',
];
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

65
core/app/Models/User.php Normal file
View file

@ -0,0 +1,65 @@
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable
{
use HasFactory, Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'email',
'password',
];
protected $appends = [
'avatar',
];
/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
/**
* @return HasMany
*/
public function resources(): HasMany
{
return $this->hasMany(Resource::class);
}
public function getAvatarAttribute(): string
{
return 'https://www.gravatar.com/avatar/'.hash('sha256', strtolower($this->email)).'?d=robohash&r=x';
}
}

View file

@ -0,0 +1,48 @@
<?php
namespace App\Providers;
use Illuminate\Auth\Notifications\ResetPassword;
use Illuminate\Support\ServiceProvider;
use Laravel\Pennant\Feature;
use Sqids\Sqids;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
$this->app->bind(Sqids::class, function () {
return new Sqids(Feature::value('id-alphabet'));
});
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
if (!$this->app->runningInConsole()) {
return;
}
$group = [
base_path('public/build/') => public_path('build'),
base_path('public/vendor/') => public_path('vendor'),
base_path('public/.htaccess') => public_path('.htaccess'),
];
if ($this->app->environment('local')) {
$group[base_path('public/hot')] = public_path('hot');
}
$this->publishes($group, 'app');
$this->publishes([
base_path('public/favicon.ico') => public_path('favicon.ico'),
base_path('public/img/') => public_path('img'),
], 'app-img');
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace App\Providers;
use App\Actions\Fortify\CreateNewUser;
use App\Actions\Fortify\ResetUserPassword;
use App\Actions\Fortify\UpdateUserPassword;
use App\Actions\Fortify\UpdateUserProfileInformation;
use App\Livewire\Auth\Login;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;
use Laravel\Fortify\Fortify;
class FortifyServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Fortify::createUsersUsing(CreateNewUser::class);
Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class);
Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);
Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
RateLimiter::for('login', function (Request $request) {
$throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())).'|'.$request->ip());
return Limit::perMinute(5)->by($throttleKey);
});
RateLimiter::for('two-factor', function (Request $request) {
return Limit::perMinute(5)->by($request->session()->get('login.id'));
});
Fortify::loginView(static function () {
return app()->call(Login::class);
});
}
}

View file

@ -0,0 +1,47 @@
<?php
namespace App\View\Components;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class AppBrand extends Component
{
/**
* Create a new component instance.
*/
public function __construct(public ?bool $onTop = false)
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return <<<'HTML'
<a href="/" wire:navigate>
<!-- Hidden when collapsed -->
<div {{ $attributes->class(["hidden-when-collapsed"]) }}>
<div class="flex items-center {{ $onTop ? 'flex-col justify-center' : 'gap-2 btn btn-link no-underline hover:no-underline flex-nowrap' }}">
<div class="avatar">
<div class="{{ $onTop ? 'w-24' : 'w-12' }}">
<img src="{{ asset('img/android-chrome-192x192.png') }}" />
</div>
</div>
<span class="font-bold text-3xl bg-gradient-to-r from-blue-500 to-green-400 bg-clip-text text-transparent ">
{{ config('app.name') }}
</span>
</div>
</div>
<!-- Display when collapsed -->
<div class="display-when-collapsed hidden mx-5 mt-4 lg:mb-6 h-[28px]">
<x-icon name="s-square-3-stack-3d" class="w-6 -mb-1 text-purple-500" />
</div>
</a>
HTML;
}
}

15
core/artisan Normal file
View file

@ -0,0 +1,15 @@
#!/usr/bin/env php
<?php
use Symfony\Component\Console\Input\ArgvInput;
define('LARAVEL_START', microtime(true));
// Register the Composer autoloader...
require __DIR__.'/vendor/autoload.php';
// Bootstrap Laravel and handle the command...
$status = (require_once __DIR__.'/bootstrap/app.php')
->handleCommand(new ArgvInput);
exit($status);

17
core/bootstrap/app.php Normal file
View file

@ -0,0 +1,17 @@
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
//
})
->withExceptions(function (Exceptions $exceptions) {
//
})->create();

2
core/bootstrap/cache/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*
!.gitignore

View file

@ -0,0 +1,6 @@
<?php
return [
App\Providers\AppServiceProvider::class,
App\Providers\FortifyServiceProvider::class,
];

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