Compare commits
280 commits
Author | SHA1 | Date | |
---|---|---|---|
Simon Vieille | 63bc4cde1c | ||
Simon Vieille | 2b43f97315 | ||
Simon Vieille | 10564e6d37 | ||
Simon Vieille | ebbb4c01e0 | ||
Simon Vieille | 43fb25d122 | ||
Simon Vieille | 4253985bc3 | ||
Simon Vieille | 79d549254f | ||
Simon Vieille | c79e96e291 | ||
Simon Vieille | 092b490ae2 | ||
Simon Vieille | fcecf74f90 | ||
c00723a246 | |||
e81b11c5ec | |||
af2212ac8c | |||
b57b0e3770 | |||
ba76a83163 | |||
2874f579dc | |||
287e1d66cb | |||
b23a4fe9c5 | |||
59bbd24b76 | |||
2dfe6ef1ca | |||
66f82ecc13 | |||
8432fc123e | |||
627927a11e | |||
67871179a8 | |||
dbec7a884e | |||
8c6ad05cbd | |||
cc0296e6e2 | |||
4f94bd5640 | |||
9d8d6b510a | |||
edb51485e1 | |||
c596f1d62d | |||
813aa8c618 | |||
39e92e949a | |||
e550bc54e2 | |||
ab808ee511 | |||
1e8baa5ee7 | |||
7f5fcc98d7 | |||
0f4767b8e0 | |||
70bac192e2 | |||
2b72994cde | |||
74d12bba3b | |||
1cacfd0bb7 | |||
740f90294d | |||
da12738b94 | |||
089cd2bb85 | |||
b22db21ea4 | |||
b9b6d968aa | |||
b8dd7aa3bf | |||
6a56bc2c36 | |||
1abc6a7b60 | |||
a9c64637ec | |||
72299ba2a5 | |||
a2ce7ed609 | |||
41abaa20d8 | |||
f7e374cebc | |||
c7e3867a93 | |||
1366e92ca0 | |||
301f22bb4a | |||
7397a1cde8 | |||
e232e44dcd | |||
edb8de9800 | |||
d0eb452847 | |||
Simon Vieille | 75a29b6372 | ||
Simon Vieille | 9bd1ff142b | ||
Simon Vieille | 69ad422624 | ||
Simon Vieille | e0af777a34 | ||
Simon Vieille | 310c5076a0 | ||
Simon Vieille | 8fcc6faebb | ||
Simon Vieille | 65b3583411 | ||
Simon Vieille | 58de4e8908 | ||
Simon Vieille | 450972ca1e | ||
Simon Vieille | a0b4137fb3 | ||
Simon Vieille | 05506fb5db | ||
Simon Vieille | ab14728bec | ||
Simon Vieille | 21cec0647a | ||
Simon Vieille | c364cafd17 | ||
Simon Vieille | 673132adf2 | ||
Simon Vieille | 58b5252986 | ||
Simon Vieille | 674846ab53 | ||
Simon Vieille | 5f942a904b | ||
Simon Vieille | 5ad22f21d2 | ||
Simon Vieille | be7003d613 | ||
Simon Vieille | 325401f37d | ||
Simon Vieille | 6addbc7de4 | ||
Simon Vieille | bda7a4a195 | ||
Simon Vieille | a79011c276 | ||
Simon Vieille | 1113c0a91f | ||
Simon Vieille | cbf05e3ce3 | ||
Simon Vieille | 2ecb972a79 | ||
Simon Vieille | b26738fe49 | ||
Simon Vieille | 69e2e919de | ||
Simon Vieille | c4c5aa867a | ||
Simon Vieille | 5b68c61cc0 | ||
Simon Vieille | 7052f2516d | ||
Simon Vieille | 52be00fc77 | ||
Simon Vieille | 3626a65f1f | ||
Simon Vieille | 70cae6f7a0 | ||
Simon Vieille | 5f1111d2a0 | ||
Simon Vieille | 1ee868b354 | ||
Simon Vieille | 2307efa341 | ||
Simon Vieille | f19fdb0ecf | ||
Simon Vieille | 4ce765ed72 | ||
Simon Vieille | 3c52a00bc4 | ||
Simon Vieille | cd88d7626a | ||
Simon Vieille | 3c274a14b9 | ||
Simon Vieille | 371a9f8bb6 | ||
Simon Vieille | bb62d1ce78 | ||
Simon Vieille | b043e42b96 | ||
Simon Vieille | 2a91b5917f | ||
Simon Vieille | 2e9b942024 | ||
Simon Vieille | 22d7389183 | ||
Simon Vieille | d0e187746a | ||
Simon Vieille | 933e360e26 | ||
Simon Vieille | c4bc470e8c | ||
Simon Vieille | 9dd0f7376b | ||
Simon Vieille | 52536ccefb | ||
Simon Vieille | f240db7ba4 | ||
Simon Vieille | aa808d07e8 | ||
Simon Vieille | bb01163e84 | ||
Simon Vieille | 1dbed2d52f | ||
Simon Vieille | 489a0411a0 | ||
Simon Vieille | 2a5d0ae1f4 | ||
Simon Vieille | 2adff5f583 | ||
Simon Vieille | b40c77d7a3 | ||
Simon Vieille | 27bccc9acd | ||
Simon Vieille | be31af32bc | ||
Simon Vieille | 246c4621c0 | ||
Simon Vieille | 4d235c4ec5 | ||
Simon Vieille | 2be2da285a | ||
Simon Vieille | 1498ab5308 | ||
Simon Vieille | 68c6ee8081 | ||
Simon Vieille | bec4edfa45 | ||
Simon Vieille | 7623546c10 | ||
Simon Vieille | 4cbd601476 | ||
Simon Vieille | 63237d50e7 | ||
Simon Vieille | 2d8637617b | ||
Simon Vieille | 81d1fa9cad | ||
Simon Vieille | 47b83ee968 | ||
Simon Vieille | 6086ceaf35 | ||
Simon Vieille | 94e2fc966d | ||
Simon Vieille | b809ee9c88 | ||
Simon Vieille | a3ef3540c1 | ||
Simon Vieille | 47e6f06bd7 | ||
Simon Vieille | a3a1822339 | ||
Simon Vieille | 7523f479d5 | ||
Simon Vieille | 59d5fed75e | ||
Simon Vieille | 4e1003159c | ||
Simon Vieille | fd36561cf1 | ||
Simon Vieille | 73b82578e2 | ||
Simon Vieille | e7b4f7f81e | ||
Simon Vieille | dc34a954f6 | ||
Simon Vieille | 3017880a64 | ||
Simon Vieille | 0424a9457e | ||
Simon Vieille | bd6a5bb44e | ||
Simon Vieille | b32133e7cb | ||
Simon Vieille | a37c6626c6 | ||
Simon Vieille | 4e1166c19d | ||
Simon Vieille | 098a0f17c0 | ||
Simon Vieille | f7ddd54d99 | ||
Simon Vieille | 39637d4094 | ||
Simon Vieille | ce5c69d467 | ||
Simon Vieille | 23802898b5 | ||
Simon Vieille | 632e6b7c7a | ||
Simon Vieille | 89925c2a34 | ||
Simon Vieille | 4be92913a5 | ||
Simon Vieille | dd11bf3898 | ||
Simon Vieille | a0c0e53816 | ||
Simon Vieille | f3a48cea18 | ||
Simon Vieille | 3705435785 | ||
Simon Vieille | 3b6e89dc63 | ||
Simon Vieille | 8e3944fa2e | ||
Simon Vieille | 4b4a28aee4 | ||
Simon Vieille | cd3d00389d | ||
Simon Vieille | 729266e0a6 | ||
Simon Vieille | a563ab3caf | ||
Simon Vieille | 3d8a4de100 | ||
Simon Vieille | 7355685009 | ||
Simon Vieille | 8d02608681 | ||
Simon Vieille | e288db77f6 | ||
Simon Vieille | 56eb728821 | ||
Simon Vieille | a3cf59417c | ||
Simon Vieille | bf4bd48775 | ||
Simon Vieille | f4b0b2869c | ||
Simon Vieille | 0cffb7d588 | ||
Simon Vieille | da4d5eb244 | ||
Simon Vieille | 321c7ed960 | ||
Simon Vieille | c239832aa3 | ||
Simon Vieille | d842c142ff | ||
Simon Vieille | 44ea80de14 | ||
Simon Vieille | 7f1616c4d5 | ||
Simon Vieille | 31f18f73f1 | ||
Simon Vieille | 8b1e38a647 | ||
Simon Vieille | 5d8996d624 | ||
Simon Vieille | c80482d7e3 | ||
Simon Vieille | ec4ce4c85b | ||
Simon Vieille | 5694d6262f | ||
Simon Vieille | 42511199e4 | ||
Simon Vieille | 165e3a723f | ||
Simon Vieille | 1e4c90240b | ||
Simon Vieille | 3b5116e172 | ||
Simon Vieille | bc48a727a6 | ||
Simon Vieille | 5769b29b69 | ||
Simon Vieille | c25c887881 | ||
Simon Vieille | 46a90689db | ||
Simon Vieille | a84e1739c0 | ||
Simon Vieille | 98e49e4134 | ||
Simon Vieille | 7b6bb707d1 | ||
Simon Vieille | ab117eef3e | ||
Simon Vieille | b4802fc3b3 | ||
Simon Vieille | 25940fc188 | ||
Simon Vieille | b9d929fddf | ||
Simon Vieille | 551e4beaa3 | ||
Simon Vieille | 6134b30a99 | ||
Simon Vieille | 85e4bdf1c2 | ||
Simon Vieille | e81f8be151 | ||
Simon Vieille | ded0279514 | ||
Simon Vieille | 83d75d2415 | ||
Simon Vieille | 10a1168009 | ||
Simon Vieille | 90499d8dc9 | ||
Simon Vieille | 0af04c9131 | ||
Simon Vieille | 661840d87c | ||
Simon Vieille | 03e401f9ce | ||
Simon Vieille | 9bc2ee26f9 | ||
Simon Vieille | 46ecb3a468 | ||
Simon Vieille | 8e3642f878 | ||
Simon Vieille | 263219850e | ||
Simon Vieille | 24cd406982 | ||
Simon Vieille | 33eec52044 | ||
Simon Vieille | 4db7936a2c | ||
Simon Vieille | f29289a36d | ||
Simon Vieille | 9fbc963ece | ||
Simon Vieille | bffb9aac59 | ||
Simon Vieille | 07f2ea1d9e | ||
Simon Vieille | 28b738d70d | ||
Simon Vieille | dad11586b2 | ||
Simon Vieille | e04f1199c4 | ||
Simon Vieille | 9d77d6ed6d | ||
Simon Vieille | a85aea5afb | ||
Simon Vieille | d442d343ba | ||
Simon Vieille | 5129e70f8b | ||
Simon Vieille | d7659735cb | ||
Simon Vieille | e50d2668f9 | ||
Simon Vieille | a6f35fa38e | ||
Simon Vieille | 933888f061 | ||
Simon Vieille | 901f5ba25e | ||
Simon Vieille | f660f12825 | ||
Simon Vieille | 1d8270ddae | ||
Simon Vieille | 3858546e4e | ||
Simon Vieille | 621ce7a82b | ||
Simon Vieille | da77abbf3c | ||
Simon Vieille | 0575c8ce59 | ||
Simon Vieille | 5810e09a45 | ||
Simon Vieille | 0739f683c7 | ||
Simon Vieille | 6c75f8ffc3 | ||
Simon Vieille | 914fef625a | ||
Simon Vieille | 67694ffe8c | ||
Simon Vieille | d19754d88e | ||
Simon Vieille | 7fdcde2689 | ||
Simon Vieille | b32509401c | ||
Simon Vieille | 690e117b0b | ||
Simon Vieille | 9d2048094f | ||
Simon Vieille | e67966df45 | ||
Simon Vieille | 23ee09b1b5 | ||
Simon Vieille | a5113cf005 | ||
Simon Vieille | ec4b3341c8 | ||
Simon Vieille | 58c8b6126e | ||
Simon Vieille | 98456ab8e5 | ||
Simon Vieille | 1cb57a138f | ||
Simon Vieille | 1f6aaf41e1 | ||
Simon Vieille | 03b45e89e3 | ||
Simon Vieille | 689056be5a | ||
Simon Vieille | 9307bcf4b1 | ||
Simon Vieille | 202d2cf78c | ||
Simon Vieille | 28f9a9f3f6 | ||
Simon Vieille | 85f44fcabb | ||
Simon Vieille | d6e38c0ef1 | ||
Simon Vieille | e47a951287 | ||
Simon Vieille | 6d76faca57 | ||
Simon Vieille | e5bb39c6a7 | ||
Simon Vieille | fbd8c64334 |
7
.env
7
.env
|
@ -9,6 +9,7 @@
|
|||
# Real environment variables win over .env files.
|
||||
#
|
||||
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
|
||||
# https://symfony.com/doc/current/configuration/secrets.html
|
||||
#
|
||||
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
|
||||
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
|
||||
|
@ -19,7 +20,7 @@ APP_SECRET=
|
|||
###< symfony/framework-bundle ###
|
||||
|
||||
###> symfony/mailer ###
|
||||
MAILER_DSN=smtp://localhost
|
||||
# MAILER_DSN=smtp://localhost
|
||||
###< symfony/mailer ###
|
||||
|
||||
###> doctrine/doctrine-bundle ###
|
||||
|
@ -27,6 +28,6 @@ MAILER_DSN=smtp://localhost
|
|||
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
|
||||
#
|
||||
DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
|
||||
# DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7"
|
||||
# DATABASE_URL="postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=13&charset=utf8"
|
||||
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8&charset=utf8mb4"
|
||||
# DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=14&charset=utf8"
|
||||
###< doctrine/doctrine-bundle ###
|
||||
|
|
|
@ -4,3 +4,4 @@ APP_SECRET='$ecretf0rt3st'
|
|||
SYMFONY_DEPRECATIONS_HELPER=999999
|
||||
PANTHER_APP_ENV=panther
|
||||
PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots
|
||||
DATABASE_URL="sqlite:///%kernel.project_dir%/var/data_tests.db"
|
||||
|
|
10
.gitignore
vendored
10
.gitignore
vendored
|
@ -12,7 +12,6 @@
|
|||
###< symfony/framework-bundle ###
|
||||
|
||||
###> symfony/phpunit-bridge ###
|
||||
.phpunit
|
||||
.phpunit.result.cache
|
||||
/phpunit.xml
|
||||
###< symfony/phpunit-bridge ###
|
||||
|
@ -24,6 +23,13 @@ npm-debug.log
|
|||
yarn-error.log
|
||||
###< symfony/webpack-encore-bundle ###
|
||||
|
||||
/public/uploads/
|
||||
/public/uploads/*
|
||||
!/public/uploads/.gitkeep
|
||||
/public/media/
|
||||
/migrations/*
|
||||
!/migrations/.gitkeep
|
||||
|
||||
###> phpunit/phpunit ###
|
||||
/phpunit.xml
|
||||
.phpunit.result.cache
|
||||
###< phpunit/phpunit ###
|
||||
|
|
1
.php-version
Normal file
1
.php-version
Normal file
|
@ -0,0 +1 @@
|
|||
8.0
|
49
.woodpecker.yml
Normal file
49
.woodpecker.yml
Normal file
|
@ -0,0 +1,49 @@
|
|||
matrix:
|
||||
PHP_VERSION:
|
||||
- 8.0
|
||||
- 8.1
|
||||
|
||||
services:
|
||||
db:
|
||||
image: mariadb:10.3
|
||||
environment:
|
||||
- MARIADB_ROOT_PASSWORD=root
|
||||
|
||||
steps:
|
||||
db_wait:
|
||||
image: gitnet.fr/deblan/timeout:latest
|
||||
commands:
|
||||
- /bin/timeout -t 30 -v -c 'while true; do nc -z -v db 3306 2>&1 | grep succeeded && exit 0; sleep 0.5; done'
|
||||
|
||||
db_create:
|
||||
image: mariadb:10.3
|
||||
commands:
|
||||
- mysql -hdb -uroot -proot -e "CREATE DATABASE app"
|
||||
|
||||
config:
|
||||
image: deblan/php:${PHP_VERSION}
|
||||
commands:
|
||||
- echo APP_ENV=prod >> .env.local
|
||||
- echo APP_SECRET=$(openssl rand -hex 32) >> .env.local
|
||||
- echo DATABASE_URL=mysql://root:root@db/app >> .env.local
|
||||
|
||||
composer:
|
||||
image: deblan/php:${PHP_VERSION}
|
||||
commands:
|
||||
- apt-get update && apt-get -y install git
|
||||
- composer install --no-scripts
|
||||
|
||||
db_migrate:
|
||||
image: deblan/php:${PHP_VERSION}
|
||||
environment:
|
||||
- PHP=php
|
||||
commands:
|
||||
- ./bin/doctrine-migrate
|
||||
|
||||
node:
|
||||
image: node:16-slim
|
||||
commands:
|
||||
- yarn
|
||||
- test -d public/js || mkdir public/js
|
||||
- test -f public/js/fos_js_routes.json || echo "{}" > public/js/fos_js_routes.json
|
||||
- npm run build
|
217
CHANGELOG.md
217
CHANGELOG.md
|
@ -1,10 +1,219 @@
|
|||
## [Unreleased]
|
||||
|
||||
## [v1.25.1] - 2024-05-13
|
||||
### Fixed
|
||||
* fix murph-npm version
|
||||
|
||||
## [v1.25.0] - 2024-05-12
|
||||
### Changed
|
||||
* upgrade murph/murph-core
|
||||
|
||||
## [v1.23.0] - 2023-09-28
|
||||
### Changed
|
||||
* upgrade murph/murph-core
|
||||
|
||||
## [v1.22.0] - 2023-09-28
|
||||
### Added
|
||||
* update woodpecker ci base file
|
||||
### Fixed
|
||||
* fix #1: add UniqueEntity constraint in the User entity
|
||||
### Changed
|
||||
* upgrade murph/murph-core
|
||||
|
||||
## [1.21.0] - 2023-08-11
|
||||
### Changed
|
||||
* upgrade murph/murph-core
|
||||
|
||||
## [1.20.0] - 2023-07-27
|
||||
### Fixed
|
||||
* fix collection widget: allow_add/allow_delete and prototype
|
||||
### Added
|
||||
* add user admin controller and simples views in default files
|
||||
* add chdir in the console entrypoint
|
||||
### Changed
|
||||
* upgrade murph/murph-core
|
||||
|
||||
## [1.19.0] - 2023-04-15
|
||||
### Changed
|
||||
* upgrade murph/murph-core
|
||||
|
||||
## [1.18.0] - 2023-01-13
|
||||
### Added
|
||||
* feat(dep): update dependencies
|
||||
* feat(update): apply new recipe for phpunit
|
||||
* feat(update): apply recipes:update doctrine/doctrine-bundle
|
||||
* feat(update): apply recipes:update doctrine/doctrine-migrations-bundle
|
||||
* feat(update): apply recipes:update liip/imagine-bundle
|
||||
* feat(update): apply recipes:update stof/doctrine-extensions-bundle
|
||||
* feat(update): apply recipes:update symfony/apache-pack
|
||||
* feat(update): apply recipes:update symfony/console
|
||||
* feat(update): apply recipes:update symfony/debug-bundle
|
||||
* feat(update): apply recipes:update symfony/flex
|
||||
* feat(update): apply recipes:update symfony/mailer
|
||||
* feat(update): apply recipes:update symfony/framework-bundle
|
||||
* feat(update): apply recipes:update symfony/monolog-bundle
|
||||
* feat(update): apply recipes:update symfony/routing
|
||||
* feat(update): apply recipes:update symfony/security-bundle
|
||||
* feat(update): apply recipes:update symfony/translation
|
||||
* feat(update): apply recipes:update symfony/twig-bundle
|
||||
* feat(update): apply recipes:update symfony/validator
|
||||
* feat(update): apply recipes:update symfony/web-profiler-bundle
|
||||
* feat(update): apply recipes:update symfony/webpack-encore-bundle
|
||||
* feat(update): apply recipes:update scheb/2fa-bundle
|
||||
### Fixed
|
||||
* fix(config): fix typo in 2fa conf
|
||||
* fix(config): fix firewall config
|
||||
|
||||
|
||||
## [1.17.0] - 2022-11-19
|
||||
### Changed
|
||||
* upgrade murph/murph-core
|
||||
* replace annotation with attributes
|
||||
* use encore from node_modules in npm scripts
|
||||
|
||||
## [1.16.0]
|
||||
### Added
|
||||
* add a admin dashboard controller
|
||||
* add meta description in base.html.twig
|
||||
### Fixed
|
||||
### Changed
|
||||
* upgrade murph/murph-core
|
||||
|
||||
## 1.2.0
|
||||
## [1.15.0]
|
||||
### Changed
|
||||
* upgrade murph/murph-core
|
||||
|
||||
## [1.14.3]
|
||||
### Added
|
||||
* add blocks in default template
|
||||
### Changed
|
||||
* upgrade murph/murph-core
|
||||
|
||||
## [1.14.2]
|
||||
|
||||
## [1.14.1]
|
||||
### Fixed
|
||||
* fix missing envvar in makefile (npm)
|
||||
|
||||
## [1.14.0]
|
||||
### Changed
|
||||
* upgrade murph/murph-core
|
||||
|
||||
## [1.13.0]
|
||||
### Changed
|
||||
* upgrade murph/murph-core
|
||||
|
||||
## [1.12.0]
|
||||
### Changed
|
||||
* upgrade murph/murph-core
|
||||
|
||||
## [1.11.0]
|
||||
### Changed
|
||||
* upgrade murph/murph-core
|
||||
* use murph-npm to install npm requirements
|
||||
|
||||
## [1.10.0]
|
||||
### Added
|
||||
* add translated title in dashboard template
|
||||
### Fixed
|
||||
* remove useless env var from makefile
|
||||
### Changed
|
||||
* upgrade murph/murph-core
|
||||
|
||||
## [1.9.1] - 2022-03-14
|
||||
### Added
|
||||
* add murph version in autoload file
|
||||
### Changed
|
||||
* remove AdminController constructor
|
||||
|
||||
## [1.9.0] - 2022-03-13
|
||||
### Added
|
||||
* add murph version in admin ui
|
||||
### Changed
|
||||
* the core is now installed with composer
|
||||
|
||||
## [1.8.0] - 2022-03-10
|
||||
### Added
|
||||
* add security roles in app configuration
|
||||
* add option to restrict node access to specific roles
|
||||
### Changed
|
||||
* rename `core/EventSuscriber` with `core/EventSubscriber`
|
||||
|
||||
## [1.7.3] - 2022-03-06
|
||||
### Added
|
||||
* add ability to rename file in the file manager
|
||||
### Fixed
|
||||
* fix user factory
|
||||
* fix user creation from ui
|
||||
|
||||
## [1.7.2] - 2022-03-03
|
||||
### Added
|
||||
* add templates to render sections and items in the admin menu
|
||||
### Fixed
|
||||
* fix the analytic table when a path is a long
|
||||
|
||||
## [1.7.1] - 2022-03-01
|
||||
### Added
|
||||
* add translations
|
||||
### Fixed
|
||||
* fix missing directories
|
||||
|
||||
## [1.7.0] - 2022-03-01
|
||||
### Fixed
|
||||
* fix the analytic referers table when a referer has a long domain
|
||||
### Changed
|
||||
* upgrade dependencies
|
||||
* move assets to the core directory
|
||||
|
||||
## [1.6.0] - 2022-02-28
|
||||
### Added
|
||||
* add block in field templates to allow override
|
||||
* merge route params in crud admin redirects
|
||||
* improve murph:user:create command
|
||||
|
||||
### Fixed
|
||||
* fix form namespace prefix in the crud controller maker
|
||||
* fix date field when the value is empty
|
||||
* fix crud batch column width
|
||||
* fix sidebar icon width
|
||||
* fix cache clear task
|
||||
|
||||
### Changed
|
||||
* remove password generation from the user factory
|
||||
|
||||
## [1.5.0] - 2022-02-25
|
||||
### Added
|
||||
* add desktop views and mobile views
|
||||
|
||||
### Changed
|
||||
* upgrade dependencies
|
||||
* replace jaybizzle/crawler-detect with matomo/device-detector
|
||||
|
||||
## [1.4.1] - 2022-02-23
|
||||
### Added
|
||||
* handle app urls in twig routing filters
|
||||
|
||||
### Fixed
|
||||
* fix views in analytics modal
|
||||
* replace empty path with "/" in analytics
|
||||
### Changed
|
||||
* update default templates
|
||||
|
||||
## [1.4.0] - 2022-02-21
|
||||
### Added
|
||||
* add basic analytics
|
||||
|
||||
## [1.3.0] - 2022-02-19
|
||||
### Added
|
||||
* add support of regexp with substitution in redirect
|
||||
* url tags can be used as redirect location
|
||||
* add builders to replace file information tags and url tags
|
||||
|
||||
### Fixed
|
||||
* fix filemanager sorting
|
||||
* fix batch action setter
|
||||
|
||||
## [1.2.0] - 2022-02-14
|
||||
### Added
|
||||
* add sort in file manager
|
||||
* add redirect manager
|
||||
|
@ -12,7 +221,7 @@
|
|||
### Changed
|
||||
* replace node-sass with sass
|
||||
|
||||
## 1.1.0
|
||||
## [1.1.0] - 2022-02-29
|
||||
### Added
|
||||
* add directory upload in file manager
|
||||
|
||||
|
@ -22,9 +231,9 @@
|
|||
### Changed
|
||||
* symfony/swiftmailer-bundle is replaced by symfony/mailer
|
||||
|
||||
## 1.0.1
|
||||
## [1.0.1] - 2022-02-25
|
||||
### Fixed
|
||||
* fix Makefile environment vars (renaming)
|
||||
* fix composer minimum stability
|
||||
|
||||
## 1.0.0
|
||||
## [1.0.0] - 2022-01-23
|
||||
|
|
20
Makefile
20
Makefile
|
@ -1,25 +1,19 @@
|
|||
COMPOSER_BIN ?= composer
|
||||
PHP_BIN ?= php8.1
|
||||
SSH_BIN ?= ssh
|
||||
WEBPACK_BIN ?= webpack
|
||||
YARN_BIN ?= yarn
|
||||
NPM_BIN ?= npm
|
||||
|
||||
all: dep asset clean
|
||||
|
||||
.ONESHELL:
|
||||
dep:
|
||||
$(COMPOSER_BIN) update --ignore-platform-reqs
|
||||
$(COMPOSER_BIN) install --ignore-platform-reqs
|
||||
$(YARN_BIN)
|
||||
all: build
|
||||
|
||||
asset-watch:
|
||||
$(WEBPACK_BIN) -w
|
||||
$(YARN_BIN)
|
||||
$(NPM_BIN) run watch
|
||||
|
||||
asset: js-routing
|
||||
$(YARN_BIN)
|
||||
$(WEBPACK_BIN)
|
||||
$(NPM_BIN) run build
|
||||
|
||||
js-routing:
|
||||
js-routing: doctrine-migration
|
||||
$(PHP_BIN) bin/console fos:js-routing:dump --format=json --target=public/js/fos_js_routes.json
|
||||
|
||||
clean:
|
||||
|
@ -28,3 +22,5 @@ clean:
|
|||
|
||||
doctrine-migration:
|
||||
PHP=$(PHP_BIN) ./bin/doctrine-migrate
|
||||
|
||||
build: clean js-routing asset
|
||||
|
|
18
README.md
18
README.md
|
@ -1,8 +1,16 @@
|
|||
# MURPH
|
||||
# Murph
|
||||
|
||||
Muprh is an open-source CMF built on top of Symfony that helps you to build your own CMS with several domains and languages. It comes with a fully implemented and customizable tree manager, a CRUD generator, a 2FA authentication, settings and tasks managers.
|
||||
Murph is an **open-source CMF** built on top of Symfony that helps you to **build your own CMS with several domains and languages**. It comes with:
|
||||
|
||||
Symfony developers will love build on Murph 💪
|
||||
End users will be fond of the interface and the powerful tools 💜
|
||||
* A fully implemented and customizable **tree manager** 🌳
|
||||
* A **CRUD generator** ✏️
|
||||
* A global **settings manager** and a navigation settings manager ⚙️
|
||||
* A **tasks manager** 🧹
|
||||
* A basic **web analytics** 📊
|
||||
* **2FA authentication** 🔒
|
||||
|
||||
## [Read the documentation](https://doc.murph-project.org/)
|
||||
**Symfony developers will love build on Murph 🧪**
|
||||
|
||||
**End users will be fond of the interface and the powerful tools 💜**
|
||||
|
||||
📗 [Read the documentation](https://doc.murph-project.org/)
|
||||
|
|
12
SECURITY.md
Normal file
12
SECURITY.md
Normal file
|
@ -0,0 +1,12 @@
|
|||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
| ---------- | --------- |
|
||||
| >= 2.0 | :x: |
|
||||
| >= 1.0 | :white_check_mark: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you discover a security vulnerability within Murph, send an email to security [at] murph-project.org.
|
115
UPGRADE.md
Normal file
115
UPGRADE.md
Normal file
|
@ -0,0 +1,115 @@
|
|||
## General process
|
||||
|
||||
Upgrade dependencies:
|
||||
|
||||
* `composer update`
|
||||
* `yarn upgrade`
|
||||
|
||||
Build:
|
||||
|
||||
* `make build`
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## Upgrade to v1.17.0
|
||||
|
||||
Replace all annotations with PHP8 attributes and change the doctrine configuration:
|
||||
|
||||
```
|
||||
# config/packages/doctrine.yaml
|
||||
doctrine:
|
||||
...
|
||||
orm:
|
||||
...
|
||||
mappings:
|
||||
App\Core\Entity:
|
||||
type: attribute
|
||||
...
|
||||
App\Entity:
|
||||
type: attribute
|
||||
...
|
||||
```
|
||||
|
||||
## Upgrade to v1.15.0
|
||||
|
||||
```
|
||||
cd public/vendor
|
||||
ln -rs ../../node_modules/grapesjs-plugin-export .
|
||||
ln -rs ../../node_modules/grapesjs-parser-postcss .
|
||||
```
|
||||
|
||||
## Upgrade to v1.10.0
|
||||
### Commands
|
||||
|
||||
```
|
||||
make doctrine-migration
|
||||
```
|
||||
|
||||
|
||||
## Upgrade to v1.9.0
|
||||
|
||||
This version uses the package `murph/murph-core`.
|
||||
|
||||
|
||||
## Upgrade to v1.8.0
|
||||
### Commands
|
||||
|
||||
```
|
||||
make doctrine-migration
|
||||
```
|
||||
|
||||
### Files
|
||||
|
||||
Event subscribers in `src/EventSubscriber` must update namespaces.
|
||||
|
||||
## Upgrade to v1.7.0
|
||||
### Commands
|
||||
|
||||
```
|
||||
yarn add sortablejs@^1.14.0
|
||||
|
||||
```
|
||||
|
||||
### Files
|
||||
|
||||
* `assets/css/_admin_extend.scss` is removed
|
||||
* `assets/css/_admin_vars.scss` is removed
|
||||
* `assets/css/_admin_vars.scss` is changed
|
||||
* `assets/js/admin` is removed
|
||||
* `assets/js/admin.js` is changed
|
||||
|
||||
|
||||
## Upgrade to v1.5.0
|
||||
### Commands
|
||||
|
||||
```
|
||||
composer remove jaybizzle/crawler-detect
|
||||
composer require matomo/device-detector
|
||||
make doctrine-migration
|
||||
```
|
||||
|
||||
## Upgrade to v1.4.0
|
||||
### Commands
|
||||
|
||||
```
|
||||
yarn remove node-sass
|
||||
yarn add sass --dev --save
|
||||
yarn add chart.js --save
|
||||
composer require jaybizzle/crawler-detect
|
||||
make doctrine-migration
|
||||
make asset
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
```
|
||||
// config/services.yaml
|
||||
services:
|
||||
App\Core\EventListener\RedirectListener:
|
||||
tags:
|
||||
- { name: kernel.event_listener, event: kernel.exception }
|
||||
|
||||
App\Core\EventListener\AnalyticListener:
|
||||
tags:
|
||||
- { name: kernel.event_listener, event: kernel.request }
|
||||
```
|
|
@ -1,5 +0,0 @@
|
|||
$theme-colors: (
|
||||
"primary": #1ab5dc,
|
||||
"primary-light": lighten(#3183aa, 40%),
|
||||
"dark-blue": #1e2430,
|
||||
) !default;
|
|
@ -1,523 +1,6 @@
|
|||
@import "./_admin_vars.scss";
|
||||
/* Custom variables */
|
||||
|
||||
$theme-colors: (
|
||||
"primary": #1ab5dc,
|
||||
"primary-light": lighten(#3183aa, 40%),
|
||||
"dark-blue": #1e2430,
|
||||
) !default;
|
||||
@import "../../vendor/murph/murph-core/src/core/Resources/assets/css/admin.scss";
|
||||
|
||||
$grid-gutter-width: 0px !default;
|
||||
$pagination-color: #343a40 !default;
|
||||
$pagination-bg: #ffffff !default;
|
||||
$pagination-active-color: #ffffff !default;
|
||||
$pagination-active-bg: #343a40 !default;
|
||||
/* Custom CSS */
|
||||
|
||||
@import "~choices.js/src/styles/choices.scss";
|
||||
@import "~bootstrap/scss/bootstrap.scss";
|
||||
@import "~@fortawesome/fontawesome-free/css/all.css";
|
||||
@import "~flag-icon-css/sass/flag-icon.scss";
|
||||
|
||||
@for $i from 1 through 100 {
|
||||
.miw-#{$i*5} {
|
||||
min-width: $i * 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.flag-icon-en {
|
||||
background-image: url(~flag-icon-css/flags/4x3/gb.svg);
|
||||
}
|
||||
|
||||
body {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
#logo {
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.choices__list--dropdown {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.choices__list--dropdown.is-active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dropdown-toggle-hide-after {
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.login {
|
||||
&-container {
|
||||
margin-top: 5%;
|
||||
margin-bottom: 5%;
|
||||
}
|
||||
|
||||
&-form {
|
||||
padding: 5%;
|
||||
}
|
||||
|
||||
&-image {
|
||||
width: 100%;
|
||||
max-width: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
padding: 71px 0 0;
|
||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
.sidebar-sticky {
|
||||
position: relative;
|
||||
top: 0;
|
||||
height: calc(100vh - 71px);
|
||||
padding-top: .5rem;
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
|
||||
&:hover {
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@supports ((position: -webkit-sticky) or (position: sticky)) {
|
||||
.sidebar-sticky {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
}
|
||||
}
|
||||
|
||||
.actions-container {
|
||||
padding-right: 25px;
|
||||
}
|
||||
|
||||
.table .thead-light {
|
||||
a, th {
|
||||
color: map-get($theme-colors, 'dark-blue');
|
||||
}
|
||||
}
|
||||
|
||||
tr.table-primary-light {
|
||||
background-color: #ecf5fa;
|
||||
}
|
||||
|
||||
.td-nowrap {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.table tr {
|
||||
td {
|
||||
transition: border 500ms ease-out;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
td {
|
||||
border-bottom: 1px solid #a8aaac;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bg-dark-blue {
|
||||
background: map-get($theme-colors, 'dark-blue');
|
||||
color: #fff;
|
||||
|
||||
.nav-item-label {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-pills {
|
||||
.nav-item {
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.nav-link:not(.active) {
|
||||
color: #333;
|
||||
background: #eee;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.sidebar {
|
||||
width: 260px;
|
||||
display: inline-block;
|
||||
|
||||
.nav-link {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
border-left: 4px solid map-get($theme-colors, 'dark-blue');
|
||||
padding-top: 14px;
|
||||
padding-bottom: 14px;
|
||||
|
||||
.fa {
|
||||
font-size: 1.2rem;
|
||||
margin-right: 5px;
|
||||
min-width: 30px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.active {
|
||||
font-weight: bold;
|
||||
border-left: 4px solid map-get($theme-colors, 'primary');
|
||||
background: map-get($theme-colors, 'dark-blue');
|
||||
}
|
||||
}
|
||||
|
||||
&-heading {
|
||||
font-size: .75rem;
|
||||
text-transform: uppercase;
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
*[data-selectable-selector] {
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
*[data-selectable-selector] {
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
*[data-sortable-item] {
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.sortable-chosen {
|
||||
background: map-get($theme-colors, 'primary-light');
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
height: 35px;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.body {
|
||||
padding-top: 60px;
|
||||
width: calc(100% - 260px);
|
||||
margin-left: 260px;
|
||||
display: inline-block;
|
||||
|
||||
.nav {
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 770px) {
|
||||
.body {
|
||||
margin-left: 50px;
|
||||
width: calc(100vw - 50px);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 50px;
|
||||
max-width: 100% !important;
|
||||
|
||||
.sidebar-sticky {
|
||||
width: 50px;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.nav {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.nav-item-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar-heading {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
th {
|
||||
&.sorted {
|
||||
&::before {
|
||||
content: '\f0dc';
|
||||
font-family: 'FontAwesome';
|
||||
color: #aaa;
|
||||
margin-right: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
max-width: 100%;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.toast-container {
|
||||
display: flex;
|
||||
position: relative;
|
||||
z-index: 4000;
|
||||
|
||||
.toast-wrapper {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 1060;
|
||||
width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.bg-tiles {
|
||||
background-color: #c1c1c1;
|
||||
background-image: linear-gradient(45deg, #646464 25%, transparent 25%, transparent 75%, #646464 75%), linear-gradient(45deg, #646464 25%, transparent 25%, transparent 75%, #646464 75%);
|
||||
background-size: 20px 20px;
|
||||
background-position: 0 0, 10px 10px;
|
||||
}
|
||||
|
||||
.tab-form {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.icon-margin {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.d-ib {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.list-checkbox {
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
.password-strenth {
|
||||
padding: 0 0 5px 0;
|
||||
margin-top: -4px;
|
||||
|
||||
.col-sm {
|
||||
height: 8px;
|
||||
border: 2px solid #fff;
|
||||
}
|
||||
|
||||
&-info {
|
||||
font-size: 13px;
|
||||
height: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
.notification-bell:not([disabled]) {
|
||||
[data-counter]:after {
|
||||
display: block;
|
||||
color: #fff;
|
||||
background: red;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
position: absolute;
|
||||
content: ' ';
|
||||
top: 4px;
|
||||
right: 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-error-icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.custom-file-label::after {
|
||||
content: "Parcourir";
|
||||
}
|
||||
|
||||
#lease_template_html {
|
||||
height: calc(100vh - 270px);
|
||||
}
|
||||
|
||||
.panel {
|
||||
&-toggler {
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&-content {
|
||||
display: block;
|
||||
|
||||
&:not(.active) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*[data-collection-delete-container] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
*[data-collection-add] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.login-image {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.tree {
|
||||
position: relative;
|
||||
background: white;
|
||||
color: #212529;
|
||||
|
||||
span {
|
||||
font-style: italic;
|
||||
letter-spacing: .4px;
|
||||
color: #a8a8a8;
|
||||
}
|
||||
|
||||
.fa-folder-open, .fa-folder {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.fa-html5 {
|
||||
color: #f21f10;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 5px;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding-bottom: 0;
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
padding-left: 15px;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
left: 0;
|
||||
width: 10px;
|
||||
height: 1px;
|
||||
margin: auto;
|
||||
content: '';
|
||||
background-color: #666;
|
||||
}
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
content: '';
|
||||
background-color: #666;
|
||||
}
|
||||
|
||||
&:last-child:after {
|
||||
height: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fieldset.form-group {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.crud-header {
|
||||
&-title {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
&-actions {
|
||||
text-align: right;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 770px) {
|
||||
&-title {
|
||||
float: left;
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
&-actions {
|
||||
float: right;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&::after {
|
||||
display: block;
|
||||
content: "";
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
|
||||
th.crud-batch-column {
|
||||
width: 20px !important;
|
||||
max-width: 20px;
|
||||
}
|
||||
|
||||
form {
|
||||
.loader {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.is-loading .loader {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.modal {
|
||||
z-index: 3000;
|
||||
}
|
||||
|
||||
.modal-dialog-large {
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
.output {
|
||||
&-console {
|
||||
background: #073642;
|
||||
line-height: normal;
|
||||
}
|
||||
}
|
||||
|
||||
@import "./_admin_extend.scss";
|
||||
|
|
2
assets/css/app.scss
Normal file
2
assets/css/app.scss
Normal file
|
@ -0,0 +1,2 @@
|
|||
/* CSS of you app */
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 163 B |
Binary file not shown.
Before Width: | Height: | Size: 32 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.5 KiB |
|
@ -1 +1 @@
|
|||
import './admin/admin.js'
|
||||
import '../../vendor/murph/murph-core/src/core/Resources/assets/js/admin.js'
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
import '../../css/admin.scss'
|
||||
|
||||
require('../../../node_modules/bootstrap/dist/js/bootstrap.min.js')
|
||||
require('./modules/table-fixed.js')()
|
||||
require('./modules/form-confirm.js')()
|
||||
require('./modules/form-file.js')()
|
||||
require('./modules/form-error.js')()
|
||||
require('./modules/form-ajax.js')()
|
||||
require('./modules/dbclick.js')()
|
||||
require('./modules/toast.js')()
|
||||
require('./modules/modal.js')()
|
||||
require('./modules/push-state.js')()
|
||||
require('./modules/password.js')()
|
||||
require('./modules/tooltip.js')()
|
||||
require('./modules/editor.js')()
|
||||
require('./modules/panel.js')()
|
||||
require('./modules/choices.js')()
|
||||
require('./modules/checkbox-checker.js')()
|
||||
require('./modules/rest-choices.js')()
|
||||
require('./modules/form-collection.js')()
|
||||
require('./modules/datepicker.js')()
|
||||
require('./modules/sortable.js')()
|
||||
require('./modules/batch.js')()
|
||||
require('./modules/file-manager.js')()
|
||||
require('./modules/file-picker.js')()
|
|
@ -1,86 +0,0 @@
|
|||
<template>
|
||||
<span>
|
||||
<span v-if="!thumb || !thumbnail" v-bind:class="icon"></span>
|
||||
<img v-if="thumb && thumbnail" v-bind:src="thumbnail">
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
img {
|
||||
max-width: 120px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import Routing from '../../../../../vendor/friendsofsymfony/jsrouting-bundle/Resources/public/js/router.min.js'
|
||||
const routes = require('../../../../../public/js/fos_js_routes.json')
|
||||
|
||||
Routing.setRoutingData(routes)
|
||||
|
||||
const map = {
|
||||
'fa fa-file-pdf': ['application/pdf'],
|
||||
'fa fa-file-image': ['image/png', 'image/jpg', 'image/jpeg', 'image/gif'],
|
||||
'fa fa-file-audio': ['application/ogg', 'audio/mp3', 'audio/mpeg', 'audio/wav'],
|
||||
'fa fa-file-archive': ['application/zip', 'multipart/x-zip', 'application/rar', 'application/x-rar-compressed', 'application/x-zip-compressed', 'application/tar', 'application/x-tar'],
|
||||
'fa fa-file-alt': ['application/rtf'],
|
||||
'fa fa-file-excel': ['application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
|
||||
'fa fa-file-powerpoint': ['application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation'],
|
||||
'fa fa-file-video': ['video/x-msvideo', 'video/mpeg']
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'FileIcon',
|
||||
data () {
|
||||
return {
|
||||
icon: null,
|
||||
thumbnail: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
defineIcon () {
|
||||
for (const icon in map) {
|
||||
if (map[icon].indexOf(this.mime) !== -1) {
|
||||
this.icon = icon
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
this.icon = 'fa fa-file'
|
||||
},
|
||||
defineThumbnail () {
|
||||
if (['image/svg', 'image/svg+xml'].indexOf(this.mime) !== -1) {
|
||||
this.thumbnail = '/' + this.path
|
||||
return
|
||||
}
|
||||
|
||||
if (['image/png', 'image/jpg', 'image/jpeg', 'image/gif'].indexOf(this.mime) === -1) {
|
||||
this.thumbnail = null
|
||||
return
|
||||
}
|
||||
|
||||
this.thumbnail = Routing.generate('liip_imagine_filter', {
|
||||
filter: 'file_manager_thumbnail_filter',
|
||||
path: this.path
|
||||
})
|
||||
}
|
||||
},
|
||||
props: {
|
||||
mime: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
path: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
thumb: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.defineIcon()
|
||||
this.defineThumbnail()
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,30 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<Files v-bind:context="context" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import Files from './Files'
|
||||
|
||||
export default {
|
||||
name: 'FileManager',
|
||||
props: {
|
||||
context: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: 'crud'
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Files
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,378 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<nav aria-label="breadcrumb" class="d-flex justify-content-between">
|
||||
<ol class="breadcrumb mb-0">
|
||||
<li class="breadcrumb-item" v-for="item in breadcrumb">
|
||||
<a class="btn btn-sm" href="#" v-on:click="setDirectory(item.path)" v-html="item.label"></a>
|
||||
</li>
|
||||
<li v-if="isLoading" class="ml-3">
|
||||
<div class="spinner-border spinner-border-sm" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<div class="d-flex">
|
||||
<div class="breadcrumb mb-0 file-manager-actions">
|
||||
<span class="btn btn-sm btn-primary ml-1" v-bind:data-modal="generateUploadLink(directory)">
|
||||
<span class="fa fa-upload" v-bind:data-modal="generateUploadLink(directory)"></span>
|
||||
</span>
|
||||
<span class="btn btn-sm btn-primary ml-1" v-bind:data-modal="generateNewDirectoryLink(directory)">
|
||||
<span class="fa fa-folder-plus" v-bind:data-modal="generateNewDirectoryLink(directory)"></span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="breadcrumb mb-0 file-manager-views">
|
||||
<select v-model="sort" class="form-control form-control-sm d-inline w-auto ml-1">
|
||||
<option value="name">Name</option>
|
||||
<option value="modification_date">Date</option>
|
||||
</select>
|
||||
<select v-model="sortDirection" class="form-control form-control-sm d-inline w-auto ml-1">
|
||||
<option value="asc">ASC</option>
|
||||
<option value="desc">DESC</option>
|
||||
</select>
|
||||
<span class="btn btn-sm btn-dark ml-1" v-on:click="setView('grid')">
|
||||
<span class="fa fa-grip-horizontal" v-on:click="setView('grid')"></span>
|
||||
</span>
|
||||
<span class="btn btn-sm btn-dark ml-1" v-on:click="setView('list')">
|
||||
<span class="fa fa-list" v-on:click="setView('list')"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="card-deck" v-if="view == 'grid'">
|
||||
<div v-if="parent" class="card mt-3 ml-3 mb-3 border-0">
|
||||
<div class="card-body p-2">
|
||||
<div class="card-text" v-on:dblclick="setDirectory(parent)">
|
||||
<div class="text-center display-4 text-warning">
|
||||
<span class="fa fa-folder"></span>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
..
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-for="item in directories" class="card mt-3 ml-3 mb-3 border-0">
|
||||
<div class="card-body p-2">
|
||||
<div class="card-text" v-on:dblclick="setDirectory(item.path)" v-bind:data-modal="generateInfoLink(item, true, context)">
|
||||
<div class="text-center">
|
||||
<div class="display-4 text-warning">
|
||||
<span class="fa fa-folder"></span>
|
||||
</div>
|
||||
|
||||
<div v-if="item.locked" class="file-manager-grid-lock">
|
||||
<span class="btn btn-sm">
|
||||
<span class="fa fa-lock"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<span v-html="item.basename"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="item in files" class="card mt-3 ml-3 mb-3 border-0" v-on:click="modalUrl = generateInfoLink(item, null, context)" v-bind:data-modal="generateInfoLink(item, null, context)">
|
||||
<div class="card-body p-2">
|
||||
<div class="card-text">
|
||||
<div class="text-center">
|
||||
<div class="display-4 text-muted">
|
||||
<FileIcon v-bind:mime="item.mime" v-bind:path="item.webPath" v-bind:thumb="true" />
|
||||
</div>
|
||||
|
||||
<div v-if="item.locked" class="file-manager-grid-lock">
|
||||
<span class="btn btn-sm">
|
||||
<span class="fa fa-lock"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<span v-html="item.basename"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive" v-if="view == 'list'">
|
||||
<table class="table">
|
||||
<tr v-if="parent" v-on:dblclick="setDirectory(parent)">
|
||||
<td width="10">
|
||||
<span class="fa fa-folder text-warning"></span>
|
||||
</td>
|
||||
<td>
|
||||
..
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr v-for="item in directories" v-on:dblclick="setDirectory(item.path)" v-bind:data-modal="generateInfoLink(item, true, context)">
|
||||
<td width="10">
|
||||
<span class="fa fa-folder text-warning"></span>
|
||||
</td>
|
||||
<td>
|
||||
<div v-if="item.locked" class="float-right">
|
||||
<span class="btn btn-sm btn-light">
|
||||
<span class="fa fa-lock"></span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span v-html="item.basename"></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-for="item in files">
|
||||
<td width="10">
|
||||
<FileIcon v-bind:mime="item.mime" v-bind:path="item.webPath" v-bind:thumb="false" />
|
||||
</td>
|
||||
<td v-on:click="modalUrl = generateInfoLink(item, null, context)" v-bind:data-modal="generateInfoLink(item, null, context)">
|
||||
<div v-if="item.locked" class="float-right">
|
||||
<span class="btn btn-sm btn-light">
|
||||
<span class="fa fa-lock"></span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span v-html="item.basename"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card {
|
||||
margin-right: 5px;
|
||||
flex: 0 0 170px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
* {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
tr {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.file-manager-views {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.file-manager-grid-lock {
|
||||
margin-top: -26px;
|
||||
padding-left: 40px;
|
||||
}
|
||||
|
||||
.breadcrumb, nav {
|
||||
border-radius: 0;
|
||||
background: #e9ecef;
|
||||
}
|
||||
|
||||
.file-manager-actions .fa {
|
||||
padding: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.breadcrumb-item + .breadcrumb-item::before {
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import Routing from '../../../../../vendor/friendsofsymfony/jsrouting-bundle/Resources/public/js/router.min.js'
|
||||
import FileIcon from './FileIcon'
|
||||
|
||||
const axios = require('axios').default
|
||||
const $ = require('jquery')
|
||||
const routes = require('../../../../../public/js/fos_js_routes.json')
|
||||
|
||||
Routing.setRoutingData(routes)
|
||||
|
||||
export default {
|
||||
name: 'Files',
|
||||
components: {
|
||||
FileIcon
|
||||
},
|
||||
props: {
|
||||
context: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
view: 'list',
|
||||
directory: null,
|
||||
directories: [],
|
||||
breadcrumb: [],
|
||||
files: [],
|
||||
sort: 'name',
|
||||
sortDirection: 'asc',
|
||||
parent: null,
|
||||
modalUrl: null,
|
||||
ajax: 0,
|
||||
isLoading: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setDirectory (directory) {
|
||||
if (!directory) {
|
||||
directory = '/'
|
||||
}
|
||||
this.directory = directory
|
||||
},
|
||||
setView (view) {
|
||||
this.view = view
|
||||
|
||||
localStorage.setItem('file-manager.view', view)
|
||||
},
|
||||
saveSort () {
|
||||
localStorage.setItem('file-manager.sort', this.sort)
|
||||
localStorage.setItem('file-manager.sortDirection', this.sortDirection)
|
||||
},
|
||||
generateInfoLink (item, directory, context) {
|
||||
if (directory) {
|
||||
return Routing.generate('admin_file_manager_info', {
|
||||
file: item.path,
|
||||
context: context,
|
||||
ajax: this.ajax
|
||||
})
|
||||
} else {
|
||||
return Routing.generate('admin_file_manager_info', {
|
||||
file: item.path + '/' + item.basename,
|
||||
context: context,
|
||||
ajax: this.ajax
|
||||
})
|
||||
}
|
||||
},
|
||||
generateUploadLink (directory) {
|
||||
return Routing.generate('admin_file_manager_upload', {
|
||||
file: directory,
|
||||
ajax: this.ajax
|
||||
})
|
||||
},
|
||||
generateNewDirectoryLink (directory) {
|
||||
return Routing.generate('admin_file_manager_directory_new', {
|
||||
file: directory,
|
||||
ajax: this.ajax
|
||||
})
|
||||
},
|
||||
buildBreadcrum (elements) {
|
||||
let path = '/'
|
||||
this.breadcrumb = []
|
||||
|
||||
for (const i in elements) {
|
||||
const element = elements[i]
|
||||
|
||||
if (element !== '/') {
|
||||
path = path + '/' + element
|
||||
|
||||
this.breadcrumb.push({
|
||||
path: path,
|
||||
label: element
|
||||
})
|
||||
} else {
|
||||
this.breadcrumb.push({
|
||||
path: '/',
|
||||
label: 'Files'
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
refresh () {
|
||||
const that = this
|
||||
this.isLoading = true
|
||||
this.files = []
|
||||
this.directories = []
|
||||
|
||||
axios.get(Routing.generate('admin_file_manager_api_directory', {
|
||||
directory: that.directory,
|
||||
context: that.context,
|
||||
ajax: this.ajax,
|
||||
_sort: this.sort,
|
||||
_sort_direction: this.sortDirection,
|
||||
time: Date.now(),
|
||||
}))
|
||||
.then((response) => {
|
||||
that.buildBreadcrum(response.data.breadcrumb)
|
||||
that.parent = response.data.parent
|
||||
that.directories = response.data.directories
|
||||
that.files = response.data.files
|
||||
that.isLoading = false
|
||||
|
||||
const query = new URLSearchParams(window.location.search)
|
||||
query.set('path', that.directory)
|
||||
|
||||
history.pushState(
|
||||
null,
|
||||
'',
|
||||
window.location.pathname + '?' + query.toString()
|
||||
)
|
||||
})
|
||||
.catch((e) => {
|
||||
alert('An error occured')
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
const view = localStorage.getItem('file-manager.view')
|
||||
const sort = localStorage.getItem('file-manager.sort')
|
||||
const sortDirection = localStorage.getItem('file-manager.sortDirection')
|
||||
|
||||
if (['grid', 'list'].indexOf(view) !== -1) {
|
||||
this.view = view
|
||||
}
|
||||
|
||||
if (['name', 'modification_date'].indexOf(sort) !== -1) {
|
||||
this.sort = sort
|
||||
}
|
||||
|
||||
if (['asc', 'desc'].indexOf(sortDirection) !== -1) {
|
||||
this.sortDirection = sortDirection
|
||||
}
|
||||
|
||||
const query = new URLSearchParams(window.location.search)
|
||||
|
||||
if (query.has('path')) {
|
||||
this.setDirectory(query.get('path'))
|
||||
} else {
|
||||
this.setDirectory('/')
|
||||
}
|
||||
|
||||
this.ajax = (['crud'].indexOf(this.context) === -1 ? 1 : 0)
|
||||
|
||||
const body = $('body')
|
||||
const events = ['file_manager.file.new', 'file_manager.directory.new', 'file_manager.directory.rename']
|
||||
const that = this
|
||||
|
||||
$(events).each((k, event) => {
|
||||
body.on(event + '.success', () => {
|
||||
$('#modal-container').modal('hide')
|
||||
that.refresh()
|
||||
})
|
||||
})
|
||||
|
||||
body.on('file_manager.info.update.success', () => {
|
||||
$('*[data-modal="' + that.modalUrl + '"]').click()
|
||||
})
|
||||
},
|
||||
watch: {
|
||||
directory (directory) {
|
||||
this.refresh()
|
||||
},
|
||||
sort (sort) {
|
||||
this.saveSort()
|
||||
this.refresh()
|
||||
},
|
||||
sortDirection (sortDirection) {
|
||||
this.saveSort()
|
||||
this.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,23 +0,0 @@
|
|||
const $ = require('jquery')
|
||||
|
||||
module.exports = () => {
|
||||
$('th.crud-batch-column input').change((e) => {
|
||||
$('td.crud-batch-column input').prop('checked', $(e.target).is(':checked'))
|
||||
})
|
||||
|
||||
const form = $('#form-batch')
|
||||
|
||||
form.submit((e) => {
|
||||
e.preventDefault()
|
||||
|
||||
const route = form.attr('action')
|
||||
const datas = form.serialize()
|
||||
|
||||
form.addClass('is-loading')
|
||||
|
||||
$.post(route, datas)
|
||||
.always(() => {
|
||||
document.location.reload()
|
||||
})
|
||||
})
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
const $ = require('jquery')
|
||||
|
||||
module.exports = function () {
|
||||
$('*[data-checkbox-ckecker]').click(function () {
|
||||
const wrapperName = $(this).attr('data-checkbox-ckecker')
|
||||
|
||||
if (!wrapperName) {
|
||||
return
|
||||
}
|
||||
|
||||
const checkboxes = $('*[data-checkbox-wrapper="' + wrapperName + '"] *[data-checkbox] input[type="checkbox"]')
|
||||
|
||||
$(checkboxes).each(function (i, v) {
|
||||
$(v).prop('checked', true)
|
||||
})
|
||||
})
|
||||
|
||||
$('*[data-checkbox-unckecker]').click(function () {
|
||||
const wrapperName = $(this).attr('data-checkbox-unckecker')
|
||||
|
||||
if (!wrapperName) {
|
||||
return
|
||||
}
|
||||
|
||||
const checkboxes = $('*[data-checkbox-wrapper="' + wrapperName + '"] *[data-checkbox] input[type="checkbox"]')
|
||||
|
||||
$(checkboxes).each(function (i, v) {
|
||||
$(v).prop('checked', false)
|
||||
})
|
||||
})
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
const Choices = require('choices.js')
|
||||
const $ = require('jquery')
|
||||
|
||||
module.exports = function () {
|
||||
$('*[data-jschoice]').each(function (key, item) {
|
||||
return new Choices(item)
|
||||
})
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
const Datepicker = require('vanillajs-datepicker')
|
||||
|
||||
const isDateSupported = () => {
|
||||
const input = document.createElement('input')
|
||||
const value = 'a'
|
||||
|
||||
input.setAttribute('type', 'date')
|
||||
input.setAttribute('value', value)
|
||||
|
||||
return input.value !== value
|
||||
}
|
||||
|
||||
const createDatePicker = (input) => {
|
||||
return new Datepicker.Datepicker(input, {
|
||||
format: 'yyyy-mm-dd'
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = () => {
|
||||
if (isDateSupported()) {
|
||||
return
|
||||
}
|
||||
|
||||
const inputs = document.querySelectorAll('input[type="date"]')
|
||||
const size = inputs.length
|
||||
|
||||
for (let i = 0, c = size; i < c; i++) {
|
||||
createDatePicker(inputs[i])
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
const $ = require('jquery')
|
||||
|
||||
module.exports = function () {
|
||||
$('*[data-dblclick]').dblclick(function (e) {
|
||||
document.location.href = $(this).attr('data-dblclick')
|
||||
})
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
const $ = require('jquery')
|
||||
|
||||
const DocumentSelector = () => {
|
||||
const forms = $('.document-selector-form')
|
||||
|
||||
const handler = function () {
|
||||
forms.each((fi, f) => {
|
||||
const form = $(f)
|
||||
const ids = form.find('.document-selector-ids')
|
||||
const btn = form.find('.document-selector-button')
|
||||
|
||||
ids.html('')
|
||||
let hasSelection = false
|
||||
|
||||
$('*[data-documents] *[data-selectable-row] input[data-selectable-checkbox]').each((i, c) => {
|
||||
const checkbox = $(c)
|
||||
|
||||
if (checkbox.is(':checked')) {
|
||||
ids.append(checkbox[0].outerHTML)
|
||||
hasSelection = true
|
||||
}
|
||||
})
|
||||
|
||||
if (hasSelection && btn.length) {
|
||||
btn.removeAttr('disabled')
|
||||
ids.find('input').prop('checked', true)
|
||||
} else {
|
||||
btn.attr('disabled', 'disabled')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
$('*[data-documents] *[data-selectable-row]').click(function () {
|
||||
window.setTimeout(handler, 100)
|
||||
})
|
||||
|
||||
$('*[data-documents] *[data-selectable-row]').on('clicked', function () {
|
||||
window.setTimeout(handler, 100)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = DocumentSelector
|
|
@ -1,626 +0,0 @@
|
|||
const $ = require('jquery')
|
||||
const Vue = require('vue').default
|
||||
const FileManager = require('../components/file-manager/FileManager').default
|
||||
|
||||
const createModal = function () {
|
||||
let container = $('#fm-modal')
|
||||
const body = $('body')
|
||||
|
||||
if (!container.length) {
|
||||
container = $('<div id="fm-modal" class="modal">')
|
||||
|
||||
body.append(container)
|
||||
}
|
||||
|
||||
container.html(`
|
||||
<div class="modal-dialog modal-dialog-large">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body">
|
||||
<div id="fm-modal-content">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`)
|
||||
|
||||
$(container).modal('show')
|
||||
|
||||
return $(container)
|
||||
}
|
||||
|
||||
const fileManagerBrowser = function (callback) {
|
||||
const container = createModal()
|
||||
|
||||
const clickCallback = (e) => {
|
||||
callback($(e.target).attr('data-value'), {})
|
||||
$('#modal-container').modal('hide')
|
||||
container.modal('hide')
|
||||
|
||||
$('body').off('click', '#file-manager-insert', clickCallback)
|
||||
}
|
||||
|
||||
$('body').on('click', '#file-manager-insert', clickCallback)
|
||||
|
||||
return new Vue({
|
||||
el: '#fm-modal-content',
|
||||
template: '<FileManager context="tinymce" />',
|
||||
components: {
|
||||
FileManager
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (typeof window.tinymce !== 'undefined') {
|
||||
window.tinymce.murph = window.tinymce.murph || {}
|
||||
window.tinymce.murph.selector = window.tinymce.murph.selector || '*[data-tinymce]'
|
||||
window.tinymce.murph.configurationBase = window.tinymce.murph.configurationBase || {
|
||||
base_url: '/vendor/tinymce/',
|
||||
cache_suffix: '?v=4.1.6',
|
||||
importcss_append: true,
|
||||
image_caption: true,
|
||||
noneditable_noneditable_class: 'mceNonEditable',
|
||||
toolbar_drawer: 'sliding',
|
||||
spellchecker_dialog: true,
|
||||
tinycomments_mode: 'embedded',
|
||||
convert_urls: false,
|
||||
file_picker_callback: fileManagerBrowser,
|
||||
file_picker_types: 'image',
|
||||
init_instance_callback: function (editor) {
|
||||
editor.on('SetContent', () => {
|
||||
window.tinymce.triggerSave(false, true)
|
||||
})
|
||||
|
||||
editor.on('Change', () => {
|
||||
window.tinymce.triggerSave(false, true)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
window.tinymce.murph.modes = window.tinymce.murph.modes || {}
|
||||
|
||||
window.tinymce.murph.modes.default = window.tinymce.murph.modes.default || {
|
||||
plugins: 'print preview importcss searchreplace visualblocks visualchars fullscreen template table charmap hr pagebreak nonbreaking toc insertdatetime advlist lists wordcount textpattern noneditable help charmap quickbars link image code autoresize',
|
||||
menubar: 'file edit view insert format tools table tc help',
|
||||
toolbar: 'undo redo | bold italic underline strikethrough | link image | fontselect fontsizeselect formatselect | alignleft aligncenter alignright alignjustify | outdent indent | numlist bullist checklist | forecolor backcolor casechange permanentpen formatpainter removeformat | pagebreak | charmap | fullscreen preview',
|
||||
quickbars_selection_toolbar: 'bold italic | quicklink h2 h3 blockquote quickimage quicktable',
|
||||
contextmenu: 'link image imagetools table configurepermanentpen'
|
||||
}
|
||||
|
||||
window.tinymce.murph.modes.light = window.tinymce.murph.modes.light || {
|
||||
contextmenu: 'link image imagetools table configurepermanentpen',
|
||||
quickbars_selection_toolbar: 'bold italic',
|
||||
toolbar: 'undo redo | bold italic underline'
|
||||
}
|
||||
|
||||
tinymce.addI18n('fr_FR', {
|
||||
Redo: 'R\u00e9tablir',
|
||||
Undo: 'Annuler',
|
||||
Cut: 'Couper',
|
||||
Copy: 'Copier',
|
||||
Paste: 'Coller',
|
||||
'Select all': 'S\u00e9lectionner tout',
|
||||
'New document': 'Nouveau document',
|
||||
Ok: 'OK',
|
||||
Cancel: 'Annuler',
|
||||
'Visual aids': 'Aides visuelles',
|
||||
Bold: 'Gras',
|
||||
Italic: 'Italique',
|
||||
Underline: 'Soulign\u00e9',
|
||||
Strikethrough: 'Barr\u00e9',
|
||||
Superscript: 'Exposant',
|
||||
Subscript: 'Indice',
|
||||
'Clear formatting': 'Effacer la mise en forme',
|
||||
'Align left': 'Aligner \u00e0 gauche',
|
||||
'Align center': 'Centrer',
|
||||
'Align right': 'Aligner \u00e0 droite',
|
||||
Justify: 'Justifier',
|
||||
'Bullet list': 'Liste \u00e0 puces',
|
||||
'Numbered list': 'Liste num\u00e9rot\u00e9e',
|
||||
'Decrease indent': 'R\u00e9duire le retrait',
|
||||
'Increase indent': 'Augmenter le retrait',
|
||||
Close: 'Fermer',
|
||||
Formats: 'Formats',
|
||||
"Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X\/C\/V keyboard shortcuts instead.": "Votre navigateur ne supporte pas l\u2019acc\u00e8s direct au presse-papiers. Merci d'utiliser les raccourcis clavier Ctrl+X\/C\/V.",
|
||||
Headers: 'En-t\u00eates',
|
||||
'Header 1': 'En-t\u00eate 1',
|
||||
'Header 2': 'En-t\u00eate 2',
|
||||
'Header 3': 'En-t\u00eate 3',
|
||||
'Header 4': 'En-t\u00eate 4',
|
||||
'Header 5': 'En-t\u00eate 5',
|
||||
'Header 6': 'En-t\u00eate 6',
|
||||
Headings: 'Titres',
|
||||
'Heading 1': 'Titre\u00a01',
|
||||
'Heading 2': 'Titre\u00a02',
|
||||
'Heading 3': 'Titre\u00a03',
|
||||
'Heading 4': 'Titre\u00a04',
|
||||
'Heading 5': 'Titre\u00a05',
|
||||
'Heading 6': 'Titre\u00a06',
|
||||
Preformatted: 'Pr\u00e9format\u00e9',
|
||||
Div: 'Div',
|
||||
Pre: 'Pre',
|
||||
Code: 'Code',
|
||||
Paragraph: 'Paragraphe',
|
||||
Blockquote: 'Blockquote',
|
||||
Inline: 'En ligne',
|
||||
Blocks: 'Blocs',
|
||||
'Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.': "Le presse-papiers est maintenant en mode \"texte plein\". Les contenus seront coll\u00e9s sans retenir les formatages jusqu'\u00e0 ce que vous d\u00e9sactiviez cette option.",
|
||||
Fonts: 'Polices',
|
||||
'Font Sizes': 'Tailles de police',
|
||||
Class: 'Classe',
|
||||
'Browse for an image': 'Rechercher une image',
|
||||
OR: 'OU',
|
||||
'Drop an image here': 'D\u00e9poser une image ici',
|
||||
Upload: 'T\u00e9l\u00e9charger',
|
||||
Block: 'Bloc',
|
||||
Align: 'Aligner',
|
||||
Default: 'Par d\u00e9faut',
|
||||
Circle: 'Cercle',
|
||||
Disc: 'Disque',
|
||||
Square: 'Carr\u00e9',
|
||||
'Lower Alpha': 'Alpha minuscule',
|
||||
'Lower Greek': 'Grec minuscule',
|
||||
'Lower Roman': 'Romain minuscule',
|
||||
'Upper Alpha': 'Alpha majuscule',
|
||||
'Upper Roman': 'Romain majuscule',
|
||||
'Anchor...': 'Ancre...',
|
||||
Name: 'Nom',
|
||||
Id: 'Id',
|
||||
'Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.': "L'Id doit commencer par une lettre suivi par des lettres, nombres, tirets, points, deux-points ou underscores",
|
||||
'You have unsaved changes are you sure you want to navigate away?': 'Vous avez des modifications non enregistr\u00e9es, \u00eates-vous s\u00fbr de quitter la page?',
|
||||
'Restore last draft': 'Restaurer le dernier brouillon',
|
||||
'Special character...': 'Caract\u00e8re sp\u00e9cial...',
|
||||
'Source code': 'Code source',
|
||||
'Insert\/Edit code sample': 'Ins\u00e9rer \/ modifier une exemple de code',
|
||||
Language: 'Langue',
|
||||
'Code sample...': 'Exemple de code...',
|
||||
'Color Picker': 'S\u00e9lecteur de couleurs',
|
||||
R: 'R',
|
||||
G: 'V',
|
||||
B: 'B',
|
||||
'Left to right': 'Gauche \u00e0 droite',
|
||||
'Right to left': 'Droite \u00e0 gauche',
|
||||
Emoticons: 'Emotic\u00f4nes',
|
||||
'Emoticons...': '\u00c9motic\u00f4nes...',
|
||||
'Metadata and Document Properties': 'M\u00e9tadonn\u00e9es et propri\u00e9t\u00e9s du document',
|
||||
Title: 'Titre',
|
||||
Keywords: 'Mots-cl\u00e9s',
|
||||
Description: 'Description',
|
||||
Robots: 'Robots',
|
||||
Author: 'Auteur',
|
||||
Encoding: 'Encodage',
|
||||
Fullscreen: 'Plein \u00e9cran',
|
||||
Action: 'Action',
|
||||
Shortcut: 'Raccourci',
|
||||
Help: 'Aide',
|
||||
Address: 'Adresse',
|
||||
'Focus to menubar': 'Cibler la barre de menu',
|
||||
'Focus to toolbar': "Cibler la barre d'outils",
|
||||
'Focus to element path': "Cibler le chemin vers l'\u00e9l\u00e9ment",
|
||||
'Focus to contextual toolbar': "Cibler la barre d'outils contextuelle",
|
||||
'Insert link (if link plugin activated)': 'Ins\u00e9rer un lien (si le module link est activ\u00e9)',
|
||||
'Save (if save plugin activated)': 'Enregistrer (si le module save est activ\u00e9)',
|
||||
'Find (if searchreplace plugin activated)': 'Rechercher (si le module searchreplace est activ\u00e9)',
|
||||
'Plugins installed ({0}):': 'Modules install\u00e9s ({0}) : ',
|
||||
'Premium plugins:': 'Modules premium :',
|
||||
'Learn more...': 'En savoir plus...',
|
||||
'You are using {0}': 'Vous utilisez {0}',
|
||||
Plugins: 'Plugins',
|
||||
'Handy Shortcuts': 'Raccourcis utiles',
|
||||
'Horizontal line': 'Ligne horizontale',
|
||||
'Insert\/edit image': 'Ins\u00e9rer\/modifier une image',
|
||||
'Alternative description': 'Description alternative',
|
||||
Accessibility: 'Accessibilit\u00e9',
|
||||
'Image is decorative': "L'image est d\u00e9corative",
|
||||
Source: 'Source',
|
||||
Dimensions: 'Dimensions',
|
||||
'Constrain proportions': 'Conserver les proportions',
|
||||
General: 'G\u00e9n\u00e9ral',
|
||||
Advanced: 'Avanc\u00e9',
|
||||
Style: 'Style',
|
||||
'Vertical space': 'Espacement vertical',
|
||||
'Horizontal space': 'Espacement horizontal',
|
||||
Border: 'Bordure',
|
||||
'Insert image': 'Ins\u00e9rer une image',
|
||||
'Image...': 'Image...',
|
||||
'Image list': "Liste d'images",
|
||||
'Rotate counterclockwise': 'Rotation anti-horaire',
|
||||
'Rotate clockwise': 'Rotation horaire',
|
||||
'Flip vertically': 'Retournement vertical',
|
||||
'Flip horizontally': 'Retournement horizontal',
|
||||
'Edit image': "Modifier l'image",
|
||||
'Image options': "Options de l'image",
|
||||
'Zoom in': 'Zoomer',
|
||||
'Zoom out': 'D\u00e9zoomer',
|
||||
Crop: 'Rogner',
|
||||
Resize: 'Redimensionner',
|
||||
Orientation: 'Orientation',
|
||||
Brightness: 'Luminosit\u00e9',
|
||||
Sharpen: 'Affiner',
|
||||
Contrast: 'Contraste',
|
||||
'Color levels': 'Niveaux de couleur',
|
||||
Gamma: 'Gamma',
|
||||
Invert: 'Inverser',
|
||||
Apply: 'Appliquer',
|
||||
Back: 'Retour',
|
||||
'Insert date\/time': 'Ins\u00e9rer date\/heure',
|
||||
'Date\/time': 'Date\/heure',
|
||||
'Insert\/edit link': 'Ins\u00e9rer\/modifier un lien',
|
||||
'Text to display': 'Texte \u00e0 afficher',
|
||||
Url: 'Url',
|
||||
'Open link in...': 'Ouvrir le lien dans...',
|
||||
'Current window': 'Fen\u00eatre active',
|
||||
None: 'n\/a',
|
||||
'New window': 'Nouvelle fen\u00eatre',
|
||||
'Open link': 'Ouvrir le lien',
|
||||
'Remove link': 'Enlever le lien',
|
||||
Anchors: 'Ancres',
|
||||
'Link...': 'Lien...',
|
||||
'Paste or type a link': 'Coller ou taper un lien',
|
||||
'The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?': "L'URL que vous avez entr\u00e9e semble \u00eatre une adresse e-mail. Voulez-vous ajouter le pr\u00e9fixe mailto: n\u00e9cessaire?",
|
||||
'The URL you entered seems to be an external link. Do you want to add the required http:\/\/ prefix?': "L'URL que vous avez entr\u00e9e semble \u00eatre un lien externe. Voulez-vous ajouter le pr\u00e9fixe http:\/\/ n\u00e9cessaire?",
|
||||
'The URL you entered seems to be an external link. Do you want to add the required https:\/\/ prefix?': "L'URL que vous avez saisie semble \u00eatre un lien externe. Voulez-vous ajouter le pr\u00e9fixe https:\/\/ requis\u00a0?",
|
||||
'Link list': 'Liste de liens',
|
||||
'Insert video': 'Ins\u00e9rer une vid\u00e9o',
|
||||
'Insert\/edit video': 'Ins\u00e9rer\/modifier une vid\u00e9o',
|
||||
'Insert\/edit media': 'Ins\u00e9rer\/modifier un m\u00e9dia',
|
||||
'Alternative source': 'Source alternative',
|
||||
'Alternative source URL': 'URL de la source alternative',
|
||||
'Media poster (Image URL)': "Affiche de m\u00e9dia (URL de l'image)",
|
||||
'Paste your embed code below:': "Collez votre code d'int\u00e9gration ci-dessous :",
|
||||
Embed: 'Int\u00e9grer',
|
||||
'Media...': 'M\u00e9dia...',
|
||||
'Nonbreaking space': 'Espace ins\u00e9cable',
|
||||
'Page break': 'Saut de page',
|
||||
'Paste as text': 'Coller comme texte',
|
||||
Preview: 'Pr\u00e9visualiser',
|
||||
'Print...': 'Imprimer...',
|
||||
Save: 'Enregistrer',
|
||||
Find: 'Chercher',
|
||||
'Replace with': 'Remplacer par',
|
||||
Replace: 'Remplacer',
|
||||
'Replace all': 'Tout remplacer',
|
||||
Previous: 'Pr\u00e9c\u00e9dente',
|
||||
Next: 'Suiv',
|
||||
'Find and Replace': 'Trouver et remplacer',
|
||||
'Find and replace...': 'Trouver et remplacer...',
|
||||
'Could not find the specified string.': 'Impossible de trouver la cha\u00eene sp\u00e9cifi\u00e9e.',
|
||||
'Match case': 'Respecter la casse',
|
||||
'Find whole words only': 'Mot entier',
|
||||
'Find in selection': 'Trouver dans la s\u00e9lection',
|
||||
Spellcheck: 'V\u00e9rification orthographique',
|
||||
'Spellcheck Language': 'Langue du correcteur orthographique',
|
||||
'No misspellings found.': "Aucune faute d'orthographe trouv\u00e9e.",
|
||||
Ignore: 'Ignorer',
|
||||
'Ignore all': 'Tout ignorer',
|
||||
Finish: 'Finie',
|
||||
'Add to Dictionary': 'Ajouter au dictionnaire',
|
||||
'Insert table': 'Ins\u00e9rer un tableau',
|
||||
'Table properties': 'Propri\u00e9t\u00e9s du tableau',
|
||||
'Delete table': 'Supprimer le tableau',
|
||||
Cell: 'Cellule',
|
||||
Row: 'Ligne',
|
||||
Column: 'Colonne',
|
||||
'Cell properties': 'Propri\u00e9t\u00e9s de la cellule',
|
||||
'Merge cells': 'Fusionner les cellules',
|
||||
'Split cell': 'Diviser la cellule',
|
||||
'Insert row before': 'Ins\u00e9rer une ligne avant',
|
||||
'Insert row after': 'Ins\u00e9rer une ligne apr\u00e8s',
|
||||
'Delete row': 'Effacer la ligne',
|
||||
'Row properties': 'Propri\u00e9t\u00e9s de la ligne',
|
||||
'Cut row': 'Couper la ligne',
|
||||
'Copy row': 'Copier la ligne',
|
||||
'Paste row before': 'Coller la ligne avant',
|
||||
'Paste row after': 'Coller la ligne apr\u00e8s',
|
||||
'Insert column before': 'Ins\u00e9rer une colonne avant',
|
||||
'Insert column after': 'Ins\u00e9rer une colonne apr\u00e8s',
|
||||
'Delete column': 'Effacer la colonne',
|
||||
Cols: 'Colonnes',
|
||||
Rows: 'Lignes',
|
||||
Width: 'Largeur',
|
||||
Height: 'Hauteur',
|
||||
'Cell spacing': 'Espacement inter-cellulles',
|
||||
'Cell padding': 'Espacement interne cellule',
|
||||
Caption: 'Titre',
|
||||
'Show caption': 'Afficher le sous-titrage',
|
||||
Left: 'Gauche',
|
||||
Center: 'Centr\u00e9',
|
||||
Right: 'Droite',
|
||||
'Cell type': 'Type de cellule',
|
||||
Scope: 'Etendue',
|
||||
Alignment: 'Alignement',
|
||||
'H Align': 'Alignement H',
|
||||
'V Align': 'Alignement V',
|
||||
Top: 'Haut',
|
||||
Middle: 'Milieu',
|
||||
Bottom: 'Bas',
|
||||
'Header cell': "Cellule d'en-t\u00eate",
|
||||
'Row group': 'Groupe de lignes',
|
||||
'Column group': 'Groupe de colonnes',
|
||||
'Row type': 'Type de ligne',
|
||||
Header: 'En-t\u00eate',
|
||||
Body: 'Corps',
|
||||
Footer: 'Pied',
|
||||
'Border color': 'Couleur de la bordure',
|
||||
'Insert template...': 'Ins\u00e9rer un mod\u00e8le...',
|
||||
Templates: 'Th\u00e8mes',
|
||||
Template: 'Mod\u00e8le',
|
||||
'Text color': 'Couleur du texte',
|
||||
'Background color': "Couleur d'arri\u00e8re-plan",
|
||||
'Custom...': 'Personnalis\u00e9...',
|
||||
'Custom color': 'Couleur personnalis\u00e9e',
|
||||
'No color': 'Aucune couleur',
|
||||
'Remove color': 'Supprimer la couleur',
|
||||
'Table of Contents': 'Table des mati\u00e8res',
|
||||
'Show blocks': 'Afficher les blocs',
|
||||
'Show invisible characters': 'Afficher les caract\u00e8res invisibles',
|
||||
'Word count': 'Nombre de mots',
|
||||
Count: 'Total',
|
||||
Document: 'Document',
|
||||
Selection: 'S\u00e9lection',
|
||||
Words: 'Mots',
|
||||
'Words: {0}': 'Mots : {0}',
|
||||
'{0} words': '{0} mots',
|
||||
File: 'Fichier',
|
||||
Edit: 'Editer',
|
||||
Insert: 'Ins\u00e9rer',
|
||||
View: 'Voir',
|
||||
Format: 'Format',
|
||||
Table: 'Tableau',
|
||||
Tools: 'Outils',
|
||||
'Powered by {0}': 'Propuls\u00e9 par {0}',
|
||||
'Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help': "Zone Texte Riche. Appuyer sur ALT-F9 pour le menu. Appuyer sur ALT-F10 pour la barre d'outils. Appuyer sur ALT-0 pour de l'aide.",
|
||||
'Image title': "Titre d'image",
|
||||
'Border width': '\u00c9paisseur de la bordure',
|
||||
'Border style': 'Style de la bordure',
|
||||
Error: 'Erreur',
|
||||
Warn: 'Avertir',
|
||||
Valid: 'Valide',
|
||||
'To open the popup, press Shift+Enter': 'Pour ouvrir la popup, appuyez sur Maj+Entr\u00e9e',
|
||||
'Rich Text Area. Press ALT-0 for help.': "Zone de texte riche. Appuyez sur ALT-0 pour l'aide.",
|
||||
'System Font': 'Police syst\u00e8me',
|
||||
'Failed to upload image: {0}': "\u00c9chec d'envoi de l'image\u00a0: {0}",
|
||||
'Failed to load plugin: {0} from url {1}': '\u00c9chec de chargement du plug-in\u00a0: {0} \u00e0 partir de l\u2019URL {1}',
|
||||
'Failed to load plugin url: {0}': "\u00c9chec de chargement de l'URL du plug-in\u00a0: {0}",
|
||||
'Failed to initialize plugin: {0}': "\u00c9chec d'initialisation du plug-in\u00a0: {0}",
|
||||
example: 'exemple',
|
||||
Search: 'Rechercher',
|
||||
All: 'Tout',
|
||||
Currency: 'Devise',
|
||||
Text: 'Texte',
|
||||
Quotations: 'Citations',
|
||||
Mathematical: 'Op\u00e9rateurs math\u00e9matiques',
|
||||
'Extended Latin': 'Latin \u00e9tendu',
|
||||
Symbols: 'Symboles',
|
||||
Arrows: 'Fl\u00e8ches',
|
||||
'User Defined': "D\u00e9fini par l'utilisateur",
|
||||
'dollar sign': 'Symbole dollar',
|
||||
'currency sign': 'Symbole devise',
|
||||
'euro-currency sign': 'Symbole euro',
|
||||
'colon sign': 'Symbole col\u00f3n',
|
||||
'cruzeiro sign': 'Symbole cruzeiro',
|
||||
'french franc sign': 'Symbole franc fran\u00e7ais',
|
||||
'lira sign': 'Symbole lire',
|
||||
'mill sign': 'Symbole milli\u00e8me',
|
||||
'naira sign': 'Symbole naira',
|
||||
'peseta sign': 'Symbole peseta',
|
||||
'rupee sign': 'Symbole roupie',
|
||||
'won sign': 'Symbole won',
|
||||
'new sheqel sign': 'Symbole nouveau ch\u00e9kel',
|
||||
'dong sign': 'Symbole dong',
|
||||
'kip sign': 'Symbole kip',
|
||||
'tugrik sign': 'Symbole tougrik',
|
||||
'drachma sign': 'Symbole drachme',
|
||||
'german penny symbol': 'Symbole pfennig',
|
||||
'peso sign': 'Symbole peso',
|
||||
'guarani sign': 'Symbole guarani',
|
||||
'austral sign': 'Symbole austral',
|
||||
'hryvnia sign': 'Symbole hryvnia',
|
||||
'cedi sign': 'Symbole cedi',
|
||||
'livre tournois sign': 'Symbole livre tournois',
|
||||
'spesmilo sign': 'Symbole spesmilo',
|
||||
'tenge sign': 'Symbole tenge',
|
||||
'indian rupee sign': 'Symbole roupie indienne',
|
||||
'turkish lira sign': 'Symbole lire turque',
|
||||
'nordic mark sign': 'Symbole du mark nordique',
|
||||
'manat sign': 'Symbole manat',
|
||||
'ruble sign': 'Symbole rouble',
|
||||
'yen character': 'Sinogramme Yen',
|
||||
'yuan character': 'Sinogramme Yuan',
|
||||
'yuan character, in hong kong and taiwan': 'Sinogramme Yuan, Hong Kong et Taiwan',
|
||||
'yen\/yuan character variant one': 'Sinogramme Yen\/Yuan, premi\u00e8re variante',
|
||||
'Loading emoticons...': 'Chargement des \u00e9motic\u00f4nes en cours...',
|
||||
'Could not load emoticons': '\u00c9chec de chargement des \u00e9motic\u00f4nes',
|
||||
People: 'Personnes',
|
||||
'Animals and Nature': 'Animaux & nature',
|
||||
'Food and Drink': 'Nourriture & boissons',
|
||||
Activity: 'Activit\u00e9',
|
||||
'Travel and Places': 'Voyages & lieux',
|
||||
Objects: 'Objets',
|
||||
Flags: 'Drapeaux',
|
||||
Characters: 'Caract\u00e8res',
|
||||
'Characters (no spaces)': 'Caract\u00e8res (espaces non compris)',
|
||||
'{0} characters': '{0}\u00a0caract\u00e8res',
|
||||
'Error: Form submit field collision.': 'Erreur\u00a0: conflit de champs lors de la soumission du formulaire.',
|
||||
'Error: No form element found.': 'Erreur : aucun \u00e9l\u00e9ment de formulaire trouv\u00e9.',
|
||||
Update: 'Mettre \u00e0 jour',
|
||||
'Color swatch': '\u00c9chantillon de couleurs',
|
||||
Turquoise: 'Turquoise',
|
||||
Green: 'Vert',
|
||||
Blue: 'Bleu',
|
||||
Purple: 'Violet',
|
||||
'Navy Blue': 'Bleu marine',
|
||||
'Dark Turquoise': 'Turquoise fonc\u00e9',
|
||||
'Dark Green': 'Vert fonc\u00e9',
|
||||
'Medium Blue': 'Bleu moyen',
|
||||
'Medium Purple': 'Violet moyen',
|
||||
'Midnight Blue': 'Bleu de minuit',
|
||||
Yellow: 'Jaune',
|
||||
Orange: 'Orange',
|
||||
Red: 'Rouge',
|
||||
'Light Gray': 'Gris clair',
|
||||
Gray: 'Gris',
|
||||
'Dark Yellow': 'Jaune fonc\u00e9',
|
||||
'Dark Orange': 'Orange fonc\u00e9',
|
||||
'Dark Red': 'Rouge fonc\u00e9',
|
||||
'Medium Gray': 'Gris moyen',
|
||||
'Dark Gray': 'Gris fonc\u00e9',
|
||||
'Light Green': 'Vert clair',
|
||||
'Light Yellow': 'Jaune clair',
|
||||
'Light Red': 'Rouge clair',
|
||||
'Light Purple': 'Violet clair',
|
||||
'Light Blue': 'Bleu clair',
|
||||
'Dark Purple': 'Violet fonc\u00e9',
|
||||
'Dark Blue': 'Bleu fonc\u00e9',
|
||||
Black: 'Noir',
|
||||
White: 'Blanc',
|
||||
'Switch to or from fullscreen mode': 'Passer en ou quitter le mode plein \u00e9cran',
|
||||
'Open help dialog': "Ouvrir la bo\u00eete de dialogue d'aide",
|
||||
history: 'historique',
|
||||
styles: 'styles',
|
||||
formatting: 'mise en forme',
|
||||
alignment: 'alignement',
|
||||
indentation: 'retrait',
|
||||
Font: 'Police',
|
||||
Size: 'Taille',
|
||||
'More...': 'Plus...',
|
||||
'Select...': 'S\u00e9lectionner...',
|
||||
Preferences: 'Pr\u00e9f\u00e9rences',
|
||||
Yes: 'Oui',
|
||||
No: 'Non',
|
||||
'Keyboard Navigation': 'Navigation au clavier',
|
||||
Version: 'Version',
|
||||
'Code view': 'Affichage du code',
|
||||
'Open popup menu for split buttons': 'Ouvrir le menu contextuel pour les boutons partag\u00e9s',
|
||||
'List Properties': 'Propri\u00e9t\u00e9s de la liste',
|
||||
'List properties...': 'Lister les propri\u00e9t\u00e9s...',
|
||||
'Start list at number': 'Liste de d\u00e9part au num\u00e9ro',
|
||||
'Line height': 'Hauteur de la ligne',
|
||||
comments: 'commentaires',
|
||||
'Format Painter': 'Reproduire la mise en forme',
|
||||
'Insert\/edit iframe': 'Ins\u00e9rer\/modifier iframe',
|
||||
Capitalization: 'Mise en majuscules',
|
||||
lowercase: 'minuscule',
|
||||
UPPERCASE: 'MAJUSCULE',
|
||||
'Title Case': 'Casse du titre',
|
||||
'permanent pen': 'feutre ind\u00e9l\u00e9bile',
|
||||
'Permanent Pen Properties': 'Propri\u00e9t\u00e9s du feutre ind\u00e9l\u00e9bile',
|
||||
'Permanent pen properties...': 'Propri\u00e9t\u00e9s du feutre ind\u00e9l\u00e9bile...',
|
||||
'case change': 'changement de cas',
|
||||
'page embed': 'int\u00e9gration de page',
|
||||
'Advanced sort...': 'Tri avanc\u00e9...',
|
||||
'Advanced Sort': 'Tri avanc\u00e9',
|
||||
'Sort table by column ascending': 'Trier le tableau par colonne ascendante',
|
||||
'Sort table by column descending': 'Trier le tableau par colonne en ordre d\u00e9croissant',
|
||||
Sort: 'Sorte',
|
||||
Order: 'Ordre',
|
||||
'Sort by': 'Trier par',
|
||||
Ascending: 'Ascendant',
|
||||
Descending: 'Descendant',
|
||||
'Column {0}': 'Colonne {0}',
|
||||
'Row {0}': 'Ligne {0}',
|
||||
'Spellcheck...': 'V\u00e9rification orthographique...',
|
||||
'Misspelled word': 'Mot mal orthographi\u00e9',
|
||||
Suggestions: 'Suggestions',
|
||||
Change: 'Changement',
|
||||
'Finding word suggestions': 'Trouver des suggestions de mots',
|
||||
Success: 'Succ\u00e8s',
|
||||
Repair: 'R\u00e9paration',
|
||||
'Issue {0} of {1}': ' {0} Erreur sur {1}',
|
||||
'Images must be marked as decorative or have an alternative text description': 'Les images doivent \u00eatre marqu\u00e9es comme d\u00e9coratives ou avoir une description textuelle alternative',
|
||||
'Images must have an alternative text description. Decorative images are not allowed.': 'Les images doivent avoir une description textuelle alternative. Les images d\u00e9coratives ne sont pas autoris\u00e9es.',
|
||||
'Or provide alternative text:': 'Ou fournissez un texte alternatif\u00a0:',
|
||||
'Make image decorative:': "Rendre l'image d\u00e9corative\u00a0:",
|
||||
'ID attribute must be unique': "L'attribut ID doit \u00eatre unique",
|
||||
'Make ID unique': "Rendre l'identifiant unique",
|
||||
'Keep this ID and remove all others': 'Conservez cet identifiant et supprimez tous les autres',
|
||||
'Remove this ID': 'Supprimer cet identifiant',
|
||||
'Remove all IDs': 'Supprimer tous les identifiants',
|
||||
Checklist: 'Liste de contr\u00f4le',
|
||||
Anchor: 'Ancre',
|
||||
'Special character': 'Caract\u00e8res sp\u00e9ciaux',
|
||||
'Code sample': 'Extrait de code',
|
||||
Color: 'Couleur',
|
||||
'Document properties': 'Propri\u00e9t\u00e9 du document',
|
||||
'Image description': "Description de l'image",
|
||||
Image: 'Image',
|
||||
'Insert link': 'Ins\u00e9rer un lien',
|
||||
Target: 'Cible',
|
||||
Link: 'Lien',
|
||||
Poster: 'Publier',
|
||||
Media: 'M\u00e9dia',
|
||||
Print: 'Imprimer',
|
||||
Prev: 'Pr\u00e9c ',
|
||||
'Find and replace': 'Trouver et remplacer',
|
||||
'Whole words': 'Mots entiers',
|
||||
'Insert template': 'Ajouter un th\u00e8me'
|
||||
})
|
||||
}
|
||||
|
||||
const buildConfiguration = (conf) => {
|
||||
const result = Object.assign({}, window.tinymce.murph.configurationBase, conf)
|
||||
|
||||
if (window.tinymce.language && !result.language) {
|
||||
result.language = window.tinymce.language
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const makeId = () => {
|
||||
let result = ''
|
||||
const characters = 'abcdefghijklmnopqrstuvwxyz0123456789'
|
||||
const charactersLength = characters.length
|
||||
|
||||
for (let i = 0; i < 20; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * charactersLength))
|
||||
}
|
||||
|
||||
return 'tinymce-' + result
|
||||
}
|
||||
|
||||
const doInitEditor = () => {
|
||||
$(window.tinymce.murph.selector).each((i, v) => {
|
||||
const element = $(v)
|
||||
let id = null
|
||||
|
||||
if (element.attr('id')) {
|
||||
id = element.attr('id')
|
||||
} else {
|
||||
id = makeId()
|
||||
element.attr('id', makeId)
|
||||
}
|
||||
|
||||
let mode = element.attr('data-tinymce')
|
||||
|
||||
if (!mode) {
|
||||
mode = 'default'
|
||||
}
|
||||
|
||||
if (!Object.prototype.hasOwnProperty.call(window.tinymce.murph.modes, mode)) {
|
||||
return
|
||||
}
|
||||
|
||||
const conf = buildConfiguration(window.tinymce.murph.modes[mode])
|
||||
conf.mode = 'exact'
|
||||
conf.elements = id
|
||||
|
||||
window.tinymce.init(conf)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = function () {
|
||||
if (typeof tinymce === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(doInitEditor)
|
||||
const config = { attributes: false, childList: true, subtree: true }
|
||||
observer.observe(document.querySelector('body'), config)
|
||||
|
||||
$(() => {
|
||||
doInitEditor()
|
||||
})
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
const Vue = require('vue').default
|
||||
|
||||
const FileManager = require('../components/file-manager/FileManager').default
|
||||
|
||||
module.exports = () => {
|
||||
if (!document.getElementById('file-manager')) {
|
||||
return
|
||||
}
|
||||
|
||||
return new Vue({
|
||||
el: '#file-manager',
|
||||
template: '<FileManager context="crud" />',
|
||||
components: {
|
||||
FileManager
|
||||
}
|
||||
})
|
||||
}
|
|
@ -1,82 +0,0 @@
|
|||
const $ = require('jquery')
|
||||
const Vue = require('vue').default
|
||||
const FileManager = require('../components/file-manager/FileManager').default
|
||||
|
||||
const createModal = function () {
|
||||
let container = $('#fm-modal')
|
||||
const body = $('body')
|
||||
|
||||
if (!container.length) {
|
||||
container = $('<div id="fm-modal" class="modal">')
|
||||
|
||||
body.append(container)
|
||||
}
|
||||
|
||||
container.html(`
|
||||
<div class="modal-dialog modal-dialog-large">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body">
|
||||
<div id="fm-modal-content">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`)
|
||||
|
||||
$(container).modal('show')
|
||||
|
||||
return $(container)
|
||||
}
|
||||
|
||||
const fileManagerBrowser = function (callback) {
|
||||
const container = createModal()
|
||||
|
||||
const clickCallback = (e) => {
|
||||
callback($(e.target).attr('data-value'), {})
|
||||
$('#modal-container').modal('hide')
|
||||
container.modal('hide')
|
||||
|
||||
$('body').off('click', '#file-manager-insert', clickCallback)
|
||||
}
|
||||
|
||||
$('body').on('click', '#file-manager-insert', clickCallback)
|
||||
|
||||
return new Vue({
|
||||
el: '#fm-modal-content',
|
||||
template: '<FileManager context="tinymce" />',
|
||||
components: {
|
||||
FileManager
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = function () {
|
||||
$('body').on('click', '.form-filepicker-picker', (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
const picker = $(e.target)
|
||||
const id = '#' + picker.attr('data-target')
|
||||
const input = $(id)
|
||||
|
||||
fileManagerBrowser((value) => {
|
||||
value = value.replace(/^\//, '')
|
||||
|
||||
picker.parents('.form-filepicker-container').find('input.form-filepicker-picker').val(value)
|
||||
input.val(value)
|
||||
})
|
||||
})
|
||||
|
||||
$('body').on('click', '.form-filepicker-reset', (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
const button = $(e.target)
|
||||
const id = '#' + button.attr('data-target')
|
||||
const input = $(id)
|
||||
|
||||
input.val('')
|
||||
button.parents('.form-filepicker-container').find('input.form-filepicker-picker').val('')
|
||||
})
|
||||
}
|
|
@ -1,79 +0,0 @@
|
|||
const $ = require('jquery')
|
||||
|
||||
module.exports = function () {
|
||||
$('body').on('submit', '*[data-form-ajax]', function (e) {
|
||||
e.preventDefault()
|
||||
|
||||
const target = e.target
|
||||
const form = $(target)
|
||||
const data = new FormData(target)
|
||||
const method = form.attr('method')
|
||||
const files = form.find('input[type=file]')
|
||||
|
||||
files.each((i, v) => {
|
||||
data.append(v.name, v)
|
||||
})
|
||||
|
||||
const options = {
|
||||
url: form.attr('action'),
|
||||
data: data,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
type: method || 'GET',
|
||||
success: function (data) {
|
||||
if (Object.prototype.hasOwnProperty.call(data, '_dispatch')) {
|
||||
$('body').trigger(data._dispatch)
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(data, '_message') && Object.prototype.hasOwnProperty.call(data, '_level')) {
|
||||
const message = data._message
|
||||
const level = data._level
|
||||
const titles = {
|
||||
notice: 'Information',
|
||||
info: 'Information',
|
||||
success: 'Success',
|
||||
warning: 'Warning',
|
||||
danger: 'Danger',
|
||||
error: 'Error'
|
||||
}
|
||||
const borders = {
|
||||
notice: '',
|
||||
info: 'border border-primary',
|
||||
success: 'border border-success',
|
||||
warning: 'border border-warning',
|
||||
danger: 'border border-danger',
|
||||
error: 'border border-danger'
|
||||
}
|
||||
const colors = {
|
||||
info: 'text-body',
|
||||
notice: 'text-body',
|
||||
success: 'text-success font-weight-bold',
|
||||
warning: 'text-warning font-weight-bold',
|
||||
danger: 'text-danger font-weight-bold',
|
||||
error: 'text-danger font-weight-bold'
|
||||
}
|
||||
|
||||
$('#toast-wrapper-main').append(
|
||||
`
|
||||
<div class="toast ${borders[level]}" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="toast-header">
|
||||
<strong class="mr-auto">${titles[level]}</strong>
|
||||
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="toast-body text-${colors[level]}">
|
||||
${message}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
|
||||
$('.toast').last().toast('show')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$.ajax(options)
|
||||
})
|
||||
}
|
|
@ -1,84 +0,0 @@
|
|||
const $ = require('jquery')
|
||||
|
||||
const DeleteHandler = (e) => {
|
||||
e.stopPropagation()
|
||||
const target = e.target
|
||||
let button = $(target)
|
||||
|
||||
if (button.is('[data-collection-delete-container]')) {
|
||||
button = button.find('*[data-collection-delete]').first()
|
||||
}
|
||||
|
||||
const id = button.attr('data-collection-delete')
|
||||
const collection = button.parents('[data-collection]')
|
||||
const item = collection.find('*[data-collection-item="' + id + '"]')
|
||||
|
||||
if (confirm('Validez-vous la suppression ?')) {
|
||||
item.remove()
|
||||
collection.trigger('collection.update')
|
||||
}
|
||||
}
|
||||
|
||||
const CollectionInitilizedAndUpdated = (e) => {
|
||||
const target = $(e.target)
|
||||
|
||||
target.find('*[data-collection-empty]').toggleClass(
|
||||
'd-none',
|
||||
target.find('*[data-collection-item]').length !== 0
|
||||
)
|
||||
|
||||
target.find('*[data-collection-nonempty]').toggleClass(
|
||||
'd-none',
|
||||
target.find('*[data-collection-item]').length === 0
|
||||
)
|
||||
}
|
||||
|
||||
const FormCollection = () => {
|
||||
$('*[data-collection]').on(
|
||||
'collection.update',
|
||||
CollectionInitilizedAndUpdated
|
||||
)
|
||||
|
||||
$('*[data-collection]').on(
|
||||
'collection.init',
|
||||
CollectionInitilizedAndUpdated
|
||||
)
|
||||
|
||||
$('body').on(
|
||||
'click',
|
||||
'*[data-collection-delete], *[data-collection-delete-container]',
|
||||
DeleteHandler
|
||||
)
|
||||
|
||||
$('body').on('click', '*[data-collection-add]', (e) => {
|
||||
e.stopPropagation()
|
||||
|
||||
const collectionId = $(e.target).attr('data-collection-add')
|
||||
const collectionContainer = $('*[data-collection="' + collectionId + '"]')
|
||||
const prototypeContent = $('#' + collectionId).html()
|
||||
let name = 0
|
||||
|
||||
collectionContainer.find('*[data-collection-item]').each(function () {
|
||||
const n = parseInt($(this).attr('data-collection-item'))
|
||||
|
||||
if (n >= name) {
|
||||
name = n + 1
|
||||
}
|
||||
})
|
||||
|
||||
collectionContainer.append(prototypeContent)
|
||||
|
||||
const item = collectionContainer.children('*[data-collection-item]:last-child')
|
||||
const deleteBtn = $('<span data-collection-delete="__name__" class="fa fa-trash"></span>')
|
||||
|
||||
item.find('*[data-collection-delete-container]').first().append(deleteBtn)
|
||||
item.html(item.html().replace(/__name__/g, name))
|
||||
item.attr('data-collection-item', name)
|
||||
|
||||
collectionContainer.trigger('collection.update')
|
||||
})
|
||||
|
||||
$('*[data-collection]').trigger('collection.init')
|
||||
}
|
||||
|
||||
module.exports = FormCollection
|
|
@ -1,15 +0,0 @@
|
|||
const $ = require('jquery')
|
||||
|
||||
module.exports = function () {
|
||||
$('body').on('submit', '*[data-form-confirm]', function (e) {
|
||||
let message = $(this).attr('data-form-confirm')
|
||||
|
||||
if (!message) {
|
||||
message = 'Confimez-vous cette action ?'
|
||||
}
|
||||
|
||||
if (!confirm(message)) {
|
||||
e.preventDefault()
|
||||
}
|
||||
})
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
const $ = require('jquery')
|
||||
|
||||
module.exports = function () {
|
||||
$('.nav a').each(function () {
|
||||
const link = $(this)
|
||||
const href = link.attr('href')
|
||||
|
||||
if (href.substr(0, 1) !== '#') {
|
||||
return
|
||||
}
|
||||
|
||||
const tab = $('.tab-pane ' + href)
|
||||
|
||||
if (!tab.length) {
|
||||
return
|
||||
}
|
||||
|
||||
if (tab.find('.form-error-message').length) {
|
||||
link.addClass('border border-danger')
|
||||
link.click()
|
||||
}
|
||||
})
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
const $ = require('jquery')
|
||||
|
||||
module.exports = function () {
|
||||
$('body').on('change', '.custom-file-input', function (event) {
|
||||
const inputFile = event.currentTarget
|
||||
|
||||
$(inputFile).parent()
|
||||
.find('.custom-file-label')
|
||||
.html(inputFile.files[0].name)
|
||||
})
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
const $ = require('jquery')
|
||||
|
||||
const openModal = function (url) {
|
||||
let container = $('#modal-container')
|
||||
const body = $('body')
|
||||
|
||||
if (!container.length) {
|
||||
container = $('<div id="modal-container" class="modal">')
|
||||
|
||||
body.append(container)
|
||||
}
|
||||
|
||||
const loader = $('<div style="position: absolute; top: 25vh; left: 50vw; z-index: 2000">')
|
||||
loader.html('<div class="spinner-border text-primary" role="status"><span class="sr-only">Loading...</span></div>')
|
||||
body.append(loader)
|
||||
|
||||
container.html('')
|
||||
|
||||
$(container).modal('show')
|
||||
|
||||
container.load(url, function () {
|
||||
loader.remove()
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = function () {
|
||||
let click = 0
|
||||
const body = $('body')
|
||||
|
||||
body.on('hidden.bs.modal', '.modal', (e) => {
|
||||
if ($('.modal.show').length) {
|
||||
$('body').addClass('modal-open')
|
||||
}
|
||||
})
|
||||
|
||||
body.on('click', '*[data-modal]', (e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
++click
|
||||
|
||||
window.setTimeout(() => {
|
||||
if (click !== 1) {
|
||||
click = 0
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
click = 0
|
||||
|
||||
let url = $(e.target).attr('data-modal')
|
||||
|
||||
if (!url) {
|
||||
url = $(e.target).parents('*[data-modal]').first().attr('data-modal')
|
||||
}
|
||||
|
||||
openModal(url)
|
||||
}, 250)
|
||||
})
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const dataModal = urlParams.get('data-modal')
|
||||
|
||||
if (dataModal) {
|
||||
openModal(dataModal)
|
||||
}
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
const $ = require('jquery')
|
||||
|
||||
const Pannel = () => {
|
||||
const panels = $('.panel')
|
||||
|
||||
panels.each((i, p) => {
|
||||
const panel = $(p)
|
||||
const content = panel.find('.panel-content').first()
|
||||
const togglers = panel.find('.panel-toggler')
|
||||
|
||||
togglers.each((k, t) => {
|
||||
const toggler = $(t)
|
||||
|
||||
if (!toggler.is('.fa')) {
|
||||
return
|
||||
}
|
||||
|
||||
if (content.is('.active')) {
|
||||
toggler.removeClass('fa-arrow-down')
|
||||
toggler.addClass('fa-arrow-up')
|
||||
} else {
|
||||
toggler.removeClass('fa-arrow-up')
|
||||
toggler.addClass('fa-arrow-down')
|
||||
}
|
||||
})
|
||||
|
||||
togglers.click(function (e) {
|
||||
e.stopPropagation()
|
||||
|
||||
content.toggleClass('active')
|
||||
|
||||
togglers.each((k, t) => {
|
||||
const toggler = $(t)
|
||||
|
||||
if (!toggler.is('.fa')) {
|
||||
return
|
||||
}
|
||||
|
||||
toggler
|
||||
.toggleClass('fa-arrow-down')
|
||||
.toggleClass('fa-arrow-up')
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = Pannel
|
|
@ -1,82 +0,0 @@
|
|||
const $ = require('jquery')
|
||||
const zxcvbn = require('zxcvbn')
|
||||
|
||||
const scoreColors = [
|
||||
'danger',
|
||||
'danger',
|
||||
'warning',
|
||||
'warning',
|
||||
'success'
|
||||
]
|
||||
|
||||
const scoreInfos = {
|
||||
'This is a top-10 common password': 'Parmis le top 10 des mots de passes communs',
|
||||
'This is a top-100 common password': 'Parmis le top 100 des mots de passes communs',
|
||||
'This is a very common password': 'Mot de passe vraiment trop commun',
|
||||
'This is similar to a commonly used password': 'Similaire à un mot de passe commun',
|
||||
'A word by itself is easy to guess': 'Ce mot est trop simple à deviner',
|
||||
'Names and surnames by themselves are easy to guess': 'Les noms ou les surnoms sont simples à deviner',
|
||||
'Common names and surnames are easy to guess': 'Les noms ou les surnoms sont simples à deviner',
|
||||
'Straight rows of keys are easy to guess': 'Combinaison de touches trop simple',
|
||||
'Short keyboard patterns are easy to guess': 'Combinaison de touches trop simple',
|
||||
"Repeats like \"aaa\" are easy to guess'": 'Les répétitions comme "aaa" sont simples à deviner',
|
||||
'Repeats like "abcabcabc" are only slightly harder to guess than "abc"': 'Les répétitions comme "abcabcabc" sont simples à deviner',
|
||||
'Sequences like abc or 6543 are easy to guess': 'Les séquences comme "abc" ou "6543" sont simples à deviner',
|
||||
'Recent years are easy to guess': 'Les années sont simples à deviner',
|
||||
'Dates are often easy to guess': 'Les dates sont souvent simples à deviner'
|
||||
}
|
||||
|
||||
const checkPassword = function (password, confirmation, indicator, submit) {
|
||||
const result = zxcvbn(password.val())
|
||||
const score = result.score
|
||||
const cols = indicator.children('.col-sm')
|
||||
const info = indicator.children('.password-strenth-info')
|
||||
|
||||
info.text('')
|
||||
cols.attr('class', 'col-sm')
|
||||
|
||||
for (let u = 0; u <= 5; u++) {
|
||||
const col = cols.eq(u)
|
||||
if (u <= score) {
|
||||
col.addClass('bg-' + scoreColors[score])
|
||||
} else {
|
||||
col.addClass('bg-light')
|
||||
}
|
||||
}
|
||||
|
||||
console.log(result)
|
||||
|
||||
info.text(scoreInfos[result.feedback.warning])
|
||||
info.attr('class', 'col-12 password-strenth-info text-' + scoreColors[score])
|
||||
|
||||
if (score < 4 || confirmation.val() !== password.val()) {
|
||||
submit.attr('disabled', 'disabled')
|
||||
} else {
|
||||
submit.removeAttr('disabled')
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = function () {
|
||||
const passwordNew = $('#form-password-new')
|
||||
const passwordConfirmation = $('#form-password-confirmation')
|
||||
const passwordSubmit = $('#form-password-submit')
|
||||
const passwordStrength = $('#form-password-strength')
|
||||
|
||||
if (passwordStrength.length) {
|
||||
passwordNew.keyup(function () {
|
||||
checkPassword(passwordNew, passwordConfirmation, passwordStrength, passwordSubmit)
|
||||
})
|
||||
|
||||
passwordNew.change(function () {
|
||||
checkPassword(passwordNew, passwordConfirmation, passwordStrength, passwordSubmit)
|
||||
})
|
||||
|
||||
passwordConfirmation.keyup(function () {
|
||||
checkPassword(passwordNew, passwordConfirmation, passwordStrength, passwordSubmit)
|
||||
})
|
||||
|
||||
passwordConfirmation.change(function () {
|
||||
checkPassword(passwordNew, passwordConfirmation, passwordStrength, passwordSubmit)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
const $ = require('jquery')
|
||||
|
||||
module.exports = function () {
|
||||
$('*[data-pushstate]').click((e) => {
|
||||
const url = $(e.target).attr('data-pushstate')
|
||||
|
||||
history.pushState({ url: url }, null, url)
|
||||
history.replaceState({ url: url }, null, url)
|
||||
})
|
||||
|
||||
const forms = $('form[data-formpushstate]')
|
||||
|
||||
const checkAndUsePushState = () => {
|
||||
const state = [window.location.pathname, window.location.search].join('')
|
||||
|
||||
$('*[data-pushstate]').each((i, v) => {
|
||||
let method = 'compare'
|
||||
|
||||
if ($(v).is('[data-pushstate-method]')) {
|
||||
method = $(v).attr('data-pushstate-method')
|
||||
}
|
||||
|
||||
let isThisOne = false
|
||||
|
||||
if (method === 'compare' && $(v).attr('data-pushstate') === state) {
|
||||
isThisOne = true
|
||||
}
|
||||
|
||||
if (method === 'indexOf' && state.indexOf($(v).attr('data-pushstate')) !== -1) {
|
||||
isThisOne = true
|
||||
}
|
||||
|
||||
if (isThisOne) {
|
||||
$(v).click()
|
||||
|
||||
forms.attr('action', state)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
checkAndUsePushState()
|
||||
|
||||
$(window).on('statechange', checkAndUsePushState, false)
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
const $ = require('jquery')
|
||||
const Choices = require('choices.js')
|
||||
|
||||
module.exports = function () {
|
||||
$('*[data-rest-choices]').each(function (key, item) {
|
||||
const url = $(this).attr('data-rest-choices')
|
||||
|
||||
new Choices(item, {
|
||||
searchPlaceholderValue: 'Chercher'
|
||||
}).setChoices(function () {
|
||||
return fetch(url)
|
||||
.then(function (response) {
|
||||
return response.json()
|
||||
})
|
||||
.then(function (data) {
|
||||
return data.map(function (d) {
|
||||
return {
|
||||
label: d.label,
|
||||
value: d.value
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
.then(function (instance) {
|
||||
})
|
||||
})
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
const $ = require('jquery')
|
||||
const Sortable = require('sortablejs').Sortable
|
||||
|
||||
module.exports = () => {
|
||||
$('*[data-sortable]').each((i, list) => {
|
||||
const element = $(list)
|
||||
const route = element.attr('data-sortable-route')
|
||||
|
||||
return new Sortable(list, {
|
||||
handle: '*[data-sortable-item]',
|
||||
sort: true,
|
||||
animation: 150,
|
||||
fallbackTolerance: 3,
|
||||
onEnd: (e) => {
|
||||
if (!route) {
|
||||
return
|
||||
}
|
||||
|
||||
const items = element.find('*[data-sortable-item]')
|
||||
const datas = { items: [] }
|
||||
|
||||
items.each((order, v) => {
|
||||
datas.items[$(v).attr('data-sortable-item')] = order + 1
|
||||
})
|
||||
|
||||
$.post(route, datas)
|
||||
.always((data) => {
|
||||
document.location.reload()
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
const $ = require('jquery')
|
||||
|
||||
const resizeTbody = (tbody) => {
|
||||
tbody.height($(window).height() - tbody.offset().top - 20)
|
||||
}
|
||||
|
||||
const tableFixed = () => {
|
||||
const tables = $('table[data-table-fixed], *[data-table-fixed] > table')
|
||||
|
||||
tables.each((i, t) => {
|
||||
const table = $(t)
|
||||
table.addClass('table-fixed')
|
||||
|
||||
const tbody = table.find('tbody')
|
||||
|
||||
resizeTbody(tbody)
|
||||
|
||||
$(window).resize(function () {
|
||||
resizeTbody(tbody)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = tableFixed
|
|
@ -1,121 +0,0 @@
|
|||
const $ = require('jquery')
|
||||
|
||||
const selectedClass = 'table-primary-light'
|
||||
|
||||
const toggleRow = (row, checkbox, checkboxIsClicked) => {
|
||||
row.toggleClass(selectedClass)
|
||||
|
||||
if (checkboxIsClicked) {
|
||||
checkbox.prop('checked', checkbox.prop('checked'))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (checkbox.length) {
|
||||
checkbox.prop('checked', !checkbox.prop('checked'))
|
||||
}
|
||||
}
|
||||
|
||||
const unactiveRow = (row, checkbox) => {
|
||||
row.removeClass(selectedClass)
|
||||
|
||||
if (checkbox.length) {
|
||||
checkbox.prop('checked', false)
|
||||
}
|
||||
}
|
||||
|
||||
const activeRow = (row, checkbox) => {
|
||||
row.addClass(selectedClass)
|
||||
|
||||
if (checkbox.length) {
|
||||
checkbox.prop('checked', true)
|
||||
}
|
||||
}
|
||||
|
||||
const tableSelectable = () => {
|
||||
const tables = $('*[data-selectable]')
|
||||
|
||||
tables.each((i, t) => {
|
||||
const table = $(t)
|
||||
const rows = table.find('*[data-selectable-row]')
|
||||
let selectedIndex = null
|
||||
|
||||
const tbody = table.find('tbody')
|
||||
|
||||
const resizer = () => {
|
||||
tbody.height($(window).height() - tbody.offset().top - 20)
|
||||
}
|
||||
|
||||
window.setInterval(resizer, 1000)
|
||||
resizer()
|
||||
$(window).resize(resizer);
|
||||
|
||||
((rows) => {
|
||||
rows.each((i, r) => {
|
||||
const row = $(r)
|
||||
const checkbox = row.find('*[data-selectable-checkbox]')
|
||||
const selectors = row.find('*[data-selectable-selector]');
|
||||
|
||||
((row, selectors, checkbox, index) => {
|
||||
selectors.click((e) => {
|
||||
if (event.target.nodeName === 'INPUT') {
|
||||
e.stopPropagation()
|
||||
|
||||
checkbox.trigger('clicked')
|
||||
|
||||
return toggleRow(row, checkbox, true)
|
||||
}
|
||||
|
||||
if (window.event.ctrlKey) {
|
||||
e.preventDefault()
|
||||
|
||||
return toggleRow(row, checkbox)
|
||||
}
|
||||
|
||||
if (window.event.button === 0) {
|
||||
if (!window.event.ctrlKey && !window.event.shiftKey) {
|
||||
rows.each((z, r2) => {
|
||||
unactiveRow($(r2), $(r2).find('*[data-selectable-checkbox]'))
|
||||
})
|
||||
|
||||
toggleRow(row, checkbox)
|
||||
|
||||
if (row.hasClass(selectedClass)) {
|
||||
selectedIndex = index
|
||||
} else {
|
||||
selectedIndex = null
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (window.event.shiftKey) {
|
||||
if (selectedIndex !== null) {
|
||||
rows.each((z, r2) => {
|
||||
if (selectedIndex <= index) {
|
||||
if (z >= selectedIndex && z <= index) {
|
||||
activeRow($(r2), $(r2).find('*[data-selectable-checkbox]'))
|
||||
} else {
|
||||
unactiveRow($(r2), $(r2).find('*[data-selectable-checkbox]'))
|
||||
}
|
||||
} else {
|
||||
if (z <= selectedIndex && z >= index) {
|
||||
activeRow($(r2), $(r2).find('*[data-selectable-checkbox]'))
|
||||
} else {
|
||||
unactiveRow($(r2), $(r2).find('*[data-selectable-checkbox]'))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// selectedIndex = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})(row, selectors, checkbox, i)
|
||||
})
|
||||
})(rows)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = tableSelectable
|
|
@ -1,11 +0,0 @@
|
|||
const $ = require('jquery')
|
||||
|
||||
module.exports = function () {
|
||||
$('.toast').toast({
|
||||
animation: true,
|
||||
autohide: true,
|
||||
delay: 5000
|
||||
})
|
||||
|
||||
$('.toast').toast('show')
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
const $ = require('jquery')
|
||||
|
||||
module.exports = function () {
|
||||
$('*[data-toggle="tooltip"]').tooltip()
|
||||
}
|
1
assets/js/app.js
Normal file
1
assets/js/app.js
Normal file
|
@ -0,0 +1 @@
|
|||
import '../css/app.scss'
|
40
bin/console
40
bin/console
|
@ -3,41 +3,17 @@
|
|||
|
||||
use App\Kernel;
|
||||
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
use Symfony\Component\Dotenv\Dotenv;
|
||||
use Symfony\Component\ErrorHandler\Debug;
|
||||
|
||||
if (!in_array(PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) {
|
||||
echo 'Warning: The console should be invoked via the CLI version of PHP, not the '.PHP_SAPI.' SAPI'.PHP_EOL;
|
||||
if (!is_file(dirname(__DIR__).'/vendor/autoload_runtime.php')) {
|
||||
throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".');
|
||||
}
|
||||
|
||||
set_time_limit(0);
|
||||
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
|
||||
|
||||
require dirname(__DIR__).'/vendor/autoload.php';
|
||||
chdir(__DIR__.'/../');
|
||||
|
||||
if (!class_exists(Application::class) || !class_exists(Dotenv::class)) {
|
||||
throw new LogicException('You need to add "symfony/framework-bundle" and "symfony/dotenv" as Composer dependencies.');
|
||||
}
|
||||
return function (array $context) {
|
||||
$kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
|
||||
|
||||
$input = new ArgvInput();
|
||||
if (null !== $env = $input->getParameterOption(['--env', '-e'], null, true)) {
|
||||
putenv('APP_ENV='.$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = $env);
|
||||
}
|
||||
|
||||
if ($input->hasParameterOption('--no-debug', true)) {
|
||||
putenv('APP_DEBUG='.$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = '0');
|
||||
}
|
||||
|
||||
(new Dotenv())->bootEnv(dirname(__DIR__).'/.env');
|
||||
|
||||
if ($_SERVER['APP_DEBUG']) {
|
||||
umask(0000);
|
||||
|
||||
if (class_exists(Debug::class)) {
|
||||
Debug::enable();
|
||||
}
|
||||
}
|
||||
|
||||
$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
|
||||
$application = new Application($kernel);
|
||||
$application->run($input);
|
||||
return new Application($kernel);
|
||||
};
|
||||
|
|
20
bin/phpunit
20
bin/phpunit
|
@ -1,13 +1,19 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
if (!file_exists(dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php')) {
|
||||
echo "Unable to find the `simple-phpunit.php` script in `vendor/symfony/phpunit-bridge/bin/`.\n";
|
||||
exit(1);
|
||||
if (!ini_get('date.timezone')) {
|
||||
ini_set('date.timezone', 'UTC');
|
||||
}
|
||||
|
||||
if (false === getenv('SYMFONY_PHPUNIT_DIR')) {
|
||||
putenv('SYMFONY_PHPUNIT_DIR='.__DIR__.'/.phpunit');
|
||||
}
|
||||
if (is_file(dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit')) {
|
||||
define('PHPUNIT_COMPOSER_INSTALL', dirname(__DIR__).'/vendor/autoload.php');
|
||||
require PHPUNIT_COMPOSER_INSTALL;
|
||||
PHPUnit\TextUI\Command::main();
|
||||
} else {
|
||||
if (!is_file(dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php')) {
|
||||
echo "Unable to find the `simple-phpunit.php` script in `vendor/symfony/phpunit-bridge/bin/`.\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
require dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php';
|
||||
require dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php';
|
||||
}
|
||||
|
|
|
@ -7,67 +7,17 @@
|
|||
"prefer-stable": true,
|
||||
"require": {
|
||||
"php": ">=8.0.0",
|
||||
"ext-ctype": "*",
|
||||
"ext-iconv": "*",
|
||||
"bjeavons/zxcvbn-php": "^1.2",
|
||||
"cocur/slugify": "^4.0",
|
||||
"composer/package-versions-deprecated": "1.11.99.1",
|
||||
"doctrine/annotations": "^1.0",
|
||||
"doctrine/doctrine-bundle": "^2.2",
|
||||
"doctrine/doctrine-migrations-bundle": "^3.0",
|
||||
"doctrine/orm": "^2.8",
|
||||
"friendsofsymfony/jsrouting-bundle": "^2.7",
|
||||
"knplabs/doctrine-behaviors": "^2.2",
|
||||
"knplabs/knp-paginator-bundle": "^5.4",
|
||||
"liip/imagine-bundle": "^2.6",
|
||||
"phpdocumentor/reflection-docblock": "^5.2",
|
||||
"scheb/2fa-google-authenticator": "^5.7",
|
||||
"scheb/2fa-qr-code": "^5.7",
|
||||
"sensio/framework-extra-bundle": "^6.1",
|
||||
"sensiolabs/ansi-to-html": "^1.2",
|
||||
"spe/filesize-extension-bundle": "~2.0.0",
|
||||
"stof/doctrine-extensions-bundle": "^1.6",
|
||||
"symfony/apache-pack": "^1.0",
|
||||
"symfony/asset": "5.4.*",
|
||||
"symfony/console": "5.4.*",
|
||||
"symfony/dotenv": "5.4.*",
|
||||
"symfony/event-dispatcher": "5.4.*",
|
||||
"symfony/expression-language": "5.4.*",
|
||||
"symfony/finder": "5.4.*",
|
||||
"symfony/flex": "^1.3.1",
|
||||
"symfony/form": "5.4.*",
|
||||
"symfony/framework-bundle": "5.4.*",
|
||||
"symfony/http-client": "5.4.*",
|
||||
"symfony/intl": "5.4.*",
|
||||
"symfony/mailer": "5.4.*",
|
||||
"symfony/mime": "5.4.*",
|
||||
"symfony/monolog-bundle": "^3.1",
|
||||
"symfony/notifier": "5.4.*",
|
||||
"symfony/process": "5.4.*",
|
||||
"symfony/property-access": "5.4.*",
|
||||
"symfony/property-info": "5.4.*",
|
||||
"symfony/proxy-manager-bridge": "5.4.*",
|
||||
"symfony/security-bundle": "5.4.*",
|
||||
"symfony/serializer": "5.4.*",
|
||||
"symfony/string": "5.4.*",
|
||||
"symfony/translation": "5.4.*",
|
||||
"symfony/twig-bundle": "^5.2",
|
||||
"symfony/validator": "5.4.*",
|
||||
"symfony/web-link": "5.4.*",
|
||||
"symfony/webpack-encore-bundle": "^1.11",
|
||||
"symfony/yaml": "5.4.*",
|
||||
"twig/extra-bundle": "^2.12|^3.0",
|
||||
"twig/twig": "^2.12|^3.0"
|
||||
"murph/murph-core": "^1.25"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/browser-kit": "^5.2",
|
||||
"symfony/css-selector": "^5.2",
|
||||
"symfony/debug-bundle": "^5.2",
|
||||
"symfony/browser-kit": "^5.4",
|
||||
"symfony/css-selector": "^5.4",
|
||||
"symfony/debug-bundle": "^5.4",
|
||||
"symfony/maker-bundle": "^1.0",
|
||||
"symfony/phpunit-bridge": "^5.2",
|
||||
"symfony/stopwatch": "^5.2",
|
||||
"symfony/var-dumper": "^5.2",
|
||||
"symfony/web-profiler-bundle": "^5.2"
|
||||
"symfony/phpunit-bridge": "^6.2",
|
||||
"symfony/stopwatch": "^5.4",
|
||||
"symfony/var-dumper": "^5.4",
|
||||
"symfony/web-profiler-bundle": "^5.4"
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
|
@ -76,13 +26,13 @@
|
|||
},
|
||||
"sort-packages": true,
|
||||
"allow-plugins": {
|
||||
"symfony/flex": true
|
||||
"symfony/flex": true,
|
||||
"symfony/runtime": true
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "src/",
|
||||
"App\\Core\\": "core/"
|
||||
"App\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
|
|
|
@ -6,7 +6,7 @@ return [
|
|||
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
|
||||
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
|
||||
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
|
||||
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true],
|
||||
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true],
|
||||
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
|
||||
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
|
||||
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
|
||||
|
|
|
@ -9,11 +9,16 @@ core:
|
|||
name: 'Simple page'
|
||||
templates:
|
||||
- {name: "Default", file: "page/simple/default.html.twig"}
|
||||
# security:
|
||||
# roles:
|
||||
# - {name: 'Role foo', role: 'ROLE_FOO'}
|
||||
# - {name: 'Role bar', role: 'ROLE_BAR'}
|
||||
file_manager:
|
||||
# mimes:
|
||||
# - image/png
|
||||
# - image/jpg
|
||||
# - image/jpeg
|
||||
# - image/webp
|
||||
# - image/gif
|
||||
# - image/svg+xml
|
||||
# - video/mp4
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
framework:
|
||||
assets:
|
||||
json_manifest_path: '%kernel.project_dir%/public/build/manifest.json'
|
5
config/packages/debug.yaml
Normal file
5
config/packages/debug.yaml
Normal file
|
@ -0,0 +1,5 @@
|
|||
when@dev:
|
||||
debug:
|
||||
# Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser.
|
||||
# See the "server:dump" command to start a new server.
|
||||
dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%"
|
|
@ -1,4 +0,0 @@
|
|||
debug:
|
||||
# Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser.
|
||||
# See the "server:dump" command to start a new server.
|
||||
dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%"
|
|
@ -1,19 +0,0 @@
|
|||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
||||
channels: ["!event"]
|
||||
# uncomment to get logging in your browser
|
||||
# you may have to allow bigger header sizes in your Web server configuration
|
||||
#firephp:
|
||||
# type: firephp
|
||||
# level: info
|
||||
#chromephp:
|
||||
# type: chromephp
|
||||
# level: info
|
||||
console:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
channels: ["!event", "!doctrine", "!console"]
|
|
@ -1,6 +0,0 @@
|
|||
web_profiler:
|
||||
toolbar: true
|
||||
intercept_redirects: false
|
||||
|
||||
framework:
|
||||
profiler: { only_exceptions: false }
|
|
@ -4,7 +4,7 @@ doctrine:
|
|||
|
||||
# IMPORTANT: You MUST configure your server version,
|
||||
# either here or in the DATABASE_URL env var (see .env file)
|
||||
#server_version: '13'
|
||||
#server_version: '14'
|
||||
orm:
|
||||
auto_generate_proxy_classes: true
|
||||
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
|
||||
|
@ -12,13 +12,13 @@ doctrine:
|
|||
mappings:
|
||||
App\Core\Entity:
|
||||
is_bundle: false
|
||||
type: annotation
|
||||
dir: '%kernel.project_dir%/core/Entity'
|
||||
type: attribute
|
||||
dir: '%kernel.project_dir%/vendor/murph/murph-core/src/core/Entity'
|
||||
prefix: 'App\Core\Entity'
|
||||
alias: App\Core\Entity
|
||||
App\Entity:
|
||||
is_bundle: false
|
||||
type: annotation
|
||||
type: attribute
|
||||
dir: '%kernel.project_dir%/src/Entity'
|
||||
prefix: 'App\Entity'
|
||||
alias: App\Entity
|
||||
|
@ -28,3 +28,29 @@ doctrine:
|
|||
dir: "%kernel.project_dir%/vendor/gedmo/doctrine-extensions/src/Tree/Entity"
|
||||
alias: GedmoTree # (optional) it will default to the name set for the mapping
|
||||
is_bundle: false
|
||||
|
||||
when@test:
|
||||
doctrine:
|
||||
dbal:
|
||||
# "TEST_TOKEN" is typically set by ParaTest
|
||||
dbname_suffix: '_test%env(default::TEST_TOKEN)%'
|
||||
|
||||
when@prod:
|
||||
doctrine:
|
||||
orm:
|
||||
auto_generate_proxy_classes: false
|
||||
proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies'
|
||||
query_cache_driver:
|
||||
type: pool
|
||||
pool: doctrine.system_cache_pool
|
||||
result_cache_driver:
|
||||
type: pool
|
||||
pool: doctrine.result_cache_pool
|
||||
|
||||
framework:
|
||||
cache:
|
||||
pools:
|
||||
doctrine.result_cache_pool:
|
||||
adapter: cache.app
|
||||
doctrine.system_cache_pool:
|
||||
adapter: cache.system
|
||||
|
|
|
@ -3,3 +3,4 @@ doctrine_migrations:
|
|||
# namespace is arbitrary but should be different from App\Migrations
|
||||
# as migrations classes should NOT be autoloaded
|
||||
'DoctrineMigrations': '%kernel.project_dir%/migrations'
|
||||
enable_profiler: false
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
framework:
|
||||
secret: '%env(APP_SECRET)%'
|
||||
#csrf_protection: true
|
||||
#http_method_override: true
|
||||
http_method_override: false
|
||||
|
||||
# Enables session support. Note that the session will ONLY be started if you read or write from it.
|
||||
# Remove or comment this section to explicitly disable session support.
|
||||
|
@ -10,8 +10,15 @@ framework:
|
|||
handler_id: null
|
||||
cookie_secure: auto
|
||||
cookie_samesite: lax
|
||||
storage_factory_id: session.storage.factory.native
|
||||
|
||||
#esi: true
|
||||
#fragments: true
|
||||
php_errors:
|
||||
log: true
|
||||
|
||||
when@test:
|
||||
framework:
|
||||
test: true
|
||||
session:
|
||||
storage_factory_id: session.storage.factory.mock_file
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# See dos how to configure the bundle: https://symfony.com/doc/current/bundles/LiipImagineBundle/basic-usage.html
|
||||
# Documentation on how to configure the bundle can be found at: https://symfony.com/doc/current/bundles/LiipImagineBundle/basic-usage.html
|
||||
liip_imagine:
|
||||
# valid drivers options include "gd" or "gmagick" or "imagick"
|
||||
driver: "imagick"
|
||||
|
|
61
config/packages/monolog.yaml
Normal file
61
config/packages/monolog.yaml
Normal file
|
@ -0,0 +1,61 @@
|
|||
monolog:
|
||||
channels:
|
||||
- deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists
|
||||
|
||||
when@dev:
|
||||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
||||
channels: ["!event"]
|
||||
# uncomment to get logging in your browser
|
||||
# you may have to allow bigger header sizes in your Web server configuration
|
||||
#firephp:
|
||||
# type: firephp
|
||||
# level: info
|
||||
#chromephp:
|
||||
# type: chromephp
|
||||
# level: info
|
||||
console:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
channels: ["!event", "!doctrine", "!console"]
|
||||
|
||||
when@test:
|
||||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: fingers_crossed
|
||||
action_level: error
|
||||
handler: nested
|
||||
excluded_http_codes: [404, 405]
|
||||
channels: ["!event"]
|
||||
nested:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
||||
|
||||
when@prod:
|
||||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: fingers_crossed
|
||||
action_level: error
|
||||
handler: nested
|
||||
excluded_http_codes: [404, 405]
|
||||
buffer_size: 50 # How many messages should be saved? Prevent memory leaks
|
||||
nested:
|
||||
type: stream
|
||||
path: php://stderr
|
||||
level: debug
|
||||
formatter: monolog.formatter.json
|
||||
console:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
channels: ["!event", "!doctrine"]
|
||||
deprecation:
|
||||
type: stream
|
||||
channels: [deprecation]
|
||||
path: php://stderr
|
|
@ -1,8 +0,0 @@
|
|||
# As of Symfony 5.1, deprecations are logged in the dedicated "deprecation" channel when it exists
|
||||
#monolog:
|
||||
# channels: [deprecation]
|
||||
# handlers:
|
||||
# deprecation:
|
||||
# type: stream
|
||||
# channels: [deprecation]
|
||||
# path: "%kernel.logs_dir%/%kernel.environment%.deprecations.log"
|
|
@ -1,20 +0,0 @@
|
|||
doctrine:
|
||||
orm:
|
||||
auto_generate_proxy_classes: false
|
||||
metadata_cache_driver:
|
||||
type: pool
|
||||
pool: doctrine.system_cache_pool
|
||||
query_cache_driver:
|
||||
type: pool
|
||||
pool: doctrine.system_cache_pool
|
||||
result_cache_driver:
|
||||
type: pool
|
||||
pool: doctrine.result_cache_pool
|
||||
|
||||
framework:
|
||||
cache:
|
||||
pools:
|
||||
doctrine.result_cache_pool:
|
||||
adapter: cache.app
|
||||
doctrine.system_cache_pool:
|
||||
adapter: cache.system
|
|
@ -1,16 +0,0 @@
|
|||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: fingers_crossed
|
||||
action_level: error
|
||||
handler: nested
|
||||
excluded_http_codes: [404, 405]
|
||||
buffer_size: 50 # How many messages should be saved? Prevent memory leaks
|
||||
nested:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
||||
console:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
channels: ["!event", "!doctrine"]
|
|
@ -1,3 +0,0 @@
|
|||
framework:
|
||||
router:
|
||||
strict_requirements: null
|
|
@ -1,4 +0,0 @@
|
|||
#webpack_encore:
|
||||
# Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes)
|
||||
# Available in version 1.2
|
||||
#cache: true
|
|
@ -5,3 +5,8 @@ framework:
|
|||
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
|
||||
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
|
||||
#default_uri: http://localhost
|
||||
|
||||
when@prod:
|
||||
framework:
|
||||
router:
|
||||
strict_requirements: null
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
scheb_two_factor:
|
||||
google:
|
||||
enabled: true
|
||||
issuer: "Muprh"
|
||||
issuer: "Murph"
|
||||
server_name:
|
||||
digits: 6
|
||||
window: 1
|
||||
|
|
|
@ -3,7 +3,16 @@ security:
|
|||
App\Entity\User:
|
||||
algorithm: auto
|
||||
|
||||
access_decision_manager:
|
||||
strategy: consensus
|
||||
allow_if_all_abstain: false
|
||||
|
||||
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
|
||||
enable_authenticator_manager: true
|
||||
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
|
||||
password_hashers:
|
||||
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
|
||||
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
|
||||
providers:
|
||||
# used to reload user from session & other features (e.g. switch_user)
|
||||
app_user_provider:
|
||||
|
@ -20,7 +29,6 @@ security:
|
|||
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
||||
security: false
|
||||
main:
|
||||
anonymous: ~
|
||||
two_factor:
|
||||
auth_form_path: 2fa_login # The route name you have used in the routes.yaml
|
||||
check_path: 2fa_login_check # The route name you have used in the routes.yaml
|
||||
|
@ -30,7 +38,7 @@ security:
|
|||
form_login:
|
||||
login_path: auth_login
|
||||
check_path: auth_login
|
||||
csrf_token_generator: security.csrf.token_manager
|
||||
enable_csrf: true
|
||||
logout:
|
||||
path: auth_logout
|
||||
target: /
|
||||
|
@ -39,6 +47,8 @@ security:
|
|||
lifetime: 604800
|
||||
path: /
|
||||
|
||||
entry_point: form_login
|
||||
|
||||
# Easy way to control access for large sections of your site
|
||||
# Note: Only the *first* access control that matches will be used
|
||||
access_control:
|
||||
|
@ -52,3 +62,16 @@ security:
|
|||
- { path: ^/admin/file_manager, roles: ROLE_WRITER }
|
||||
- { path: ^/admin, roles: ROLE_USER }
|
||||
- { path: ^/_internal, roles: IS_AUTHENTICATED_ANONYMOUSLY }
|
||||
|
||||
when@test:
|
||||
security:
|
||||
password_hashers:
|
||||
# By default, password hashers are resource intensive and take time. This is
|
||||
# important to generate secure password hashes. In tests however, secure hashes
|
||||
# are not important, waste resources and increase test times. The following
|
||||
# reduces the work factor to the lowest possible values.
|
||||
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
|
||||
algorithm: auto
|
||||
cost: 4 # Lowest possible value for bcrypt
|
||||
time_cost: 3 # Lowest possible value for argon
|
||||
memory_cost: 10 # Lowest possible value for argon
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Read the documentation: https://symfony.com/doc/current/bundles/StofDoctrineExtensionsBundle/index.html
|
||||
# See the official DoctrineExtensions documentation for more details: https://github.com/Atlantic18/DoctrineExtensions/tree/master/doc/
|
||||
# See the official DoctrineExtensions documentation for more details: https://github.com/doctrine-extensions/DoctrineExtensions/tree/main/doc
|
||||
stof_doctrine_extensions:
|
||||
default_locale: en_US
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
framework:
|
||||
test: true
|
||||
session:
|
||||
storage_id: session.storage.mock_file
|
|
@ -1,12 +0,0 @@
|
|||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: fingers_crossed
|
||||
action_level: error
|
||||
handler: nested
|
||||
excluded_http_codes: [404, 405]
|
||||
channels: ["!event"]
|
||||
nested:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
|
@ -1,2 +0,0 @@
|
|||
twig:
|
||||
strict_variables: true
|
|
@ -1,3 +0,0 @@
|
|||
framework:
|
||||
validation:
|
||||
not_compromised_password: false
|
|
@ -1,6 +0,0 @@
|
|||
web_profiler:
|
||||
toolbar: false
|
||||
intercept_redirects: false
|
||||
|
||||
framework:
|
||||
profiler: { collect: false }
|
|
@ -1,2 +0,0 @@
|
|||
#webpack_encore:
|
||||
# strict_mode: false
|
|
@ -3,6 +3,13 @@ framework:
|
|||
translator:
|
||||
default_path: '%kernel.project_dir%/translations'
|
||||
paths:
|
||||
- '%kernel.project_dir%/core/Resources/translations'
|
||||
- '%kernel.project_dir%/vendor/murph/murph-core/src/core/Resources/translations'
|
||||
fallbacks:
|
||||
- en
|
||||
# providers:
|
||||
# crowdin:
|
||||
# dsn: '%env(CROWDIN_DSN)%'
|
||||
# loco:
|
||||
# dsn: '%env(LOCO_DSN)%'
|
||||
# lokalise:
|
||||
# dsn: '%env(LOKALISE_DSN)%'
|
||||
|
|
|
@ -3,4 +3,8 @@ twig:
|
|||
form_themes: ['@Core/form/bootstrap_4_form_theme.html.twig']
|
||||
paths:
|
||||
'%kernel.project_dir%/templates/core/': Core
|
||||
'%kernel.project_dir%/core/Resources/views/': Core
|
||||
'%kernel.project_dir%/vendor/murph/murph-core/src/core/Resources/views/': Core
|
||||
|
||||
when@test:
|
||||
twig:
|
||||
strict_variables: true
|
||||
|
|
|
@ -6,3 +6,8 @@ framework:
|
|||
# For instance, basic validation constraints will be inferred from Doctrine's metadata.
|
||||
#auto_mapping:
|
||||
# App\Entity\: []
|
||||
|
||||
when@test:
|
||||
framework:
|
||||
validation:
|
||||
not_compromised_password: false
|
||||
|
|
15
config/packages/web_profiler.yaml
Normal file
15
config/packages/web_profiler.yaml
Normal file
|
@ -0,0 +1,15 @@
|
|||
when@dev:
|
||||
web_profiler:
|
||||
toolbar: true
|
||||
intercept_redirects: false
|
||||
|
||||
framework:
|
||||
profiler: { only_exceptions: false }
|
||||
|
||||
when@test:
|
||||
web_profiler:
|
||||
toolbar: false
|
||||
intercept_redirects: false
|
||||
|
||||
framework:
|
||||
profiler: { collect: false }
|
|
@ -7,7 +7,12 @@ webpack_encore:
|
|||
# Set attributes that will be rendered on all script and link tags
|
||||
script_attributes:
|
||||
defer: true
|
||||
# Uncomment (also under link_attributes) if using Turbo Drive
|
||||
# https://turbo.hotwired.dev/handbook/drive#reloading-when-assets-change
|
||||
# 'data-turbo-track': reload
|
||||
# link_attributes:
|
||||
# Uncomment if using Turbo Drive
|
||||
# 'data-turbo-track': reload
|
||||
|
||||
# If using Encore.enableIntegrityHashes() and need the crossorigin attribute (default: false, or use 'anonymous' or 'use-credentials')
|
||||
# crossorigin: 'anonymous'
|
||||
|
@ -20,11 +25,21 @@ webpack_encore:
|
|||
|
||||
# If you have multiple builds:
|
||||
# builds:
|
||||
# pass "frontend" as the 3rg arg to the Twig functions
|
||||
# {{ encore_entry_script_tags('entry1', null, 'frontend') }}
|
||||
|
||||
# frontend: '%kernel.project_dir%/public/frontend/build'
|
||||
|
||||
# Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes)
|
||||
# Put in config/packages/prod/webpack_encore.yaml
|
||||
# cache: true
|
||||
# pass the build name as the 3rd argument to the Twig functions
|
||||
# {{ encore_entry_script_tags('entry1', null, 'frontend') }}
|
||||
|
||||
framework:
|
||||
assets:
|
||||
json_manifest_path: '%kernel.project_dir%/public/build/manifest.json'
|
||||
|
||||
#when@prod:
|
||||
# webpack_encore:
|
||||
# # Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes)
|
||||
# # Available in version 1.2
|
||||
# cache: true
|
||||
|
||||
#when@test:
|
||||
# webpack_encore:
|
||||
# strict_mode: false
|
||||
|
|
|
@ -3,7 +3,7 @@ controllers:
|
|||
type: annotation
|
||||
|
||||
core_controllers:
|
||||
resource: ../../core/Controller/
|
||||
resource: ../../vendor/murph/murph-core/src/core/Controller/
|
||||
type: annotation
|
||||
|
||||
kernel:
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
_errors:
|
||||
resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
|
||||
prefix: /_error
|
|
@ -1,7 +0,0 @@
|
|||
web_profiler_wdt:
|
||||
resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml'
|
||||
prefix: /_wdt
|
||||
|
||||
web_profiler_profiler:
|
||||
resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml'
|
||||
prefix: /_profiler
|
4
config/routes/framework.yaml
Normal file
4
config/routes/framework.yaml
Normal file
|
@ -0,0 +1,4 @@
|
|||
when@dev:
|
||||
_errors:
|
||||
resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
|
||||
prefix: /_error
|
|
@ -1,7 +1,7 @@
|
|||
2fa_login:
|
||||
path: /2fa
|
||||
defaults:
|
||||
_controller: "scheb_two_factor.form_controller:form"
|
||||
_controller: "scheb_two_factor.form_controller::form"
|
||||
|
||||
2fa_login_check:
|
||||
path: /2fa_check
|
||||
|
|
8
config/routes/web_profiler.yaml
Normal file
8
config/routes/web_profiler.yaml
Normal file
|
@ -0,0 +1,8 @@
|
|||
when@dev:
|
||||
web_profiler_wdt:
|
||||
resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml'
|
||||
prefix: /_wdt
|
||||
|
||||
web_profiler_profiler:
|
||||
resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml'
|
||||
prefix: /_profiler
|
|
@ -2,7 +2,7 @@
|
|||
# Files in the packages/ subdirectory configure your dependencies.
|
||||
|
||||
# Put parameters here that don't need to change on each machine where the app is deployed
|
||||
# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
|
||||
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
|
||||
parameters:
|
||||
|
||||
services:
|
||||
|
@ -11,18 +11,29 @@ services:
|
|||
autowire: true # Automatically injects dependencies in your services.
|
||||
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
|
||||
|
||||
# makes classes in src/ available to be used as services
|
||||
# this creates a service per class whose id is the fully-qualified class name
|
||||
# Murph services
|
||||
App\Core\:
|
||||
resource: '../core/'
|
||||
resource: '../vendor/murph/murph-core/src/core/'
|
||||
exclude:
|
||||
- '../core/DependencyInjection/'
|
||||
- '../core/Entity/'
|
||||
- '../vendor/murph/murph-core/src/core/DependencyInjection/'
|
||||
- '../vendor/murph/murph-core/src/core/Entity/'
|
||||
|
||||
# Redirections
|
||||
App\Core\EventListener\RedirectListener:
|
||||
tags:
|
||||
- { name: kernel.event_listener, event: kernel.exception }
|
||||
|
||||
# Analytics
|
||||
App\Core\EventListener\AnalyticListener:
|
||||
tags:
|
||||
- { name: kernel.event_listener, event: kernel.request }
|
||||
|
||||
# A/B Testing
|
||||
App\Core\EventListener\AbListener:
|
||||
tags:
|
||||
- { name: kernel.event_listener, event: kernel.request }
|
||||
- { name: kernel.event_listener, event: kernel.response }
|
||||
|
||||
App\:
|
||||
resource: '../src/'
|
||||
exclude:
|
||||
|
@ -32,11 +43,11 @@ services:
|
|||
- '../src/Tests/'
|
||||
|
||||
App\Core\Maker\:
|
||||
resource: '../core/Maker/'
|
||||
resource: '../vendor/murph/murph-core/src/core/Maker/'
|
||||
tags: ['maker.command']
|
||||
|
||||
App\Core\Controller\:
|
||||
resource: '../core/Controller/'
|
||||
resource: '../vendor/murph/murph-core/src/core/Controller/'
|
||||
tags: ['controller.service_arguments']
|
||||
|
||||
App\Controller\:
|
||||
|
@ -54,8 +65,8 @@ services:
|
|||
calls:
|
||||
- [ setAnnotationReader, [ "@annotation_reader" ] ]
|
||||
|
||||
App\UrlGenerator\FooUrlGenerator:
|
||||
public: true
|
||||
# App\UrlGenerator\FooUrlGenerator:
|
||||
# public: true
|
||||
|
||||
# add more service definitions when explicit configuration is needed
|
||||
# please note that last definitions always *replace* previous ones
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Core\Annotation;
|
||||
|
||||
use Doctrine\Common\Annotations\Annotation;
|
||||
|
||||
/**
|
||||
* class UrlGenerator.
|
||||
*
|
||||
* @author Simon Vieille <simon@deblan.fr>
|
||||
* @Annotation
|
||||
*/
|
||||
class UrlGenerator
|
||||
{
|
||||
public string $service;
|
||||
|
||||
public string $method;
|
||||
|
||||
public array $options = [];
|
||||
}
|
|
@ -1,96 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Core\Authenticator;
|
||||
|
||||
use App\Entity\User;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
||||
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
|
||||
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
use Symfony\Component\Security\Csrf\CsrfToken;
|
||||
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
|
||||
use Symfony\Component\Security\Http\Util\TargetPathTrait;
|
||||
|
||||
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
|
||||
{
|
||||
use TargetPathTrait;
|
||||
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
private UrlGeneratorInterface $urlGenerator;
|
||||
|
||||
private CsrfTokenManagerInterface $csrfTokenManager;
|
||||
|
||||
private UserPasswordEncoderInterface $passwordEncoder;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager, UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordEncoderInterface $passwordEncoder)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
$this->csrfTokenManager = $csrfTokenManager;
|
||||
$this->passwordEncoder = $passwordEncoder;
|
||||
}
|
||||
|
||||
public function supports(Request $request)
|
||||
{
|
||||
return 'auth_login' === $request->attributes->get('_route') && $request->isMethod('POST');
|
||||
}
|
||||
|
||||
public function getCredentials(Request $request)
|
||||
{
|
||||
$credentials = [
|
||||
'email' => $request->request->get('_username'),
|
||||
'password' => $request->request->get('_password'),
|
||||
'csrf_token' => $request->request->get('_csrf_token'),
|
||||
];
|
||||
|
||||
$request->getSession()->set(Security::LAST_USERNAME, $credentials['email']);
|
||||
|
||||
return $credentials;
|
||||
}
|
||||
|
||||
public function getUser($credentials, UserProviderInterface $userProvider)
|
||||
{
|
||||
$token = new CsrfToken('authenticate', $credentials['csrf_token']);
|
||||
|
||||
if (!$this->csrfTokenManager->isTokenValid($token)) {
|
||||
throw new InvalidCsrfTokenException();
|
||||
}
|
||||
|
||||
$user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $credentials['email']]);
|
||||
|
||||
if (!$user) {
|
||||
// fail authentication with a custom error
|
||||
throw new CustomUserMessageAuthenticationException('Email could not be found.');
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function checkCredentials($credentials, UserInterface $user)
|
||||
{
|
||||
return $this->passwordEncoder->isPasswordValid($user, $credentials['password']);
|
||||
}
|
||||
|
||||
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
|
||||
{
|
||||
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
|
||||
return new RedirectResponse($targetPath);
|
||||
}
|
||||
|
||||
return new RedirectResponse($this->urlGenerator->generate('admin_dashboard_index'));
|
||||
}
|
||||
|
||||
protected function getLoginUrl()
|
||||
{
|
||||
return $this->urlGenerator->generate('auth_login');
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Core\Bundle;
|
||||
|
||||
use App\Core\DependencyInjection\CoreExtension;
|
||||
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
||||
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
|
||||
|
||||
class CoreBundle extends Bundle
|
||||
{
|
||||
public function getContainerExtension(): ?ExtensionInterface
|
||||
{
|
||||
return new CoreExtension();
|
||||
}
|
||||
}
|
|
@ -1,82 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Core\Cache;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Output\BufferedOutput;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
use Symfony\Component\HttpKernel\KernelInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\HttpClient\Exception\ClientException;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\HttpClient\Exception\TransportException;
|
||||
|
||||
/**
|
||||
* class SymfonyCacheManager.
|
||||
*
|
||||
* @author Simon Vieille <simon@deblan.fr>
|
||||
*/
|
||||
class SymfonyCacheManager
|
||||
{
|
||||
protected KernelInterface $kernel;
|
||||
protected HttpClientInterface $httpClient;
|
||||
protected UrlGeneratorInterface $urlGenerator;
|
||||
|
||||
public function __construct(KernelInterface $kernel, HttpClientInterface $httpClient, UrlGeneratorInterface $urlGenerator)
|
||||
{
|
||||
$this->kernel = $kernel;
|
||||
$this->httpClient = $httpClient;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
}
|
||||
|
||||
public function cleanRouting()
|
||||
{
|
||||
$finder = new Finder();
|
||||
$finder
|
||||
->in($this->kernel->getCacheDir())
|
||||
->depth('== 0')
|
||||
->name('url_*.php*')
|
||||
;
|
||||
|
||||
$pingUrl = $this->urlGenerator->generate('_ping', [], UrlGeneratorInterface::ABSOLUTE_URL);
|
||||
|
||||
foreach ($finder as $file) {
|
||||
unlink((string) $file->getPathname());
|
||||
}
|
||||
|
||||
try {
|
||||
// Hack: used to regenerate cache of url generator
|
||||
$this->httpClient->request('POST', $pingUrl);
|
||||
} catch (ClientException $e) {
|
||||
} catch (TransportException $e) {
|
||||
}
|
||||
}
|
||||
|
||||
public function cleanAll(OutputInterface $output = null)
|
||||
{
|
||||
$application = new Application($this->kernel);
|
||||
$application->setAutoExit(false);
|
||||
|
||||
if (null === $output) {
|
||||
$output = new BufferedOutput();
|
||||
}
|
||||
|
||||
$input = new ArrayInput([
|
||||
'command' => 'cache:clear',
|
||||
'-e' => $this->kernel->getEnvironment(),
|
||||
'--no-warmup' => null,
|
||||
'--ansi' => null,
|
||||
]);
|
||||
|
||||
$application->run($input, $output);
|
||||
|
||||
$input = new ArrayInput([
|
||||
'command' => 'cache:warmup',
|
||||
'-e' => $this->kernel->getEnvironment(),
|
||||
]);
|
||||
|
||||
$application->run($input, $output);
|
||||
}
|
||||
}
|
|
@ -1,79 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Core\Command;
|
||||
|
||||
use App\Core\Factory\UserFactory;
|
||||
use App\Core\Manager\EntityManager;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\ConfirmationQuestion;
|
||||
use Symfony\Component\Console\Question\Question;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
class UserCreateCommand extends Command
|
||||
{
|
||||
protected static $defaultName = 'murph:user:create';
|
||||
protected static $defaultDescription = 'Creates a user';
|
||||
protected UserFactory $userFactory;
|
||||
protected EntityManager $entityManager;
|
||||
|
||||
public function __construct(UserFactory $userFactory, EntityManager $entityManager)
|
||||
{
|
||||
$this->userFactory = $userFactory;
|
||||
$this->entityManager = $entityManager;
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setDescription(self::$defaultDescription)
|
||||
->addArgument('email', InputArgument::OPTIONAL, 'E-mail')
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$helper = $this->getHelper('question');
|
||||
|
||||
$emailQuestion = new Question('E-mail: ');
|
||||
$emailQuestion->setValidator(function ($value) {
|
||||
if (empty($value)) {
|
||||
throw new \RuntimeException(
|
||||
'The email must not be empty.'
|
||||
);
|
||||
}
|
||||
|
||||
return $value;
|
||||
});
|
||||
|
||||
$passwordQuestion = new Question('Password (leave empty to generate a random password): ');
|
||||
$passwordQuestion->setHidden(true);
|
||||
$isAdminQuestion = new ConfirmationQuestion('Is admin? [y/n] ', false);
|
||||
$isWriterQuestion = new ConfirmationQuestion('Is writer? [y/n] ', false);
|
||||
|
||||
$email = $input->getArgument('email');
|
||||
|
||||
if (empty($email)) {
|
||||
$email = $helper->ask($input, $output, $emailQuestion);
|
||||
}
|
||||
|
||||
$password = $helper->ask($input, $output, $passwordQuestion);
|
||||
$isAdmin = $helper->ask($input, $output, $isAdminQuestion);
|
||||
$isWriter = $helper->ask($input, $output, $isWriterQuestion);
|
||||
|
||||
$user = $this->userFactory->create($email, $password);
|
||||
$user->setIsAdmin($isAdmin);
|
||||
$user->setIsWriter($isWriter);
|
||||
|
||||
$this->entityManager->create($user);
|
||||
|
||||
$io->success('User created!');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
|
@ -1,150 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Core\Controller\Account;
|
||||
|
||||
use App\Core\Controller\Admin\AdminController;
|
||||
use App\Core\Manager\EntityManager;
|
||||
use App\Repository\UserRepository;
|
||||
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Google\GoogleAuthenticatorInterface;
|
||||
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Google\GoogleAuthenticatorInterface as TotpAuthenticatorInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
||||
use Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface;
|
||||
use ZxcvbnPhp\Zxcvbn;
|
||||
|
||||
/**
|
||||
* @Route("/admin/account")
|
||||
*/
|
||||
class AccountAdminController extends AdminController
|
||||
{
|
||||
/**
|
||||
* @Route("/", name="admin_account")
|
||||
*/
|
||||
public function account(Request $request, TotpAuthenticatorInterface $totpAuthenticatorService): Response
|
||||
{
|
||||
$account = $this->getUser();
|
||||
|
||||
return $this->render('@Core/account/admin/edit.html.twig', [
|
||||
'account' => $account,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/2fa", name="admin_account_2fa")
|
||||
*/
|
||||
public function twoFactorAuthentication(
|
||||
Request $request,
|
||||
GoogleAuthenticatorInterface $totpAuthenticatorService,
|
||||
EntityManager $entityManager
|
||||
): Response {
|
||||
if ($request->isMethod('GET')) {
|
||||
return $this->redirectToRoute('admin_account');
|
||||
}
|
||||
|
||||
$account = $this->getUser();
|
||||
$csrfToken = $request->request->get('_csrf_token');
|
||||
$enable = (bool) $request->request->get('enable');
|
||||
$code = $request->request->get('code', '');
|
||||
$secret = $request->request->get('secret', '');
|
||||
$qrCodeContent = null;
|
||||
|
||||
if ($this->isCsrfTokenValid('2fa', $csrfToken)) {
|
||||
if ($enable && !$account->isTotpAuthenticationEnabled()) {
|
||||
if (empty($secret)) {
|
||||
$secret = $totpAuthenticatorService->generateSecret();
|
||||
|
||||
$account->setTotpSecret($secret);
|
||||
|
||||
$qrCodeContent = $totpAuthenticatorService->getQRContent($account);
|
||||
} else {
|
||||
$account->setTotpSecret($secret);
|
||||
|
||||
$qrCodeContent = $totpAuthenticatorService->getQRContent($account);
|
||||
|
||||
if (!$totpAuthenticatorService->checkCode($account, $code)) {
|
||||
$this->addFlash('error', 'The code is not valid.');
|
||||
} else {
|
||||
$this->addFlash('success', 'Double authentication enabled.');
|
||||
|
||||
$entityManager->update($account);
|
||||
|
||||
return $this->redirectToRoute('admin_account');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$enable && $account->isTotpAuthenticationEnabled()) {
|
||||
$account->setTotpSecret(null);
|
||||
|
||||
$entityManager->update($account);
|
||||
|
||||
$this->addFlash('success', 'Double authentication disabled.');
|
||||
|
||||
return $this->redirectToRoute('admin_account');
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('@Core/account/admin/edit.html.twig', [
|
||||
'account' => $account,
|
||||
'twoFaKey' => $secret,
|
||||
'twoFaQrCodeContent' => $qrCodeContent,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/password", name="admin_account_password", methods={"POST"})
|
||||
*/
|
||||
public function password(
|
||||
Request $request,
|
||||
UserRepository $repository,
|
||||
TokenGeneratorInterface $tokenGenerator,
|
||||
UserPasswordEncoderInterface $encoder,
|
||||
EntityManager $entityManager
|
||||
): Response {
|
||||
$account = $this->getUser();
|
||||
$csrfToken = $request->request->get('_csrf_token');
|
||||
|
||||
if ($this->isCsrfTokenValid('password', $csrfToken)) {
|
||||
$password = $request->request->get('password');
|
||||
|
||||
if (!$encoder->isPasswordValid($account, $password)) {
|
||||
$this->addFlash('error', 'The form is not valid.');
|
||||
|
||||
return $this->redirectToRoute('admin_account');
|
||||
}
|
||||
|
||||
$password1 = $request->request->get('password1');
|
||||
$password2 = $request->request->get('password2');
|
||||
|
||||
$zxcvbn = new Zxcvbn();
|
||||
$strength = $zxcvbn->passwordStrength($password1, []);
|
||||
|
||||
if (4 === $strength['score'] && $password1 === $password2) {
|
||||
$account
|
||||
->setPassword($encoder->encodePassword($account, $password1))
|
||||
->setConfirmationToken($tokenGenerator->generateToken())
|
||||
;
|
||||
|
||||
$entityManager->update($account);
|
||||
|
||||
$this->addFlash('success', 'Password updated.');
|
||||
|
||||
return $this->redirectToRoute('admin_account');
|
||||
}
|
||||
}
|
||||
|
||||
$this->addFlash('error', 'The form is not valid.');
|
||||
|
||||
return $this->redirectToRoute('admin_account');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getSection(): string
|
||||
{
|
||||
return 'account';
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Core\Controller\Admin;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
abstract class AdminController extends AbstractController
|
||||
{
|
||||
protected array $coreParameters;
|
||||
|
||||
public function __construct(ParameterBagInterface $parameters)
|
||||
{
|
||||
$this->coreParameters = $parameters->get('core');
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/_ping", name="_ping")
|
||||
*/
|
||||
public function ping()
|
||||
{
|
||||
return $this->json(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function render(string $view, array $parameters = [], Response $response = null): Response
|
||||
{
|
||||
$parameters['section'] = $this->getSection();
|
||||
$parameters['site_name'] = $this->coreParameters['site']['name'];
|
||||
$parameters['site_logo'] = $this->coreParameters['site']['logo'];
|
||||
|
||||
return parent::render($view, $parameters, $response);
|
||||
}
|
||||
|
||||
abstract protected function getSection(): string;
|
||||
}
|
|
@ -1,340 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Core\Controller\Admin\Crud;
|
||||
|
||||
use App\Core\Controller\Admin\AdminController;
|
||||
use App\Core\Crud\CrudConfiguration;
|
||||
use App\Core\Entity\EntityInterface;
|
||||
use App\Core\Manager\EntityManager;
|
||||
use App\Core\Repository\RepositoryQuery;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Session\Session;
|
||||
|
||||
/**
|
||||
* class CrudController.
|
||||
*
|
||||
* @author Simon Vieille <simon@deblan.fr>
|
||||
*/
|
||||
abstract class CrudController extends AdminController
|
||||
{
|
||||
protected array $filters = [];
|
||||
protected array $sort = [
|
||||
'label' => null,
|
||||
'direction' => null,
|
||||
];
|
||||
|
||||
abstract protected function getConfiguration(): CrudConfiguration;
|
||||
|
||||
protected function doIndex(int $page = 1, RepositoryQuery $query, Request $request, Session $session): Response
|
||||
{
|
||||
$configuration = $this->getConfiguration();
|
||||
|
||||
$this->applySort('index', $query, $request);
|
||||
$this->updateFilters($request, $session);
|
||||
|
||||
$pager = $query
|
||||
->usefilters($this->filters)
|
||||
->paginate($page, $configuration->getmaxperpage('index'))
|
||||
;
|
||||
|
||||
return $this->render($this->getConfiguration()->getView('index'), [
|
||||
'configuration' => $configuration,
|
||||
'pager' => $pager,
|
||||
'sort' => $this->sort,
|
||||
'filters' => [
|
||||
'show' => null !== $configuration->getForm('filter'),
|
||||
'isEmpty' => empty($this->filters),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
protected function doNew(EntityInterface $entity, EntityManager $entityManager, Request $request, callable $beforeCreate = null): Response
|
||||
{
|
||||
$configuration = $this->getConfiguration();
|
||||
|
||||
$this->prepareEntity($entity);
|
||||
|
||||
$form = $this->createForm($configuration->getForm('new'), $entity, $configuration->getFormOptions('new'));
|
||||
|
||||
if ($request->isMethod('POST')) {
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isValid()) {
|
||||
if (null !== $beforeCreate) {
|
||||
call_user_func_array($beforeCreate, [$entity, $form, $request]);
|
||||
}
|
||||
|
||||
$entityManager->create($entity);
|
||||
$this->addFlash('success', 'The data has been saved.');
|
||||
|
||||
return $this->redirectToRoute($configuration->getPageRoute('edit'), [
|
||||
'entity' => $entity->getId(),
|
||||
]);
|
||||
}
|
||||
$this->addFlash('warning', 'The form is not valid.');
|
||||
}
|
||||
|
||||
return $this->render($configuration->getView('new'), [
|
||||
'form' => $form->createView(),
|
||||
'configuration' => $configuration,
|
||||
'entity' => $entity,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function doShow(EntityInterface $entity): Response
|
||||
{
|
||||
$configuration = $this->getConfiguration();
|
||||
|
||||
return $this->render($configuration->getView('show'), [
|
||||
'entity' => $entity,
|
||||
'configuration' => $configuration,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function doEdit(EntityInterface $entity, EntityManager $entityManager, Request $request, callable $beforeUpdate = null): Response
|
||||
{
|
||||
$configuration = $this->getConfiguration();
|
||||
|
||||
$this->prepareEntity($entity);
|
||||
|
||||
$form = $this->createForm($configuration->getForm('edit'), $entity, $configuration->getFormOptions('edit'));
|
||||
|
||||
if ($request->isMethod('POST')) {
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isValid()) {
|
||||
if (null !== $beforeUpdate) {
|
||||
call_user_func_array($beforeUpdate, [$entity, $form, $request]);
|
||||
}
|
||||
|
||||
$entityManager->update($entity);
|
||||
$this->addFlash('success', 'The data has been saved.');
|
||||
|
||||
return $this->redirectToRoute($configuration->getPageRoute('edit'), [
|
||||
'entity' => $entity->getId(),
|
||||
]);
|
||||
}
|
||||
$this->addFlash('warning', 'The form is not valid.');
|
||||
}
|
||||
|
||||
return $this->render($configuration->getView('edit'), [
|
||||
'form' => $form->createView(),
|
||||
'configuration' => $configuration,
|
||||
'entity' => $entity,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function doSort(int $page = 1, RepositoryQuery $query, EntityManager $entityManager, Request $request, Session $session): Response
|
||||
{
|
||||
$configuration = $this->getConfiguration();
|
||||
$context = $request->query->get('context', 'index');
|
||||
|
||||
if (!$configuration->getIsSortableCollection($context)) {
|
||||
throw $this->createNotFoundException();
|
||||
}
|
||||
|
||||
$this->applySort($context, $query, $request);
|
||||
$this->updateFilters($request, $session);
|
||||
|
||||
$pager = $query
|
||||
->useFilters($this->filters)
|
||||
->paginate($page, $configuration->getMaxPerPage($context))
|
||||
;
|
||||
|
||||
if ($this->isCsrfTokenValid('sort', $request->query->get('_token'))) {
|
||||
$items = $request->request->get('items', []);
|
||||
$setter = 'set'.$configuration->getSortableCollectionProperty();
|
||||
$orderStart = ($page - 1) * $configuration->getMaxPerPage($context);
|
||||
|
||||
foreach ($pager as $key => $entity) {
|
||||
if (isset($items[$key + 1])) {
|
||||
$entity->{$setter}($items[$key + 1] + $orderStart);
|
||||
|
||||
$entityManager->update($entity);
|
||||
}
|
||||
}
|
||||
|
||||
$this->addFlash('success', 'The data has been saved.');
|
||||
} else {
|
||||
$this->addFlash('warning', 'The form is not valid.');
|
||||
}
|
||||
|
||||
return $this->json([]);
|
||||
}
|
||||
|
||||
protected function doBatch(int $page = 1, RepositoryQuery $query, EntityManager $entityManager, Request $request, Session $session): Response
|
||||
{
|
||||
$configuration = $this->getConfiguration();
|
||||
$datas = $request->request->get('batch', []);
|
||||
|
||||
$context = $datas['context'] ?? 'index';
|
||||
$target = $datas['target'] ?? null;
|
||||
$action = $datas['action'] ?? null;
|
||||
$token = $datas['_token'] ?? null;
|
||||
$items = $datas['items'] ?? [];
|
||||
$batchAction = $configuration->getBatchAction($context, $action);
|
||||
|
||||
if (empty($context) || empty($action) || empty($target)) {
|
||||
return $this->json([]);
|
||||
}
|
||||
|
||||
if (!$this->isCsrfTokenValid('batch', $token) || empty($batchAction)) {
|
||||
$this->addFlash('warning', 'The form is not valid.');
|
||||
|
||||
return $this->json([]);
|
||||
}
|
||||
|
||||
$callback = $batchAction['callback'];
|
||||
|
||||
$this->applySort($context, $query, $request);
|
||||
$this->updateFilters($request, $session);
|
||||
|
||||
$query->useFilters($this->filters);
|
||||
|
||||
if ('selection' === $target) {
|
||||
$isSelection = true;
|
||||
$pager = $query->paginate($page, $configuration->getMaxPerPage($context));
|
||||
} else {
|
||||
$isSelection = false;
|
||||
$pager = $query->find();
|
||||
}
|
||||
|
||||
foreach ($pager as $key => $entity) {
|
||||
if (($isSelection && isset($items[$key + 1])) || !$isSelection) {
|
||||
$callback($entity, $entityManager);
|
||||
}
|
||||
}
|
||||
|
||||
$this->addFlash('success', 'Batch action done.');
|
||||
|
||||
return $this->json([]);
|
||||
}
|
||||
|
||||
protected function doDelete(EntityInterface $entity, EntityManager $entityManager, Request $request, callable $beforeDelete = null): Response
|
||||
{
|
||||
$configuration = $this->getConfiguration();
|
||||
|
||||
if ($this->isCsrfTokenValid('delete'.$entity->getId(), $request->request->get('_token'))) {
|
||||
if (null !== $beforeDelete) {
|
||||
call_user_func($beforeDelete, $entity);
|
||||
}
|
||||
|
||||
$entityManager->delete($entity);
|
||||
|
||||
$this->addFlash('success', 'The data has been removed.');
|
||||
}
|
||||
|
||||
return $this->redirectToRoute($configuration->getPageRoute('index'));
|
||||
}
|
||||
|
||||
protected function doFilter(Session $session): Response
|
||||
{
|
||||
$configuration = $this->getConfiguration();
|
||||
$type = $configuration->getForm('filter');
|
||||
|
||||
if (null === $type) {
|
||||
throw $this->createNotFoundException();
|
||||
}
|
||||
|
||||
$form = $this->createForm($type);
|
||||
$form->submit($session->get($form->getName(), []));
|
||||
|
||||
return $this->render($configuration->getView('filter'), [
|
||||
'form' => $form->createView(),
|
||||
'configuration' => $configuration,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function updateFilters(Request $request, Session $session)
|
||||
{
|
||||
$configuration = $this->getConfiguration();
|
||||
$type = $configuration->getForm('filter');
|
||||
|
||||
if (null === $type) {
|
||||
return;
|
||||
}
|
||||
|
||||
$form = $this->createForm($type);
|
||||
|
||||
if ($request->query->has($form->getName())) {
|
||||
$filters = $request->query->get($form->getName());
|
||||
|
||||
if ('0' === $filters) {
|
||||
$filters = [];
|
||||
}
|
||||
} elseif ($session->has($form->getName())) {
|
||||
$filters = $session->get($form->getName());
|
||||
} else {
|
||||
$filters = [];
|
||||
}
|
||||
|
||||
$form->submit($filters);
|
||||
|
||||
if (empty($filters)) {
|
||||
$this->filters = $filters;
|
||||
$session->set($form->getName(), $filters);
|
||||
} elseif ($form->isValid()) {
|
||||
$this->filters = $form->getData();
|
||||
$session->set($form->getName(), $filters);
|
||||
}
|
||||
}
|
||||
|
||||
protected function prepareEntity(EntityInterface $entity)
|
||||
{
|
||||
$configuration = $this->getConfiguration();
|
||||
|
||||
if ($configuration->isI18n()) {
|
||||
foreach ($configuration->getLocales() as $locale) {
|
||||
$entity->addTranslation($entity->translate($locale, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function applySort(string $context, RepositoryQuery $query, Request $request)
|
||||
{
|
||||
$configuration = $this->getConfiguration();
|
||||
|
||||
if ($configuration->getIsSortableCollection($context)) {
|
||||
$query->orderBy(sprintf('.%s', $configuration->getSortableCollectionProperty()));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$defaultSort = $configuration->getDefaultSort($context);
|
||||
|
||||
$name = $request->query->get('_sort', $defaultSort['label'] ?? null);
|
||||
$direction = strtolower($request->query->get('_sort_direction', $defaultSort['direction'] ?? 'asc'));
|
||||
|
||||
if (!in_array($direction, ['asc', 'desc'])) {
|
||||
$direction = 'asc';
|
||||
}
|
||||
|
||||
foreach ($configuration->getFields($context) as $label => $field) {
|
||||
$sortOption = $field['options']['sort'] ?? null;
|
||||
|
||||
if (null === $sortOption) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($sortOption[0] !== $name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sorter = $sortOption[1];
|
||||
|
||||
if (is_string($sorter)) {
|
||||
$query->orderBy($sorter, $direction);
|
||||
} else {
|
||||
call_user_func_array($sorter, [$query, $direction]);
|
||||
}
|
||||
|
||||
$this->sort = [
|
||||
'label' => $label,
|
||||
'direction' => $direction,
|
||||
];
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,155 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Core\Controller\Auth;
|
||||
|
||||
use App\Core\Event\Account\PasswordRequestEvent;
|
||||
use App\Core\Manager\EntityManager;
|
||||
use App\Repository\UserRepository;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
||||
use Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
||||
use ZxcvbnPhp\Zxcvbn;
|
||||
|
||||
class AuthController extends AbstractController
|
||||
{
|
||||
protected array $coreParameters;
|
||||
|
||||
public function __construct(ParameterBagInterface $parameters)
|
||||
{
|
||||
$this->coreParameters = $parameters->get('core');
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/login", name="auth_login")
|
||||
*/
|
||||
public function login(AuthenticationUtils $authenticationUtils): Response
|
||||
{
|
||||
if ($this->getUser()) {
|
||||
return $this->redirectToRoute('admin_dashboard_index');
|
||||
}
|
||||
|
||||
$error = $authenticationUtils->getLastAuthenticationError();
|
||||
$lastUsername = $authenticationUtils->getLastUsername();
|
||||
|
||||
return $this->render('@Core/auth/login.html.twig', [
|
||||
'last_username' => $lastUsername,
|
||||
'error' => $error,
|
||||
'site_name' => $this->coreParameters['site']['name'],
|
||||
'site_logo' => $this->coreParameters['site']['logo'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/resetting/request", name="auth_resetting_request")
|
||||
*/
|
||||
public function requestResetting(Request $request, UserRepository $repository, EventDispatcherInterface $eventDispatcher): Response
|
||||
{
|
||||
if ($this->getUser()) {
|
||||
return $this->redirectToRoute('admin_dashboard_index');
|
||||
}
|
||||
|
||||
if ($request->isMethod('POST')) {
|
||||
$csrfToken = $request->request->get('_csrf_token');
|
||||
|
||||
if (!$this->isCsrfTokenValid('resetting_request', $csrfToken)) {
|
||||
throw $this->createAccessDeniedException();
|
||||
}
|
||||
|
||||
$username = trim((string) $request->request->get('username'));
|
||||
|
||||
if (!$username) {
|
||||
throw $this->createAccessDeniedException();
|
||||
}
|
||||
|
||||
$account = $repository->findOneByEmail($username);
|
||||
|
||||
if ($account) {
|
||||
$requestedAt = $account->getPasswordRequestedAt();
|
||||
|
||||
if (null === $requestedAt || $requestedAt->getTimestamp() < (time() - 3600 / 2)) {
|
||||
$eventDispatcher->dispatch(new PasswordRequestEvent($account), PasswordRequestEvent::EVENT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('@Core/auth/resetting_request.html.twig', [
|
||||
'email_sent' => $request->isMethod('POST'),
|
||||
'site_name' => $this->coreParameters['site']['name'],
|
||||
'site_logo' => $this->coreParameters['site']['logo'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/resetting/update/{token}", name="auth_resetting_update")
|
||||
*/
|
||||
public function requestUpdate(
|
||||
string $token,
|
||||
Request $request,
|
||||
UserRepository $repository,
|
||||
TokenGeneratorInterface $tokenGenerator,
|
||||
UserPasswordEncoderInterface $encoder,
|
||||
EntityManager $entityManager
|
||||
): Response {
|
||||
if ($this->getUser()) {
|
||||
return $this->redirectToRoute('admin_dashboard_index');
|
||||
}
|
||||
|
||||
$account = $repository->findOneByConfirmationToken($token);
|
||||
$passwordUpdated = false;
|
||||
$expired = true;
|
||||
|
||||
if ($account) {
|
||||
$requestedAt = $account->getPasswordRequestedAt();
|
||||
$expired = (null === $requestedAt || ($requestedAt->getTimestamp() < (time() - 3600 * 2)));
|
||||
}
|
||||
|
||||
if ($request->isMethod('POST') && !$expired) {
|
||||
$csrfToken = $request->request->get('_csrf_token');
|
||||
|
||||
if ($this->isCsrfTokenValid('resetting_update', $csrfToken)) {
|
||||
$password = $request->request->get('password');
|
||||
$password2 = $request->request->get('password2');
|
||||
|
||||
$zxcvbn = new Zxcvbn();
|
||||
$strength = $zxcvbn->passwordStrength($password, []);
|
||||
|
||||
if (4 === $strength['score'] && $password === $password2) {
|
||||
$account
|
||||
->setPassword($encoder->encodePassword(
|
||||
$account,
|
||||
$password
|
||||
))
|
||||
->setConfirmationToken($tokenGenerator->generateToken())
|
||||
->setPasswordRequestedAt(new \DateTime('now'))
|
||||
;
|
||||
|
||||
$entityManager->update($account);
|
||||
|
||||
$passwordUpdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('@Core/auth/resetting_update.html.twig', [
|
||||
'password_updated' => $passwordUpdated,
|
||||
'token' => $token,
|
||||
'expired' => $expired,
|
||||
'site_name' => $this->coreParameters['site']['name'],
|
||||
'site_logo' => $this->coreParameters['site']['logo'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/logout", name="auth_logout")
|
||||
*/
|
||||
public function logout()
|
||||
{
|
||||
throw new \Exception('This method can be blank - it will be intercepted by the logout key on your firewall');
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Core\Controller\Dashboard;
|
||||
|
||||
use App\Core\Controller\Admin\AdminController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* @Route("/admin")
|
||||
*/
|
||||
class DashboardAdminController extends AdminController
|
||||
{
|
||||
/**
|
||||
* @Route("/", name="admin_dashboard_index")
|
||||
*/
|
||||
public function index(): Response
|
||||
{
|
||||
return $this->render('@Core/dashboard/index.html.twig', [
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getSection(): string
|
||||
{
|
||||
return 'dashboard';
|
||||
}
|
||||
}
|
|
@ -1,363 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Core\Controller\FileManager;
|
||||
|
||||
use App\Core\Controller\Admin\AdminController;
|
||||
use App\Core\FileManager\FsFileManager;
|
||||
use App\Core\Form\FileManager\DirectoryCreateType;
|
||||
use App\Core\Form\FileManager\DirectoryRenameType;
|
||||
use App\Core\Form\FileManager\FileInformationType;
|
||||
use App\Core\Form\FileManager\FileUploadType;
|
||||
use App\Core\Manager\EntityManager;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* @Route("/admin/file_manager")
|
||||
*/
|
||||
class FileManagerAdminController extends AdminController
|
||||
{
|
||||
/**
|
||||
* @Route("/", name="admin_file_manager_index")
|
||||
*/
|
||||
public function index(): Response
|
||||
{
|
||||
return $this->render('@Core/file_manager/index.html.twig');
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/api/directory", name="admin_file_manager_api_directory", options={"expose"=true})
|
||||
*/
|
||||
public function directory(FsFileManager $manager, Request $request): Response
|
||||
{
|
||||
$options = [
|
||||
'sort' => $request->query->get('_sort', 'name'),
|
||||
'sort_direction' => $request->query->get('_sort_direction', 'asc'),
|
||||
];
|
||||
|
||||
$files = $manager->list($request->query->get('directory', '/'), $options);
|
||||
|
||||
return $this->json($files);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/info/{tab}/{context}/{ajax}", name="admin_file_manager_info", options={"expose"=true})
|
||||
*/
|
||||
public function info(
|
||||
FsFileManager $manager,
|
||||
Request $request,
|
||||
EntityManager $entityManager,
|
||||
TranslatorInterface $translator,
|
||||
string $context = 'crud',
|
||||
string $tab = 'information',
|
||||
bool $ajax = false
|
||||
): Response {
|
||||
$splInfo = $manager->getSplInfo($request->query->get('file'));
|
||||
|
||||
if (!$splInfo) {
|
||||
throw $this->createNotFoundException();
|
||||
}
|
||||
|
||||
$fileInfo = $manager->getFileInformation($request->query->get('file'));
|
||||
$path = $manager->getPathUri().'/'.$splInfo->getRelativePathname();
|
||||
|
||||
$form = $this->createForm(FileInformationType::class, $fileInfo);
|
||||
|
||||
if ($request->isMethod('POST')) {
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isValid()) {
|
||||
$entityManager->update($fileInfo);
|
||||
|
||||
if (!$request->isXmlHttpRequest()) {
|
||||
$this->addFlash('success', 'The data has been saved.');
|
||||
} else {
|
||||
return $this->json([
|
||||
'_error' => 0,
|
||||
'_message' => $translator->trans('The data has been saved.'),
|
||||
'_level' => 'success',
|
||||
'_dispatch' => 'file_manager.info.update.success',
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
if (!$request->isXmlHttpRequest()) {
|
||||
$this->addFlash('warning', 'The form is not valid.');
|
||||
} else {
|
||||
return $this->json([
|
||||
'_error' => 1,
|
||||
'_message' => $translator->trans('The form is not valid.'),
|
||||
'_level' => 'warning',
|
||||
'_dispatch' => 'file_manager.info.update.error',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('admin_file_manager_index', [
|
||||
'data-modal' => $this->generateUrl('admin_file_manager_info', [
|
||||
'file' => $request->query->get('file'),
|
||||
'tab' => 'attributes',
|
||||
]),
|
||||
'path' => $splInfo->getRelativePath(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->render('@Core/file_manager/info.html.twig', [
|
||||
'splInfo' => $splInfo,
|
||||
'path' => $path,
|
||||
'isLocked' => $manager->isLocked($splInfo->getRelativePathname()),
|
||||
'tab' => $tab,
|
||||
'form' => $form->createView(),
|
||||
'context' => $context,
|
||||
'ajax' => $ajax,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/directory/new/{ajax}", name="admin_file_manager_directory_new", options={"expose"=true}, methods={"GET", "POST"})
|
||||
*/
|
||||
public function directoryNew(FsFileManager $manager, Request $request, TranslatorInterface $translator, bool $ajax = false): Response
|
||||
{
|
||||
$splInfo = $manager->getSplInfo($request->query->get('file'));
|
||||
|
||||
if (!$splInfo) {
|
||||
throw $this->createNotFoundException();
|
||||
}
|
||||
|
||||
if (!$splInfo->isDir()) {
|
||||
throw $this->createNotFoundException();
|
||||
}
|
||||
|
||||
if ($manager->isLocked($request->query->get('file'))) {
|
||||
return $this->render('@Core/file_manager/directory_new.html.twig', [
|
||||
'locked' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
$form = $this->createForm(DirectoryCreateType::class);
|
||||
|
||||
if ($request->isMethod('POST')) {
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isValid()) {
|
||||
$status = $manager->createDirectory($form->get('name')->getData(), $request->query->get('file'));
|
||||
|
||||
if (true === $status) {
|
||||
if (!$request->isXmlHttpRequest()) {
|
||||
$this->addFlash('success', 'Directory created.');
|
||||
} else {
|
||||
return $this->json([
|
||||
'_error' => 0,
|
||||
'_message' => $translator->trans('Directory created.'),
|
||||
'_level' => 'success',
|
||||
'_dispatch' => 'file_manager.directory.new.success',
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
if (!$request->isXmlHttpRequest()) {
|
||||
$this->addFlash('warning', 'Directory not created.');
|
||||
} else {
|
||||
return $this->json([
|
||||
'_error' => 1,
|
||||
'_message' => $translator->trans('Directory not created.'),
|
||||
'_level' => 'warning',
|
||||
'_dispatch' => 'file_manager.directory.new.error',
|
||||
]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$this->addFlash('warning', 'Unauthorized char(s).');
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('admin_file_manager_index', [
|
||||
'path' => $splInfo->getRelativePathname(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->render('@Core/file_manager/directory_new.html.twig', [
|
||||
'form' => $form->createView(),
|
||||
'file' => $request->query->get('file'),
|
||||
'ajax' => $ajax,
|
||||
'locked' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/directory/rename/{ajax}", name="admin_file_manager_directory_rename", methods={"GET", "POST"})
|
||||
*/
|
||||
public function directoryRename(FsFileManager $manager, Request $request, TranslatorInterface $translator, bool $ajax = false): Response
|
||||
{
|
||||
$splInfo = $manager->getSplInfo($request->query->get('file'));
|
||||
|
||||
if (!$splInfo) {
|
||||
throw $this->createNotFoundException();
|
||||
}
|
||||
|
||||
if (!$splInfo->isDir()) {
|
||||
throw $this->createNotFoundException();
|
||||
}
|
||||
|
||||
if ($manager->isLocked($request->query->get('file'))) {
|
||||
return $this->render('@Core/file_manager/directory_rename.html.twig', [
|
||||
'locked' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
$form = $this->createForm(DirectoryRenameType::class);
|
||||
|
||||
if ($request->isMethod('POST')) {
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isValid()) {
|
||||
$status = $manager->renameDirectory($form->get('name')->getData(), $request->query->get('file'));
|
||||
|
||||
if (true === $status) {
|
||||
if (!$request->isXmlHttpRequest()) {
|
||||
$this->addFlash('success', 'Directory renamed.');
|
||||
} else {
|
||||
return $this->json([
|
||||
'_error' => 0,
|
||||
'_message' => $translator->trans('Directory renamed.'),
|
||||
'_level' => 'success',
|
||||
'_dispatch' => 'file_manager.directory.rename.success',
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
if (!$request->isXmlHttpRequest()) {
|
||||
$this->addFlash('warning', 'Directory not renamed.');
|
||||
} else {
|
||||
return $this->json([
|
||||
'_error' => 1,
|
||||
'_message' => $translator->trans('Directory not renamed.'),
|
||||
'_level' => 'warning',
|
||||
'_dispatch' => 'file_manager.directory.rename.error',
|
||||
]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$this->addFlash('warning', 'Unauthorized char(s).');
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('admin_file_manager_index', [
|
||||
'path' => $splInfo->getRelativePath(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->render('@Core/file_manager/directory_rename.html.twig', [
|
||||
'form' => $form->createView(),
|
||||
'file' => $request->query->get('file'),
|
||||
'locked' => false,
|
||||
'ajax' => $ajax,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/upload/{ajax}", name="admin_file_manager_upload", options={"expose"=true}, methods={"GET", "POST"})
|
||||
*/
|
||||
public function upload(FsFileManager $manager, Request $request, TranslatorInterface $translator, bool $ajax = false): Response
|
||||
{
|
||||
$splInfo = $manager->getSplInfo($request->query->get('file'));
|
||||
|
||||
if (!$splInfo) {
|
||||
throw $this->createNotFoundException();
|
||||
}
|
||||
|
||||
if (!$splInfo->isDir()) {
|
||||
throw $this->createAccessDeniedException();
|
||||
}
|
||||
|
||||
if ($manager->isLocked($request->query->get('file'))) {
|
||||
return $this->render('@Core/file_manager/upload.html.twig', [
|
||||
'locked' => true,
|
||||
]);
|
||||
}
|
||||
$form = $this->createForm(FileUploadType::class, null, [
|
||||
'mimes' => $manager->getMimes(),
|
||||
]);
|
||||
|
||||
if ($request->isMethod('POST')) {
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isValid()) {
|
||||
if ($form->get('files')->getData()) {
|
||||
$manager->upload(
|
||||
$form->get('files')->getData(),
|
||||
$request->query->get('file')
|
||||
);
|
||||
}
|
||||
|
||||
if ($form->get('directory')->getData()) {
|
||||
$manager->upload(
|
||||
$form->get('directory')->getData(),
|
||||
$request->query->get('file'),
|
||||
$_FILES['file_upload']['full_path']['directory'] ?? []
|
||||
);
|
||||
}
|
||||
|
||||
if (!$request->isXmlHttpRequest()) {
|
||||
$this->addFlash('success', 'Files uploaded.');
|
||||
} else {
|
||||
return $this->json([
|
||||
'_error' => 0,
|
||||
'_message' => $translator->trans('Files uploaded.'),
|
||||
'_level' => 'success',
|
||||
'_dispatch' => 'file_manager.file.new.success',
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
if (!$request->isXmlHttpRequest()) {
|
||||
$this->addFlash('warning', 'Unauthorized file type(s).');
|
||||
} else {
|
||||
return $this->json([
|
||||
'_error' => 1,
|
||||
'_message' => $translator->trans('Unauthorized file type(s).'),
|
||||
'_level' => 'warning',
|
||||
'_dispatch' => 'file_manager.file.new.error',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('admin_file_manager_index', [
|
||||
'path' => $splInfo->getRelativePathname(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->render('@Core/file_manager/upload.html.twig', [
|
||||
'form' => $form->createView(),
|
||||
'file' => $request->query->get('file'),
|
||||
'locked' => false,
|
||||
'ajax' => $ajax,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/delete", name="admin_file_manager_delete", methods={"DELETE"})
|
||||
*/
|
||||
public function delete(FsFileManager $manager, Request $request): Response
|
||||
{
|
||||
$path = $request->request->get('file');
|
||||
$splInfo = $manager->getSplInfo($request->request->get('file'));
|
||||
|
||||
if (!$splInfo) {
|
||||
throw $this->createNotFoundException();
|
||||
}
|
||||
|
||||
if ($this->isCsrfTokenValid('delete', $request->request->get('_token'))) {
|
||||
if ($manager->delete($path)) {
|
||||
$this->addFlash('success', 'The data has been removed.');
|
||||
} else {
|
||||
$this->addFlash('warning', 'The data has not been removed.');
|
||||
}
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('admin_file_manager_index', [
|
||||
'path' => $splInfo->getRelativePath(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getSection(): string
|
||||
{
|
||||
return 'file_manager';
|
||||
}
|
||||
}
|
|
@ -1,154 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Core\Controller\Redirect;
|
||||
|
||||
use App\Core\Controller\Admin\Crud\CrudController;
|
||||
use App\Core\Crud\CrudConfiguration;
|
||||
use App\Core\Crud\Field;
|
||||
use App\Core\Entity\EntityInterface;
|
||||
use App\Core\Entity\Redirect as Entity;
|
||||
use App\Core\Factory\RedirectFactory as Factory;
|
||||
use App\Core\Form\Filter\RedirectFilterType as FilterType;
|
||||
use App\Core\Form\RedirectType as Type;
|
||||
use App\Core\Manager\EntityManager;
|
||||
use App\Core\Repository\RedirectRepositoryQuery as RepositoryQuery;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Session\Session;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
class RedirectAdminController extends CrudController
|
||||
{
|
||||
/**
|
||||
* @Route("/admin/redirect/{page}", name="admin_redirect_index", methods={"GET"}, requirements={"page":"\d+"})
|
||||
*/
|
||||
public function index(RepositoryQuery $query, Request $request, Session $session, int $page = 1): Response
|
||||
{
|
||||
return $this->doIndex($page, $query, $request, $session);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/admin/redirect/new", name="admin_redirect_new", methods={"GET", "POST"})
|
||||
*/
|
||||
public function new(Factory $factory, EntityManager $entityManager, Request $request): Response
|
||||
{
|
||||
return $this->doNew($factory->create(), $entityManager, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/admin/redirect/show/{entity}", name="admin_redirect_show", methods={"GET"})
|
||||
*/
|
||||
public function show(Entity $entity): Response
|
||||
{
|
||||
return $this->doShow($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/admin/redirect/filter", name="admin_redirect_filter", methods={"GET"})
|
||||
*/
|
||||
public function filter(Session $session): Response
|
||||
{
|
||||
return $this->doFilter($session);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/admin/redirect/edit/{entity}", name="admin_redirect_edit", methods={"GET", "POST"})
|
||||
*/
|
||||
public function edit(Entity $entity, EntityManager $entityManager, Request $request): Response
|
||||
{
|
||||
return $this->doEdit($entity, $entityManager, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/admin/redirect/sort/{page}", name="admin_redirect_sort", methods={"POST"}, requirements={"page":"\d+"})
|
||||
*/
|
||||
public function sort(RepositoryQuery $query, EntityManager $entityManager, Request $request, Session $session, int $page = 1): Response
|
||||
{
|
||||
return $this->doSort($page, $query, $entityManager, $request, $session);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/admin/redirect/batch/{page}", name="admin_redirect_batch", methods={"POST"}, requirements={"page":"\d+"})
|
||||
*/
|
||||
public function batch(RepositoryQuery $query, EntityManager $entityManager, Request $request, Session $session, int $page = 1): Response
|
||||
{
|
||||
return $this->doBatch($page, $query, $entityManager, $request, $session);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/admin/redirect/delete/{entity}", name="admin_redirect_delete", methods={"DELETE"})
|
||||
*/
|
||||
public function delete(Entity $entity, EntityManager $entityManager, Request $request): Response
|
||||
{
|
||||
return $this->doDelete($entity, $entityManager, $request);
|
||||
}
|
||||
|
||||
protected function getConfiguration(): CrudConfiguration
|
||||
{
|
||||
return CrudConfiguration::create()
|
||||
->setPageTitle('index', 'Redirects')
|
||||
->setPageTitle('edit', '{label}')
|
||||
->setPageTitle('new', 'New redirect')
|
||||
|
||||
->setPageRoute('index', 'admin_redirect_index')
|
||||
->setPageRoute('new', 'admin_redirect_new')
|
||||
->setPageRoute('edit', 'admin_redirect_edit')
|
||||
->setPageRoute('sort', 'admin_redirect_sort')
|
||||
->setPageRoute('batch', 'admin_redirect_batch')
|
||||
->setPageRoute('delete', 'admin_redirect_delete')
|
||||
->setPageRoute('filter', 'admin_redirect_filter')
|
||||
|
||||
->setForm('edit', Type::class, [])
|
||||
->setForm('new', Type::class)
|
||||
->setForm('filter', FilterType::class)
|
||||
|
||||
->setView('form', '@Core/redirect/redirect_admin/_form.html.twig')
|
||||
->setMaxPerPage('index', 100)
|
||||
->setIsSortableCollection('index', true)
|
||||
|
||||
->setAction('index', 'show', false)
|
||||
->setAction('edit', 'show', false)
|
||||
|
||||
->setField('index', 'Label', Field\TextField::class, [
|
||||
'property' => 'label',
|
||||
'attr' => ['class' => 'col-4'],
|
||||
])
|
||||
->setField('index', 'Rule', Field\TextField::class, [
|
||||
'view' => '@Core/redirect/redirect_admin/field/rule.html.twig',
|
||||
'attr' => ['class' => 'col-4'],
|
||||
])
|
||||
->setField('index', 'Enabled', Field\ButtonField::class, [
|
||||
'property_builder' => function(EntityInterface $entity) {
|
||||
return $entity->getIsEnabled() ? 'Yes' : 'No';
|
||||
},
|
||||
'attr' => ['class' => 'col-2'],
|
||||
'button_attr_builder' => function(EntityInterface $entity) {
|
||||
return ['class' => 'btn btn-sm btn-'.($entity->getIsEnabled() ? 'success' : 'primary')];
|
||||
},
|
||||
])
|
||||
->setField('index', 'Type', Field\ButtonField::class, [
|
||||
'property' => 'redirectCode',
|
||||
'attr' => ['class' => 'col-2'],
|
||||
'button_attr' => ['class' => 'btn btn-sm btn-light border-secondary font-weight-bold'],
|
||||
])
|
||||
->setBatchAction('index', 'enable', 'Enable', function (EntityInterface $entity, EntityManager $manager) {
|
||||
$entity->setIsEnabled(true);
|
||||
|
||||
$manager->update($entity);
|
||||
})
|
||||
->setBatchAction('index', 'disable', 'Disable', function (EntityInterface $entity, EntityManager $manager) {
|
||||
$entity->setIsEnabled(false);
|
||||
|
||||
$manager->update($entity);
|
||||
})
|
||||
->setBatchAction('index', 'delete', 'Delete', function (EntityInterface $entity, EntityManager $manager) {
|
||||
$manager->delete($entity);
|
||||
})
|
||||
;
|
||||
}
|
||||
|
||||
protected function getSection(): string
|
||||
{
|
||||
return 'site_navigation';
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue