Compare commits

...

264 commits

Author SHA1 Message Date
Simon Vieille 79d549254f Merge branch 'develop'
All checks were successful
ci/woodpecker/push/woodpecker/2 Pipeline was successful
ci/woodpecker/push/woodpecker/1 Pipeline was successful
2024-03-25 16:00:53 +01:00
Simon Vieille c79e96e291
apply consensus as access decision manager strategy
All checks were successful
ci/woodpecker/push/woodpecker/2 Pipeline was successful
ci/woodpecker/push/woodpecker/1 Pipeline was successful
2024-03-25 16:00:51 +01:00
Simon Vieille 092b490ae2 Merge branch 'develop'
Some checks failed
ci/woodpecker/push/woodpecker/1 Pipeline failed
ci/woodpecker/push/woodpecker/2 Pipeline was successful
2024-03-25 15:46:57 +01:00
Simon Vieille fcecf74f90
add default voter
Some checks failed
ci/woodpecker/push/woodpecker/2 Pipeline failed
ci/woodpecker/push/woodpecker/1 Pipeline failed
2024-03-25 15:46:53 +01:00
Simon Vieille c00723a246 Merge branch 'develop'
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/tag/woodpecker Pipeline failed
ci/woodpecker/push/woodpecker/2 Pipeline was successful
ci/woodpecker/push/woodpecker/1 Pipeline was successful
2023-11-01 16:34:57 +01:00
Simon Vieille e81b11c5ec
release v1.23.0
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-11-01 16:34:54 +01:00
Simon Vieille af2212ac8c Merge branch 'develop'
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-11-01 16:33:49 +01:00
Simon Vieille b57b0e3770
update murph
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-11-01 16:33:44 +01:00
Simon Vieille ba76a83163 Merge branch 'develop' 2023-09-28 18:14:35 +02:00
Simon Vieille 2874f579dc
update changelog 2023-09-28 18:14:22 +02:00
Simon Vieille 287e1d66cb
fix #1: add UniqueEntity constraint in the User entity 2023-09-28 18:13:18 +02:00
Simon Vieille b23a4fe9c5
update changelog 2023-09-07 14:11:11 +02:00
Simon Vieille 59bbd24b76
update woodpecker ci base file 2023-09-07 14:10:26 +02:00
Simon Vieille 2dfe6ef1ca Merge branch 'develop' 2023-08-11 09:48:53 +02:00
Simon Vieille 66f82ecc13
release v1.21.0 2023-08-11 09:48:50 +02:00
Simon Vieille 8432fc123e Merge branch 'develop' 2023-07-27 18:17:48 +02:00
Simon Vieille 627927a11e
update murph 2023-07-27 18:17:15 +02:00
Simon Vieille 67871179a8
update murph 2023-07-27 18:16:36 +02:00
Simon Vieille dbec7a884e
update changelog 2023-07-20 09:47:12 +02:00
Simon Vieille 8c6ad05cbd
add chdir in the console entrypoint 2023-07-20 09:21:17 +02:00
Simon Vieille cc0296e6e2
fix user crud controllers 2023-07-19 21:29:52 +02:00
Simon Vieille 4f94bd5640
add user admin controller and simples views in default files 2023-07-19 21:24:15 +02:00
Simon Vieille 9d8d6b510a Merge branch 'develop'
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2023-04-15 10:53:11 +02:00
Simon Vieille edb51485e1
release v1.19.0
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-15 10:53:04 +02:00
Simon Vieille c596f1d62d
add image/webp in default configuration documentation
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-03-02 19:11:11 +01:00
Simon Vieille 813aa8c618
feat(upgrade): yarn lock 2023-01-25 20:55:14 +01:00
Simon Vieille 39e92e949a Merge branch 'develop'
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/tag/woodpecker Pipeline was successful
2023-01-13 18:27:52 +01:00
Simon Vieille e550bc54e2
release v1.18.0
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-01-13 18:27:44 +01:00
Simon Vieille ab808ee511
ci(conf): add ci
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-01-13 18:14:30 +01:00
Simon Vieille 1e8baa5ee7 Merge branch 'feature/updates' into develop 2023-01-08 20:55:51 +01:00
Simon Vieille 7f5fcc98d7
feat(dep): update composer.json
remove symfony/flex

update murph/murph-core
2023-01-08 20:55:46 +01:00
Simon Vieille 0f4767b8e0
fix(config): fix firewall config
add entry_point

remove anonymous
2023-01-08 20:53:34 +01:00
Simon Vieille 70bac192e2
fix(config): fix typo in 2fa conf 2023-01-08 20:52:55 +01:00
Simon Vieille 2b72994cde
feat(update): apply recipes:update scheb/2fa-bundle 2023-01-08 19:27:58 +01:00
Simon Vieille 74d12bba3b
feat(update): apply recipes:update symfony/webpack-encore-bundle 2023-01-08 19:26:29 +01:00
Simon Vieille 1cacfd0bb7
feat(update): apply recipes:update symfony/web-profiler-bundle 2023-01-08 19:25:03 +01:00
Simon Vieille 740f90294d
feat(update): apply recipes:update symfony/validator 2023-01-08 19:24:49 +01:00
Simon Vieille da12738b94
feat(update): apply recipes:update symfony/twig-bundle 2023-01-08 19:24:37 +01:00
Simon Vieille 089cd2bb85
feat(update): apply recipes:update symfony/translation 2023-01-08 19:23:15 +01:00
Simon Vieille b22db21ea4
feat(update): apply recipes:update symfony/security-bundle 2023-01-08 19:22:59 +01:00
Simon Vieille b9b6d968aa
feat(update): apply recipes:update symfony/routing 2023-01-08 19:21:15 +01:00
Simon Vieille b8dd7aa3bf
feat(update): apply recipes:update symfony/monolog-bundle 2023-01-08 19:20:56 +01:00
Simon Vieille 6a56bc2c36
feat(update): apply recipes:update symfony/framework-bundle 2023-01-08 19:20:22 +01:00
Simon Vieille 1abc6a7b60
feat(update): apply recipes:update symfony/mailer 2023-01-08 19:18:18 +01:00
Simon Vieille a9c64637ec
feat(update): apply recipes:update symfony/flex 2023-01-08 19:17:22 +01:00
Simon Vieille 72299ba2a5
feat(update): apply recipes:update symfony/debug-bundle 2023-01-08 19:16:59 +01:00
Simon Vieille a2ce7ed609
feat(update): apply recipes:update symfony/console 2023-01-08 19:16:48 +01:00
Simon Vieille 41abaa20d8
feat(update): apply recipes:update symfony/apache-pack 2023-01-08 19:16:12 +01:00
Simon Vieille f7e374cebc
feat(update): apply recipes:update stof/doctrine-extensions-bundle 2023-01-08 19:15:53 +01:00
Simon Vieille c7e3867a93
feat(update): apply recipes:update liip/imagine-bundle 2023-01-08 19:15:02 +01:00
Simon Vieille 1366e92ca0
feat(update): apply recipes:update doctrine/doctrine-migrations-bundle 2023-01-08 19:14:36 +01:00
Simon Vieille 301f22bb4a
feat(update): apply recipes:update doctrine/doctrine-bundle 2023-01-08 19:13:47 +01:00
Simon Vieille 7397a1cde8
feat(test): apply new recipe for phpunit 2023-01-08 19:09:12 +01:00
Simon Vieille e232e44dcd
chore(git): add ignored files
phpunit files
2023-01-08 19:07:35 +01:00
Simon Vieille edb8de9800
feat(test): add database for tests 2023-01-08 19:06:56 +01:00
Simon Vieille d0eb452847
feat(dep): update dependencies 2023-01-08 19:06:38 +01:00
Simon Vieille 75a29b6372 Merge branch 'develop' 2022-11-19 20:35:43 +01:00
Simon Vieille 9bd1ff142b
add documentation 2022-11-19 20:35:30 +01:00
Simon Vieille 69ad422624 Merge branch 'develop' 2022-11-19 19:57:09 +01:00
Simon Vieille e0af777a34
release v1.17.0 2022-11-19 19:57:05 +01:00
Simon Vieille 310c5076a0 Merge branch 'develop' 2022-11-19 19:54:16 +01:00
Simon Vieille 8fcc6faebb
release v1.17.0 2022-11-19 19:54:06 +01:00
Simon Vieille 65b3583411 Merge branch 'develop' 2022-11-19 19:48:32 +01:00
Simon Vieille 58de4e8908
replace annotation with attributes 2022-11-19 19:46:03 +01:00
Simon Vieille 450972ca1e
add encore from node_modules in npm scripts 2022-11-19 19:18:08 +01:00
Simon Vieille a0b4137fb3 Merge branch 'develop' 2022-09-06 12:08:26 +02:00
Simon Vieille 05506fb5db
update murph 2022-09-06 12:07:58 +02:00
Simon Vieille ab14728bec Merge branch 'develop' 2022-09-06 12:02:29 +02:00
Simon Vieille 21cec0647a
update changelog 2022-09-06 12:02:20 +02:00
Simon Vieille c364cafd17 Merge branch 'develop' 2022-06-12 19:07:59 +02:00
Simon Vieille 673132adf2
add meta description in base.html.twig 2022-06-12 19:07:55 +02:00
Simon Vieille 58b5252986 Merge branch 'develop' 2022-05-20 14:24:06 +02:00
Simon Vieille 674846ab53
update services 2022-05-20 14:23:53 +02:00
Simon Vieille 5f942a904b Merge branch 'develop' 2022-05-14 11:32:17 +02:00
Simon Vieille 5ad22f21d2
create an empty directory of core in templates 2022-05-14 11:31:47 +02:00
Simon Vieille be7003d613
remove core directory from template 2022-05-14 11:30:09 +02:00
Simon Vieille 325401f37d Merge branch 'develop' 2022-05-13 22:42:12 +02:00
Simon Vieille 6addbc7de4
add a admin dashboard controller 2022-05-13 22:42:06 +02:00
Simon Vieille bda7a4a195 Merge branch 'develop' 2022-05-09 14:58:43 +02:00
Simon Vieille a79011c276
update changelog 2022-05-09 14:58:37 +02:00
Simon Vieille 1113c0a91f Merge branch 'develop' 2022-05-09 14:55:33 +02:00
Simon Vieille cbf05e3ce3
update changelog 2022-05-09 14:55:31 +02:00
Simon Vieille 2ecb972a79
upgrade yarn.lock 2022-05-09 14:54:41 +02:00
Simon Vieille b26738fe49 Merge branch 'develop' 2022-05-05 16:41:05 +02:00
Simon Vieille 69e2e919de
add grapesjs-component-code-editor and grapesjs-parser-postcss symlinks 2022-05-05 16:40:59 +02:00
Simon Vieille c4c5aa867a Merge branch 'develop' 2022-05-05 14:41:21 +02:00
Simon Vieille 5b68c61cc0
upgrade murph 2022-05-05 14:41:17 +02:00
Simon Vieille 7052f2516d
release v1.14.3 2022-04-30 12:37:12 +02:00
Simon Vieille 52be00fc77 Merge branch 'develop' 2022-04-30 12:34:23 +02:00
Simon Vieille 3626a65f1f
release v1.14.2 2022-04-30 12:34:16 +02:00
Simon Vieille 70cae6f7a0
upgrade murph 2022-04-30 12:33:19 +02:00
Simon Vieille 5f1111d2a0 Merge branch 'develop' 2022-04-25 21:08:48 +02:00
Simon Vieille 1ee868b354
update changelog 2022-04-25 21:08:44 +02:00
Simon Vieille 2307efa341
add blocks in default template 2022-04-25 21:08:25 +02:00
Simon Vieille f19fdb0ecf Merge branch 'develop' 2022-04-22 15:29:09 +02:00
Simon Vieille 4ce765ed72
add security policy 2022-04-22 15:29:05 +02:00
Simon Vieille 3c52a00bc4 Merge branch 'develop' 2022-04-20 15:32:54 +02:00
Simon Vieille cd88d7626a
upgrade yarn.lock 2022-04-20 15:32:50 +02:00
Simon Vieille 3c274a14b9 Merge branch 'develop' 2022-04-20 15:28:33 +02:00
Simon Vieille 371a9f8bb6
update changelog 2022-04-20 15:28:30 +02:00
Simon Vieille bb62d1ce78 Merge branch 'develop' 2022-04-20 15:27:42 +02:00
Simon Vieille b043e42b96
fix missing envvar in makefile (npm) 2022-04-20 15:27:29 +02:00
Simon Vieille 2a91b5917f Merge branch 'develop' 2022-04-20 14:51:52 +02:00
Simon Vieille 2e9b942024
upgrade murph 2022-04-20 14:51:50 +02:00
Simon Vieille 22d7389183 Merge branch 'develop' 2022-04-20 14:48:01 +02:00
Simon Vieille d0e187746a
upgrade murph 2022-04-20 14:47:58 +02:00
Simon Vieille 933e360e26 Merge branch 'develop' 2022-04-17 18:32:39 +02:00
Simon Vieille c4bc470e8c
upgrade murph 2022-04-17 18:32:36 +02:00
Simon Vieille 9dd0f7376b Merge branch 'develop' 2022-03-26 16:19:12 +01:00
Simon Vieille 52536ccefb update yarn.lock 2022-03-26 16:19:10 +01:00
Simon Vieille f240db7ba4 Merge branch 'develop' 2022-03-26 16:13:40 +01:00
Simon Vieille aa808d07e8 upgrade murph/murph-core 2022-03-26 16:13:37 +01:00
Simon Vieille bb01163e84 Merge branch 'develop' 2022-03-22 15:01:33 +01:00
Simon Vieille 1dbed2d52f upgrade murph/murph-core 2022-03-22 15:01:28 +01:00
Simon Vieille 489a0411a0 update changelog 2022-03-22 15:00:48 +01:00
Simon Vieille 2a5d0ae1f4 Merge branch 'develop' 2022-03-21 11:21:22 +01:00
Simon Vieille 2adff5f583 update changelog 2022-03-21 11:21:20 +01:00
Simon Vieille b40c77d7a3 use murph-npm to install npm requirements 2022-03-21 11:20:17 +01:00
Simon Vieille 27bccc9acd Merge branch 'develop' 2022-03-17 10:59:41 +01:00
Simon Vieille be31af32bc update changelog, murph version, upgrade doc 2022-03-17 10:59:39 +01:00
Simon Vieille 246c4621c0 Merge branch 'develop' 2022-03-16 12:51:55 +01:00
Simon Vieille 4d235c4ec5 update changelog 2022-03-16 12:51:53 +01:00
Simon Vieille 2be2da285a remove useless env var from makefile 2022-03-16 12:51:44 +01:00
Simon Vieille 1498ab5308 update dashboard template 2022-03-16 12:51:22 +01:00
Simon Vieille 68c6ee8081 Merge branch 'develop' 2022-03-14 10:47:31 +01:00
Simon Vieille bec4edfa45 upgrade murph/murph-core 2022-03-14 10:47:29 +01:00
Simon Vieille 7623546c10 Merge branch 'develop' 2022-03-14 10:14:34 +01:00
Simon Vieille 4cbd601476 backports murph-core 2022-03-14 10:14:31 +01:00
Simon Vieille 63237d50e7 Merge branch 'develop' 2022-03-14 10:13:27 +01:00
Simon Vieille 2d8637617b upgrade murph/murph-core 2022-03-14 10:13:23 +01:00
Simon Vieille 81d1fa9cad Merge branch 'develop' 2022-03-13 21:17:03 +01:00
Simon Vieille 47b83ee968 update upgrade doc 2022-03-13 21:17:01 +01:00
Simon Vieille 6086ceaf35 Merge branch 'develop' 2022-03-13 21:14:14 +01:00
Simon Vieille 94e2fc966d update changelog 2022-03-13 21:14:04 +01:00
Simon Vieille b809ee9c88 use murph/murph-core instead of the core directory 2022-03-13 21:12:20 +01:00
Simon Vieille a3ef3540c1 Merge branch 'develop' 2022-03-13 12:20:11 +01:00
Simon Vieille 47e6f06bd7 add default value for security operator 2022-03-13 12:20:04 +01:00
Simon Vieille a3a1822339 Merge branch 'develop' 2022-03-11 15:24:35 +01:00
Simon Vieille 7523f479d5 change apparence of murph version 2022-03-11 15:24:33 +01:00
Simon Vieille 59d5fed75e Merge branch 'develop' 2022-03-11 09:49:45 +01:00
Simon Vieille 4e1003159c update changelog and murph version 2022-03-11 09:49:25 +01:00
Simon Vieille fd36561cf1 add murph version in admin ui 2022-03-11 09:47:35 +01:00
Simon Vieille 73b82578e2 Merge branch 'develop' 2022-03-10 22:31:19 +01:00
Simon Vieille e7b4f7f81e refactoring of RequestSecurityEventSubscriber 2022-03-10 22:31:17 +01:00
Simon Vieille dc34a954f6 Merge branch 'develop' 2022-03-10 22:29:59 +01:00
Simon Vieille 3017880a64 refactoring of RequestSecurityEventSubscriber 2022-03-10 22:29:45 +01:00
Simon Vieille 0424a9457e Merge branch 'develop' 2022-03-10 22:01:06 +01:00
Simon Vieille bd6a5bb44e update translations 2022-03-10 22:00:53 +01:00
Simon Vieille b32133e7cb Merge branch 'develop' 2022-03-10 21:50:09 +01:00
Simon Vieille a37c6626c6 fix Node::getSecurityRoles 2022-03-10 21:50:01 +01:00
Simon Vieille 4e1166c19d Merge branch 'develop' 2022-03-10 21:38:33 +01:00
Simon Vieille 098a0f17c0 update changelog and upgrade doc 2022-03-10 21:38:05 +01:00
Simon Vieille f7ddd54d99 Merge branch 'feature/node-security' into develop 2022-03-10 21:37:14 +01:00
Simon Vieille 39637d4094 update changelog and upgrade doc 2022-03-10 21:37:11 +01:00
Simon Vieille ce5c69d467 rename core/EventSuscriber with core/EventSubscriber
add security roles in app configuration

add option to restrict node access to specific roles
2022-03-10 21:32:22 +01:00
Simon Vieille 23802898b5 Merge branch 'develop' 2022-03-06 22:17:41 +01:00
Simon Vieille 632e6b7c7a update changelog 2022-03-06 22:17:38 +01:00
Simon Vieille 89925c2a34 fix user creation from ui 2022-03-06 22:15:50 +01:00
Simon Vieille 4be92913a5 Merge branch 'develop' 2022-03-06 22:09:21 +01:00
Simon Vieille dd11bf3898 update changelog 2022-03-06 22:09:17 +01:00
Simon Vieille a0c0e53816 fix user factory 2022-03-06 22:08:42 +01:00
Simon Vieille f3a48cea18 Merge branch 'develop' 2022-03-06 21:39:41 +01:00
Simon Vieille 3705435785 fix typo 2022-03-06 21:39:36 +01:00
Simon Vieille 3b6e89dc63 Merge branch 'develop' 2022-03-06 15:30:17 +01:00
Simon Vieille 8e3944fa2e handle file renaming in ajax context 2022-03-06 15:30:13 +01:00
Simon Vieille 4b4a28aee4 Merge branch 'develop' 2022-03-06 15:21:10 +01:00
Simon Vieille cd3d00389d Merge branch 'feature/filenanaming' into develop 2022-03-06 15:20:51 +01:00
Simon Vieille 729266e0a6 update changelog 2022-03-06 15:20:46 +01:00
Simon Vieille a563ab3caf add ability to rename file in file manager 2022-03-06 15:19:09 +01:00
Simon Vieille 3d8a4de100 Merge branch 'develop' 2022-03-04 23:12:04 +01:00
Simon Vieille 7355685009 transform file extension to lower case 2022-03-04 23:11:46 +01:00
Simon Vieille 8d02608681 Merge branch 'develop' 2022-03-03 14:29:02 +01:00
Simon Vieille e288db77f6 Merge branch 'develop' of gitnet.fr:murph/murph-skeleton into develop 2022-03-03 14:28:30 +01:00
Simon Vieille 56eb728821 Merge branch 'develop' 2022-03-03 14:28:04 +01:00
Simon Vieille a3cf59417c update changelog 2022-03-03 14:28:01 +01:00
Simon Vieille bf4bd48775 Merge branch 'develop' 2022-03-03 10:39:18 +01:00
Simon Vieille f4b0b2869c add templates to render sections and items in the admin menu 2022-03-03 10:39:16 +01:00
Simon Vieille 0cffb7d588 add templates to render sections and items in the admin menu 2022-03-03 10:38:55 +01:00
Simon Vieille da4d5eb244 update readme 2022-03-03 10:34:15 +01:00
Simon Vieille 321c7ed960 refactoring of admin menu 2022-03-03 10:32:56 +01:00
Simon Vieille c239832aa3 update readme 2022-03-02 11:33:48 +01:00
Simon Vieille d842c142ff update readme 2022-03-02 11:18:53 +01:00
Simon Vieille 44ea80de14 update readme 2022-03-02 11:15:28 +01:00
Simon Vieille 7f1616c4d5 update readme 2022-03-02 11:14:49 +01:00
Simon Vieille 31f18f73f1 update readme 2022-03-02 11:14:20 +01:00
Simon Vieille 8b1e38a647 update readme 2022-03-02 11:11:04 +01:00
Simon Vieille 5d8996d624 update readme 2022-03-02 11:10:40 +01:00
Simon Vieille c80482d7e3 update readme 2022-03-02 11:10:18 +01:00
Simon Vieille ec4ce4c85b update changelog 2022-03-01 18:48:47 +01:00
Simon Vieille 5694d6262f fix analytic table when a path is a long 2022-03-01 18:47:58 +01:00
Simon Vieille 42511199e4 update default template 2022-03-01 16:16:50 +01:00
Simon Vieille 165e3a723f update default template 2022-03-01 16:15:38 +01:00
Simon Vieille 1e4c90240b update changelog 2022-03-01 14:12:43 +01:00
Simon Vieille 3b5116e172 update changelog 2022-03-01 13:49:19 +01:00
Simon Vieille bc48a727a6 add translations 2022-03-01 13:48:29 +01:00
Simon Vieille 5769b29b69 update admin favicon 2022-03-01 13:42:52 +01:00
Simon Vieille c25c887881 update gitignore 2022-03-01 13:34:08 +01:00
Simon Vieille 46a90689db update changelog (v1.7) 2022-03-01 13:28:20 +01:00
Simon Vieille a84e1739c0 update changelog 2022-02-28 21:59:51 +01:00
Simon Vieille 98e49e4134 fix analytic table when the referer is a long domain 2022-02-28 21:58:01 +01:00
Simon Vieille 7b6bb707d1 update changelog 2022-02-28 18:02:08 +01:00
Simon Vieille ab117eef3e update default scss files 2022-02-28 17:58:52 +01:00
Simon Vieille b4802fc3b3 move assets to the core directory of Murph 2022-02-28 14:48:34 +01:00
Simon Vieille 25940fc188 move assets to the core directory of Murph 2022-02-28 14:18:45 +01:00
Simon Vieille b9d929fddf upgrade dependencies 2022-02-28 13:56:09 +01:00
Simon Vieille 551e4beaa3 add sidebar scroll on tablet 2022-02-28 11:33:28 +01:00
Simon Vieille 6134b30a99 update changelog 2022-02-28 09:44:18 +01:00
Simon Vieille 85e4bdf1c2 improve murph:user:create command (add sections and the generated password is displayed) 2022-02-28 09:37:22 +01:00
Simon Vieille e81f8be151 remove password generation from the user factory 2022-02-28 09:35:54 +01:00
Simon Vieille ded0279514 update changelog 2022-02-26 23:22:38 +01:00
Simon Vieille 83d75d2415 fix cache clear task 2022-02-26 23:13:18 +01:00
Simon Vieille 10a1168009 update changelog 2022-02-26 20:58:36 +01:00
Simon Vieille 90499d8dc9 fix sidebar icon width 2022-02-26 20:54:46 +01:00
Simon Vieille 0af04c9131 merge route params in crud admin redirects 2022-02-26 20:32:13 +01:00
Simon Vieille 661840d87c fix crud batch column width 2022-02-26 20:09:46 +01:00
Simon Vieille 03e401f9ce update changelog 2022-02-26 19:56:33 +01:00
Simon Vieille 9bc2ee26f9 add block in field templates to allow override 2022-02-26 19:56:10 +01:00
Simon Vieille 46ecb3a468 update changelog 2022-02-26 19:43:56 +01:00
Simon Vieille 8e3642f878 fix date field when the value is empty 2022-02-26 19:43:11 +01:00
Simon Vieille 263219850e fix form namespace prefix in the crud controller maker 2022-02-25 17:42:18 +01:00
Simon Vieille 24cd406982 update changelog and upgrade doc 2022-02-25 15:39:57 +01:00
Simon Vieille 33eec52044 add mobile and desktop views 2022-02-25 15:15:09 +01:00
Simon Vieille 4db7936a2c update visibility of entities properties 2022-02-25 14:36:44 +01:00
Simon Vieille f29289a36d update changelog 2022-02-23 13:33:41 +01:00
Simon Vieille 9fbc963ece Merge branch 'feature/upgrade' into develop 2022-02-23 13:32:53 +01:00
Simon Vieille bffb9aac59 update dependencies 2022-02-23 13:31:37 +01:00
Simon Vieille 07f2ea1d9e update dependencies 2022-02-23 13:20:17 +01:00
Simon Vieille 28b738d70d update changelog 2022-02-23 11:01:48 +01:00
Simon Vieille dad11586b2 update changelog 2022-02-23 11:01:23 +01:00
Simon Vieille e04f1199c4 update default template 2022-02-23 10:59:02 +01:00
Simon Vieille 9d77d6ed6d handle app url in twig routing filters 2022-02-23 10:58:52 +01:00
Simon Vieille a85aea5afb fix views in analytics modal
replace empty path with / in analytics
2022-02-22 14:16:34 +01:00
Simon Vieille d442d343ba update default logo 2022-02-22 00:01:13 +01:00
Simon Vieille 5129e70f8b update changelog 2022-02-21 23:58:40 +01:00
Simon Vieille d7659735cb update default template 2022-02-21 23:58:08 +01:00
Simon Vieille e50d2668f9 update changelog 2022-02-21 19:45:25 +01:00
Simon Vieille a6f35fa38e update makefile rules 2022-02-21 19:44:37 +01:00
Simon Vieille 933888f061 update upgrade doc 2022-02-21 19:38:00 +01:00
Simon Vieille 901f5ba25e update makefile rules: using npm binary instead of webpack binary 2022-02-21 19:37:21 +01:00
Simon Vieille f660f12825 update upgrade doc 2022-02-21 16:40:28 +01:00
Simon Vieille 1d8270ddae fix analytics listener 2022-02-21 16:40:15 +01:00
Simon Vieille 3858546e4e fix analytics listener 2022-02-21 16:40:01 +01:00
Simon Vieille 621ce7a82b add UPGRADE.md file 2022-02-21 16:15:35 +01:00
Simon Vieille da77abbf3c update changelog 2022-02-21 16:15:22 +01:00
Simon Vieille 0575c8ce59 Merge branch 'feature/visit' into develop 2022-02-21 16:12:20 +01:00
Simon Vieille 5810e09a45 add sort of datas by view in DateRangeAnalytic 2022-02-21 16:12:05 +01:00
Simon Vieille 0739f683c7 refactoring of RangeAnalytic 2022-02-21 09:09:57 +01:00
Simon Vieille 6c75f8ffc3 update translations 2022-02-20 23:08:27 +01:00
Simon Vieille 914fef625a update translations 2022-02-20 23:06:39 +01:00
Simon Vieille 67694ffe8c add no result when views and referers are empty 2022-02-20 23:04:44 +01:00
Simon Vieille d19754d88e add analytics in node list
fix node form
2022-02-20 22:42:20 +01:00
Simon Vieille 7fdcde2689 fix large modal margin 2022-02-20 22:41:16 +01:00
Simon Vieille b32509401c add analytics in assets 2022-02-20 21:08:32 +01:00
Simon Vieille 690e117b0b trigger events when modal content is reloaded 2022-02-20 21:08:14 +01:00
Simon Vieille 9d2048094f add chartjs 2022-02-20 21:05:53 +01:00
Simon Vieille e67966df45 add analytic chart and table 2022-02-20 21:05:46 +01:00
Simon Vieille 23ee09b1b5 NodeAdminController now extends AbstractController 2022-02-20 21:05:18 +01:00
Simon Vieille a5113cf005 rename enableViewCounter with enableAnalytic 2022-02-20 16:12:22 +01:00
Simon Vieille ec4b3341c8 - move NodeView to Analytic/View
- move NodeViewListener to AnalyticListener
- add referer
2022-02-20 15:25:54 +01:00
Simon Vieille 58c8b6126e remove useless class 2022-02-20 13:08:54 +01:00
Simon Vieille 98456ab8e5 add node view listener 2022-02-19 23:35:41 +01:00
Simon Vieille 1cb57a138f add node view entity 2022-02-19 23:33:46 +01:00
Simon Vieille 1f6aaf41e1 add node view counter option 2022-02-19 23:33:19 +01:00
Simon Vieille 03b45e89e3 upgrade knplabs/knp-paginator-bundle 2022-02-19 22:19:23 +01:00
345 changed files with 6358 additions and 21648 deletions

7
.env
View file

@ -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 ###

View file

@ -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
View file

@ -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
View file

@ -0,0 +1 @@
8.0

49
.woodpecker.yml Normal file
View 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

View file

@ -1,10 +1,201 @@
## [Unreleased]
## [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.3.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
@ -14,7 +205,7 @@
* fix filemanager sorting
* fix batch action setter
## 1.2.0
## [1.2.0] - 2022-02-14
### Added
* add sort in file manager
* add redirect manager
@ -22,7 +213,7 @@
### Changed
* replace node-sass with sass
## 1.1.0
## [1.1.0] - 2022-02-29
### Added
* add directory upload in file manager
@ -32,9 +223,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

View file

@ -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

View file

@ -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
View 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
View 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 }
```

View file

@ -1,5 +0,0 @@
$theme-colors: (
"primary": #1ab5dc,
"primary-light": lighten(#3183aa, 40%),
"dark-blue": #1e2430,
) !default;

View file

@ -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
View 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

View file

@ -1 +1 @@
import './admin/admin.js'
import '../../vendor/murph/murph-core/src/core/Resources/assets/js/admin.js'

View file

@ -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')()

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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()
})
})
}

View file

@ -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)
})
})
}

View file

@ -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)
})
}

View file

@ -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])
}
}

View file

@ -1,7 +0,0 @@
const $ = require('jquery')
module.exports = function () {
$('*[data-dblclick]').dblclick(function (e) {
document.location.href = $(this).attr('data-dblclick')
})
}

View file

@ -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

View file

@ -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()
})
}

View file

@ -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
}
})
}

View file

@ -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('')
})
}

View file

@ -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">&times;</span>
</button>
</div>
<div class="toast-body text-${colors[level]}">
${message}
</div>
</div>
`
)
$('.toast').last().toast('show')
}
}
}
$.ajax(options)
})
}

View file

@ -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

View file

@ -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()
}
})
}

View file

@ -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()
}
})
}

View file

@ -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)
})
}

View file

@ -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)
}
}

View file

@ -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

View file

@ -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)
})
}
}

View file

@ -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)
}

View file

@ -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) {
})
})
}

View file

@ -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()
})
}
})
})
}

View file

@ -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

View file

@ -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

View file

@ -1,11 +0,0 @@
const $ = require('jquery')
module.exports = function () {
$('.toast').toast({
animation: true,
autohide: true,
delay: 5000
})
$('.toast').toast('show')
}

View file

@ -1,5 +0,0 @@
const $ = require('jquery')
module.exports = function () {
$('*[data-toggle="tooltip"]').tooltip()
}

1
assets/js/app.js Normal file
View file

@ -0,0 +1 @@
import '../css/app.scss'

View file

@ -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);
};

View file

@ -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';
}

View file

@ -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.23"
},
"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": {

View file

@ -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],

View file

@ -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

View file

@ -1,3 +0,0 @@
framework:
assets:
json_manifest_path: '%kernel.project_dir%/public/build/manifest.json'

View 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)%"

View file

@ -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)%"

View file

@ -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"]

View file

@ -1,6 +0,0 @@
web_profiler:
toolbar: true
intercept_redirects: false
framework:
profiler: { only_exceptions: false }

View file

@ -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

View file

@ -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

View file

@ -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

View 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"

View 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

View file

@ -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"

View file

@ -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

View file

@ -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"]

View file

@ -1,3 +0,0 @@
framework:
router:
strict_requirements: null

View file

@ -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

View file

@ -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

View file

@ -2,7 +2,7 @@
scheb_two_factor:
google:
enabled: true
issuer: "Muprh"
issuer: "Murph"
server_name:
digits: 6
window: 1

View file

@ -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

View file

@ -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

View file

@ -1,4 +0,0 @@
framework:
test: true
session:
storage_id: session.storage.mock_file

View 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

View file

@ -1,2 +0,0 @@
twig:
strict_variables: true

View file

@ -1,3 +0,0 @@
framework:
validation:
not_compromised_password: false

View file

@ -1,6 +0,0 @@
web_profiler:
toolbar: false
intercept_redirects: false
framework:
profiler: { collect: false }

View file

@ -1,2 +0,0 @@
#webpack_encore:
# strict_mode: false

View file

@ -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)%'

View file

@ -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

View file

@ -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

View 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 }

View file

@ -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

View file

@ -3,7 +3,7 @@ controllers:
type: annotation
core_controllers:
resource: ../../core/Controller/
resource: ../../vendor/murph/murph-core/src/core/Controller/
type: annotation
kernel:

View file

@ -1,3 +0,0 @@
_errors:
resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
prefix: /_error

View file

@ -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

View file

@ -0,0 +1,4 @@
when@dev:
_errors:
resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
prefix: /_error

View file

@ -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

View 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

View file

@ -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

View file

@ -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 = [];
}

View file

@ -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');
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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';
}
}

View file

@ -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;
}

View file

@ -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;
}
}
}

View file

@ -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');
}
}

View file

@ -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';
}
}

View file

@ -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';
}
}

View file

@ -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-6'],
])
->setField('index', 'Enabled', Field\ButtonField::class, [
'property_builder' => function(EntityInterface $entity) {
return $entity->getIsEnabled() ? 'Yes' : 'No';
},
'attr' => ['class' => 'col-1'],
'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-1'],
'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