first commit

This commit is contained in:
afornerot 2025-04-09 20:38:48 +02:00
commit f8addc15f4
116 changed files with 17614 additions and 0 deletions

20
.env Normal file
View File

@ -0,0 +1,20 @@
APP_ENV=dev
APP_SECRET=changeme
DATABASE_URL="mysql://user:changeme@mariadb:3306/nineconfig"
MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
APP_NAME="Nineconfig"
APP_NOREPLY=admin@noreply.fr
MODE_AUTH=SQL
CAS_HOST=auth.cadoles.com
CAS_PORT=443
CAS_PATH=/cas
CAS_USERNAME=uid
CAS_MAIL=mail
CAS_LASTNAME=lastname
CAS_FIRSTNAME=firstname
CORPUS_URL=
CORPUS_USERNAME=
CORPUS_PASSWORD=

6
.env.test Normal file
View File

@ -0,0 +1,6 @@
# define your env variables for the test env here
KERNEL_CLASS='App\Kernel'
APP_SECRET='$ecretf0rt3st'
SYMFONY_DEPRECATIONS_HELPER=999999
PANTHER_APP_ENV=panther
PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots

29
.gitignore vendored Normal file
View File

@ -0,0 +1,29 @@
###> symfony/framework-bundle ###
/.env.local
/.env.local.php
/.env.*.local
/config/secrets/prod/prod.decrypt.private.php
/public/bundles/
/var/
/vendor/
/public/uploads
/uploads
###< symfony/framework-bundle ###
###> phpunit/phpunit ###
/phpunit.xml
.phpunit.result.cache
###< phpunit/phpunit ###
###> symfony/phpunit-bridge ###
.phpunit.result.cache
/phpunit.xml
###< symfony/phpunit-bridge ###
###> phpstan/phpstan ###
phpstan.neon
###< phpstan/phpstan ###
###> friendsofphp/php-cs-fixer ###
/.php-cs-fixer.php
/.php-cs-fixer.cache
###< friendsofphp/php-cs-fixer ###

45
.php-cs-fixer.dist.php Normal file
View File

@ -0,0 +1,45 @@
<?php
$finder = PhpCsFixer\Finder::create()
->in(__DIR__.'/src')
->name('*.php')
;
// TODO: Définir les règles de style communes
// spécifiques au projet
return (new PhpCsFixer\Config())
->setRules([
'@Symfony' => true,
'concat_space' => ['spacing' => 'none'],
'array_syntax' => ['syntax' => 'short'],
'combine_consecutive_issets' => true,
'explicit_indirect_variable' => true,
'no_useless_return' => true,
'ordered_imports' => true,
'no_unused_imports' => true,
'no_spaces_after_function_name' => true,
'no_spaces_inside_parenthesis' => true,
'ternary_operator_spaces' => true,
'class_definition' => ['single_line' => true],
'whitespace_after_comma_in_array' => true,
// phpdoc
'phpdoc_add_missing_param_annotation' => ['only_untyped' => true],
'phpdoc_order' => true,
'phpdoc_types_order' => [
'null_adjustment' => 'always_last',
'sort_algorithm' => 'alpha',
],
'phpdoc_no_empty_return' => false,
'phpdoc_summary' => false,
'general_phpdoc_annotation_remove' => [
'annotations' => [
'expectedExceptionMessageRegExp',
'expectedException',
'expectedExceptionMessage',
'author',
],
],
])
->setFinder($finder)
;

5
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"amatl.generatePdfOnSave": false,
"amatl.generateHtmlOnSave": false,
"vscode-php-cs-fixer.toolPath": "/home/arno/git/nine/nineconfig/vendor/bin/php-cs-fixer"
}

21
bin/console Executable file
View File

@ -0,0 +1,21 @@
#!/usr/bin/env php
<?php
use App\Kernel;
use Symfony\Bundle\FrameworkBundle\Console\Application;
if (!is_dir(dirname(__DIR__).'/vendor')) {
throw new LogicException('Dependencies are missing. Try running "composer install".');
}
if (!is_file(dirname(__DIR__).'/vendor/autoload_runtime.php')) {
throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".');
}
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
return function (array $context) {
$kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
return new Application($kernel);
};

23
bin/phpunit Executable file
View File

@ -0,0 +1,23 @@
#!/usr/bin/env php
<?php
if (!ini_get('date.timezone')) {
ini_set('date.timezone', 'UTC');
}
if (is_file(dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit')) {
if (PHP_VERSION_ID >= 80000) {
require dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit';
} else {
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';
}

37
compose.yaml Normal file
View File

@ -0,0 +1,37 @@
services:
mariadb:
image: mariadb
container_name: nineconfig-mariadb
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: changeme
MYSQL_DATABASE: nineconfig
MYSQL_USER: user
MYSQL_PASSWORD: changeme
volumes:
- mariadb-data:/var/lib/mysql
nineconfig:
build:
context: .
dockerfile: ./misc/docker/Dockerfile
container_name: nineconfig-app
restart: unless-stopped
image: reg.cadoles.com/envole/nineconfig
depends_on:
- mariadb
ports:
- "8008:80"
volumes:
- .:/app:delegated
adminer:
image: adminer
container_name: nineconfig-adminer
restart: always
ports:
- 6088:8080
volumes:
mariadb-data:

114
composer.json Normal file
View File

@ -0,0 +1,114 @@
{
"type": "project",
"license": "proprietary",
"minimum-stability": "stable",
"prefer-stable": true,
"require": {
"php": ">=8.2",
"ext-ctype": "*",
"ext-iconv": "*",
"apereo/phpcas": "^1.6",
"doctrine/dbal": "^3",
"doctrine/doctrine-bundle": "^2.13",
"doctrine/doctrine-migrations-bundle": "^3.3",
"doctrine/orm": "^3.3",
"friendsofsymfony/rest-bundle": "^3.8",
"oneup/uploader-bundle": "^5.0",
"phpdocumentor/reflection-docblock": "^5.4",
"phpstan/phpdoc-parser": "^1.33",
"ramsey/uuid": "^4.7",
"symfony/asset": "7.1.*",
"symfony/console": "7.1.*",
"symfony/doctrine-messenger": "7.1.*",
"symfony/dotenv": "7.1.*",
"symfony/expression-language": "7.1.*",
"symfony/flex": "^2",
"symfony/form": "7.1.*",
"symfony/framework-bundle": "7.1.*",
"symfony/http-client": "7.1.*",
"symfony/intl": "7.1.*",
"symfony/mailer": "7.1.*",
"symfony/mime": "7.1.*",
"symfony/monolog-bundle": "^3.0",
"symfony/notifier": "7.1.*",
"symfony/process": "7.1.*",
"symfony/property-access": "7.1.*",
"symfony/property-info": "7.1.*",
"symfony/runtime": "7.1.*",
"symfony/security-bundle": "7.1.*",
"symfony/serializer": "7.1.*",
"symfony/stimulus-bundle": "^2.21",
"symfony/string": "7.1.*",
"symfony/translation": "7.1.*",
"symfony/twig-bundle": "7.1.*",
"symfony/ux-turbo": "^2.21",
"symfony/validator": "7.1.*",
"symfony/web-link": "7.1.*",
"symfony/yaml": "7.1.*",
"twig/extra-bundle": "^2.12|^3.0",
"twig/twig": "^2.12|^3.0"
},
"config": {
"allow-plugins": {
"php-http/discovery": true,
"symfony/flex": true,
"symfony/runtime": true
},
"sort-packages": true
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"App\\Tests\\": "tests/"
}
},
"replace": {
"symfony/polyfill-ctype": "*",
"symfony/polyfill-iconv": "*",
"symfony/polyfill-php72": "*",
"symfony/polyfill-php73": "*",
"symfony/polyfill-php74": "*",
"symfony/polyfill-php80": "*",
"symfony/polyfill-php81": "*",
"symfony/polyfill-php82": "*"
},
"scripts": {
"auto-scripts": {
"cache:clear": "symfony-cmd",
"assets:install %PUBLIC_DIR%": "symfony-cmd"
},
"post-install-cmd": [
"@auto-scripts"
],
"post-update-cmd": [
"@auto-scripts"
]
},
"conflict": {
"symfony/symfony": "*"
},
"extra": {
"symfony": {
"allow-contrib": false,
"require": "7.1.*"
}
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.65",
"phpstan/phpstan": "^2.0",
"phpstan/phpstan-doctrine": "^2.0",
"phpstan/phpstan-symfony": "^2.0",
"phpunit/phpunit": "^9.5",
"symfony/browser-kit": "7.1.*",
"symfony/css-selector": "7.1.*",
"symfony/debug-bundle": "7.1.*",
"symfony/maker-bundle": "^1.0",
"symfony/phpunit-bridge": "^7.1",
"symfony/stopwatch": "7.1.*",
"symfony/web-profiler-bundle": "7.1.*"
}
}

11530
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

18
config/bundles.php Normal file
View File

@ -0,0 +1,18 @@
<?php
return [
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true],
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true],
Symfony\UX\Turbo\TurboBundle::class => ['all' => true],
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
Oneup\UploaderBundle\OneupUploaderBundle::class => ['all' => true],
FOS\RestBundle\FOSRestBundle::class => ['all' => true],
];

220
config/nineconfig/dicos.yml Normal file
View File

@ -0,0 +1,220 @@
global:
id: global
label: Global
style: col-md-12
vars:
APP_ENV:
id: APP_ENV
label: Type d'environnement
source: env/.env
type: choice
choices: "PROD,DEV"
required: true
WEB_URL:
id: WEB_URL
label: Hostname
source: env/.env
type: text
required: true
PROTOCOLE:
id: PROTOCOLE
label: Protocole
source: env/.env
type: choice
choices: "http,https"
required: true
REDIRECT_PATH:
id: REDIRECT_PATH
label: Redirection par défaut
source: env/.env
type: text
required: false
authentification:
id: authentification
label: Authentification
style: col-md-12
vars:
MODE_AUTH:
id: MODE_AUTH
label: Mode d'Authentification
source: env/.env
type: choice
choices: "SQL,CAS"
required: true
master: modeauth
KEYCLOAK_ACTIVATE:
id: KEYCLOAK_ACTIVATE
label: Activer Keycloak
source: env/.env
type: bool
required: true
master: keycloakactive
slave: modeauth=CAS
CAS_HOST:
id: CAS_HOST
label: Host du serveur CAS
source: env/.env
type: string
required: true
slave: modeauth=CAS
CAS_PORT:
id: CAS_PORT
label: Port du serveur CAS
source: env/.env
type: string
required: true
slave: modeauth=CAS
CAS_PATH:
id: CAS_PATH
label: Path du serveur CAS
source: env/.env
type: string
required: true
slave: modeauth=CAS
admin:
id: admin
label: Compte Administrateur
style: col-md-12
vars:
ADMIN_USER:
id: ADMIN_USER
label: Login du compte administrateur
source: env/.env
type: text
required: true
KEYCLOAK_USER:
id: KEYCLOAK_USER
label: Login de Keycloak
source: env/.env
type: string
required: true
slave: keycloakactive=1
ADMIN_EMAIL:
id: ADMIN_EMAIL
label: Email du compte administrateur
source: env/.env
type: text
required: true
# password:
# CAS_PASSWORD=${ADMIN_PASSWORD}-keycloak
applications:
id: applications
label: Applications
style: col-md-12
vars:
KOMGA_ACTIVATE:
id: KOMGA_ACTIVATE
label: Activer Komga
source: env/.env
type: bool
required: true
master: komgaactivate
NINEBOARD_ACTIVATE:
id: NINEBOARD_ACTIVATE
label: Activer Nineboard
source: env/.env
type: bool
required: true
master: nineboardactivate
NINECOMPTA_ACTIVATE:
id: NINECOMPTA_ACTIVATE
label: Activer Ninecompta
source: env/.env
type: bool
required: true
master: ninecomptaactivate
NINEFOLIO_ACTIVATE:
id: NINEFOLIO_ACTIVATE
label: Activer Ninefolio
source: env/.env
type: bool
required: true
master: ninefolioactivate
komga:
id: komga
label: Komga
style: col-md-6
vars:
KOMGA_LOCAL:
id: KOMGA_LOCAL
label: Komba local
source: env/.env
type: bool
required: true
slave: komgaactivate=1
master: komgalocal
KOMGA_URL:
id: KOMGA_URL
label: Url du Komga distant
source: env/.env
type: text
required: true
slave: komgalocal=0
nineboard:
id: nineboard
label: Nineboard
style: col-md-6
vars:
NINEBOARD_LOCAL:
id: NINEBOARD_LOCAL
label: Nineboard local
source: env/.env
type: bool
required: true
slave: nineboardactivate=1
master: nineboardlocal
NINEBOARD_URL:
id: NINEBOARD_URL
label: Url du Nineboard distant
source: env/.env
type: text
required: true
slave: nineboardlocal=0
ninecompta:
id: ninecompta
label: Ninecompta
style: col-md-6
vars:
NINECOMPTA_LOCAL:
id: NINECOMPTA_LOCAL
label: Ninecompta local
source: env/.env
type: bool
required: true
slave: ninecomptaactivate=1
master: ninecomptalocal
NINECOMPTA_URL:
id: NINECOMPTA_URL
label: Url du Ninecompta distant
source: env/.env
type: text
required: true
slave: ninecomptalocal=0
ninefolio:
id: ninefolio
label: Ninefolio
style: col-md-6
vars:
NINEFOLIO_LOCAL:
id: NINEFOLIO_LOCAL
label: Ninefolio local
source: env/.env
type: bool
required: true
slave: ninefolioactivate=1
master: ninefoliolocal
NINEFOLIO_URL:
id: NINEFOLIO_URL
label: Url du Ninefolio distant
source: env/.env
type: text
required: true
slave: ninefoliolocal=0

View File

@ -0,0 +1,19 @@
framework:
cache:
# Unique name of your app: used to compute stable namespaces for cache keys.
#prefix_seed: your_vendor_name/app_name
# The "app" cache stores to the filesystem by default.
# The data in this cache should persist between deploys.
# Other options include:
# Redis
#app: cache.adapter.redis
#default_redis_provider: redis://localhost
# APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
#app: cache.adapter.apcu
# Namespaced pools use the above "app" backend by default
#pools:
#my.dedicated.cache: null

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

@ -0,0 +1,54 @@
doctrine:
dbal:
url: '%env(resolve:DATABASE_URL)%'
# IMPORTANT: You MUST configure your server version,
# either here or in the DATABASE_URL env var (see .env file)
#server_version: '16'
profiling_collect_backtrace: '%kernel.debug%'
use_savepoints: true
orm:
auto_generate_proxy_classes: true
enable_lazy_ghost_objects: true
report_fields_where_declared: true
validate_xml_mapping: true
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
identity_generation_preferences:
Doctrine\DBAL\Platforms\PostgreSQLPlatform: identity
auto_mapping: true
mappings:
App:
type: attribute
is_bundle: false
dir: '%kernel.project_dir%/src/Entity'
prefix: 'App\Entity'
alias: App
controller_resolver:
auto_mapping: 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

@ -0,0 +1,6 @@
doctrine_migrations:
migrations_paths:
# 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

@ -0,0 +1,15 @@
# Read the documentation: https://fosrestbundle.readthedocs.io/en/3.x/
fos_rest:
# param_fetcher_listener: true
# allowed_methods_listener: true
routing_loader: false
# view:
# view_response_listener: true
# exception:
# codes:
# App\Exception\MyException: 403
# messages:
# App\Exception\MyException: Forbidden area.
# format_listener:
# rules:
# - { path: ^/api, prefer_extension: true, fallback_format: json, priorities: [ json, html ] }

View File

@ -0,0 +1,16 @@
# see https://symfony.com/doc/current/reference/configuration/framework.html
framework:
secret: '%env(APP_SECRET)%'
#csrf_protection: true
# Note that the session will be started ONLY if you read or write from it.
session: true
#esi: true
#fragments: true
when@test:
framework:
test: true
session:
storage_factory_id: session.storage.factory.mock_file

View File

@ -0,0 +1,3 @@
framework:
mailer:
dsn: '%env(MAILER_DSN)%'

View File

@ -0,0 +1,29 @@
framework:
messenger:
failure_transport: failed
transports:
# https://symfony.com/doc/current/messenger.html#transport-configuration
async:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
options:
use_notify: true
check_delayed_interval: 60000
retry_strategy:
max_retries: 3
multiplier: 2
failed: 'doctrine://default?queue_name=failed'
# sync: 'sync://'
default_bus: messenger.bus.default
buses:
messenger.bus.default: []
routing:
Symfony\Component\Mailer\Messenger\SendEmailMessage: async
Symfony\Component\Notifier\Message\ChatMessage: async
Symfony\Component\Notifier\Message\SmsMessage: async
# Route your messages to the transports
# 'App\Message\YourMessage': async

View File

@ -0,0 +1,62 @@
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
formatter: monolog.formatter.json

View File

@ -0,0 +1,12 @@
framework:
notifier:
chatter_transports:
texter_transports:
channel_policy:
# use chat/slack, chat/telegram, sms/twilio or sms/nexmo
urgent: ['email']
high: ['email']
medium: ['email']
low: ['email']
admin_recipients:
- { email: admin@example.com }

View File

@ -0,0 +1,6 @@
oneup_uploader:
mappings:
avatar:
frontend: dropzone
logo:
frontend: dropzone

View File

@ -0,0 +1,10 @@
framework:
router:
# 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

@ -0,0 +1,42 @@
security:
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: "auto"
providers:
sql_provider:
entity:
class: App\Entity\User
property: username
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
pattern: ^/
provider: sql_provider
custom_authenticators:
- App\Security\DynamicAuthenticator
form_login:
login_path: app_login
check_path: app_login
enable_csrf: true
default_target_path: /
logout:
path: app_logout
access_control:
- { path: ^/login, roles: PUBLIC_ACCESS }
- { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/, roles: ROLE_USER }
when@test:
security:
password_hashers:
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

@ -0,0 +1,7 @@
framework:
default_locale: en
translator:
default_path: '%kernel.project_dir%/translations'
fallbacks:
- en
providers:

View File

@ -0,0 +1,9 @@
twig:
file_name_pattern: "*.twig"
form_themes: ['bootstrap_5_layout.html.twig']
globals:
appName: "%appName%"
when@test:
twig:
strict_variables: true

View File

@ -0,0 +1,11 @@
framework:
validation:
# Enables validator auto-mapping support.
# 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,17 @@
when@dev:
web_profiler:
toolbar: true
intercept_redirects: false
framework:
profiler:
only_exceptions: false
collect_serializer_data: true
when@test:
web_profiler:
toolbar: false
intercept_redirects: false
framework:
profiler: { collect: false }

5
config/preload.php Normal file
View File

@ -0,0 +1,5 @@
<?php
if (file_exists(dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php')) {
require dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php';
}

5
config/routes.yaml Normal file
View File

@ -0,0 +1,5 @@
controllers:
resource:
path: ../src/Controller/
namespace: App\Controller
type: attribute

View File

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

View File

@ -0,0 +1,3 @@
oneup_uploader:
resource: .
type: uploader

View File

@ -0,0 +1,3 @@
_security_logout:
resource: security.route_loader.logout
type: service

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

30
config/services.yaml Normal file
View File

@ -0,0 +1,30 @@
parameters:
appEnv: "%env(resolve:APP_ENV)%"
appSecret: "%env(resolve:APP_SECRET)%"
appName: "%env(resolve:APP_NAME)%"
appNoreply: "%env(resolve:APP_NOREPLY)%"
modeAuth: "%env(resolve:MODE_AUTH)%"
casHost: "%env(resolve:CAS_HOST)%"
casPort: "%env(resolve:CAS_PORT)%"
casPath: "%env(resolve:CAS_PATH)%"
casUsername: "%env(resolve:CAS_USERNAME)%"
casMail: "%env(resolve:CAS_MAIL)%"
casLastname: "%env(resolve:CAS_LASTNAME)%"
casFirstname: "%env(resolve:CAS_FIRSTNAME)%"
services:
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
App\:
resource: "../src/"
exclude:
- "../src/DependencyInjection/"
- "../src/Entity/"
- "../src/Kernel.php"
App\Security\DynamicAuthenticator:
arguments:
$modeAuth: '%env(MODE_AUTH)%'

246
env/.env vendored Normal file
View File

@ -0,0 +1,246 @@
# == GLOBAL ===============================================================================================================================
# ATTENTION
# si vous souhaiter faire tourner nine sur localhost vous devez ajouter dans votre host = 127.0.0.0 nine.local
# votre localhost devra aussi disposer d'un certificat
# si vous changer le web_url penser à modifier nine.local par votre web_url dans volume/keycloak/nine/realm-export.json
# sinon il vous faudra vous connecter à keycloak pour changer l'url du client nine
# Si vous souhaitez faire marcher wordpress il est indispensable que nineapache écoute soit le port 80 ou 443
# RELEASE SYSTEM = linux ou eole si eole le réseau du compose sera supprimé et regénéré à chaque UP
RELEASE_SYSTEM=linux
# GLOBAL
APP_ENV=PROD
WEB_URL=nine.local
PROTOCOLE=https
REDIRECT_PATH=/ninegate
# ADMIN USER
ADMIN_USER=admin
ADMIN_PASSWORD=changeme
ADMIN_EMAIL=admin@noreply.fr
# MASTERIDENTITY
# SQL or SSO or (to do LDAP)
MASTERIDENTITY=SQL
# AUTHENTIFICATION
# SQL or CAS (todo LDAP or OPENID)
MODE_AUTH=CAS
# REGISTRATION
# none or byadmin or byuser
MODEREGISTRATION=byadmin
# SERVER SMTP
MAILER_DSN=sendmail://default
# NINEAPACHE
# Il sert de reverse proxy
NINEAPACHE_SERVICE_NAME=nineapache
NINEAPACHE_ACTIVATE=1
NINEAPACHE_LOCAL=1
NINEAPACHE_LETSENCRYPT=0
# FAKESMTP
# fake-smtp server
FAKESMTP_SERVICE_NAME=fakesmtp
FAKESMTP_LOCAL=0
FAKESMTP_ACTIVATE=1
# MTA
# passerelle courriel
MTA_SERVICE_NAME=mta
MTA_ACTIVATE=0
MTA_LOCAL=1
MTA_RELAY_HOST=
MTA_RELAY_PORT=
MTA_RELAY_USER="user"
MTA_RELAY_HOST="$FAKESMTP_SERVICE_NAME"
MTA_RELAY_PORT=2525
# MARIADB
MARIADB_SERVICE_NAME=mariadb
MARIADB_ACTIVATE=1
MARIADB_LOCAL=1
MARIADB_HOST=${MARIADB_SERVICE_NAME}
MARIADB_PORT=3306
MARIADB_ROOT_PASSWORD=${ADMIN_PASSWORD}
MARIADB_USER=user
MARIADB_PASSWORD=${ADMIN_PASSWORD}
# REDIS
REDIS_SERVICE_NAME=redis
REDIS_ACTIVATE=1
REDIS_LOCAL=1
REDIS_HOST=${REDIS_SERVICE_NAME}
REDIS_PORT=6379
# SENTINEL
SENTINEL_SERVICE_NAME=sentinel
SENTINEL_ACTIVATE=0
SENTINEL_LOCAL=1
SENTINEL_HOST=${SENTINEL_SERVICE_NAME}
SENTINEL_PORT=26379
# MINIO
MINIO_SERVICE_NAME=minio
MINIO_ACTIVATE=0
MINIO_LOCAL=1
MINIO_HOST=${MINIO_SERVICE_NAME}
MINIO_PORT=9000
MINIO_URL=${PROTOCOLE}://${WEB_URL}:9001
# OPENLDAP
# LDAP_SYNC Si MASTERIDENTITY = SQL permet la synchronisation des utilisateurs SQL vers LDAP
OPENLDAP_SERVICE_NAME=openldap
OPENLDAP_ACTIVATE=1
OPENLDAP_LOCAL=1
# LDAP
LDAP_ACTIVATE=${OPENLDAP_ACTIVATE}
LDAP_TYPE=LDAP
LDAP_HOST=${OPENLDAP_SERVICE_NAME}
LDAP_PORT=1389
LDAP_TLS=0
LDAP_BASEDN=dc=nine,dc=org
LDAP_ADMIN_USERNAME=${ADMIN_USER}
LDAP_USER=cn=${LDAP_ADMIN_USERNAME},${LDAP_BASEDN}
LDAP_PASSWORD=${ADMIN_PASSWORD}
LDAP_OUORGANISATION=nine
LDAP_OUNIVEAU01=niveau01
LDAP_OUNIVEAU02=niveau02
LDAP_OUNIVEAU03=niveau03
LDAP_OUNIVEAU04=niveau04
LDAP_OUGROUP=groups
LDAP_BASEORGANISATION=ou=${LDAP_OUORGANISATION},${LDAP_BASEDN}
LDAP_BASENIVEAU01=ou=${LDAP_OUNIVEAU01},${LDAP_BASEORGANISATION}
LDAP_BASENIVEAU02=ou=${LDAP_OUNIVEAU02},${LDAP_BASEORGANISATION}
LDAP_BASENIVEAU03=ou=${LDAP_OUNIVEAU03},${LDAP_BASEORGANISATION}
LDAP_BASENIVEAU04=ou=${LDAP_OUNIVEAU04},${LDAP_BASEORGANISATION}
LDAP_BASEUSER=ou=users,${LDAP_BASEORGANISATION}
LDAP_BASEGROUP=ou=${LDAP_OUGROUP},${LDAP_BASEORGANISATION}
LDAP_SYNC=1
LDAP_TEMPLATE=open
LDAP_USERNAME=uid
LDAP_FIRSTNAME=givenName
LDAP_LASTNAME=sn
LDAP_DISPLAYNAME=displayName
LDAP_EMAIL=mail
LDAP_MEMBEROF=memberOf
LDAP_GROUP_GID=gidnumber
LDAP_GROUP_NAME=cn
LDAP_GROUP_MEMBER=memberUid
LDAP_GROUP_MEMBERISDN=0
LDAP_LOGIN_FILTER="(&(${LDAP_USERNAME}=%uid)(objectClass=person)(!(description=Computer)))"
LDAP_USER_FILTER="(&(${LDAP_USERNAME}=*)(objectClass=person)(!(description=Computer)))"
LDAP_GROUP_FILTER="(&(objectClass=posixGroup))"
SCRIBE_GROUP=1
SCRIBE_MASTER=1
OPENLDAPREQNIVEAU01="(&(uid=*)(objectclass=inetOrgPerson)(!(description=Computer)))"
OPENLDAPSYNCHROGROUP=0
OPENLDAPREQGROUP=
# KEYCLOAK
KEYCLOAK_SERVICE_NAME=keycloak
KEYCLOAK_ACTIVATE=1
KEYCLOAK_LOCAL=1
KEYCLOAK_USER=${ADMIN_USER}-keycloak
KEYCLOAK_PASSWORD=${ADMIN_PASSWORD}-keycloak
# CAS
CAS_HOST=${WEB_URL}
CAS_PORT=443
CAS_PATH=/auth/realms/nine/protocol/cas
CAS_URL=${PROTOCOLE}://${CAS_HOST}:${CAS_PORT}
CAS_USERNAME=${LDAP_USERNAME}
CAS_MAIL=${LDAP_EMAIL}
CAS_LASTNAME=${LDAP_LASTNAME}
CAS_FIRSTNAME=${LDAP_FIRSTNAME}
# DOKUWIKI
DOKUWIKI_SERVICE_NAME=dokuwiki
DOKUWIKI_ACTIVATE=0
DOKUWIKI_LOCAL=1
DOKUWIKI_URL=${PROTOCOLE}://${WEB_URL}/dokuwiki
# KOMGA
KOMGA_SERVICE_NAME=komga
KOMGA_ACTIVATE=0
KOMGA_LOCAL=1
KOMGA_URL=${PROTOCOLE}://${WEB_URL}/komga
# NEXTCLOUD
NEXTCLOUD_SERVICE_NAME=nextcloud
NEXTCLOUD_ACTIVATE=1
NEXTCLOUD_LOCAL=1
NEXTCLOUD_URL=${PROTOCOLE}://${WEB_URL}/nextcloud
NEXTCLOUD_SAMBA=0
# NINEBOARD
NINEBOARD_SERVICE_NAME=nineboard
NINEBOARD_ACTIVATE=1
NINEBOARD_LOCAL=1
NINEBOARD_URL=${PROTOCOLE}://${WEB_URL}/nineboard
# NINECOMPTA
NINECOMPTA_SERVICE_NAME=ninecompta
NINECOMPTA_ACTIVATE=0
NINECOMPTA_LOCAL=1
NINECOMPTA_URL=${PROTOCOLE}://${WEB_URL}/ninecompta
# NINEFOLIO
NINEFOLIO_SERVICE_NAME=ninefolio
NINEFOLIO_ACTIVATE=0
NINEFOLIO_LOCAL=1
NINEFOLIO_URL=${PROTOCOLE}://${WEB_URL}/ninefolio
# NINEGATE
NINEGATE_SERVICE_NAME=ninegate
NINEGATE_ACTIVATE=1
NINEGATE_LOCAL=1
NINEGATE_URL=${PROTOCOLE}://${WEB_URL}/ninegate
# NINESCHOOL
NINESCHOOL_SERVICE_NAME=nineschool
NINESCHOOL_ACTIVATE=1
NINESCHOOL_LOCAL=1
NINESCHOOL_URL=${PROTOCOLE}://${WEB_URL}/nineschool
# NINESKELETOR
NINESKELETOR_SERVICE_NAME=nineskeletor
NINESKELETOR_ACTIVATE=0
NINESKELETOR_LOCAL=1
NINESKELETOR_URL=${PROTOCOLE}://${WEB_URL}/nineskeletor
# PIWIGO
PIWIGO_SERVICE_NAME=piwigo
PIWIGO_ACTIVATE=0
PIWIGO_LOCAL=1
PIWIGO_URL=${PROTOCOLE}://${WEB_URL}/piwigo
# WORDPRESS
WORDPRESS_SERVICE_NAME=wordpress
WORDPRESS_ACTIVATE=1
WORDPRESS_LOCAL=1
WORDPRESS_URL=${PROTOCOLE}://${WEB_URL}/wordpress
# ADMINER
ADMINER_SERVICE_NAME=adminer
ADMINER_ACTIVATE=1
ADMINER_LOCAL=1
ADMINER_URL="${PROTOCOLE}://${WEB_URL}/adminer/?server=${MARIADB_SERVICE_NAME}&username=${MARIADB_USER}"
# PHPLDAPADMIN
PHPLDAPADMIN_SERVICE_NAME=phpldapadmin
PHPLDAPADMIN_ACTIVATE=1
PHPLDAPADMIN_LOCAL=1
PHPLDAPADMIN_URL=${PROTOCOLE}://${WEB_URL}/phpldapadmin

5
env/.env.local vendored Normal file
View File

@ -0,0 +1,5 @@
MODE_AUTH=SQL
KEYCLOAK_ACTIVATE=0
KOMGA_ACTIVATE=1
NINECOMPTA_ACTIVATE=1
NINEFOLIO_ACTIVATE=1

0
migrations/.gitignore vendored Normal file
View File

31
misc/docker/Dockerfile Normal file
View File

@ -0,0 +1,31 @@
FROM reg.cadoles.com/envole/nineapache:8.2
USER root
COPY ./misc/docker/apache.conf /etc/apache2/conf.d/nine/site.conf
RUN echo "* * * * * /app/bin/console app:Cron --env=prod" >> /var/spool/cron/crontabs/root
WORKDIR /app
COPY . .
# Installation des dépendances composer
RUN composer install --no-interaction
RUN mkdir -p /app/uploads
RUN mkdir -p /app/public/uploads
RUN mkdir -p /app/public/uploads/avatar
RUN mkdir -p /app/public/uploads/logo
RUN chown apache /app/uploads -R
RUN chown apache /app/public/uploads -R
RUN chmod u+w /app/public/uploads -R
RUN chmod u+w /app/uploads -R
RUN cp -rf /app/public/medias/logo /app/public/uploads/logo
RUN cp -rf /app/public/medias/avatar /app/public/uploads/avatar
RUN mkdir -p /app/var
RUN chown apache /app/var -R
RUN chmod u+w /app/var -R
CMD /app/misc/script/reconfigure.sh && /etc/apache2/apache2.sh

19
misc/docker/apache.conf Executable file
View File

@ -0,0 +1,19 @@
LoadModule rewrite_module modules/mod_rewrite.so
ServerName nineapache.local
DocumentRoot "/app/public"
Alias /ninewiki /app/public
<Directory "/app/public">
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
RewriteEngine On
RewriteCond %{REQUEST_URI}::$0 ^(/.+)/(.*)::\2$
RewriteRule .* - [E=BASE:%1]
RewriteCond %{HTTP:Authorization} .+
RewriteRule ^ - [E=HTTP_AUTHORIZATION:%0]
RewriteCond %{ENV:REDIRECT_STATUS} =""
RewriteRule ^index\.php(?:/(.*)|$) %{ENV:BASE}/$1 [R=301,L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ %{ENV:BASE}/index.php [L]
</Directory>

13
misc/script/reconfigure.sh Executable file
View File

@ -0,0 +1,13 @@
#!/bin/bash
set -eo pipefail
# Se positionner sur la racine du projet
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
cd ${DIR}
cd ../..
DIR=$(pwd)
bin/console d:s:u --force --complete
bin/console app:init
exec $@

38
phpunit.xml.dist Normal file
View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- https://phpunit.readthedocs.io/en/latest/configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="tests/bootstrap.php"
convertDeprecationsToExceptions="false"
>
<php>
<ini name="display_errors" value="1" />
<ini name="error_reporting" value="-1" />
<server name="APP_ENV" value="test" force="true" />
<server name="SHELL_VERBOSITY" value="-1" />
<server name="SYMFONY_PHPUNIT_REMOVE" value="" />
<server name="SYMFONY_PHPUNIT_VERSION" value="9.5" />
</php>
<testsuites>
<testsuite name="Project Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">src</directory>
</include>
</coverage>
<listeners>
<listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener" />
</listeners>
<extensions>
</extensions>
</phpunit>

9
public/index.php Normal file
View File

@ -0,0 +1,9 @@
<?php
use App\Kernel;
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
return function (array $context) {
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
};

86
public/lib/app/app.css Normal file
View File

@ -0,0 +1,86 @@
html,
body {
height: 100%
}
body {
display: flex;
flex-direction: column;
align-items: stretch;
}
.avatar{
border-radius: 100%;
width:35px;
height:35px;
}
.bigavatar{
border-radius: 100%;
width:100px;
height:100px;
}
.navbar-brand img {
width: 40px;
margin-right: 10px;
}
.navbar-toggler {
border: none;
box-shadow: none !important;
}
main {
display: flex;
flex-grow: 1;
align-items: stretch;
}
sidebar {
width: 300px;
padding: 15px;
background-color: rgba(var(--bs-dark-rgb));
display: flex;
flex-direction: column;
}
sidebar div {
display: flex;
flex-direction: column;
border-bottom: 1px solid var(--bs-body-color);
padding-bottom: 10px;
}
sidebar a {
text-decoration: none;
color: var(--bs-body-color);
;
opacity: 0.6;
margin: 5px 0px;
}
sidebar a:hover {
text-decoration: none;
opacity: 1;
}
content {
flex-grow: 1;
align-items: stretch;
padding: 15px;
}
@media (max-width: 991px) {
sidebar {
width: 70px;
}
sidebar i {
font-size: 200%;
}
sidebar span {
display: none;
}
}

23
public/lib/app/app.js Normal file
View File

@ -0,0 +1,23 @@
function ModalLoad(idmodal,title,path) {
$("#"+idmodal+" .modal-header h4").text(title);
$("#"+idmodal+" #framemodal").attr("src",path);
}
$(document).ready(function(){
$("#selectproject").on("change", function() {
url=$(this).data("change");
$.ajax({
type: "POST",
url: url,
data: {
id: $(this).val()
},
success: function (result) {
location.reload();
}
});
});
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,30 @@
$(document).ready(function() {
$.extend( $.fn.dataTable.defaults, {
responsive: true,
"iDisplayLength": 1000,
"oLanguage": {
"sThousands": " ",
"sProcessing": "Traitement en cours...",
"sSearch": "Rechercher&nbsp;:",
"sLengthMenu": "Afficher _MENU_ &eacute;l&eacute;ments",
"sInfo": "Affichage de l'&eacute;lement _START_ &agrave; _END_ sur _TOTAL_ &eacute;l&eacute;ments",
"sInfoEmpty": "Affichage de l'&eacute;lement 0 &agrave; 0 sur 0 &eacute;l&eacute;ments",
"sInfoFiltered": "(filtr&eacute; de _MAX_ &eacute;l&eacute;ments au total)",
"sInfoPostFix": "",
"sLoadingRecords": "Chargement en cours...",
"sZeroRecords": "Aucun &eacute;l&eacute;ment &agrave; afficher",
"sEmptyTable": "Aucune donnée disponible dans le tableau",
"oPaginate": {
"sFirst": "Premier",
"sPrevious": "Pr&eacute;c&eacute;dent",
"sNext": "Suivant",
"sLast": "Dernier"
},
"oAria": {
"sSortAscending": ": activer pour trier la colonne par ordre croissant",
"sSortDescending": ": activer pour trier la colonne par ordre décroissant"
}
},
//"stateSave": true
});
});

File diff suppressed because one or more lines are too long

22
public/lib/datatables/datatables.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,44 @@
/* Dropzone avec style Bootstrap */
.dropzone {
min-height: 200px; /* Taille minimale */
border-radius: 0.375rem; /* Coin arrondi */
background: #f8f9fa; /* Couleur de fond (bg-light) */
display: flex;
align-items: center;
justify-content: center;
}
/* Message de Dropzone */
.dropzone .dz-message {
font-size: 1rem;
font-weight: 500;
color: #6c757d; /* Texte gris (text-muted) */
}
/* Fichiers téléchargés */
.dropzone .dz-preview {
margin: 10px;
border: 1px solid #dee2e6; /* Bordure des fichiers */
border-radius: 0.375rem;
background: white;
padding: 10px;
display: flex;
align-items: center;
}
.dropzone .dz-preview .dz-filename {
font-size: 0.9rem;
color: #495057; /* Texte plus foncé */
}
.dropzone .dz-preview .dz-size {
font-size: 0.8rem;
color: #adb5bd;
}
.dropzone .dz-preview .dz-remove {
font-size: 0.8rem;
color: #dc3545; /* Bouton rouge */
text-decoration: underline;
cursor: pointer;
}

1
public/lib/dropzone/dropzone.min.css vendored Normal file

File diff suppressed because one or more lines are too long

1
public/lib/dropzone/dropzone.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 B

View File

@ -0,0 +1,41 @@
/*
* imgAreaSelect animated border style
*/
.imgareaselect-border1 {
background: url(border-anim-v.gif) repeat-y left top;
}
.imgareaselect-border2 {
background: url(border-anim-h.gif) repeat-x left top;
}
.imgareaselect-border3 {
background: url(border-anim-v.gif) repeat-y right top;
}
.imgareaselect-border4 {
background: url(border-anim-h.gif) repeat-x left bottom;
}
.imgareaselect-border1, .imgareaselect-border2,
.imgareaselect-border3, .imgareaselect-border4 {
filter: alpha(opacity=50);
opacity: 0.5;
}
.imgareaselect-handle {
background-color: #fff;
border: solid 1px #000;
filter: alpha(opacity=50);
opacity: 0.5;
}
.imgareaselect-outer {
background-color: #000;
filter: alpha(opacity=50);
opacity: 0.5;
}
.imgareaselect-selection {
}

View File

@ -0,0 +1,41 @@
/*
* imgAreaSelect default style
*/
.imgareaselect-border1 {
background: url(border-v.gif) repeat-y left top;
}
.imgareaselect-border2 {
background: url(border-h.gif) repeat-x left top;
}
.imgareaselect-border3 {
background: url(border-v.gif) repeat-y right top;
}
.imgareaselect-border4 {
background: url(border-h.gif) repeat-x left bottom;
}
.imgareaselect-border1, .imgareaselect-border2,
.imgareaselect-border3, .imgareaselect-border4 {
filter: alpha(opacity=50);
opacity: 0.5;
}
.imgareaselect-handle {
background-color: #fff;
border: solid 1px #000;
filter: alpha(opacity=50);
opacity: 0.5;
}
.imgareaselect-outer {
background-color: #000;
filter: alpha(opacity=50);
opacity: 0.5;
}
.imgareaselect-selection {
}

View File

@ -0,0 +1,36 @@
/*
* imgAreaSelect style to be used with deprecated options
*/
.imgareaselect-border1, .imgareaselect-border2,
.imgareaselect-border3, .imgareaselect-border4 {
filter: alpha(opacity=50);
opacity: 0.5;
}
.imgareaselect-border1 {
border: solid 1px #000;
}
.imgareaselect-border2 {
border: dashed 1px #fff;
}
.imgareaselect-handle {
background-color: #fff;
border: solid 1px #000;
filter: alpha(opacity=50);
opacity: 0.5;
}
.imgareaselect-outer {
background-color: #000;
filter: alpha(opacity=40);
opacity: 0.4;
}
.imgareaselect-selection {
background-color: #fff;
filter: alpha(opacity=0);
opacity: 0;
}

View File

@ -0,0 +1,1205 @@
/*
* imgAreaSelect jQuery plugin
* version 0.9.11-rc.1
*
* Copyright (c) 2008-2013 Michal Wojciechowski (odyniec.net)
*
* Dual licensed under the MIT (MIT-LICENSE.txt)
* and GPL (GPL-LICENSE.txt) licenses.
*
* http://odyniec.net/projects/imgareaselect/
*
*/
(function($) {
/*
* Math functions will be used extensively, so it's convenient to make a few
* shortcuts
*/
var abs = Math.abs,
max = Math.max,
min = Math.min,
round = Math.round;
/**
* Create a new HTML div element
*
* @return A jQuery object representing the new element
*/
function div() {
return $('<div/>');
}
/**
* imgAreaSelect initialization
*
* @param img
* A HTML image element to attach the plugin to
* @param options
* An options object
*/
$.imgAreaSelect = function (img, options) {
var
/* jQuery object representing the image */
$img = $(img),
/* Has the image finished loading? */
imgLoaded,
/* Plugin elements */
/* Container box */
$box = div(),
/* Selection area */
$area = div(),
/* Border (four divs) */
$border = div().add(div()).add(div()).add(div()),
/* Outer area (four divs) */
$outer = div().add(div()).add(div()).add(div()),
/* Handles (empty by default, initialized in setOptions()) */
$handles = $([]),
/*
* Additional element to work around a cursor problem in Opera
* (explained later)
*/
$areaOpera,
/* Image position (relative to viewport) */
left, top,
/* Image offset (as returned by .offset()) */
imgOfs = { left: 0, top: 0 },
/* Image dimensions (as returned by .width() and .height()) */
imgWidth, imgHeight,
/*
* jQuery object representing the parent element that the plugin
* elements are appended to
*/
$parent,
/* Parent element offset (as returned by .offset()) */
parOfs = { left: 0, top: 0 },
/* Base z-index for plugin elements */
zIndex = 0,
/* Plugin elements position */
position = 'absolute',
/* X/Y coordinates of the starting point for move/resize operations */
startX, startY,
/* Horizontal and vertical scaling factors */
scaleX, scaleY,
/* Current resize mode ("nw", "se", etc.) */
resize,
/* Selection area constraints */
minWidth, minHeight, maxWidth, maxHeight,
/* Aspect ratio to maintain (floating point number) */
aspectRatio,
/* Are the plugin elements currently displayed? */
shown,
/* Current selection (relative to parent element) */
x1, y1, x2, y2,
/* Current selection (relative to scaled image) */
selection = { x1: 0, y1: 0, x2: 0, y2: 0, width: 0, height: 0 },
/* Document element */
docElem = document.documentElement,
/* User agent */
ua = navigator.userAgent,
/* Various helper variables used throughout the code */
$p, d, i, o, w, h, adjusted;
/*
* Translate selection coordinates (relative to scaled image) to viewport
* coordinates (relative to parent element)
*/
/**
* Translate selection X to viewport X
*
* @param x
* Selection X
* @return Viewport X
*/
function viewX(x) {
return x + imgOfs.left - parOfs.left;
}
/**
* Translate selection Y to viewport Y
*
* @param y
* Selection Y
* @return Viewport Y
*/
function viewY(y) {
return y + imgOfs.top - parOfs.top;
}
/*
* Translate viewport coordinates to selection coordinates
*/
/**
* Translate viewport X to selection X
*
* @param x
* Viewport X
* @return Selection X
*/
function selX(x) {
return x - imgOfs.left + parOfs.left;
}
/**
* Translate viewport Y to selection Y
*
* @param y
* Viewport Y
* @return Selection Y
*/
function selY(y) {
return y - imgOfs.top + parOfs.top;
}
/*
* Translate event coordinates (relative to document) to viewport
* coordinates
*/
/**
* Get event X and translate it to viewport X
*
* @param event
* The event object
* @return Viewport X
*/
function evX(event) {
return event.pageX - parOfs.left;
}
/**
* Get event Y and translate it to viewport Y
*
* @param event
* The event object
* @return Viewport Y
*/
function evY(event) {
return event.pageY - parOfs.top;
}
/**
* Get the current selection
*
* @param noScale
* If set to <code>true</code>, scaling is not applied to the
* returned selection
* @return Selection object
*/
function getSelection(noScale) {
var sx = noScale || scaleX, sy = noScale || scaleY;
return { x1: round(selection.x1 * sx),
y1: round(selection.y1 * sy),
x2: round(selection.x2 * sx),
y2: round(selection.y2 * sy),
width: round(selection.x2 * sx) - round(selection.x1 * sx),
height: round(selection.y2 * sy) - round(selection.y1 * sy) };
}
/**
* Set the current selection
*
* @param x1
* X coordinate of the upper left corner of the selection area
* @param y1
* Y coordinate of the upper left corner of the selection area
* @param x2
* X coordinate of the lower right corner of the selection area
* @param y2
* Y coordinate of the lower right corner of the selection area
* @param noScale
* If set to <code>true</code>, scaling is not applied to the
* new selection
*/
function setSelection(x1, y1, x2, y2, noScale) {
var sx = noScale || scaleX, sy = noScale || scaleY;
selection = {
x1: round(x1 / sx || 0),
y1: round(y1 / sy || 0),
x2: round(x2 / sx || 0),
y2: round(y2 / sy || 0)
};
selection.width = selection.x2 - selection.x1;
selection.height = selection.y2 - selection.y1;
}
/**
* Recalculate image and parent offsets
*/
function adjust() {
/*
* Do not adjust if image has not yet loaded or if width is not a
* positive number. The latter might happen when imgAreaSelect is put
* on a parent element which is then hidden.
*/
if (!imgLoaded || !$img.width())
return;
/*
* Get image offset. The .offset() method returns float values, so they
* need to be rounded.
*/
imgOfs = { left: round($img.offset().left), top: round($img.offset().top) };
/* Get image dimensions */
imgWidth = $img.innerWidth();
imgHeight = $img.innerHeight();
imgOfs.top += ($img.outerHeight() - imgHeight) >> 1;
imgOfs.left += ($img.outerWidth() - imgWidth) >> 1;
/* Set minimum and maximum selection area dimensions */
minWidth = round(options.minWidth / scaleX) || 0;
minHeight = round(options.minHeight / scaleY) || 0;
maxWidth = round(min(options.maxWidth / scaleX || 1<<24, imgWidth));
maxHeight = round(min(options.maxHeight / scaleY || 1<<24, imgHeight));
/*
* Workaround for jQuery 1.3.2 incorrect offset calculation, originally
* observed in Safari 3. Firefox 2 is also affected.
*/
if ($().jquery == '1.3.2' && position == 'fixed' &&
!docElem['getBoundingClientRect'])
{
imgOfs.top += max(document.body.scrollTop, docElem.scrollTop);
imgOfs.left += max(document.body.scrollLeft, docElem.scrollLeft);
}
/* Determine parent element offset */
parOfs = /absolute|relative/.test($parent.css('position')) ?
{ left: round($parent.offset().left) - $parent.scrollLeft(),
top: round($parent.offset().top) - $parent.scrollTop() } :
position == 'fixed' ?
{ left: $(document).scrollLeft(), top: $(document).scrollTop() } :
{ left: 0, top: 0 };
left = viewX(0);
top = viewY(0);
/*
* Check if selection area is within image boundaries, adjust if
* necessary
*/
if (selection.x2 > imgWidth || selection.y2 > imgHeight)
doResize();
}
/**
* Update plugin elements
*
* @param resetKeyPress
* If set to <code>false</code>, this instance's keypress
* event handler is not activated
*/
function update(resetKeyPress) {
/* If plugin elements are hidden, do nothing */
if (!shown) return;
/*
* Set the position and size of the container box and the selection area
* inside it
*/
$box.css({ left: viewX(selection.x1), top: viewY(selection.y1) })
.add($area).width(w = selection.width).height(h = selection.height);
/*
* Reset the position of selection area, borders, and handles (IE6/IE7
* position them incorrectly if we don't do this)
*/
$area.add($border).add($handles).css({ left: 0, top: 0 });
/* Set border dimensions */
$border
.width(max(w - $border.outerWidth() + $border.innerWidth(), 0))
.height(max(h - $border.outerHeight() + $border.innerHeight(), 0));
/* Arrange the outer area elements */
$($outer[0]).css({ left: left, top: top,
width: selection.x1, height: imgHeight });
$($outer[1]).css({ left: left + selection.x1, top: top,
width: w, height: selection.y1 });
$($outer[2]).css({ left: left + selection.x2, top: top,
width: imgWidth - selection.x2, height: imgHeight });
$($outer[3]).css({ left: left + selection.x1, top: top + selection.y2,
width: w, height: imgHeight - selection.y2 });
w -= $handles.outerWidth();
h -= $handles.outerHeight();
/* Arrange handles */
switch ($handles.length) {
case 8:
$($handles[4]).css({ left: w >> 1 });
$($handles[5]).css({ left: w, top: h >> 1 });
$($handles[6]).css({ left: w >> 1, top: h });
$($handles[7]).css({ top: h >> 1 });
case 4:
$handles.slice(1,3).css({ left: w });
$handles.slice(2,4).css({ top: h });
}
if (resetKeyPress !== false) {
/*
* Need to reset the document keypress event handler -- unbind the
* current handler
*/
if ($.imgAreaSelect.onKeyPress != docKeyPress)
$(document).unbind($.imgAreaSelect.keyPress,
$.imgAreaSelect.onKeyPress);
if (options.keys)
/*
* Set the document keypress event handler to this instance's
* docKeyPress() function
*/
$(document)[$.imgAreaSelect.keyPress](
$.imgAreaSelect.onKeyPress = docKeyPress);
}
/*
* Internet Explorer displays 1px-wide dashed borders incorrectly by
* filling the spaces between dashes with white. Toggling the margin
* property between 0 and "auto" fixes this in IE6 and IE7 (IE8 is still
* broken). This workaround is not perfect, as it requires setTimeout()
* and thus causes the border to flicker a bit, but I haven't found a
* better solution.
*
* Note: This only happens with CSS borders, set with the borderWidth,
* borderOpacity, borderColor1, and borderColor2 options (which are now
* deprecated). Borders created with GIF background images are fine.
*/
if (msie && $border.outerWidth() - $border.innerWidth() == 2) {
$border.css('margin', 0);
setTimeout(function () { $border.css('margin', 'auto'); }, 0);
}
}
/**
* Do the complete update sequence: recalculate offsets, update the
* elements, and set the correct values of x1, y1, x2, and y2.
*
* @param resetKeyPress
* If set to <code>false</code>, this instance's keypress
* event handler is not activated
*/
function doUpdate(resetKeyPress) {
adjust();
update(resetKeyPress);
x1 = viewX(selection.x1); y1 = viewY(selection.y1);
x2 = viewX(selection.x2); y2 = viewY(selection.y2);
}
/**
* Hide or fade out an element (or multiple elements)
*
* @param $elem
* A jQuery object containing the element(s) to hide/fade out
* @param fn
* Callback function to be called when fadeOut() completes
*/
function hide($elem, fn) {
options.fadeSpeed ? $elem.fadeOut(options.fadeSpeed, fn) : $elem.hide();
}
/**
* Selection area mousemove event handler
*
* @param event
* The event object
*/
function areaMouseMove(event) {
var x = selX(evX(event)) - selection.x1,
y = selY(evY(event)) - selection.y1;
if (!adjusted) {
adjust();
adjusted = true;
$box.one('mouseout', function () { adjusted = false; });
}
/* Clear the resize mode */
resize = '';
if (options.resizable) {
/*
* Check if the mouse pointer is over the resize margin area and set
* the resize mode accordingly
*/
if (y <= options.resizeMargin)
resize = 'n';
else if (y >= selection.height - options.resizeMargin)
resize = 's';
if (x <= options.resizeMargin)
resize += 'w';
else if (x >= selection.width - options.resizeMargin)
resize += 'e';
}
$box.css('cursor', resize ? resize + '-resize' :
options.movable ? 'move' : '');
if ($areaOpera)
$areaOpera.toggle();
}
/**
* Document mouseup event handler
*
* @param event
* The event object
*/
function docMouseUp(event) {
/* Set back the default cursor */
$('body').css('cursor', '');
/*
* If autoHide is enabled, or if the selection has zero width/height,
* hide the selection and the outer area
*/
if (options.autoHide || selection.width * selection.height == 0)
hide($box.add($outer), function () { $(this).hide(); });
$(document).unbind('mousemove', selectingMouseMove);
$box.mousemove(areaMouseMove);
options.onSelectEnd(img, getSelection());
}
/**
* Selection area mousedown event handler
*
* @param event
* The event object
* @return false
*/
function areaMouseDown(event) {
if (event.which != 1) return false;
adjust();
if (resize) {
/* Resize mode is in effect */
$('body').css('cursor', resize + '-resize');
x1 = viewX(selection[/w/.test(resize) ? 'x2' : 'x1']);
y1 = viewY(selection[/n/.test(resize) ? 'y2' : 'y1']);
$(document).mousemove(selectingMouseMove)
.one('mouseup', docMouseUp);
$box.unbind('mousemove', areaMouseMove);
}
else if (options.movable) {
startX = left + selection.x1 - evX(event);
startY = top + selection.y1 - evY(event);
$box.unbind('mousemove', areaMouseMove);
$(document).mousemove(movingMouseMove)
.one('mouseup', function () {
options.onSelectEnd(img, getSelection());
$(document).unbind('mousemove', movingMouseMove);
$box.mousemove(areaMouseMove);
});
}
else
$img.mousedown(event);
return false;
}
/**
* Adjust the x2/y2 coordinates to maintain aspect ratio (if defined)
*
* @param xFirst
* If set to <code>true</code>, calculate x2 first. Otherwise,
* calculate y2 first.
*/
function fixAspectRatio(xFirst) {
if (aspectRatio)
if (xFirst) {
x2 = max(left, min(left + imgWidth,
x1 + abs(y2 - y1) * aspectRatio * (x2 > x1 || -1)));
y2 = round(max(top, min(top + imgHeight,
y1 + abs(x2 - x1) / aspectRatio * (y2 > y1 || -1))));
x2 = round(x2);
}
else {
y2 = max(top, min(top + imgHeight,
y1 + abs(x2 - x1) / aspectRatio * (y2 > y1 || -1)));
x2 = round(max(left, min(left + imgWidth,
x1 + abs(y2 - y1) * aspectRatio * (x2 > x1 || -1))));
y2 = round(y2);
}
}
/**
* Resize the selection area respecting the minimum/maximum dimensions and
* aspect ratio
*/
function doResize() {
/*
* Make sure the top left corner of the selection area stays within
* image boundaries (it might not if the image source was dynamically
* changed).
*/
x1 = min(x1, left + imgWidth);
y1 = min(y1, top + imgHeight);
if (abs(x2 - x1) < minWidth) {
/* Selection width is smaller than minWidth */
x2 = x1 - minWidth * (x2 < x1 || -1);
if (x2 < left)
x1 = left + minWidth;
else if (x2 > left + imgWidth)
x1 = left + imgWidth - minWidth;
}
if (abs(y2 - y1) < minHeight) {
/* Selection height is smaller than minHeight */
y2 = y1 - minHeight * (y2 < y1 || -1);
if (y2 < top)
y1 = top + minHeight;
else if (y2 > top + imgHeight)
y1 = top + imgHeight - minHeight;
}
x2 = max(left, min(x2, left + imgWidth));
y2 = max(top, min(y2, top + imgHeight));
fixAspectRatio(abs(x2 - x1) < abs(y2 - y1) * aspectRatio);
if (abs(x2 - x1) > maxWidth) {
/* Selection width is greater than maxWidth */
x2 = x1 - maxWidth * (x2 < x1 || -1);
fixAspectRatio();
}
if (abs(y2 - y1) > maxHeight) {
/* Selection height is greater than maxHeight */
y2 = y1 - maxHeight * (y2 < y1 || -1);
fixAspectRatio(true);
}
selection = { x1: selX(min(x1, x2)), x2: selX(max(x1, x2)),
y1: selY(min(y1, y2)), y2: selY(max(y1, y2)),
width: abs(x2 - x1), height: abs(y2 - y1) };
update();
options.onSelectChange(img, getSelection());
}
/**
* Mousemove event handler triggered when the user is selecting an area
*
* @param event
* The event object
* @return false
*/
function selectingMouseMove(event) {
x2 = /w|e|^$/.test(resize) || aspectRatio ? evX(event) : viewX(selection.x2);
y2 = /n|s|^$/.test(resize) || aspectRatio ? evY(event) : viewY(selection.y2);
doResize();
return false;
}
/**
* Move the selection area
*
* @param newX1
* New viewport X1
* @param newY1
* New viewport Y1
*/
function doMove(newX1, newY1) {
x2 = (x1 = newX1) + selection.width;
y2 = (y1 = newY1) + selection.height;
$.extend(selection, { x1: selX(x1), y1: selY(y1), x2: selX(x2),
y2: selY(y2) });
update();
options.onSelectChange(img, getSelection());
}
/**
* Mousemove event handler triggered when the selection area is being moved
*
* @param event
* The event object
* @return false
*/
function movingMouseMove(event) {
x1 = max(left, min(startX + evX(event), left + imgWidth - selection.width));
y1 = max(top, min(startY + evY(event), top + imgHeight - selection.height));
doMove(x1, y1);
event.preventDefault();
return false;
}
/**
* Start selection
*/
function startSelection() {
$(document).unbind('mousemove', startSelection);
adjust();
x2 = x1;
y2 = y1;
doResize();
resize = '';
if (!$outer.is(':visible'))
/* Show the plugin elements */
$box.add($outer).hide().fadeIn(options.fadeSpeed||0);
shown = true;
$(document).unbind('mouseup', cancelSelection)
.mousemove(selectingMouseMove).one('mouseup', docMouseUp);
$box.unbind('mousemove', areaMouseMove);
options.onSelectStart(img, getSelection());
}
/**
* Cancel selection
*/
function cancelSelection() {
$(document).unbind('mousemove', startSelection)
.unbind('mouseup', cancelSelection);
hide($box.add($outer));
setSelection(selX(x1), selY(y1), selX(x1), selY(y1));
/* If this is an API call, callback functions should not be triggered */
if (!(this instanceof $.imgAreaSelect)) {
options.onSelectChange(img, getSelection());
options.onSelectEnd(img, getSelection());
}
}
/**
* Image mousedown event handler
*
* @param event
* The event object
* @return false
*/
function imgMouseDown(event) {
/* Ignore the event if animation is in progress */
if (event.which != 1 || $outer.is(':animated')) return false;
adjust();
startX = x1 = evX(event);
startY = y1 = evY(event);
/* Selection will start when the mouse is moved */
$(document).mousemove(startSelection).mouseup(cancelSelection);
return false;
}
/**
* Window resize event handler
*/
function windowResize() {
doUpdate(false);
}
/**
* Image load event handler. This is the final part of the initialization
* process.
*/
function imgLoad() {
imgLoaded = true;
/* Set options */
setOptions(options = $.extend({
classPrefix: 'imgareaselect',
movable: true,
parent: 'body',
resizable: true,
resizeMargin: 10,
onInit: function () {},
onSelectStart: function () {},
onSelectChange: function () {},
onSelectEnd: function () {}
}, options));
$box.add($outer).css({ visibility: '' });
if (options.show) {
shown = true;
adjust();
update();
$box.add($outer).hide().fadeIn(options.fadeSpeed||0);
}
/*
* Call the onInit callback. The setTimeout() call is used to ensure
* that the plugin has been fully initialized and the object instance is
* available (so that it can be obtained in the callback).
*/
setTimeout(function () { options.onInit(img, getSelection()); }, 0);
}
/**
* Document keypress event handler
*
* @param event
* The event object
* @return false
*/
var docKeyPress = function(event) {
var k = options.keys, d, t, key = event.keyCode;
d = !isNaN(k.alt) && (event.altKey || event.originalEvent.altKey) ? k.alt :
!isNaN(k.ctrl) && event.ctrlKey ? k.ctrl :
!isNaN(k.shift) && event.shiftKey ? k.shift :
!isNaN(k.arrows) ? k.arrows : 10;
if (k.arrows == 'resize' || (k.shift == 'resize' && event.shiftKey) ||
(k.ctrl == 'resize' && event.ctrlKey) ||
(k.alt == 'resize' && (event.altKey || event.originalEvent.altKey)))
{
/* Resize selection */
switch (key) {
case 37:
/* Left */
d = -d;
case 39:
/* Right */
t = max(x1, x2);
x1 = min(x1, x2);
x2 = max(t + d, x1);
fixAspectRatio();
break;
case 38:
/* Up */
d = -d;
case 40:
/* Down */
t = max(y1, y2);
y1 = min(y1, y2);
y2 = max(t + d, y1);
fixAspectRatio(true);
break;
default:
return;
}
doResize();
}
else {
/* Move selection */
x1 = min(x1, x2);
y1 = min(y1, y2);
switch (key) {
case 37:
/* Left */
doMove(max(x1 - d, left), y1);
break;
case 38:
/* Up */
doMove(x1, max(y1 - d, top));
break;
case 39:
/* Right */
doMove(x1 + min(d, imgWidth - selX(x2)), y1);
break;
case 40:
/* Down */
doMove(x1, y1 + min(d, imgHeight - selY(y2)));
break;
default:
return;
}
}
return false;
};
/**
* Apply style options to plugin element (or multiple elements)
*
* @param $elem
* A jQuery object representing the element(s) to style
* @param props
* An object that maps option names to corresponding CSS
* properties
*/
function styleOptions($elem, props) {
for (var option in props)
if (options[option] !== undefined)
$elem.css(props[option], options[option]);
}
/**
* Set plugin options
*
* @param newOptions
* The new options object
*/
function setOptions(newOptions) {
if (newOptions.parent)
($parent = $(newOptions.parent)).append($box).append($outer);
/* Merge the new options with the existing ones */
$.extend(options, newOptions);
adjust();
if (newOptions.handles != null) {
/* Recreate selection area handles */
$handles.remove();
$handles = $([]);
i = newOptions.handles ? newOptions.handles == 'corners' ? 4 : 8 : 0;
while (i--)
$handles = $handles.add(div());
/* Add a class to handles and set the CSS properties */
$handles.addClass(options.classPrefix + '-handle').css({
position: 'absolute',
/*
* The font-size property needs to be set to zero, otherwise
* Internet Explorer makes the handles too large
*/
fontSize: 0,
zIndex: zIndex + 1 || 1
});
/*
* If handle width/height has not been set with CSS rules, set the
* default 5px
*/
if (!parseInt($handles.css('width')) >= 0)
$handles.width(5).height(5);
/*
* If the borderWidth option is in use, add a solid border to
* handles
*/
if (o = options.borderWidth)
$handles.css({ borderWidth: o, borderStyle: 'solid' });
/* Apply other style options */
styleOptions($handles, { borderColor1: 'border-color',
borderColor2: 'background-color',
borderOpacity: 'opacity' });
}
/* Calculate scale factors */
scaleX = options.imageWidth / imgWidth || 1;
scaleY = options.imageHeight / imgHeight || 1;
/* Set selection */
if (newOptions.x1 != null) {
setSelection(newOptions.x1, newOptions.y1, newOptions.x2,
newOptions.y2);
newOptions.show = !newOptions.hide;
}
if (newOptions.keys)
/* Enable keyboard support */
options.keys = $.extend({ shift: 1, ctrl: 'resize' },
newOptions.keys);
/* Add classes to plugin elements */
$outer.addClass(options.classPrefix + '-outer');
$area.addClass(options.classPrefix + '-selection');
for (i = 0; i++ < 4;)
$($border[i-1]).addClass(options.classPrefix + '-border' + i);
/* Apply style options */
styleOptions($area, { selectionColor: 'background-color',
selectionOpacity: 'opacity' });
styleOptions($border, { borderOpacity: 'opacity',
borderWidth: 'border-width' });
styleOptions($outer, { outerColor: 'background-color',
outerOpacity: 'opacity' });
if (o = options.borderColor1)
$($border[0]).css({ borderStyle: 'solid', borderColor: o });
if (o = options.borderColor2)
$($border[1]).css({ borderStyle: 'dashed', borderColor: o });
/* Append all the selection area elements to the container box */
$box.append($area.add($border).add($areaOpera)).append($handles);
if (msie) {
if (o = ($outer.css('filter')||'').match(/opacity=(\d+)/))
$outer.css('opacity', o[1]/100);
if (o = ($border.css('filter')||'').match(/opacity=(\d+)/))
$border.css('opacity', o[1]/100);
}
if (newOptions.hide)
hide($box.add($outer));
else if (newOptions.show && imgLoaded) {
shown = true;
$box.add($outer).fadeIn(options.fadeSpeed||0);
doUpdate();
}
/* Calculate the aspect ratio factor */
aspectRatio = (d = (options.aspectRatio || '').split(/:/))[0] / d[1];
$img.add($outer).unbind('mousedown', imgMouseDown);
if (options.disable || options.enable === false) {
/* Disable the plugin */
$box.unbind('mousemove', areaMouseMove).unbind('mousedown', areaMouseDown);
$(window).unbind('resize', windowResize);
}
else {
if (options.enable || options.disable === false) {
/* Enable the plugin */
if (options.resizable || options.movable)
$box.mousemove(areaMouseMove).mousedown(areaMouseDown);
$(window).resize(windowResize);
}
if (!options.persistent)
$img.add($outer).mousedown(imgMouseDown);
}
options.enable = options.disable = undefined;
}
/**
* Remove plugin completely
*/
this.remove = function () {
/*
* Call setOptions with { disable: true } to unbind the event handlers
*/
setOptions({ disable: true });
$box.add($outer).remove();
};
/*
* Public API
*/
/**
* Get current options
*
* @return An object containing the set of options currently in use
*/
this.getOptions = function () { return options; };
/**
* Set plugin options
*
* @param newOptions
* The new options object
*/
this.setOptions = setOptions;
/**
* Get the current selection
*
* @param noScale
* If set to <code>true</code>, scaling is not applied to the
* returned selection
* @return Selection object
*/
this.getSelection = getSelection;
/**
* Set the current selection
*
* @param x1
* X coordinate of the upper left corner of the selection area
* @param y1
* Y coordinate of the upper left corner of the selection area
* @param x2
* X coordinate of the lower right corner of the selection area
* @param y2
* Y coordinate of the lower right corner of the selection area
* @param noScale
* If set to <code>true</code>, scaling is not applied to the
* new selection
*/
this.setSelection = setSelection;
/**
* Cancel selection
*/
this.cancelSelection = cancelSelection;
/**
* Update plugin elements
*
* @param resetKeyPress
* If set to <code>false</code>, this instance's keypress
* event handler is not activated
*/
this.update = doUpdate;
/* Do the dreaded browser detection */
var msie = (/msie ([\w.]+)/i.exec(ua)||[])[1],
opera = /opera/i.test(ua),
safari = /webkit/i.test(ua) && !/chrome/i.test(ua);
/*
* Traverse the image's parent elements (up to <body>) and find the
* highest z-index
*/
$p = $img;
while ($p.length) {
zIndex = max(zIndex,
!isNaN($p.css('z-index')) ? $p.css('z-index') : zIndex);
/* Also check if any of the ancestor elements has fixed position */
if ($p.css('position') == 'fixed')
position = 'fixed';
$p = $p.parent(':not(body)');
}
/*
* If z-index is given as an option, it overrides the one found by the
* above loop
*/
zIndex = options.zIndex || zIndex;
if (msie)
$img.attr('unselectable', 'on');
/*
* In MSIE and WebKit, we need to use the keydown event instead of keypress
*/
$.imgAreaSelect.keyPress = msie || safari ? 'keydown' : 'keypress';
/*
* There is a bug affecting the CSS cursor property in Opera (observed in
* versions up to 10.00) that prevents the cursor from being updated unless
* the mouse leaves and enters the element again. To trigger the mouseover
* event, we're adding an additional div to $box and we're going to toggle
* it when mouse moves inside the selection area.
*/
if (opera)
$areaOpera = div().css({ width: '100%', height: '100%',
position: 'absolute', zIndex: zIndex + 2 || 2 });
/*
* We initially set visibility to "hidden" as a workaround for a weird
* behaviour observed in Google Chrome 1.0.154.53 (on Windows XP). Normally
* we would just set display to "none", but, for some reason, if we do so
* then Chrome refuses to later display the element with .show() or
* .fadeIn().
*/
$box.add($outer).css({ visibility: 'hidden', position: position,
overflow: 'hidden', zIndex: zIndex || '0' });
$box.css({ zIndex: zIndex + 2 || 2 });
$area.add($border).css({ position: 'absolute', fontSize: 0 });
/*
* If the image has been fully loaded, or if it is not really an image (eg.
* a div), call imgLoad() immediately; otherwise, bind it to be called once
* on image load event.
*/
img.complete || img.readyState == 'complete' || !$img.is('img') ?
imgLoad() : $img.one('load', imgLoad);
/*
* MSIE 9.0 doesn't always fire the image load event -- resetting the src
* attribute seems to trigger it. The check is for version 7 and above to
* accommodate for MSIE 9 running in compatibility mode.
*/
if (!imgLoaded && msie && msie >= 7)
img.src = img.src;
};
/**
* Invoke imgAreaSelect on a jQuery object containing the image(s)
*
* @param options
* Options object
* @return The jQuery object or a reference to imgAreaSelect instance (if the
* <code>instance</code> option was specified)
*/
$.fn.imgAreaSelect = function (options) {
options = options || {};
this.each(function () {
/* Is there already an imgAreaSelect instance bound to this element? */
if ($(this).data('imgAreaSelect')) {
/* Yes there is -- is it supposed to be removed? */
if (options.remove) {
/* Remove the plugin */
$(this).data('imgAreaSelect').remove();
$(this).removeData('imgAreaSelect');
}
else
/* Reset options */
$(this).data('imgAreaSelect').setOptions(options);
}
else if (!options.remove) {
/* No exising instance -- create a new one */
/*
* If neither the "enable" nor the "disable" option is present, add
* "enable" as the default
*/
if (options.enable === undefined && options.disable === undefined)
options.enable = true;
$(this).data('imgAreaSelect', new $.imgAreaSelect(this, options));
}
});
if (options.instance)
/*
* Return the imgAreaSelect instance bound to the first element in the
* set
*/
return $(this).data('imgAreaSelect');
return this;
};
})(jQuery);

2
public/lib/jquery/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

8
public/lib/select2/select2.init.js vendored Normal file
View File

@ -0,0 +1,8 @@
$(document).ready(function(){
$('.select2').select2(
{
language: "fr",
theme: 'bootstrap-5'
}
);
});

1
public/lib/select2/select2.min.css vendored Normal file

File diff suppressed because one or more lines are too long

2
public/lib/select2/select2.min.js vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 936 B

BIN
public/medias/logo/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -0,0 +1,78 @@
<?php
namespace App\Command;
use App\Entity\Project;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Ramsey\Uuid\Uuid;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
#[AsCommand(
name: 'app:init',
description: 'Initialisation of the app',
)]
class InitCommand extends Command
{
private EntityManagerInterface $em;
private ParameterBagInterface $params;
private UserPasswordHasherInterface $passwordHasher;
public function __construct(EntityManagerInterface $em, ParameterBagInterface $params, UserPasswordHasherInterface $passwordHasher)
{
$this->em = $em;
$this->params = $params;
$this->passwordHasher = $passwordHasher;
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$io->title('APP:INIT');
$io->text('Initialisation of the app');
$io->text('');
// Création d'un project par defaut
$io->text("> Création d'un project par defaut");
$project = $this->em->getRepository("App\Entity\Project")->findOneBy([], ['id' => 'ASC']);
if (!$project) {
$project = new Project();
$project->setTitle($this->params->get('appName'));
$project->setUuid(Uuid::uuid4());
$project->setGitUrl("https://github.com/afornerot/amatl-doc.git");
$this->em->persist($project);
$this->em->flush();
}
$user = $this->em->getRepository("App\Entity\User")->findOneBy(['username' => 'admin']);
if (!$user) {
$io->text('> Création du compte admin par defaut');
$user = new User();
$hashedPassword = $this->passwordHasher->hashPassword(
$user,
$this->params->get('appSecret')
);
$user->setUsername('admin');
$user->setPassword($hashedPassword);
$user->setAvatar('medias/avatar/admin.jpg');
$user->setEmail($this->params->get('appNoreply'));
$user->addProject($project);
$user->setProject($project);
$this->em->persist($user);
}
$user->setRoles(['ROLE_ADMIN']);
$this->em->flush();
return Command::SUCCESS;
}
}

0
src/Controller/.gitignore vendored Normal file
View File

View File

@ -0,0 +1,49 @@
<?php
namespace App\Controller;
use App\Form\DicosType;
use App\Service\DicosService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class HomeController extends AbstractController
{
private DicosService $dicosService;
public function __construct(DicosService $dicosService)
{
$this->dicosService = $dicosService;
}
#[Route('/', name: 'app_home')]
public function home(Request $request): Response
{
$data = $this->dicosService->load();
$form = $this->createForm(DicosType::class, $data['data'], ['dicos' => $data['dicos']]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$values = $form->getData();
$this->dicosService->save($data, $values);
}
return $this->render('home/home.html.twig', [
'usemenu' => true,
'usesidebar' => false,
'maxwidth' => 1500,
'dicos' => $data['dicos'],
'form' => $form,
]);
}
#[Route('/admin', name: 'app_admin')]
public function admin(): Response
{
return $this->render('home/blank.html.twig', [
'usemenu' => true,
'usesidebar' => true,
]);
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
class SecurityController extends AbstractController
{
#[Route(path: '/login', name: 'app_login')]
public function login(AuthenticationUtils $authenticationUtils): Response
{
// get the login error if there is one
$error = $authenticationUtils->getLastAuthenticationError();
// last username entered by the user
$lastUsername = $authenticationUtils->getLastUsername();
return $this->render('security/login.html.twig', [
'last_username' => $lastUsername,
'error' => $error,
]);
}
#[Route(path: '/logout', name: 'app_logout')]
public function logout(): void
{
throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
}
}

View File

@ -0,0 +1,80 @@
<?php
namespace App\Controller;
use App\Service\ImageService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class UploadController extends AbstractController
{
private ImageService $imageService;
public function __construct(ImageService $imageService)
{
$this->imageService = $imageService;
}
#[Route('/user/upload/crop01/{endpoint}', name: 'app_user_upload_crop01')]
public function crop01(string $endpoint, Request $request): Response
{
$reportThumb = $request->get('reportThumb');
return $this->render('upload\crop01.html.twig', [
'useheader' => false,
'usemenu' => false,
'usesidebar' => false,
'endpoint' => $endpoint,
'reportThumb' => $reportThumb,
]);
}
#[Route('/user/upload/crop02', name: 'app_user_upload_crop02')]
public function crop02(Request $request): Response
{
$reportThumb = $request->get('reportThumb');
$path = $request->get('path');
$file = $request->get('file');
$image = $this->getParameter('kernel.project_dir').'/public/'.$path.'/'.$file;
$thumb = $this->getParameter('kernel.project_dir').'/public/'.$path.'/thumb_'.$file;
// Redimentionner
$this->imageService->resizeImage($image, 700, 700);
// Construction du formulaire
$form = $this->createFormBuilder()
->add('submit', SubmitType::class, ['label' => 'Valider', 'attr' => ['class' => 'btn btn-success']])
->add('x1', HiddenType::class)
->add('y1', HiddenType::class)
->add('x2', HiddenType::class)
->add('y2', HiddenType::class)
->add('w', HiddenType::class)
->add('h', HiddenType::class)
->getForm();
// Récupération des data du formulaire
$form->handleRequest($request);
$toReport = false;
// Sur validation on généère la miniature croppée
if ($form->isSubmitted() && $form->isValid()) {
$data = $form->getData();
$toReport = true;
$this->imageService->cropImage($image, $thumb, $data['x1'], $data['y1'], $data['w'], $data['h'], 150, 150);
}
return $this->render('upload\crop02.html.twig', [
'useheader' => false,
'usemenu' => false,
'usesidebar' => false,
'reportThumb' => $reportThumb,
'image' => $path.'/'.$file,
'thumb' => $path.'/thumb_'.$file,
'form' => $form,
'toReport' => $toReport,
]);
}
}

View File

@ -0,0 +1,157 @@
<?php
namespace App\Controller;
use App\Entity\User;
use App\Form\UserType;
use App\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route;
class UserController extends AbstractController
{
#[Route('/admin/user', name: 'app_admin_user')]
public function list(UserRepository $userRepository): Response
{
$users = $userRepository->findAll();
return $this->render('user/list.html.twig', [
'usemenu' => true,
'usesidebar' => true,
'title' => 'Liste des Utilisateurs',
'routesubmit' => 'app_admin_user_submit',
'routeupdate' => 'app_admin_user_update',
'users' => $users,
]);
}
#[Route('/admin/user/submit', name: 'app_admin_user_submit')]
public function submit(Request $request, UserPasswordHasherInterface $passwordHasher, EntityManagerInterface $em): Response
{
$user = new User();
$form = $this->createForm(UserType::class, $user, ['mode' => 'submit', 'modeAuth' => $this->getParameter('modeAuth')]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$user = $form->getData();
$hashedPassword = $passwordHasher->hashPassword(
$user,
$user->getPassword()
);
$user->setPassword($hashedPassword);
$em->persist($user);
$em->flush();
return $this->redirectToRoute('app_admin_user');
}
return $this->render('user/edit.html.twig', [
'usemenu' => true,
'usesidebar' => true,
'title' => 'Création Utilisateur',
'routecancel' => 'app_admin_user',
'routedelete' => 'app_admin_user_delete',
'mode' => 'submit',
'form' => $form,
]);
}
#[Route('/admin/user/update/{id}', name: 'app_admin_user_update')]
public function update(int $id, Request $request, UserPasswordHasherInterface $passwordHasher, EntityManagerInterface $em): Response
{
$user = $em->getRepository(User::class)->find($id);
if (!$user) {
return $this->redirectToRoute('app_admin_user');
}
$hashedPassword = $user->getPassword();
$form = $this->createForm(UserType::class, $user, ['mode' => 'update', 'modeAuth' => $this->getParameter('modeAuth')]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$user = $form->getData();
if ($user->getPassword()) {
$hashedPassword = $passwordHasher->hashPassword(
$user,
$user->getPassword()
);
}
$user->setPassword($hashedPassword);
$em->flush();
return $this->redirectToRoute('app_admin_user');
}
return $this->render('user/edit.html.twig', [
'usemenu' => true,
'usesidebar' => true,
'title' => 'Modification Utilisateur = '.$user->getUsername(),
'routecancel' => 'app_admin_user',
'routedelete' => 'app_admin_user_delete',
'mode' => 'update',
'form' => $form,
]);
}
#[Route('/admin/user/delete/{id}', name: 'app_admin_user_delete')]
public function delete(int $id, EntityManagerInterface $em): Response
{
$user = $em->getRepository(User::class)->find($id);
if (!$user) {
return $this->redirectToRoute('app_admin_user');
}
// Tentative de suppression
try {
$em->remove($user);
$em->flush();
} catch (\Exception $e) {
$this->addflash('error', $e->getMessage());
return $this->redirectToRoute('app_admin_user_update', ['id' => $id]);
}
return $this->redirectToRoute('app_admin_user');
}
#[Route('/user', name: 'app_user_profil')]
public function profil(Request $request, UserPasswordHasherInterface $passwordHasher, EntityManagerInterface $em): Response
{
$user = $em->getRepository(User::class)->find($this->getUser());
if (!$user) {
return $this->redirectToRoute('app_home');
}
$hashedPassword = $user->getPassword();
$form = $this->createForm(UserType::class, $user, ['mode' => 'profil', 'modeAuth' => $this->getParameter('modeAuth')]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$user = $form->getData();
if ($user->getPassword()) {
$hashedPassword = $passwordHasher->hashPassword(
$user,
$user->getPassword()
);
}
$user->setPassword($hashedPassword);
$em->flush();
return $this->redirectToRoute('app_home');
}
return $this->render('user/edit.html.twig', [
'usemenu' => true,
'usesidebar' => false,
'title' => 'Profil = '.$user->getUsername(),
'routecancel' => 'app_home',
'routedelete' => '',
'mode' => 'profil',
'form' => $form,
]);
}
}

0
src/Entity/.gitignore vendored Normal file
View File

142
src/Entity/User.php Normal file
View File

@ -0,0 +1,142 @@
<?php
namespace App\Entity;
use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_USERNAME', fields: ['username'])]
#[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_EMAIL', fields: ['email'])]
#[UniqueEntity(fields: ['email'], message: 'Cet email est déjà utilisé.')]
#[UniqueEntity(fields: ['username'], message: 'Ce nom dutilisateur est déjà pris.')]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 180, unique: true)]
private ?string $username = null;
#[ORM\Column]
private array $roles = [];
#[ORM\Column]
#[Assert\Regex(
pattern: '/^(?=.*[a-z])(?=.*[A-Z])(?=.*\W).{8,}$/',
message: 'Le mot de passe doit contenir au moins 8 caractères, une lettre majuscule, une lettre minuscule et un caractère spécial.'
)]
private ?string $password = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $avatar = null;
#[ORM\Column(length: 255, unique: true)]
#[Assert\Email(message: 'Veuillez entrer un email valide.')]
private ?string $email = null;
public function getId(): ?int
{
return $this->id;
}
public function getUsername(): ?string
{
return $this->username;
}
public function setUsername(string $username): static
{
$this->username = $username;
return $this;
}
/**
* A visual identifier that represents this user.
*
* @see UserInterface
*/
public function getUserIdentifier(): string
{
return (string) $this->username;
}
/**
* @see UserInterface
*
* @return list<string>
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
/**
* @param list<string> $roles
*/
public function setRoles(array $roles): static
{
$this->roles = $roles;
return $this;
}
/**
* @see PasswordAuthenticatedUserInterface
*/
public function getPassword(): ?string
{
return $this->password;
}
public function setPassword(?string $password): static
{
$this->password = $password;
return $this;
}
/**
* @see UserInterface
*/
public function eraseCredentials(): void
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
public function getAvatar(): ?string
{
return $this->avatar ? $this->avatar : 'medias/avatar/noavatar.png';
}
public function setAvatar(?string $avatar): static
{
$this->avatar = $avatar;
return $this;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(string $email): static
{
$this->email = $email;
return $this;
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace App\EventListener;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\Security\Http\Event\LogoutEvent;
final class LogoutListener
{
private ParameterBagInterface $parameterBag;
public function __construct(ParameterBagInterface $parameterBag)
{
$this->parameterBag = $parameterBag;
}
#[AsEventListener(event: LogoutEvent::class)]
public function onLogoutEvent(LogoutEvent $event): void
{
if ('CAS' == $this->parameterBag->get('modeAuth')) {
$request = $event->getRequest();
$host = $request->headers->get('X-Forwarded-Host') ?? $request->getHost().($request->getPort() ? ':'.$request->getPort() : '');
$scheme = $request->headers->get('X-Forwarded-Proto') ?? $request->getScheme();
$url = $scheme.'://'.$host;
\phpCAS::client(CAS_VERSION_2_0, $this->parameterBag->get('casHost'), (int) $this->parameterBag->get('casPort'), $this->parameterBag->get('casPath'), $url, false);
\phpCAS::setNoCasServerValidation();
$url.=$request->getBaseUrl();
\phpCAS::logoutWithRedirectService($url);
}
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\EventListener;
use App\Entity\User;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Routing\RouterInterface;
final class SessionListener
{
private Security $security;
private RouterInterface $router;
public function __construct(Security $security, RouterInterface $router)
{
$this->security = $security;
$this->router = $router;
}
#[AsEventListener(event: KernelEvents::REQUEST)]
public function onKernelRequest(RequestEvent $event): void
{
$request = $event->getRequest();
$session = $request->getSession();
$user = $this->security->getUser();
if ($user instanceof User) {
}
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace App\EventListener;
use Oneup\UploaderBundle\Event\PostPersistEvent;
use Oneup\UploaderBundle\Event\ValidationEvent;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpKernel\KernelInterface;
final class UploadListener
{
private string $projectDir;
public function __construct(KernelInterface $kernel)
{
// Utiliser le projectDir pour construire le chemin
$this->projectDir = $kernel->getProjectDir();
}
#[AsEventListener(event: 'oneup_uploader.validation')]
public function onOneupUploaderValidation(ValidationEvent $event): void
{
// On s'assure que le repertoire de destination existe bien
$fs = new Filesystem();
$fs->mkdir($this->projectDir.'/public/uploads');
$fs->mkdir($this->projectDir.'/public/uploads/'.$event->getType());
}
#[AsEventListener(event: 'oneup_uploader.post_persist')]
public function onOneupUploaderPostPersit(PostPersistEvent $event): void
{
$file = $event->getFile();
$type = $event->getType();
$filename = $file->getFilename();
$response = $event->getResponse();
$response['file'] = $filename;
$response['path'] = 'uploads/'.$type;
$response['filepath'] = 'uploads/'.$type.'/'.$filename;
}
}

81
src/Form/DicosType.php Normal file
View File

@ -0,0 +1,81 @@
<?php
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class DicosType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('submit', SubmitType::class, [
'label' => 'Valider',
'attr' => ['class' => 'btn btn-success no-print'],
]);
foreach ($options['dicos'] as $section) {
foreach ($section['vars'] as $var) {
switch ($var['type']) {
case 'text':
$type = TextType::class;
break;
case 'integer':
$type = IntegerType::class;
break;
case 'choice':
$type = ChoiceType::class;
break;
case 'bool':
$type = ChoiceType::class;
break;
default:
$type = TextType::class;
break;
}
$options = [
'label' => ($var['label'] ?? $var['id']),
'required' => ($var['required'] ?? false),
];
if ('bool' === $var['type']) {
$options['placeholder'] = '--- Sélectionnez une option ---';
$options['choices'] = ['oui' => 1, 'non' => 0];
}
if ('choice' === $var['type']) {
$options['placeholder'] = '--- Sélectionnez une option ---';
$values = array_map('trim', explode(',', $var['choices']));
$options['choices'] = array_combine($values, $values);
}
$options['attr'] = ['class' => '', 'data-section' => $section['id']];
if (array_key_exists('master', $var)) {
$options['attr']['data-master'] = $var['master'];
$options['attr']['class'] .= 'master ';
}
if (array_key_exists('slave', $var)) {
$options['attr']['data-slave'] = $var['slave'];
$options['attr']['class'] .= 'slave ';
}
$builder->add($var['id'], $type, $options);
}
}
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'dicos' => [],
]);
}
}

View File

@ -0,0 +1,999 @@
<?php
namespace App\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\OptionsResolver\OptionsResolver;
class FontawsomeType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'choices' => $this->getFontAwesomeIcons(),
]);
}
public function getParent(): string
{
return ChoiceType::class;
}
private function getFontAwesomeIcons(): array
{
$icons = [
'ad',
'address-book',
'address-card',
'adjust',
'air-freshener',
'align-center',
'align-justify',
'align-left',
'align-right',
'allergies',
'ambulance',
'american-sign-language-interpreting',
'anchor',
'angle-double-down',
'angle-double-left',
'angle-double-right',
'angle-double-up',
'angle-down',
'angle-left',
'angle-right',
'angle-up',
'angry',
'ankh',
'apple-alt',
'archive',
'archway',
'arrow-alt-circle-down',
'arrow-alt-circle-left',
'arrow-alt-circle-right',
'arrow-alt-circle-up',
'arrow-circle-down',
'arrow-circle-left',
'arrow-circle-right',
'arrow-circle-up',
'arrow-down',
'arrow-left',
'arrow-right',
'arrow-up',
'arrows-alt',
'arrows-alt-h',
'arrows-alt-v',
'assistive-listening-systems',
'asterisk',
'at',
'atlas',
'atom',
'audio-description',
'award',
'baby',
'baby-carriage',
'backspace',
'backward',
'bacon',
'bahai',
'balance-scale',
'balance-scale-left',
'balance-scale-right',
'ban',
'band-aid',
'barcode',
'bars',
'baseball-ball',
'basketball-ball',
'bath',
'battery-empty',
'battery-full',
'battery-half',
'battery-quarter',
'battery-three-quarters',
'bed',
'beer',
'bell',
'bell-slash',
'bezier-curve',
'bible',
'bicycle',
'biking',
'binoculars',
'biohazard',
'birthday-cake',
'blender',
'blender-phone',
'blind',
'blog',
'bold',
'bolt',
'bomb',
'bone',
'bong',
'book',
'book-dead',
'book-medical',
'book-open',
'book-reader',
'bookmark',
'border-all',
'border-none',
'border-style',
'bowling-ball',
'box',
'box-open',
'boxes',
'braille',
'brain',
'bread-slice',
'briefcase',
'briefcase-medical',
'broadcast-tower',
'broom',
'brush',
'bug',
'building',
'bullhorn',
'bullseye',
'burn',
'bus',
'bus-alt',
'business-time',
'calculator',
'calendar',
'calendar-alt',
'calendar-check',
'calendar-day',
'calendar-minus',
'calendar-plus',
'calendar-times',
'calendar-week',
'camera',
'camera-retro',
'campground',
'candy-cane',
'cannabis',
'capsules',
'car',
'car-alt',
'car-battery',
'car-crash',
'car-side',
'caravan',
'caret-down',
'caret-left',
'caret-right',
'caret-square-down',
'caret-square-left',
'caret-square-right',
'caret-square-up',
'caret-up',
'carrot',
'cart-arrow-down',
'cart-plus',
'cash-register',
'cat',
'certificate',
'chair',
'chalkboard',
'chalkboard-teacher',
'charging-station',
'chart-area',
'chart-bar',
'chart-line',
'chart-pie',
'check',
'check-circle',
'check-double',
'check-square',
'cheese',
'chess',
'chess-bishop',
'chess-board',
'chess-king',
'chess-knight',
'chess-pawn',
'chess-queen',
'chess-rook',
'chevron-circle-down',
'chevron-circle-left',
'chevron-circle-right',
'chevron-circle-up',
'chevron-down',
'chevron-left',
'chevron-right',
'chevron-up',
'child',
'church',
'circle',
'circle-notch',
'city',
'clinic-medical',
'clipboard',
'clipboard-check',
'clipboard-list',
'clock',
'clone',
'closed-captioning',
'cloud',
'cloud-download-alt',
'cloud-meatball',
'cloud-moon',
'cloud-moon-rain',
'cloud-rain',
'cloud-showers-heavy',
'cloud-sun',
'cloud-sun-rain',
'cloud-upload-alt',
'cocktail',
'code',
'code-branch',
'coffee',
'cog',
'cogs',
'coins',
'columns',
'comment',
'comment-alt',
'comment-dollar',
'comment-dots',
'comment-medical',
'comment-slash',
'comments',
'comments-dollar',
'compact-disc',
'compass',
'compress',
'compress-alt',
'compress-arrows-alt',
'concierge-bell',
'cookie',
'cookie-bite',
'copy',
'copyright',
'couch',
'credit-card',
'crop',
'crop-alt',
'cross',
'crosshairs',
'crow',
'crown',
'crutch',
'cube',
'cubes',
'cut',
'database',
'deaf',
'democrat',
'desktop',
'dharmachakra',
'diagnoses',
'dice',
'dice-d20',
'dice-d6',
'dice-five',
'dice-four',
'dice-one',
'dice-six',
'dice-three',
'dice-two',
'digital-tachograph',
'directions',
'divide',
'dizzy',
'dna',
'dog',
'dollar-sign',
'dolly',
'dolly-flatbed',
'donate',
'door-closed',
'door-open',
'dot-circle',
'dove',
'download',
'drafting-compass',
'dragon',
'draw-polygon',
'drum',
'drum-steelpan',
'drumstick-bite',
'dumbbell',
'dumpster',
'dumpster-fire',
'dungeon',
'edit',
'egg',
'eject',
'ellipsis-h',
'ellipsis-v',
'envelope',
'envelope-open',
'envelope-open-text',
'envelope-square',
'equals',
'eraser',
'ethernet',
'euro-sign',
'exchange-alt',
'exclamation',
'exclamation-circle',
'exclamation-triangle',
'expand',
'expand-alt',
'expand-arrows-alt',
'external-link-alt',
'external-link-square-alt',
'eye',
'eye-dropper',
'eye-slash',
'fan',
'fast-backward',
'fast-forward',
'fax',
'feather',
'feather-alt',
'female',
'fighter-jet',
'file',
'file-alt',
'file-archive',
'file-audio',
'file-code',
'file-contract',
'file-csv',
'file-download',
'file-excel',
'file-export',
'file-image',
'file-import',
'file-invoice',
'file-invoice-dollar',
'file-medical',
'file-medical-alt',
'file-pdf',
'file-powerpoint',
'file-prescription',
'file-signature',
'file-upload',
'file-video',
'file-word',
'fill',
'fill-drip',
'film',
'filter',
'fingerprint',
'fire',
'fire-alt',
'fire-extinguisher',
'first-aid',
'fish',
'fist-raised',
'flag',
'flag-checkered',
'flag-usa',
'flask',
'flushed',
'folder',
'folder-minus',
'folder-open',
'folder-plus',
'font',
'football-ball',
'forward',
'frog',
'frown',
'frown-open',
'funnel-dollar',
'futbol',
'gamepad',
'gas-pump',
'gavel',
'gem',
'genderless',
'ghost',
'gift',
'gifts',
'glass-cheers',
'glass-martini',
'glass-martini-alt',
'glass-whiskey',
'glasses',
'globe',
'globe-africa',
'globe-americas',
'globe-asia',
'globe-europe',
'golf-ball',
'gopuram',
'graduation-cap',
'greater-than',
'greater-than-equal',
'grimace',
'grin',
'grin-alt',
'grin-beam',
'grin-beam-sweat',
'grin-hearts',
'grin-squint',
'grin-squint-tears',
'grin-stars',
'grin-tears',
'grin-tongue',
'grin-tongue-squint',
'grin-tongue-wink',
'grin-wink',
'grip-horizontal',
'grip-lines',
'grip-lines-vertical',
'grip-vertical',
'guitar',
'h-square',
'hamburger',
'hammer',
'hamsa',
'hand-holding',
'hand-holding-heart',
'hand-holding-usd',
'hand-lizard',
'hand-middle-finger',
'hand-paper',
'hand-peace',
'hand-point-down',
'hand-point-left',
'hand-point-right',
'hand-point-up',
'hand-pointer',
'hand-rock',
'hand-scissors',
'hand-spock',
'hands',
'hands-helping',
'handshake',
'hanukiah',
'hard-hat',
'hashtag',
'hat-cowboy',
'hat-cowboy-side',
'hat-wizard',
'hdd',
'heading',
'headphones',
'headphones-alt',
'headset',
'heart',
'heart-broken',
'heartbeat',
'helicopter',
'highlighter',
'hiking',
'hippo',
'history',
'hockey-puck',
'holly-berry',
'home',
'horse',
'horse-head',
'hospital',
'hospital-alt',
'hospital-symbol',
'hot-tub',
'hotdog',
'hotel',
'hourglass',
'hourglass-end',
'hourglass-half',
'hourglass-start',
'house-damage',
'hryvnia',
'i-cursor',
'ice-cream',
'icicles',
'icons',
'id-badge',
'id-card',
'id-card-alt',
'igloo',
'image',
'images',
'inbox',
'indent',
'industry',
'infinity',
'info',
'info-circle',
'italic',
'jedi',
'joint',
'journal-whills',
'kaaba',
'key',
'keyboard',
'khanda',
'kiss',
'kiss-beam',
'kiss-wink-heart',
'kiwi-bird',
'landmark',
'language',
'laptop',
'laptop-code',
'laptop-medical',
'laugh',
'laugh-beam',
'laugh-squint',
'laugh-wink',
'layer-group',
'leaf',
'lemon',
'less-than',
'less-than-equal',
'level-down-alt',
'level-up-alt',
'life-ring',
'lightbulb',
'link',
'lira-sign',
'list',
'list-alt',
'list-ol',
'list-ul',
'location-arrow',
'lock',
'lock-open',
'long-arrow-alt-down',
'long-arrow-alt-left',
'long-arrow-alt-right',
'long-arrow-alt-up',
'low-vision',
'luggage-cart',
'magic',
'magnet',
'mail-bulk',
'male',
'map',
'map-marked',
'map-marked-alt',
'map-marker',
'map-marker-alt',
'map-pin',
'map-signs',
'marker',
'mars',
'mars-double',
'mars-stroke',
'mars-stroke-h',
'mars-stroke-v',
'mask',
'medal',
'medkit',
'meh',
'meh-blank',
'meh-rolling-eyes',
'memory',
'menorah',
'mercury',
'meteor',
'microchip',
'microphone',
'microphone-alt',
'microphone-alt-slash',
'microphone-slash',
'microscope',
'minus',
'minus-circle',
'minus-square',
'mitten',
'mobile',
'mobile-alt',
'money-bill',
'money-bill-alt',
'money-bill-wave',
'money-bill-wave-alt',
'money-check',
'money-check-alt',
'monument',
'moon',
'mortar-pestle',
'mosque',
'motorcycle',
'mountain',
'mouse',
'mouse-pointer',
'mug-hot',
'music',
'network-wired',
'neuter',
'newspaper',
'not-equal',
'notes-medical',
'object-group',
'object-ungroup',
'oil-can',
'om',
'otter',
'outdent',
'pager',
'paint-brush',
'paint-roller',
'palette',
'pallet',
'paper-plane',
'paperclip',
'parachute-box',
'paragraph',
'parking',
'passport',
'pastafarianism',
'paste',
'pause',
'pause-circle',
'paw',
'peace',
'pen',
'pen-alt',
'pen-fancy',
'pen-nib',
'pen-square',
'pencil-alt',
'pencil-ruler',
'people-carry',
'pepper-hot',
'percent',
'percentage',
'person-booth',
'phone',
'phone-alt',
'phone-slash',
'phone-square',
'phone-square-alt',
'phone-volume',
'photo-video',
'piggy-bank',
'pills',
'pizza-slice',
'place-of-worship',
'plane',
'plane-arrival',
'plane-departure',
'play',
'play-circle',
'plug',
'plus',
'plus-circle',
'plus-square',
'podcast',
'poll',
'poll-h',
'poo',
'poo-storm',
'poop',
'portrait',
'pound-sign',
'power-off',
'pray',
'praying-hands',
'prescription',
'prescription-bottle',
'prescription-bottle-alt',
'print',
'procedures',
'project-diagram',
'puzzle-piece',
'qrcode',
'question',
'question-circle',
'quidditch',
'quote-left',
'quote-right',
'quran',
'radiation',
'radiation-alt',
'rainbow',
'random',
'receipt',
'record-vinyl',
'recycle',
'redo',
'redo-alt',
'registered',
'remove-format',
'reply',
'reply-all',
'republican',
'restroom',
'retweet',
'ribbon',
'ring',
'road',
'robot',
'rocket',
'route',
'rss',
'rss-square',
'ruble-sign',
'ruler',
'ruler-combined',
'ruler-horizontal',
'ruler-vertical',
'running',
'rupee-sign',
'sad-cry',
'sad-tear',
'satellite',
'satellite-dish',
'save',
'school',
'screwdriver',
'scroll',
'sd-card',
'search',
'search-dollar',
'search-location',
'search-minus',
'search-plus',
'seedling',
'server',
'shapes',
'share',
'share-alt',
'share-alt-square',
'share-square',
'shekel-sign',
'shield-alt',
'ship',
'shipping-fast',
'shoe-prints',
'shopping-bag',
'shopping-basket',
'shopping-cart',
'shower',
'shuttle-van',
'sign',
'sign-in-alt',
'sign-language',
'sign-out-alt',
'signal',
'signature',
'sim-card',
'sitemap',
'skating',
'skiing',
'skiing-nordic',
'skull',
'skull-crossbones',
'slash',
'sleigh',
'sliders-h',
'smile',
'smile-beam',
'smile-wink',
'smog',
'smoking',
'smoking-ban',
'sms',
'snowboarding',
'snowflake',
'snowman',
'snowplow',
'socks',
'solar-panel',
'sort',
'sort-alpha-down',
'sort-alpha-down-alt',
'sort-alpha-up',
'sort-alpha-up-alt',
'sort-amount-down',
'sort-amount-down-alt',
'sort-amount-up',
'sort-amount-up-alt',
'sort-down',
'sort-numeric-down',
'sort-numeric-down-alt',
'sort-numeric-up',
'sort-numeric-up-alt',
'sort-up',
'spa',
'space-shuttle',
'spell-check',
'spider',
'spinner',
'splotch',
'spray-can',
'square',
'square-full',
'square-root-alt',
'stamp',
'star',
'star-and-crescent',
'star-half',
'star-half-alt',
'star-of-david',
'star-of-life',
'step-backward',
'step-forward',
'stethoscope',
'sticky-note',
'stop',
'stop-circle',
'stopwatch',
'store',
'store-alt',
'stream',
'street-view',
'strikethrough',
'stroopwafel',
'subscript',
'subway',
'suitcase',
'suitcase-rolling',
'sun',
'superscript',
'surprise',
'swatchbook',
'swimmer',
'swimming-pool',
'synagogue',
'sync',
'sync-alt',
'syringe',
'table',
'table-tennis',
'tablet',
'tablet-alt',
'tablets',
'tachometer-alt',
'tag',
'tags',
'tape',
'tasks',
'taxi',
'teeth',
'teeth-open',
'temperature-high',
'temperature-low',
'tenge',
'terminal',
'text-height',
'text-width',
'th',
'th-large',
'th-list',
'theater-masks',
'thermometer',
'thermometer-empty',
'thermometer-full',
'thermometer-half',
'thermometer-quarter',
'thermometer-three-quarters',
'thumbs-down',
'thumbs-up',
'thumbtack',
'ticket-alt',
'times',
'times-circle',
'tint',
'tint-slash',
'tired',
'toggle-off',
'toggle-on',
'toilet',
'toilet-paper',
'toolbox',
'tools',
'tooth',
'torah',
'torii-gate',
'tractor',
'trademark',
'traffic-light',
'trailer',
'train',
'tram',
'transgender',
'transgender-alt',
'trash',
'trash-alt',
'trash-restore',
'trash-restore-alt',
'tree',
'trophy',
'truck',
'truck-loading',
'truck-monster',
'truck-moving',
'truck-pickup',
'tshirt',
'tty',
'tv',
'umbrella',
'umbrella-beach',
'underline',
'undo',
'undo-alt',
'universal-access',
'university',
'unlink',
'unlock',
'unlock-alt',
'upload',
'user',
'user-alt',
'user-alt-slash',
'user-astronaut',
'user-check',
'user-circle',
'user-clock',
'user-cog',
'user-edit',
'user-friends',
'user-graduate',
'user-injured',
'user-lock',
'user-md',
'user-minus',
'user-ninja',
'user-nurse',
'user-plus',
'user-secret',
'user-shield',
'user-slash',
'user-tag',
'user-tie',
'user-times',
'users',
'users-cog',
'utensil-spoon',
'utensils',
'vector-square',
'venus',
'venus-double',
'venus-mars',
'vial',
'vials',
'video',
'video-slash',
'vihara',
'voicemail',
'volleyball-ball',
'volume-down',
'volume-mute',
'volume-off',
'volume-up',
'vote-yea',
'vr-cardboard',
'walking',
'wallet',
'warehouse',
'water',
'wave-square',
'weight',
'weight-hanging',
'wheelchair',
'wifi',
'wind',
'window-close',
'window-maximize',
'window-minimize',
'window-restore',
'wine-bottle',
'wine-glass',
'wine-glass-alt',
'won-sign',
'wrench',
'x-ray',
'yen-sign',
'yin-yang', ];
$tbicons = [];
foreach ($icons as $value) {
$tbicons[$value] = 'fas fa-'.$value;
}
// Liste d'exemples d'icônes Font Awesome
return $tbicons;
}
}

66
src/Form/UserType.php Normal file
View File

@ -0,0 +1,66 @@
<?php
namespace App\Form;
use App\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class UserType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('submit', SubmitType::class, [
'label' => 'Valider',
'attr' => ['class' => 'btn btn-success no-print'],
])
->add('username', TextType::class, [
'label' => 'Login',
])
->add('avatar', HiddenType::class)
->add('email', EmailType::class, [
'label' => 'Email',
]);
if ('profil' != $options['mode']) {
$builder
->add('roles', ChoiceType::class, [
'choices' => ['ROLE_ADMIN' => 'ROLE_ADMIN', 'ROLE_USER' => 'ROLE_USER'],
'multiple' => true,
'expanded' => true,
]);
}
if ('SQL' === $options['modeAuth']) {
$builder
->add('password', RepeatedType::class, [
'type' => PasswordType::class,
'required' => ('submit' == $options['mode'] ? true : false),
'options' => ['always_empty' => true],
'first_options' => ['label' => 'Mot de Passe', 'attr' => ['class' => 'form-control', 'style' => 'margin-bottom:15px', 'autocomplete' => 'new-password']],
'second_options' => ['label' => 'Confirmer Mot de Passe', 'attr' => ['class' => 'form-control', 'style' => 'margin-bottom:15px']],
]);
}
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => User::class,
'mode' => 'submit',
'modeAuth' => 'SQL',
]);
}
}

11
src/Kernel.php Normal file
View File

@ -0,0 +1,11 @@
<?php
namespace App;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
class Kernel extends BaseKernel
{
use MicroKernelTrait;
}

0
src/Repository/.gitignore vendored Normal file
View File

View File

@ -0,0 +1,35 @@
<?php
namespace App\Repository;
use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
/**
* @extends ServiceEntityRepository<User>
*/
class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, User::class);
}
/**
* Used to upgrade (rehash) the user's password automatically over time.
*/
public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void
{
if (!$user instanceof User) {
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', $user::class));
}
$user->setPassword($newHashedPassword);
$this->getEntityManager()->persist($user);
$this->getEntityManager()->flush();
}
}

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