summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore7
-rw-r--r--README.md2
-rw-r--r--bin/copy_directory.php25
-rw-r--r--bin/doctrine18
-rw-r--r--composer-setup.php1748
-rw-r--r--composer.json32
-rw-r--r--config/.htaccess4
-rw-r--r--deploiement.txt58
-rw-r--r--public/assets/arrow-down-nb.svg36
-rw-r--r--public/assets/arrow-down.svg40
-rw-r--r--public/assets/arrow-up-nb.svg1
-rw-r--r--public/assets/arrow-up.svg40
-rw-r--r--public/assets/book-open-nb.svg1
-rw-r--r--public/assets/book-open.svg40
-rw-r--r--public/assets/calendar-nb.svg1
-rw-r--r--public/assets/calendar.svg40
-rw-r--r--public/assets/delete-bin-nb.svg1
-rw-r--r--public/assets/delete-bin.svg48
-rw-r--r--public/assets/edit-nb.svg1
-rw-r--r--public/assets/edit.svg44
-rw-r--r--public/assets/facebook-nb.svg1
-rw-r--r--public/assets/facebook.svg37
-rw-r--r--public/assets/favicon48x48.pngbin0 -> 3067 bytes
-rw-r--r--public/assets/fond-piscine.jpgbin0 -> 24757 bytes
-rw-r--r--public/assets/home.svg1
-rw-r--r--public/assets/instagram-nb.svg1
-rw-r--r--public/assets/instagram.svg37
-rw-r--r--public/assets/logo-120x75.jpgbin0 -> 12240 bytes
-rw-r--r--public/assets/logo-150x94.jpgbin0 -> 13506 bytes
-rw-r--r--public/assets/logo-fond-bleu.pngbin0 -> 61236 bytes
-rw-r--r--public/assets/logo2.jpgbin0 -> 36899 bytes
-rw-r--r--public/assets/perdu.jpgbin0 -> 107765 bytes
-rw-r--r--public/assets/share-nb.svg1
-rw-r--r--public/assets/share.svg40
-rw-r--r--public/css/body.css39
-rw-r--r--public/css/foot.css120
-rw-r--r--public/css/head.css59
-rw-r--r--public/css/main.css110
-rw-r--r--public/css/nav.css95
-rw-r--r--public/css/tinymce.css6
-rw-r--r--public/erreur404.html13
-rw-r--r--public/images-mini/DPpiscines.pngbin0 -> 15359 bytes
-rw-r--r--public/images-mini/kerne-elagage.pngbin0 -> 31766 bytes
-rw-r--r--public/images-mini/kerne.pngbin0 -> 13272 bytes
-rw-r--r--public/images-mini/mako.pngbin0 -> 3011 bytes
-rw-r--r--public/images/DPpiscines.pngbin0 -> 15359 bytes
-rw-r--r--public/images/kerne-elagage.pngbin0 -> 31766 bytes
-rw-r--r--public/images/kerne.pngbin0 -> 13272 bytes
-rw-r--r--public/images/mako.pngbin0 -> 3011 bytes
-rw-r--r--public/index.php78
-rw-r--r--public/js/galery.js4
-rw-r--r--public/js/main.js30
-rw-r--r--public/js/tinymce.js164
-rw-r--r--src/Config.php76
-rw-r--r--src/controller/Director.php101
-rw-r--r--src/controller/Security.php111
-rw-r--r--src/controller/URL.php88
-rw-r--r--src/controller/ajax.php104
-rw-r--r--src/controller/installation.php144
-rw-r--r--src/controller/password.php357
-rw-r--r--src/controller/post.php17
-rw-r--r--src/model/Menu.php53
-rw-r--r--src/model/Path.php84
-rw-r--r--src/model/doctrine-bootstrap.php31
-rw-r--r--src/model/entities/Article.php77
-rw-r--r--src/model/entities/Image.php91
-rw-r--r--src/model/entities/Node.php168
-rw-r--r--src/model/entities/NodeData.php62
-rw-r--r--src/model/entities/Page.php97
-rw-r--r--src/model/entities/User.php47
-rw-r--r--src/view/AbstractBuilder.php52
-rw-r--r--src/view/ArticleBuilder.php60
-rw-r--r--src/view/BlogBuilder.php49
-rw-r--r--src/view/BreadcrumbBuilder.php53
-rw-r--r--src/view/FooterBuilder.php64
-rw-r--r--src/view/GaleryBuilder.php49
-rw-r--r--src/view/GridBuilder.php55
-rw-r--r--src/view/HeadBuilder.php68
-rw-r--r--src/view/HeaderBuilder.php64
-rw-r--r--src/view/LoginBuilder.php15
-rw-r--r--src/view/MainBuilder.php30
-rw-r--r--src/view/NavBuilder.php61
-rw-r--r--src/view/NewBuilder.php93
-rw-r--r--src/view/ViewBuilder.php16
-rw-r--r--src/view/password.php152
-rw-r--r--src/view/templates/article.php15
-rw-r--r--src/view/templates/blog.php6
-rw-r--r--src/view/templates/footer.php15
-rw-r--r--src/view/templates/galery.php10
-rw-r--r--src/view/templates/grid.php8
-rw-r--r--src/view/templates/head.php11
-rw-r--r--src/view/templates/header.php23
-rw-r--r--src/view/templates/new.php21
93 files changed, 5721 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..631f78b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
1vendor/
2node_modules/
3public/js/tinymce/
4public/js/tinymce-langs/
5config/config.ini
6data/
7bdd_nageurs.sql
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..dc94cd6
--- /dev/null
+++ b/README.md
@@ -0,0 +1,2 @@
1Les Nageurs Bigoudens - Club, Ecole de natation et Perfectionnement
2https://nageurs-bigoudens.fr/ \ No newline at end of file
diff --git a/bin/copy_directory.php b/bin/copy_directory.php
new file mode 100644
index 0000000..2c001c0
--- /dev/null
+++ b/bin/copy_directory.php
@@ -0,0 +1,25 @@
1<?php
2// bin/copy_directory.php
3//
4// appel dans le composer.json
5
6function copyDirectory($source, $destination) {
7 if (!is_dir($destination)) {
8 mkdir($destination, 0777, true);
9 }
10
11 $iterator = new RecursiveIteratorIterator(
12 new RecursiveDirectoryIterator($source, RecursiveDirectoryIterator::SKIP_DOTS),
13 RecursiveIteratorIterator::SELF_FIRST
14 );
15
16 foreach ($iterator as $item) {
17 if ($item->isDir()) {
18 mkdir($destination . DIRECTORY_SEPARATOR . $iterator->getSubPathName());
19 } else {
20 copy($item, $destination . DIRECTORY_SEPARATOR . $iterator->getSubPathName());
21 }
22 }
23}
24
25copyDirectory($argv[1], $argv[2]);
diff --git a/bin/doctrine b/bin/doctrine
new file mode 100644
index 0000000..8017267
--- /dev/null
+++ b/bin/doctrine
@@ -0,0 +1,18 @@
1#!/usr/bin/env php
2<?php
3// bin/doctrine
4
5use Doctrine\ORM\Tools\Console\ConsoleRunner;
6use Doctrine\ORM\Tools\Console\EntityManagerProvider\SingleManagerProvider;
7
8chdir('bin'); // pour être au même niveau que l'appli dans /public
9
10require('../src/Config.php');
11Config::load('../config/config.ini');
12
13require '../src/model/doctrine-bootstrap.php'; // relatif
14//require __DIR__ . '/../src/model/doctrine-bootstrap.php'; // absolu
15
16ConsoleRunner::run(
17 new SingleManagerProvider($entityManager)
18);
diff --git a/composer-setup.php b/composer-setup.php
new file mode 100644
index 0000000..a5efbed
--- /dev/null
+++ b/composer-setup.php
@@ -0,0 +1,1748 @@
1<?php
2
3/*
4 * This file is part of Composer.
5 *
6 * (c) Nils Adermann <naderman@naderman.de>
7 * Jordi Boggiano <j.boggiano@seld.be>
8 *
9 * For the full copyright and license information, please view the LICENSE
10 * file that was distributed with this source code.
11 */
12
13setupEnvironment();
14process(is_array($argv) ? $argv : array());
15
16/**
17 * Initializes various values
18 *
19 * @throws RuntimeException If uopz extension prevents exit calls
20 */
21function setupEnvironment()
22{
23 ini_set('display_errors', 1);
24
25 if (extension_loaded('uopz') && !(ini_get('uopz.disable') || ini_get('uopz.exit'))) {
26 // uopz works at opcode level and disables exit calls
27 if (function_exists('uopz_allow_exit')) {
28 @uopz_allow_exit(true);
29 } else {
30 throw new RuntimeException('The uopz extension ignores exit calls and breaks this installer.');
31 }
32 }
33
34 $installer = 'ComposerInstaller';
35
36 if (defined('PHP_WINDOWS_VERSION_MAJOR')) {
37 if ($version = getenv('COMPOSERSETUP')) {
38 $installer = sprintf('Composer-Setup.exe/%s', $version);
39 }
40 }
41
42 define('COMPOSER_INSTALLER', $installer);
43}
44
45/**
46 * Processes the installer
47 */
48function process($argv)
49{
50 // Determine ANSI output from --ansi and --no-ansi flags
51 setUseAnsi($argv);
52
53 $help = in_array('--help', $argv) || in_array('-h', $argv);
54 if ($help) {
55 displayHelp();
56 exit(0);
57 }
58
59 $check = in_array('--check', $argv);
60 $force = in_array('--force', $argv);
61 $quiet = in_array('--quiet', $argv);
62 $channel = 'stable';
63 if (in_array('--snapshot', $argv)) {
64 $channel = 'snapshot';
65 } elseif (in_array('--preview', $argv)) {
66 $channel = 'preview';
67 } elseif (in_array('--1', $argv)) {
68 $channel = '1';
69 } elseif (in_array('--2', $argv)) {
70 $channel = '2';
71 } elseif (in_array('--2.2', $argv)) {
72 $channel = '2.2';
73 }
74 $disableTls = in_array('--disable-tls', $argv);
75 $installDir = getOptValue('--install-dir', $argv, false);
76 $version = getOptValue('--version', $argv, false);
77 $filename = getOptValue('--filename', $argv, 'composer.phar');
78 $cafile = getOptValue('--cafile', $argv, false);
79
80 if (!checkParams($installDir, $version, $cafile)) {
81 exit(1);
82 }
83
84 $ok = checkPlatform($warnings, $quiet, $disableTls, true);
85
86 if ($check) {
87 // Only show warnings if we haven't output any errors
88 if ($ok) {
89 showWarnings($warnings);
90 showSecurityWarning($disableTls);
91 }
92 exit($ok ? 0 : 1);
93 }
94
95 if ($ok || $force) {
96 if ($channel === '1' && !$quiet) {
97 out('Warning: You forced the install of Composer 1.x via --1, but Composer 2.x is the latest stable version. Updating to it via composer self-update --stable is recommended.', 'error');
98 }
99
100 $installer = new Installer($quiet, $disableTls, $cafile);
101 if ($installer->run($version, $installDir, $filename, $channel)) {
102 showWarnings($warnings);
103 showSecurityWarning($disableTls);
104 exit(0);
105 }
106 }
107
108 exit(1);
109}
110
111/**
112 * Displays the help
113 */
114function displayHelp()
115{
116 echo <<<EOF
117Composer Installer
118------------------
119Options
120--help this help
121--check for checking environment only
122--force forces the installation
123--ansi force ANSI color output
124--no-ansi disable ANSI color output
125--quiet do not output unimportant messages
126--install-dir="..." accepts a target installation directory
127--preview install the latest version from the preview (alpha/beta/rc) channel instead of stable
128--snapshot install the latest version from the snapshot (dev builds) channel instead of stable
129--1 install the latest stable Composer 1.x (EOL) version
130--2 install the latest stable Composer 2.x version
131--2.2 install the latest stable Composer 2.2.x (LTS) version
132--version="..." accepts a specific version to install instead of the latest
133--filename="..." accepts a target filename (default: composer.phar)
134--disable-tls disable SSL/TLS security for file downloads
135--cafile="..." accepts a path to a Certificate Authority (CA) certificate file for SSL/TLS verification
136
137EOF;
138}
139
140/**
141 * Sets the USE_ANSI define for colorizing output
142 *
143 * @param array $argv Command-line arguments
144 */
145function setUseAnsi($argv)
146{
147 // --no-ansi wins over --ansi
148 if (in_array('--no-ansi', $argv)) {
149 define('USE_ANSI', false);
150 } elseif (in_array('--ansi', $argv)) {
151 define('USE_ANSI', true);
152 } else {
153 define('USE_ANSI', outputSupportsColor());
154 }
155}
156
157/**
158 * Returns whether color output is supported
159 *
160 * @return bool
161 */
162function outputSupportsColor()
163{
164 if (false !== getenv('NO_COLOR') || !defined('STDOUT')) {
165 return false;
166 }
167
168 if ('Hyper' === getenv('TERM_PROGRAM')) {
169 return true;
170 }
171
172 if (defined('PHP_WINDOWS_VERSION_BUILD')) {
173 return (function_exists('sapi_windows_vt100_support')
174 && sapi_windows_vt100_support(STDOUT))
175 || false !== getenv('ANSICON')
176 || 'ON' === getenv('ConEmuANSI')
177 || 'xterm' === getenv('TERM');
178 }
179
180 if (function_exists('stream_isatty')) {
181 return stream_isatty(STDOUT);
182 }
183
184 if (function_exists('posix_isatty')) {
185 return posix_isatty(STDOUT);
186 }
187
188 $stat = fstat(STDOUT);
189 // Check if formatted mode is S_IFCHR
190 return $stat ? 0020000 === ($stat['mode'] & 0170000) : false;
191}
192
193/**
194 * Returns the value of a command-line option
195 *
196 * @param string $opt The command-line option to check
197 * @param array $argv Command-line arguments
198 * @param mixed $default Default value to be returned
199 *
200 * @return mixed The command-line value or the default
201 */
202function getOptValue($opt, $argv, $default)
203{
204 $optLength = strlen($opt);
205
206 foreach ($argv as $key => $value) {
207 $next = $key + 1;
208 if (0 === strpos($value, $opt)) {
209 if ($optLength === strlen($value) && isset($argv[$next])) {
210 return trim($argv[$next]);
211 } else {
212 return trim(substr($value, $optLength + 1));
213 }
214 }
215 }
216
217 return $default;
218}
219
220/**
221 * Checks that user-supplied params are valid
222 *
223 * @param mixed $installDir The required istallation directory
224 * @param mixed $version The required composer version to install
225 * @param mixed $cafile Certificate Authority file
226 *
227 * @return bool True if the supplied params are okay
228 */
229function checkParams($installDir, $version, $cafile)
230{
231 $result = true;
232
233 if (false !== $installDir && !is_dir($installDir)) {
234 out("The defined install dir ({$installDir}) does not exist.", 'info');
235 $result = false;
236 }
237
238 if (false !== $version && 1 !== preg_match('/^\d+\.\d+\.\d+(\-(alpha|beta|RC)\d*)*$/', $version)) {
239 out("The defined install version ({$version}) does not match release pattern.", 'info');
240 $result = false;
241 }
242
243 if (false !== $cafile && (!file_exists($cafile) || !is_readable($cafile))) {
244 out("The defined Certificate Authority (CA) cert file ({$cafile}) does not exist or is not readable.", 'info');
245 $result = false;
246 }
247 return $result;
248}
249
250/**
251 * Checks the platform for possible issues running Composer
252 *
253 * Errors are written to the output, warnings are saved for later display.
254 *
255 * @param array $warnings Populated by method, to be shown later
256 * @param bool $quiet Quiet mode
257 * @param bool $disableTls Bypass tls
258 * @param bool $install If we are installing, rather than diagnosing
259 *
260 * @return bool True if there are no errors
261 */
262function checkPlatform(&$warnings, $quiet, $disableTls, $install)
263{
264 getPlatformIssues($errors, $warnings, $install);
265
266 // Make openssl warning an error if tls has not been specifically disabled
267 if (isset($warnings['openssl']) && !$disableTls) {
268 $errors['openssl'] = $warnings['openssl'];
269 unset($warnings['openssl']);
270 }
271
272 if (!empty($errors)) {
273 // Composer-Setup.exe uses "Some settings" to flag platform errors
274 out('Some settings on your machine make Composer unable to work properly.', 'error');
275 out('Make sure that you fix the issues listed below and run this script again:', 'error');
276 outputIssues($errors);
277 return false;
278 }
279
280 if (empty($warnings) && !$quiet) {
281 out('All settings correct for using Composer', 'success');
282 }
283 return true;
284}
285
286/**
287 * Checks platform configuration for common incompatibility issues
288 *
289 * @param array $errors Populated by method
290 * @param array $warnings Populated by method
291 * @param bool $install If we are installing, rather than diagnosing
292 *
293 * @return bool If any errors or warnings have been found
294 */
295function getPlatformIssues(&$errors, &$warnings, $install)
296{
297 $errors = array();
298 $warnings = array();
299
300 if ($iniPath = php_ini_loaded_file()) {
301 $iniMessage = PHP_EOL.'The php.ini used by your command-line PHP is: ' . $iniPath;
302 } else {
303 $iniMessage = PHP_EOL.'A php.ini file does not exist. You will have to create one.';
304 }
305 $iniMessage .= PHP_EOL.'If you can not modify the ini file, you can also run `php -d option=value` to modify ini values on the fly. You can use -d multiple times.';
306
307 if (ini_get('detect_unicode')) {
308 $errors['unicode'] = array(
309 'The detect_unicode setting must be disabled.',
310 'Add the following to the end of your `php.ini`:',
311 ' detect_unicode = Off',
312 $iniMessage
313 );
314 }
315
316 if (extension_loaded('suhosin')) {
317 $suhosin = ini_get('suhosin.executor.include.whitelist');
318 $suhosinBlacklist = ini_get('suhosin.executor.include.blacklist');
319 if (false === stripos($suhosin, 'phar') && (!$suhosinBlacklist || false !== stripos($suhosinBlacklist, 'phar'))) {
320 $errors['suhosin'] = array(
321 'The suhosin.executor.include.whitelist setting is incorrect.',
322 'Add the following to the end of your `php.ini` or suhosin.ini (Example path [for Debian]: /etc/php5/cli/conf.d/suhosin.ini):',
323 ' suhosin.executor.include.whitelist = phar '.$suhosin,
324 $iniMessage
325 );
326 }
327 }
328
329 if (!function_exists('json_decode')) {
330 $errors['json'] = array(
331 'The json extension is missing.',
332 'Install it or recompile php without --disable-json'
333 );
334 }
335
336 if (!extension_loaded('Phar')) {
337 $errors['phar'] = array(
338 'The phar extension is missing.',
339 'Install it or recompile php without --disable-phar'
340 );
341 }
342
343 if (!extension_loaded('filter')) {
344 $errors['filter'] = array(
345 'The filter extension is missing.',
346 'Install it or recompile php without --disable-filter'
347 );
348 }
349
350 if (!extension_loaded('hash')) {
351 $errors['hash'] = array(
352 'The hash extension is missing.',
353 'Install it or recompile php without --disable-hash'
354 );
355 }
356
357 if (!extension_loaded('iconv') && !extension_loaded('mbstring')) {
358 $errors['iconv_mbstring'] = array(
359 'The iconv OR mbstring extension is required and both are missing.',
360 'Install either of them or recompile php without --disable-iconv'
361 );
362 }
363
364 if (!ini_get('allow_url_fopen')) {
365 $errors['allow_url_fopen'] = array(
366 'The allow_url_fopen setting is incorrect.',
367 'Add the following to the end of your `php.ini`:',
368 ' allow_url_fopen = On',
369 $iniMessage
370 );
371 }
372
373 if (extension_loaded('ionCube Loader') && ioncube_loader_iversion() < 40009) {
374 $ioncube = ioncube_loader_version();
375 $errors['ioncube'] = array(
376 'Your ionCube Loader extension ('.$ioncube.') is incompatible with Phar files.',
377 'Upgrade to ionCube 4.0.9 or higher or remove this line (path may be different) from your `php.ini` to disable it:',
378 ' zend_extension = /usr/lib/php5/20090626+lfs/ioncube_loader_lin_5.3.so',
379 $iniMessage
380 );
381 }
382
383 if (version_compare(PHP_VERSION, '5.3.2', '<')) {
384 $errors['php'] = array(
385 'Your PHP ('.PHP_VERSION.') is too old, you must upgrade to PHP 5.3.2 or higher.'
386 );
387 }
388
389 if (version_compare(PHP_VERSION, '5.3.4', '<')) {
390 $warnings['php'] = array(
391 'Your PHP ('.PHP_VERSION.') is quite old, upgrading to PHP 5.3.4 or higher is recommended.',
392 'Composer works with 5.3.2+ for most people, but there might be edge case issues.'
393 );
394 }
395
396 if (!extension_loaded('openssl')) {
397 $warnings['openssl'] = array(
398 'The openssl extension is missing, which means that secure HTTPS transfers are impossible.',
399 'If possible you should enable it or recompile php with --with-openssl'
400 );
401 }
402
403 if (extension_loaded('openssl') && OPENSSL_VERSION_NUMBER < 0x1000100f) {
404 // Attempt to parse version number out, fallback to whole string value.
405 $opensslVersion = trim(strstr(OPENSSL_VERSION_TEXT, ' '));
406 $opensslVersion = substr($opensslVersion, 0, strpos($opensslVersion, ' '));
407 $opensslVersion = $opensslVersion ? $opensslVersion : OPENSSL_VERSION_TEXT;
408
409 $warnings['openssl_version'] = array(
410 'The OpenSSL library ('.$opensslVersion.') used by PHP does not support TLSv1.2 or TLSv1.1.',
411 'If possible you should upgrade OpenSSL to version 1.0.1 or above.'
412 );
413 }
414
415 if (!defined('HHVM_VERSION') && !extension_loaded('apcu') && ini_get('apc.enable_cli')) {
416 $warnings['apc_cli'] = array(
417 'The apc.enable_cli setting is incorrect.',
418 'Add the following to the end of your `php.ini`:',
419 ' apc.enable_cli = Off',
420 $iniMessage
421 );
422 }
423
424 if (!$install && extension_loaded('xdebug')) {
425 $warnings['xdebug_loaded'] = array(
426 'The xdebug extension is loaded, this can slow down Composer a little.',
427 'Disabling it when using Composer is recommended.'
428 );
429
430 if (ini_get('xdebug.profiler_enabled')) {
431 $warnings['xdebug_profile'] = array(
432 'The xdebug.profiler_enabled setting is enabled, this can slow down Composer a lot.',
433 'Add the following to the end of your `php.ini` to disable it:',
434 ' xdebug.profiler_enabled = 0',
435 $iniMessage
436 );
437 }
438 }
439
440 if (!extension_loaded('zlib')) {
441 $warnings['zlib'] = array(
442 'The zlib extension is not loaded, this can slow down Composer a lot.',
443 'If possible, install it or recompile php with --with-zlib',
444 $iniMessage
445 );
446 }
447
448 if (defined('PHP_WINDOWS_VERSION_BUILD')
449 && (version_compare(PHP_VERSION, '7.2.23', '<')
450 || (version_compare(PHP_VERSION, '7.3.0', '>=')
451 && version_compare(PHP_VERSION, '7.3.10', '<')))) {
452 $warnings['onedrive'] = array(
453 'The Windows OneDrive folder is not supported on PHP versions below 7.2.23 and 7.3.10.',
454 'Upgrade your PHP ('.PHP_VERSION.') to use this location with Composer.'
455 );
456 }
457
458 if (extension_loaded('uopz') && !(ini_get('uopz.disable') || ini_get('uopz.exit'))) {
459 $warnings['uopz'] = array(
460 'The uopz extension ignores exit calls and may not work with all Composer commands.',
461 'Disabling it when using Composer is recommended.'
462 );
463 }
464
465 ob_start();
466 phpinfo(INFO_GENERAL);
467 $phpinfo = ob_get_clean();
468 if (preg_match('{Configure Command(?: *</td><td class="v">| *=> *)(.*?)(?:</td>|$)}m', $phpinfo, $match)) {
469 $configure = $match[1];
470
471 if (false !== strpos($configure, '--enable-sigchild')) {
472 $warnings['sigchild'] = array(
473 'PHP was compiled with --enable-sigchild which can cause issues on some platforms.',
474 'Recompile it without this flag if possible, see also:',
475 ' https://bugs.php.net/bug.php?id=22999'
476 );
477 }
478
479 if (false !== strpos($configure, '--with-curlwrappers')) {
480 $warnings['curlwrappers'] = array(
481 'PHP was compiled with --with-curlwrappers which will cause issues with HTTP authentication and GitHub.',
482 'Recompile it without this flag if possible'
483 );
484 }
485 }
486
487 // Stringify the message arrays
488 foreach ($errors as $key => $value) {
489 $errors[$key] = PHP_EOL.implode(PHP_EOL, $value);
490 }
491
492 foreach ($warnings as $key => $value) {
493 $warnings[$key] = PHP_EOL.implode(PHP_EOL, $value);
494 }
495
496 return !empty($errors) || !empty($warnings);
497}
498
499
500/**
501 * Outputs an array of issues
502 *
503 * @param array $issues
504 */
505function outputIssues($issues)
506{
507 foreach ($issues as $issue) {
508 out($issue, 'info');
509 }
510 out('');
511}
512
513/**
514 * Outputs any warnings found
515 *
516 * @param array $warnings
517 */
518function showWarnings($warnings)
519{
520 if (!empty($warnings)) {
521 out('Some settings on your machine may cause stability issues with Composer.', 'error');
522 out('If you encounter issues, try to change the following:', 'error');
523 outputIssues($warnings);
524 }
525}
526
527/**
528 * Outputs an end of process warning if tls has been bypassed
529 *
530 * @param bool $disableTls Bypass tls
531 */
532function showSecurityWarning($disableTls)
533{
534 if ($disableTls) {
535 out('You have instructed the Installer not to enforce SSL/TLS security on remote HTTPS requests.', 'info');
536 out('This will leave all downloads during installation vulnerable to Man-In-The-Middle (MITM) attacks', 'info');
537 }
538}
539
540/**
541 * colorize output
542 */
543function out($text, $color = null, $newLine = true)
544{
545 $styles = array(
546 'success' => "\033[0;32m%s\033[0m",
547 'error' => "\033[31;31m%s\033[0m",
548 'info' => "\033[33;33m%s\033[0m"
549 );
550
551 $format = '%s';
552
553 if (isset($styles[$color]) && USE_ANSI) {
554 $format = $styles[$color];
555 }
556
557 if ($newLine) {
558 $format .= PHP_EOL;
559 }
560
561 printf($format, $text);
562}
563
564/**
565 * Returns the system-dependent Composer home location, which may not exist
566 *
567 * @return string
568 */
569function getHomeDir()
570{
571 $home = getenv('COMPOSER_HOME');
572 if ($home) {
573 return $home;
574 }
575
576 $userDir = getUserDir();
577
578 if (defined('PHP_WINDOWS_VERSION_MAJOR')) {
579 return $userDir.'/Composer';
580 }
581
582 $dirs = array();
583
584 if (useXdg()) {
585 // XDG Base Directory Specifications
586 $xdgConfig = getenv('XDG_CONFIG_HOME');
587 if (!$xdgConfig) {
588 $xdgConfig = $userDir . '/.config';
589 }
590
591 $dirs[] = $xdgConfig . '/composer';
592 }
593
594 $dirs[] = $userDir . '/.composer';
595
596 // select first dir which exists of: $XDG_CONFIG_HOME/composer or ~/.composer
597 foreach ($dirs as $dir) {
598 if (is_dir($dir)) {
599 return $dir;
600 }
601 }
602
603 // if none exists, we default to first defined one (XDG one if system uses it, or ~/.composer otherwise)
604 return $dirs[0];
605}
606
607/**
608 * Returns the location of the user directory from the environment
609 * @throws RuntimeException If the environment value does not exists
610 *
611 * @return string
612 */
613function getUserDir()
614{
615 $userEnv = defined('PHP_WINDOWS_VERSION_MAJOR') ? 'APPDATA' : 'HOME';
616 $userDir = getenv($userEnv);
617
618 if (!$userDir) {
619 throw new RuntimeException('The '.$userEnv.' or COMPOSER_HOME environment variable must be set for composer to run correctly');
620 }
621
622 return rtrim(strtr($userDir, '\\', '/'), '/');
623}
624
625/**
626 * @return bool
627 */
628function useXdg()
629{
630 foreach (array_keys($_SERVER) as $key) {
631 if (strpos($key, 'XDG_') === 0) {
632 return true;
633 }
634 }
635
636 if (is_dir('/etc/xdg')) {
637 return true;
638 }
639
640 return false;
641}
642
643function validateCaFile($contents)
644{
645 // assume the CA is valid if php is vulnerable to
646 // https://www.sektioneins.de/advisories/advisory-012013-php-openssl_x509_parse-memory-corruption-vulnerability.html
647 if (
648 PHP_VERSION_ID <= 50327
649 || (PHP_VERSION_ID >= 50400 && PHP_VERSION_ID < 50422)
650 || (PHP_VERSION_ID >= 50500 && PHP_VERSION_ID < 50506)
651 ) {
652 return !empty($contents);
653 }
654
655 return (bool) openssl_x509_parse($contents);
656}
657
658class Installer
659{
660 private $quiet;
661 private $disableTls;
662 private $cafile;
663 private $displayPath;
664 private $target;
665 private $tmpFile;
666 private $tmpCafile;
667 private $baseUrl;
668 private $algo;
669 private $errHandler;
670 private $httpClient;
671 private $pubKeys = array();
672 private $installs = array();
673
674 /**
675 * Constructor - must not do anything that throws an exception
676 *
677 * @param bool $quiet Quiet mode
678 * @param bool $disableTls Bypass tls
679 * @param mixed $cafile Path to CA bundle, or false
680 */
681 public function __construct($quiet, $disableTls, $caFile)
682 {
683 if (($this->quiet = $quiet)) {
684 ob_start();
685 }
686 $this->disableTls = $disableTls;
687 $this->cafile = $caFile;
688 $this->errHandler = new ErrorHandler();
689 }
690
691 /**
692 * Runs the installer
693 *
694 * @param mixed $version Specific version to install, or false
695 * @param mixed $installDir Specific installation directory, or false
696 * @param string $filename Specific filename to save to, or composer.phar
697 * @param string $channel Specific version channel to use
698 * @throws Exception If anything other than a RuntimeException is caught
699 *
700 * @return bool If the installation succeeded
701 */
702 public function run($version, $installDir, $filename, $channel)
703 {
704 try {
705 $this->initTargets($installDir, $filename);
706 $this->initTls();
707 $this->httpClient = new HttpClient($this->disableTls, $this->cafile);
708 $result = $this->install($version, $channel);
709
710 // in case --1 or --2 is passed, we leave the default channel for next self-update to stable
711 if (1 === preg_match('{^\d+$}D', $channel)) {
712 $channel = 'stable';
713 }
714
715 if ($result && $channel !== 'stable' && !$version && defined('PHP_BINARY')) {
716 $null = (defined('PHP_WINDOWS_VERSION_MAJOR') ? 'NUL' : '/dev/null');
717 @exec(escapeshellarg(PHP_BINARY) .' '.escapeshellarg($this->target).' self-update --'.$channel.' --set-channel-only -q > '.$null.' 2> '.$null, $output);
718 }
719 } catch (Exception $e) {
720 $result = false;
721 }
722
723 // Always clean up
724 $this->cleanUp($result);
725
726 if (isset($e)) {
727 // Rethrow anything that is not a RuntimeException
728 if (!$e instanceof RuntimeException) {
729 throw $e;
730 }
731 out($e->getMessage(), 'error');
732 }
733 return $result;
734 }
735
736 /**
737 * Initialization methods to set the required filenames and composer url
738 *
739 * @param mixed $installDir Specific installation directory, or false
740 * @param string $filename Specific filename to save to, or composer.phar
741 * @throws RuntimeException If the installation directory is not writable
742 */
743 protected function initTargets($installDir, $filename)
744 {
745 $this->displayPath = ($installDir ? rtrim($installDir, '/').'/' : '').$filename;
746 $installDir = $installDir ? realpath($installDir) : getcwd();
747
748 if (!is_writeable($installDir)) {
749 throw new RuntimeException('The installation directory "'.$installDir.'" is not writable');
750 }
751
752 $this->target = $installDir.DIRECTORY_SEPARATOR.$filename;
753 $this->tmpFile = $installDir.DIRECTORY_SEPARATOR.basename($this->target, '.phar').'-temp.phar';
754
755 $uriScheme = $this->disableTls ? 'http' : 'https';
756 $this->baseUrl = $uriScheme.'://getcomposer.org';
757 }
758
759 /**
760 * A wrapper around methods to check tls and write public keys
761 * @throws RuntimeException If SHA384 is not supported
762 */
763 protected function initTls()
764 {
765 if ($this->disableTls) {
766 return;
767 }
768
769 if (!in_array('sha384', array_map('strtolower', openssl_get_md_methods()))) {
770 throw new RuntimeException('SHA384 is not supported by your openssl extension');
771 }
772
773 $this->algo = defined('OPENSSL_ALGO_SHA384') ? OPENSSL_ALGO_SHA384 : 'SHA384';
774 $home = $this->getComposerHome();
775
776 $this->pubKeys = array(
777 'dev' => $this->installKey(self::getPKDev(), $home, 'keys.dev.pub'),
778 'tags' => $this->installKey(self::getPKTags(), $home, 'keys.tags.pub')
779 );
780
781 if (empty($this->cafile) && !HttpClient::getSystemCaRootBundlePath()) {
782 $this->cafile = $this->tmpCafile = $this->installKey(HttpClient::getPackagedCaFile(), $home, 'cacert-temp.pem');
783 }
784 }
785
786 /**
787 * Returns the Composer home directory, creating it if required
788 * @throws RuntimeException If the directory cannot be created
789 *
790 * @return string
791 */
792 protected function getComposerHome()
793 {
794 $home = getHomeDir();
795
796 if (!is_dir($home)) {
797 $this->errHandler->start();
798
799 if (!mkdir($home, 0777, true)) {
800 throw new RuntimeException(sprintf(
801 'Unable to create Composer home directory "%s": %s',
802 $home,
803 $this->errHandler->message
804 ));
805 }
806 $this->installs[] = $home;
807 $this->errHandler->stop();
808 }
809 return $home;
810 }
811
812 /**
813 * Writes public key data to disc
814 *
815 * @param string $data The public key(s) in pem format
816 * @param string $path The directory to write to
817 * @param string $filename The name of the file
818 * @throws RuntimeException If the file cannot be written
819 *
820 * @return string The path to the saved data
821 */
822 protected function installKey($data, $path, $filename)
823 {
824 $this->errHandler->start();
825
826 $target = $path.DIRECTORY_SEPARATOR.$filename;
827 $installed = file_exists($target);
828 $write = file_put_contents($target, $data, LOCK_EX);
829 @chmod($target, 0644);
830
831 $this->errHandler->stop();
832
833 if (!$write) {
834 throw new RuntimeException(sprintf('Unable to write %s to: %s', $filename, $path));
835 }
836
837 if (!$installed) {
838 $this->installs[] = $target;
839 }
840
841 return $target;
842 }
843
844 /**
845 * The main install function
846 *
847 * @param mixed $version Specific version to install, or false
848 * @param string $channel Version channel to use
849 *
850 * @return bool If the installation succeeded
851 */
852 protected function install($version, $channel)
853 {
854 $retries = 3;
855 $result = false;
856 $infoMsg = 'Downloading...';
857 $infoType = 'info';
858
859 while ($retries--) {
860 if (!$this->quiet) {
861 out($infoMsg, $infoType);
862 $infoMsg = 'Retrying...';
863 $infoType = 'error';
864 }
865
866 if (!$this->getVersion($channel, $version, $url, $error)) {
867 out($error, 'error');
868 continue;
869 }
870
871 if (!$this->downloadToTmp($url, $signature, $error)) {
872 out($error, 'error');
873 continue;
874 }
875
876 if (!$this->verifyAndSave($version, $signature, $error)) {
877 out($error, 'error');
878 continue;
879 }
880
881 $result = true;
882 break;
883 }
884
885 if (!$this->quiet) {
886 if ($result) {
887 out(PHP_EOL."Composer (version {$version}) successfully installed to: {$this->target}", 'success');
888 out("Use it: php {$this->displayPath}", 'info');
889 out('');
890 } else {
891 out('The download failed repeatedly, aborting.', 'error');
892 }
893 }
894 return $result;
895 }
896
897 /**
898 * Sets the version url, downloading version data if required
899 *
900 * @param string $channel Version channel to use
901 * @param false|string $version Version to install, or set by method
902 * @param null|string $url The versioned url, set by method
903 * @param null|string $error Set by method on failure
904 *
905 * @return bool If the operation succeeded
906 */
907 protected function getVersion($channel, &$version, &$url, &$error)
908 {
909 $error = '';
910
911 if ($version) {
912 if (empty($url)) {
913 $url = $this->baseUrl."/download/{$version}/composer.phar";
914 }
915 return true;
916 }
917
918 $this->errHandler->start();
919
920 if ($this->downloadVersionData($data, $error)) {
921 $this->parseVersionData($data, $channel, $version, $url);
922 }
923
924 $this->errHandler->stop();
925 return empty($error);
926 }
927
928 /**
929 * Downloads and json-decodes version data
930 *
931 * @param null|array $data Downloaded version data, set by method
932 * @param null|string $error Set by method on failure
933 *
934 * @return bool If the operation succeeded
935 */
936 protected function downloadVersionData(&$data, &$error)
937 {
938 $url = $this->baseUrl.'/versions';
939 $errFmt = 'The "%s" file could not be %s: %s';
940
941 if (!$json = $this->httpClient->get($url)) {
942 $error = sprintf($errFmt, $url, 'downloaded', $this->errHandler->message);
943 return false;
944 }
945
946 if (!$data = json_decode($json, true)) {
947 $error = sprintf($errFmt, $url, 'json-decoded', $this->getJsonError());
948 return false;
949 }
950 return true;
951 }
952
953 /**
954 * A wrapper around the methods needed to download and save the phar
955 *
956 * @param string $url The versioned download url
957 * @param null|string $signature Set by method on successful download
958 * @param null|string $error Set by method on failure
959 *
960 * @return bool If the operation succeeded
961 */
962 protected function downloadToTmp($url, &$signature, &$error)
963 {
964 $error = '';
965 $errFmt = 'The "%s" file could not be downloaded: %s';
966 $sigUrl = $url.'.sig';
967 $this->errHandler->start();
968
969 if (!$fh = fopen($this->tmpFile, 'w')) {
970 $error = sprintf('Could not create file "%s": %s', $this->tmpFile, $this->errHandler->message);
971
972 } elseif (!$this->getSignature($sigUrl, $signature)) {
973 $error = sprintf($errFmt, $sigUrl, $this->errHandler->message);
974
975 } elseif (!fwrite($fh, $this->httpClient->get($url))) {
976 $error = sprintf($errFmt, $url, $this->errHandler->message);
977 }
978
979 if (is_resource($fh)) {
980 fclose($fh);
981 }
982 $this->errHandler->stop();
983 return empty($error);
984 }
985
986 /**
987 * Verifies the downloaded file and saves it to the target location
988 *
989 * @param string $version The composer version downloaded
990 * @param string $signature The digital signature to check
991 * @param null|string $error Set by method on failure
992 *
993 * @return bool If the operation succeeded
994 */
995 protected function verifyAndSave($version, $signature, &$error)
996 {
997 $error = '';
998
999 if (!$this->validatePhar($this->tmpFile, $pharError)) {
1000 $error = 'The download is corrupt: '.$pharError;
1001
1002 } elseif (!$this->verifySignature($version, $signature, $this->tmpFile)) {
1003 $error = 'Signature mismatch, could not verify the phar file integrity';
1004
1005 } else {
1006 $this->errHandler->start();
1007
1008 if (!rename($this->tmpFile, $this->target)) {
1009 $error = sprintf('Could not write to file "%s": %s', $this->target, $this->errHandler->message);
1010 }
1011 chmod($this->target, 0755);
1012 $this->errHandler->stop();
1013 }
1014
1015 return empty($error);
1016 }
1017
1018 /**
1019 * Parses an array of version data to match the required channel
1020 *
1021 * @param array $data Downloaded version data
1022 * @param mixed $channel Version channel to use
1023 * @param false|string $version Set by method
1024 * @param mixed $url The versioned url, set by method
1025 */
1026 protected function parseVersionData(array $data, $channel, &$version, &$url)
1027 {
1028 foreach ($data[$channel] as $candidate) {
1029 if ($candidate['min-php'] <= PHP_VERSION_ID) {
1030 $version = $candidate['version'];
1031 $url = $this->baseUrl.$candidate['path'];
1032 break;
1033 }
1034 }
1035
1036 if (!$version) {
1037 $error = sprintf(
1038 'None of the %d %s version(s) of Composer matches your PHP version (%s / ID: %d)',
1039 count($data[$channel]),
1040 $channel,
1041 PHP_VERSION,
1042 PHP_VERSION_ID
1043 );
1044 throw new RuntimeException($error);
1045 }
1046 }
1047
1048 /**
1049 * Downloads the digital signature of required phar file
1050 *
1051 * @param string $url The signature url
1052 * @param null|string $signature Set by method on success
1053 *
1054 * @return bool If the download succeeded
1055 */
1056 protected function getSignature($url, &$signature)
1057 {
1058 if (!$result = $this->disableTls) {
1059 $signature = $this->httpClient->get($url);
1060
1061 if ($signature) {
1062 $signature = json_decode($signature, true);
1063 $signature = base64_decode($signature['sha384']);
1064 $result = true;
1065 }
1066 }
1067
1068 return $result;
1069 }
1070
1071 /**
1072 * Verifies the signature of the downloaded phar
1073 *
1074 * @param string $version The composer versione
1075 * @param string $signature The downloaded digital signature
1076 * @param string $file The temp phar file
1077 *
1078 * @return bool If the operation succeeded
1079 */
1080 protected function verifySignature($version, $signature, $file)
1081 {
1082 if (!$result = $this->disableTls) {
1083 $path = preg_match('{^[0-9a-f]{40}$}', $version) ? $this->pubKeys['dev'] : $this->pubKeys['tags'];
1084 $pubkeyid = openssl_pkey_get_public('file://'.$path);
1085
1086 $result = 1 === openssl_verify(
1087 file_get_contents($file),
1088 $signature,
1089 $pubkeyid,
1090 $this->algo
1091 );
1092
1093 // PHP 8 automatically frees the key instance and deprecates the function
1094 if (PHP_VERSION_ID < 80000) {
1095 openssl_free_key($pubkeyid);
1096 }
1097 }
1098
1099 return $result;
1100 }
1101
1102 /**
1103 * Validates the downloaded phar file
1104 *
1105 * @param string $pharFile The temp phar file
1106 * @param null|string $error Set by method on failure
1107 *
1108 * @return bool If the operation succeeded
1109 */
1110 protected function validatePhar($pharFile, &$error)
1111 {
1112 if (ini_get('phar.readonly')) {
1113 return true;
1114 }
1115
1116 try {
1117 // Test the phar validity
1118 $phar = new Phar($pharFile);
1119 // Free the variable to unlock the file
1120 unset($phar);
1121 $result = true;
1122
1123 } catch (Exception $e) {
1124 if (!$e instanceof UnexpectedValueException && !$e instanceof PharException) {
1125 throw $e;
1126 }
1127 $error = $e->getMessage();
1128 $result = false;
1129 }
1130 return $result;
1131 }
1132
1133 /**
1134 * Returns a string representation of the last json error
1135 *
1136 * @return string The error string or code
1137 */
1138 protected function getJsonError()
1139 {
1140 if (function_exists('json_last_error_msg')) {
1141 return json_last_error_msg();
1142 } else {
1143 return 'json_last_error = '.json_last_error();
1144 }
1145 }
1146
1147 /**
1148 * Cleans up resources at the end of the installation
1149 *
1150 * @param bool $result If the installation succeeded
1151 */
1152 protected function cleanUp($result)
1153 {
1154 if (!$result) {
1155 // Output buffered errors
1156 if ($this->quiet) {
1157 $this->outputErrors();
1158 }
1159 // Clean up stuff we created
1160 $this->uninstall();
1161 } elseif ($this->tmpCafile) {
1162 @unlink($this->tmpCafile);
1163 }
1164 }
1165
1166 /**
1167 * Outputs unique errors when in quiet mode
1168 *
1169 */
1170 protected function outputErrors()
1171 {
1172 $errors = explode(PHP_EOL, ob_get_clean());
1173 $shown = array();
1174
1175 foreach ($errors as $error) {
1176 if ($error && !in_array($error, $shown)) {
1177 out($error, 'error');
1178 $shown[] = $error;
1179 }
1180 }
1181 }
1182
1183 /**
1184 * Uninstalls newly-created files and directories on failure
1185 *
1186 */
1187 protected function uninstall()
1188 {
1189 foreach (array_reverse($this->installs) as $target) {
1190 if (is_file($target)) {
1191 @unlink($target);
1192 } elseif (is_dir($target)) {
1193 @rmdir($target);
1194 }
1195 }
1196
1197 if ($this->tmpFile !== null && file_exists($this->tmpFile)) {
1198 @unlink($this->tmpFile);
1199 }
1200 }
1201
1202 public static function getPKDev()
1203 {
1204 return <<<PKDEV
1205-----BEGIN PUBLIC KEY-----
1206MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAnBDHjZS6e0ZMoK3xTD7f
1207FNCzlXjX/Aie2dit8QXA03pSrOTbaMnxON3hUL47Lz3g1SC6YJEMVHr0zYq4elWi
1208i3ecFEgzLcj+pZM5X6qWu2Ozz4vWx3JYo1/a/HYdOuW9e3lwS8VtS0AVJA+U8X0A
1209hZnBmGpltHhO8hPKHgkJtkTUxCheTcbqn4wGHl8Z2SediDcPTLwqezWKUfrYzu1f
1210o/j3WFwFs6GtK4wdYtiXr+yspBZHO3y1udf8eFFGcb2V3EaLOrtfur6XQVizjOuk
12118lw5zzse1Qp/klHqbDRsjSzJ6iL6F4aynBc6Euqt/8ccNAIz0rLjLhOraeyj4eNn
12128iokwMKiXpcrQLTKH+RH1JCuOVxQ436bJwbSsp1VwiqftPQieN+tzqy+EiHJJmGf
1213TBAbWcncicCk9q2md+AmhNbvHO4PWbbz9TzC7HJb460jyWeuMEvw3gNIpEo2jYa9
1214pMV6cVqnSa+wOc0D7pC9a6bne0bvLcm3S+w6I5iDB3lZsb3A9UtRiSP7aGSo7D72
12158tC8+cIgZcI7k9vjvOqH+d7sdOU2yPCnRY6wFh62/g8bDnUpr56nZN1G89GwM4d4
1216r/TU7BQQIzsZgAiqOGXvVklIgAMiV0iucgf3rNBLjjeNEwNSTTG9F0CtQ+7JLwaE
1217wSEuAuRm+pRqi8BRnQ/GKUcCAwEAAQ==
1218-----END PUBLIC KEY-----
1219PKDEV;
1220 }
1221
1222 public static function getPKTags()
1223 {
1224 return <<<PKTAGS
1225-----BEGIN PUBLIC KEY-----
1226MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0Vi/2K6apCVj76nCnCl2
1227MQUPdK+A9eqkYBacXo2wQBYmyVlXm2/n/ZsX6pCLYPQTHyr5jXbkQzBw8SKqPdlh
1228vA7NpbMeNCz7wP/AobvUXM8xQuXKbMDTY2uZ4O7sM+PfGbptKPBGLe8Z8d2sUnTO
1229bXtX6Lrj13wkRto7st/w/Yp33RHe9SlqkiiS4MsH1jBkcIkEHsRaveZzedUaxY0M
1230mba0uPhGUInpPzEHwrYqBBEtWvP97t2vtfx8I5qv28kh0Y6t+jnjL1Urid2iuQZf
1231noCMFIOu4vksK5HxJxxrN0GOmGmwVQjOOtxkwikNiotZGPR4KsVj8NnBrLX7oGuM
1232nQvGciiu+KoC2r3HDBrpDeBVdOWxDzT5R4iI0KoLzFh2pKqwbY+obNPS2bj+2dgJ
1233rV3V5Jjry42QOCBN3c88wU1PKftOLj2ECpewY6vnE478IipiEu7EAdK8Zwj2LmTr
1234RKQUSa9k7ggBkYZWAeO/2Ag0ey3g2bg7eqk+sHEq5ynIXd5lhv6tC5PBdHlWipDK
1235tl2IxiEnejnOmAzGVivE1YGduYBjN+mjxDVy8KGBrjnz1JPgAvgdwJ2dYw4Rsc/e
1236TzCFWGk/HM6a4f0IzBWbJ5ot0PIi4amk07IotBXDWwqDiQTwyuGCym5EqWQ2BD95
1237RGv89BPD+2DLnJysngsvVaUCAwEAAQ==
1238-----END PUBLIC KEY-----
1239PKTAGS;
1240 }
1241}
1242
1243class ErrorHandler
1244{
1245 public $message;
1246 protected $active;
1247
1248 /**
1249 * Handle php errors
1250 *
1251 * @param mixed $code The error code
1252 * @param mixed $msg The error message
1253 */
1254 public function handleError($code, $msg)
1255 {
1256 if ($this->message) {
1257 $this->message .= PHP_EOL;
1258 }
1259 $this->message .= preg_replace('{^file_get_contents\(.*?\): }', '', $msg);
1260 }
1261
1262 /**
1263 * Starts error-handling if not already active
1264 *
1265 * Any message is cleared
1266 */
1267 public function start()
1268 {
1269 if (!$this->active) {
1270 set_error_handler(array($this, 'handleError'));
1271 $this->active = true;
1272 }
1273 $this->message = '';
1274 }
1275
1276 /**
1277 * Stops error-handling if active
1278 *
1279 * Any message is preserved until the next call to start()
1280 */
1281 public function stop()
1282 {
1283 if ($this->active) {
1284 restore_error_handler();
1285 $this->active = false;
1286 }
1287 }
1288}
1289
1290class NoProxyPattern
1291{
1292 private $composerInNoProxy = false;
1293 private $rulePorts = array();
1294
1295 public function __construct($pattern)
1296 {
1297 $rules = preg_split('{[\s,]+}', $pattern, null, PREG_SPLIT_NO_EMPTY);
1298
1299 if ($matches = preg_grep('{getcomposer\.org(?::\d+)?}i', $rules)) {
1300 $this->composerInNoProxy = true;
1301
1302 foreach ($matches as $match) {
1303 if (strpos($match, ':') !== false) {
1304 list(, $port) = explode(':', $match);
1305 $this->rulePorts[] = (int) $port;
1306 }
1307 }
1308 }
1309 }
1310
1311 /**
1312 * Returns true if NO_PROXY contains getcomposer.org
1313 *
1314 * @param string $url http(s)://getcomposer.org
1315 *
1316 * @return bool
1317 */
1318 public function test($url)
1319 {
1320 if (!$this->composerInNoProxy) {
1321 return false;
1322 }
1323
1324 if (empty($this->rulePorts)) {
1325 return true;
1326 }
1327
1328 if (strpos($url, 'http://') === 0) {
1329 $port = 80;
1330 } else {
1331 $port = 443;
1332 }
1333
1334 return in_array($port, $this->rulePorts);
1335 }
1336}
1337
1338class HttpClient {
1339
1340 /** @var null|string */
1341 private static $caPath;
1342
1343 private $options = array('http' => array());
1344 private $disableTls = false;
1345
1346 public function __construct($disableTls = false, $cafile = false)
1347 {
1348 $this->disableTls = $disableTls;
1349 if ($this->disableTls === false) {
1350 if (!empty($cafile) && !is_dir($cafile)) {
1351 if (!is_readable($cafile) || !validateCaFile(file_get_contents($cafile))) {
1352 throw new RuntimeException('The configured cafile (' .$cafile. ') was not valid or could not be read.');
1353 }
1354 }
1355 $options = $this->getTlsStreamContextDefaults($cafile);
1356 $this->options = array_replace_recursive($this->options, $options);
1357 }
1358 }
1359
1360 public function get($url)
1361 {
1362 $context = $this->getStreamContext($url);
1363 $result = file_get_contents($url, false, $context);
1364
1365 if ($result && extension_loaded('zlib')) {
1366 $decode = false;
1367 foreach ($http_response_header as $header) {
1368 if (preg_match('{^content-encoding: *gzip *$}i', $header)) {
1369 $decode = true;
1370 continue;
1371 } elseif (preg_match('{^HTTP/}i', $header)) {
1372 $decode = false;
1373 }
1374 }
1375
1376 if ($decode) {
1377 if (version_compare(PHP_VERSION, '5.4.0', '>=')) {
1378 $result = zlib_decode($result);
1379 } else {
1380 // work around issue with gzuncompress & co that do not work with all gzip checksums
1381 $result = file_get_contents('compress.zlib://data:application/octet-stream;base64,'.base64_encode($result));
1382 }
1383
1384 if (!$result) {
1385 throw new RuntimeException('Failed to decode zlib stream');
1386 }
1387 }
1388 }
1389
1390 return $result;
1391 }
1392
1393 protected function getStreamContext($url)
1394 {
1395 if ($this->disableTls === false) {
1396 if (PHP_VERSION_ID < 50600) {
1397 $this->options['ssl']['SNI_server_name'] = parse_url($url, PHP_URL_HOST);
1398 }
1399 }
1400 // Keeping the above mostly isolated from the code copied from Composer.
1401 return $this->getMergedStreamContext($url);
1402 }
1403
1404 protected function getTlsStreamContextDefaults($cafile)
1405 {
1406 $ciphers = implode(':', array(
1407 'ECDHE-RSA-AES128-GCM-SHA256',
1408 'ECDHE-ECDSA-AES128-GCM-SHA256',
1409 'ECDHE-RSA-AES256-GCM-SHA384',
1410 'ECDHE-ECDSA-AES256-GCM-SHA384',
1411 'DHE-RSA-AES128-GCM-SHA256',
1412 'DHE-DSS-AES128-GCM-SHA256',
1413 'kEDH+AESGCM',
1414 'ECDHE-RSA-AES128-SHA256',
1415 'ECDHE-ECDSA-AES128-SHA256',
1416 'ECDHE-RSA-AES128-SHA',
1417 'ECDHE-ECDSA-AES128-SHA',
1418 'ECDHE-RSA-AES256-SHA384',
1419 'ECDHE-ECDSA-AES256-SHA384',
1420 'ECDHE-RSA-AES256-SHA',
1421 'ECDHE-ECDSA-AES256-SHA',
1422 'DHE-RSA-AES128-SHA256',
1423 'DHE-RSA-AES128-SHA',
1424 'DHE-DSS-AES128-SHA256',
1425 'DHE-RSA-AES256-SHA256',
1426 'DHE-DSS-AES256-SHA',
1427 'DHE-RSA-AES256-SHA',
1428 'AES128-GCM-SHA256',
1429 'AES256-GCM-SHA384',
1430 'AES128-SHA256',
1431 'AES256-SHA256',
1432 'AES128-SHA',
1433 'AES256-SHA',
1434 'AES',
1435 'CAMELLIA',
1436 'DES-CBC3-SHA',
1437 '!aNULL',
1438 '!eNULL',
1439 '!EXPORT',
1440 '!DES',
1441 '!RC4',
1442 '!MD5',
1443 '!PSK',
1444 '!aECDH',
1445 '!EDH-DSS-DES-CBC3-SHA',
1446 '!EDH-RSA-DES-CBC3-SHA',
1447 '!KRB5-DES-CBC3-SHA',
1448 ));
1449
1450 /**
1451 * CN_match and SNI_server_name are only known once a URL is passed.
1452 * They will be set in the getOptionsForUrl() method which receives a URL.
1453 *
1454 * cafile or capath can be overridden by passing in those options to constructor.
1455 */
1456 $options = array(
1457 'ssl' => array(
1458 'ciphers' => $ciphers,
1459 'verify_peer' => true,
1460 'verify_depth' => 7,
1461 'SNI_enabled' => true,
1462 )
1463 );
1464
1465 /**
1466 * Attempt to find a local cafile or throw an exception.
1467 * The user may go download one if this occurs.
1468 */
1469 if (!$cafile) {
1470 $cafile = self::getSystemCaRootBundlePath();
1471 }
1472 if (is_dir($cafile)) {
1473 $options['ssl']['capath'] = $cafile;
1474 } elseif ($cafile) {
1475 $options['ssl']['cafile'] = $cafile;
1476 } else {
1477 throw new RuntimeException('A valid cafile could not be located automatically.');
1478 }
1479
1480 /**
1481 * Disable TLS compression to prevent CRIME attacks where supported.
1482 */
1483 if (version_compare(PHP_VERSION, '5.4.13') >= 0) {
1484 $options['ssl']['disable_compression'] = true;
1485 }
1486
1487 return $options;
1488 }
1489
1490 /**
1491 * function copied from Composer\Util\StreamContextFactory::initOptions
1492 *
1493 * Any changes should be applied there as well, or backported here.
1494 *
1495 * @param string $url URL the context is to be used for
1496 * @return resource Default context
1497 * @throws \RuntimeException if https proxy required and OpenSSL uninstalled
1498 */
1499 protected function getMergedStreamContext($url)
1500 {
1501 $options = $this->options;
1502
1503 // Handle HTTP_PROXY/http_proxy on CLI only for security reasons
1504 if ((PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') && (!empty($_SERVER['HTTP_PROXY']) || !empty($_SERVER['http_proxy']))) {
1505 $proxy = parse_url(!empty($_SERVER['http_proxy']) ? $_SERVER['http_proxy'] : $_SERVER['HTTP_PROXY']);
1506 }
1507
1508 // Prefer CGI_HTTP_PROXY if available
1509 if (!empty($_SERVER['CGI_HTTP_PROXY'])) {
1510 $proxy = parse_url($_SERVER['CGI_HTTP_PROXY']);
1511 }
1512
1513 // Override with HTTPS proxy if present and URL is https
1514 if (preg_match('{^https://}i', $url) && (!empty($_SERVER['HTTPS_PROXY']) || !empty($_SERVER['https_proxy']))) {
1515 $proxy = parse_url(!empty($_SERVER['https_proxy']) ? $_SERVER['https_proxy'] : $_SERVER['HTTPS_PROXY']);
1516 }
1517
1518 // Remove proxy if URL matches no_proxy directive
1519 if (!empty($_SERVER['NO_PROXY']) || !empty($_SERVER['no_proxy']) && parse_url($url, PHP_URL_HOST)) {
1520 $pattern = new NoProxyPattern(!empty($_SERVER['no_proxy']) ? $_SERVER['no_proxy'] : $_SERVER['NO_PROXY']);
1521 if ($pattern->test($url)) {
1522 unset($proxy);
1523 }
1524 }
1525
1526 if (!empty($proxy)) {
1527 $proxyURL = isset($proxy['scheme']) ? $proxy['scheme'] . '://' : '';
1528 $proxyURL .= isset($proxy['host']) ? $proxy['host'] : '';
1529
1530 if (isset($proxy['port'])) {
1531 $proxyURL .= ":" . $proxy['port'];
1532 } elseif (strpos($proxyURL, 'http://') === 0) {
1533 $proxyURL .= ":80";
1534 } elseif (strpos($proxyURL, 'https://') === 0) {
1535 $proxyURL .= ":443";
1536 }
1537
1538 // check for a secure proxy
1539 if (strpos($proxyURL, 'https://') === 0) {
1540 if (!extension_loaded('openssl')) {
1541 throw new RuntimeException('You must enable the openssl extension to use a secure proxy.');
1542 }
1543 if (strpos($url, 'https://') === 0) {
1544 throw new RuntimeException('PHP does not support https requests through a secure proxy.');
1545 }
1546 }
1547
1548 // http(s):// is not supported in proxy
1549 $proxyURL = str_replace(array('http://', 'https://'), array('tcp://', 'ssl://'), $proxyURL);
1550
1551 $options['http'] = array(
1552 'proxy' => $proxyURL,
1553 );
1554
1555 // add request_fulluri for http requests
1556 if ('http' === parse_url($url, PHP_URL_SCHEME)) {
1557 $options['http']['request_fulluri'] = true;
1558 }
1559
1560 // handle proxy auth if present
1561 if (isset($proxy['user'])) {
1562 $auth = rawurldecode($proxy['user']);
1563 if (isset($proxy['pass'])) {
1564 $auth .= ':' . rawurldecode($proxy['pass']);
1565 }
1566 $auth = base64_encode($auth);
1567
1568 $options['http']['header'] = "Proxy-Authorization: Basic {$auth}\r\n";
1569 }
1570 }
1571
1572 if (isset($options['http']['header'])) {
1573 $options['http']['header'] .= "Connection: close\r\n";
1574 } else {
1575 $options['http']['header'] = "Connection: close\r\n";
1576 }
1577 if (extension_loaded('zlib')) {
1578 $options['http']['header'] .= "Accept-Encoding: gzip\r\n";
1579 }
1580 $options['http']['header'] .= "User-Agent: ".COMPOSER_INSTALLER."\r\n";
1581 $options['http']['protocol_version'] = 1.1;
1582 $options['http']['timeout'] = 600;
1583
1584 return stream_context_create($options);
1585 }
1586
1587 /**
1588 * This method was adapted from Sslurp.
1589 * https://github.com/EvanDotPro/Sslurp
1590 *
1591 * (c) Evan Coury <me@evancoury.com>
1592 *
1593 * For the full copyright and license information, please see below:
1594 *
1595 * Copyright (c) 2013, Evan Coury
1596 * All rights reserved.
1597 *
1598 * Redistribution and use in source and binary forms, with or without modification,
1599 * are permitted provided that the following conditions are met:
1600 *
1601 * * Redistributions of source code must retain the above copyright notice,
1602 * this list of conditions and the following disclaimer.
1603 *
1604 * * Redistributions in binary form must reproduce the above copyright notice,
1605 * this list of conditions and the following disclaimer in the documentation
1606 * and/or other materials provided with the distribution.
1607 *
1608 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
1609 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
1610 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
1611 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
1612 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
1613 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
1614 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
1615 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
1616 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
1617 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
1618 */
1619 public static function getSystemCaRootBundlePath()
1620 {
1621 if (self::$caPath !== null) {
1622 return self::$caPath;
1623 }
1624
1625 // If SSL_CERT_FILE env variable points to a valid certificate/bundle, use that.
1626 // This mimics how OpenSSL uses the SSL_CERT_FILE env variable.
1627 $envCertFile = getenv('SSL_CERT_FILE');
1628 if ($envCertFile && is_readable($envCertFile) && validateCaFile(file_get_contents($envCertFile))) {
1629 return self::$caPath = $envCertFile;
1630 }
1631
1632 // If SSL_CERT_DIR env variable points to a valid certificate/bundle, use that.
1633 // This mimics how OpenSSL uses the SSL_CERT_FILE env variable.
1634 $envCertDir = getenv('SSL_CERT_DIR');
1635 if ($envCertDir && is_dir($envCertDir) && is_readable($envCertDir)) {
1636 return self::$caPath = $envCertDir;
1637 }
1638
1639 $configured = ini_get('openssl.cafile');
1640 if ($configured && strlen($configured) > 0 && is_readable($configured) && validateCaFile(file_get_contents($configured))) {
1641 return self::$caPath = $configured;
1642 }
1643
1644 $configured = ini_get('openssl.capath');
1645 if ($configured && is_dir($configured) && is_readable($configured)) {
1646 return self::$caPath = $configured;
1647 }
1648
1649 $caBundlePaths = array(
1650 '/etc/pki/tls/certs/ca-bundle.crt', // Fedora, RHEL, CentOS (ca-certificates package)
1651 '/etc/ssl/certs/ca-certificates.crt', // Debian, Ubuntu, Gentoo, Arch Linux (ca-certificates package)
1652 '/etc/ssl/ca-bundle.pem', // SUSE, openSUSE (ca-certificates package)
1653 '/usr/local/share/certs/ca-root-nss.crt', // FreeBSD (ca_root_nss_package)
1654 '/usr/ssl/certs/ca-bundle.crt', // Cygwin
1655 '/opt/local/share/curl/curl-ca-bundle.crt', // OS X macports, curl-ca-bundle package
1656 '/usr/local/share/curl/curl-ca-bundle.crt', // Default cURL CA bunde path (without --with-ca-bundle option)
1657 '/usr/share/ssl/certs/ca-bundle.crt', // Really old RedHat?
1658 '/etc/ssl/cert.pem', // OpenBSD
1659 '/usr/local/etc/ssl/cert.pem', // FreeBSD 10.x
1660 '/usr/local/etc/openssl/cert.pem', // OS X homebrew, openssl package
1661 '/usr/local/etc/openssl@1.1/cert.pem', // OS X homebrew, openssl@1.1 package
1662 '/opt/homebrew/etc/openssl@3/cert.pem', // macOS silicon homebrew, openssl@3 package
1663 '/opt/homebrew/etc/openssl@1.1/cert.pem', // macOS silicon homebrew, openssl@1.1 package
1664 );
1665
1666 foreach ($caBundlePaths as $caBundle) {
1667 if (@is_readable($caBundle) && validateCaFile(file_get_contents($caBundle))) {
1668 return self::$caPath = $caBundle;
1669 }
1670 }
1671
1672 foreach ($caBundlePaths as $caBundle) {
1673 $caBundle = dirname($caBundle);
1674 if (is_dir($caBundle) && glob($caBundle.'/*')) {
1675 return self::$caPath = $caBundle;
1676 }
1677 }
1678
1679 return self::$caPath = false;
1680 }
1681
1682 public static function getPackagedCaFile()
1683 {
1684 return <<<CACERT
1685##
1686## Bundle of CA Root Certificates for Let's Encrypt
1687##
1688## See https://letsencrypt.org/certificates/#root-certificates
1689##
1690## ISRG Root X1 (RSA 4096) expires Jun 04 11:04:38 2035 GMT
1691## ISRG Root X2 (ECDSA P-384) expires Sep 17 16:00:00 2040 GMT
1692##
1693## Both these are self-signed CA root certificates
1694##
1695
1696ISRG Root X1
1697============
1698-----BEGIN CERTIFICATE-----
1699MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
1700TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
1701cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
1702WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
1703ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
1704MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
1705h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
17060TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
1707A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
1708T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
1709B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
1710B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
1711KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
1712OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
1713jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
1714qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
1715rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
1716HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
1717hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
1718ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
17193BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
1720NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
1721ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
1722TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
1723jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
1724oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
17254RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
1726mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
1727emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
1728-----END CERTIFICATE-----
1729
1730ISRG Root X2
1731============
1732-----BEGIN CERTIFICATE-----
1733MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw
1734CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg
1735R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00
1736MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT
1737ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw
1738EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW
1739+1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9
1740ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T
1741AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI
1742zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW
1743tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1
1744/q4AaOeMSQ+2b1tbFfLn
1745-----END CERTIFICATE-----
1746CACERT;
1747 }
1748}
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..c4e089b
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,32 @@
1{
2 "name": "nageurs/bigoudens",
3 "description": "site web du club de natation: les nageurs bigoudens à Pont-l'Abbé",
4 "require": {
5 "doctrine/dbal": "^4.2",
6 "doctrine/orm": "^3.3",
7 "symfony/cache": "^7.2",
8 "composer": "*",
9 "htmlawed/htmlawed": "^1.2",
10 "tinymce/tinymce": "^7.7",
11 "tweeb/tinymce-i18n": "^2.0"
12 },
13 "scripts": {
14 "post-install-cmd": [
15 "php bin/copy_directory.php vendor/tinymce/tinymce public/js/tinymce",
16 "php bin/copy_directory.php vendor/tweeb/tinymce-i18n/langs public/js/tinymce-langs"
17 ],
18 "post-update-cmd": [
19 "php bin/copy_directory.php vendor/tinymce/tinymce public/js/tinymce",
20 "php bin/copy_directory.php vendor/tweeb/tinymce-i18n/langs public/js/tinymce-langs"
21 ]
22 },
23 "authors": [
24 {
25 "name": "polo",
26 "email": "paul.jusot@fdn.fr"
27 }
28 ],
29 "autoload": {
30 "classmap": ["src/"]
31 }
32}
diff --git a/config/.htaccess b/config/.htaccess
new file mode 100644
index 0000000..1ea74d0
--- /dev/null
+++ b/config/.htaccess
@@ -0,0 +1,4 @@
1<Files "config.ini">
2 Order Allow,Deny
3 Deny from all
4</Files> \ No newline at end of file
diff --git a/deploiement.txt b/deploiement.txt
new file mode 100644
index 0000000..5346145
--- /dev/null
+++ b/deploiement.txt
@@ -0,0 +1,58 @@
1déploiement
2
3=> procédure pour un hébergement "normal" avec un accès SSH,
4ne marche pas sans la formule pro d'OVH
5- peu de manips (le renommage) peuvent être faites avec filezilla
6- pour le reste il faut contourner avec des scripts PHP faisant des exec()
7
8=> pour utiliser PHP-cli il faut utiliser le chemin de PHP /usr/local/php8.3/bin
9ou l'ajouter au PATH en éditant .bash_profile (facile avec filezilla):
10trouver la ligne PATH=$PATH:$HOME/bin
11et y coller :$HOME/usr/local/php8.3/bin
12
13=> utiliser PHP-cli depuis un script web:
14$param = '-r "echo \'hello world\';"';
15$commande = 'php ' . $param; // OU
16$commande = '/usr/local/php8.3/bin/php ' . $param;
17$sortie = null;
18$code_retour = null;
19exec($commande, $sortie, $code_retour);
20
21
221/ se connecter au serveur en ssh et télécharger les fichiers:
23cd /var/www
24git clone git@ordipolo.fr:nageurs
25
262/ renommer le dossier du site pour apache
27mv nageurs dossier_du_site
28
293/ côté client, envoyer les fichier config.ini et bdd_nageurs.sql en FTP
30placer config.ini à la racine du site
31
32retour côté serveur en ssh
33
344/ mieux vaut utiliser composer et la console doctrine sans être root,
35changer les permissions:
36chown -R www-data www-data dossier_du_site
37
385/ télécharger les dépendances PHP (dont doctrine) en lisant le composer.json:
39sudo -u www-data php composer.phar update
40
416/ générer l'autoloader
42composer dump-autoload -o
43
447/ se connecter à la BDD et créer la base:
45mysql/mariadb -u root -p
46CREATE DATABASES nageurs CHARACTER SET utf8mb4;
47l'utilisateur doit avoir les droits SELECT, INSERT, UPDATE, DELETE, CREATE, DROP et ALTER
48
498/ créer les tables:
50sudo -u www-data php bin/doctrine orm:schema-tool:create
51et remplir la base de données:
52mariadb -D nageurs -u root -p < bdd_nageurs.sql
53
549/ le 1er chargement d'une page est long,
55le cache de symfony inclu fait ensuite son job
56
57sinon, regarder ici:
58https://www.doctrine-project.org/projects/doctrine-orm/en/3.3/reference/caching.html
diff --git a/public/assets/arrow-down-nb.svg b/public/assets/arrow-down-nb.svg
new file mode 100644
index 0000000..84ad2f3
--- /dev/null
+++ b/public/assets/arrow-down-nb.svg
@@ -0,0 +1,36 @@
1<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2<svg
3 viewBox="0 0 24 24"
4 fill="currentColor"
5 version="1.1"
6 id="svg1"
7 sodipodi:docname="arrow-down-nb.svg"
8 inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
9 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
10 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
11 xmlns="http://www.w3.org/2000/svg"
12 xmlns:svg="http://www.w3.org/2000/svg">
13 <defs
14 id="defs1" />
15 <sodipodi:namedview
16 id="namedview1"
17 pagecolor="#ffffff"
18 bordercolor="#000000"
19 borderopacity="0.25"
20 inkscape:showpageshadow="2"
21 inkscape:pageopacity="0.0"
22 inkscape:pagecheckerboard="0"
23 inkscape:deskcolor="#d1d1d1"
24 inkscape:zoom="14.75"
25 inkscape:cx="12"
26 inkscape:cy="12.033898"
27 inkscape:window-width="1261"
28 inkscape:window-height="1032"
29 inkscape:window-x="655"
30 inkscape:window-y="0"
31 inkscape:window-maximized="1"
32 inkscape:current-layer="svg1" />
33 <path
34 d="M13.0001 16.1716L18.3641 10.8076L19.7783 12.2218L12.0001 20L4.22192 12.2218L5.63614 10.8076L11.0001 16.1716V4H13.0001V16.1716Z"
35 id="path1" />
36</svg>
diff --git a/public/assets/arrow-down.svg b/public/assets/arrow-down.svg
new file mode 100644
index 0000000..bd7d579
--- /dev/null
+++ b/public/assets/arrow-down.svg
@@ -0,0 +1,40 @@
1<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2<svg
3 viewBox="0 0 24 24"
4 fill="currentColor"
5 version="1.1"
6 id="svg1"
7 sodipodi:docname="arrow-down.svg"
8 inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
9 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
10 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
11 xmlns="http://www.w3.org/2000/svg"
12 xmlns:svg="http://www.w3.org/2000/svg">
13 <defs
14 id="defs1" />
15 <sodipodi:namedview
16 id="namedview1"
17 pagecolor="#ffffff"
18 bordercolor="#000000"
19 borderopacity="0.25"
20 inkscape:showpageshadow="2"
21 inkscape:pageopacity="0.0"
22 inkscape:pagecheckerboard="0"
23 inkscape:deskcolor="#d1d1d1"
24 inkscape:zoom="14.75"
25 inkscape:cx="12"
26 inkscape:cy="12.033898"
27 inkscape:window-width="1261"
28 inkscape:window-height="1032"
29 inkscape:window-x="655"
30 inkscape:window-y="0"
31 inkscape:window-maximized="1"
32 inkscape:current-layer="svg1" />
33 <path
34 d="M13.0001 16.1716L18.3641 10.8076L19.7783 12.2218L12.0001 20L4.22192 12.2218L5.63614 10.8076L11.0001 16.1716V4H13.0001V16.1716Z"
35 id="path1" />
36 <path
37 style="fill:#dd3333;stroke:#dd3333;stroke-width:0.256239"
38 d="M 8.1361932,16.102295 4.2726833,12.238339 4.9491052,11.559369 5.6255269,10.8804 8.2707084,13.524946 c 1.4548498,1.4545 2.6679196,2.644546 2.6957116,2.644546 0.02779,0 0.05053,-2.730509 0.05053,-6.067797 V 4.0338983 H 12 12.983051 v 6.0677967 c 0,3.337288 0.02274,6.067797 0.05053,6.067797 0.02779,0 1.240862,-1.190046 2.695712,-2.644546 l 2.645181,-2.644546 0.676496,0.679044 0.676496,0.679044 -3.863881,3.863881 -3.863881,3.863882 z"
39 id="path2" />
40</svg>
diff --git a/public/assets/arrow-up-nb.svg b/public/assets/arrow-up-nb.svg
new file mode 100644
index 0000000..f845bf1
--- /dev/null
+++ b/public/assets/arrow-up-nb.svg
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M13.0001 7.82843V20H11.0001V7.82843L5.63614 13.1924L4.22192 11.7782L12.0001 4L19.7783 11.7782L18.3641 13.1924L13.0001 7.82843Z"></path></svg> \ No newline at end of file
diff --git a/public/assets/arrow-up.svg b/public/assets/arrow-up.svg
new file mode 100644
index 0000000..c88794d
--- /dev/null
+++ b/public/assets/arrow-up.svg
@@ -0,0 +1,40 @@
1<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2<svg
3 viewBox="0 0 24 24"
4 fill="currentColor"
5 version="1.1"
6 id="svg1"
7 sodipodi:docname="arrow-up.svg"
8 inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
9 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
10 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
11 xmlns="http://www.w3.org/2000/svg"
12 xmlns:svg="http://www.w3.org/2000/svg">
13 <defs
14 id="defs1" />
15 <sodipodi:namedview
16 id="namedview1"
17 pagecolor="#ffffff"
18 bordercolor="#000000"
19 borderopacity="0.25"
20 inkscape:showpageshadow="2"
21 inkscape:pageopacity="0.0"
22 inkscape:pagecheckerboard="0"
23 inkscape:deskcolor="#d1d1d1"
24 inkscape:zoom="7.7781746"
25 inkscape:cx="19.734707"
26 inkscape:cy="19.220448"
27 inkscape:window-width="956"
28 inkscape:window-height="1032"
29 inkscape:window-x="960"
30 inkscape:window-y="0"
31 inkscape:window-maximized="1"
32 inkscape:current-layer="svg1" />
33 <path
34 d="M13.0001 7.82843V20H11.0001V7.82843L5.63614 13.1924L4.22192 11.7782L12.0001 4L19.7783 11.7782L18.3641 13.1924L13.0001 7.82843Z"
35 id="path1" />
36 <path
37 style="fill:#dd3333;stroke:#dd3333;stroke-width:0.485914"
38 d="M 11.056579,13.854013 V 7.7161883 L 8.3569078,10.413563 5.6572369,13.110938 4.9829803,12.436682 4.3087238,11.762425 8.1323616,7.9387874 11.955999,4.1151496 l 3.85685,3.8552422 3.856848,3.8552422 -0.642652,0.642652 -0.642652,0.642652 -2.69967,-2.697375 -2.699671,-2.6973747 v 6.1378247 6.137824 h -0.964237 -0.964236 z"
39 id="path2" />
40</svg>
diff --git a/public/assets/book-open-nb.svg b/public/assets/book-open-nb.svg
new file mode 100644
index 0000000..462bb9d
--- /dev/null
+++ b/public/assets/book-open-nb.svg
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M13 21V23H11V21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H9C10.1947 3 11.2671 3.52375 12 4.35418C12.7329 3.52375 13.8053 3 15 3H21C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H13ZM20 19V5H15C13.8954 5 13 5.89543 13 7V19H20ZM11 19V7C11 5.89543 10.1046 5 9 5H4V19H11Z"></path></svg> \ No newline at end of file
diff --git a/public/assets/book-open.svg b/public/assets/book-open.svg
new file mode 100644
index 0000000..383bc6d
--- /dev/null
+++ b/public/assets/book-open.svg
@@ -0,0 +1,40 @@
1<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2<svg
3 viewBox="0 0 24 24"
4 fill="currentColor"
5 version="1.1"
6 id="svg1"
7 sodipodi:docname="book-open.svg"
8 inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
9 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
10 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
11 xmlns="http://www.w3.org/2000/svg"
12 xmlns:svg="http://www.w3.org/2000/svg">
13 <defs
14 id="defs1" />
15 <sodipodi:namedview
16 id="namedview1"
17 pagecolor="#ffffff"
18 bordercolor="#000000"
19 borderopacity="0.25"
20 inkscape:showpageshadow="2"
21 inkscape:pageopacity="0.0"
22 inkscape:pagecheckerboard="0"
23 inkscape:deskcolor="#d1d1d1"
24 inkscape:zoom="16.333333"
25 inkscape:cx="13.714286"
26 inkscape:cy="10.010204"
27 inkscape:window-width="1367"
28 inkscape:window-height="1032"
29 inkscape:window-x="549"
30 inkscape:window-y="0"
31 inkscape:window-maximized="1"
32 inkscape:current-layer="svg1" />
33 <path
34 d="M13 21V23H11V21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H9C10.1947 3 11.2671 3.52375 12 4.35418C12.7329 3.52375 13.8053 3 15 3H21C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H13ZM20 19V5H15C13.8954 5 13 5.89543 13 7V19H20ZM11 19V7C11 5.89543 10.1046 5 9 5H4V19H11Z"
35 id="path1" />
36 <path
37 style="fill:#dd3333;stroke:#dd3333;stroke-width:0.462799"
38 d="m 11.020408,21.985643 v -0.97354 L 6.8668354,20.975439 2.7132626,20.938776 2.3974476,20.641921 2.0816327,20.345066 V 12 3.6549341 l 0.3162118,-0.2968548 0.3162119,-0.2968548 3.5103187,0.00234 c 3.2760872,0.00218 3.5522704,0.01942 4.1390279,0.2583701 0.34579,0.1408189 0.852538,0.4359086 1.126107,0.655755 l 0.497399,0.3997205 0.605683,-0.4446121 c 1.1434,-0.8393329 1.270609,-0.8605967 5.183033,-0.8663802 l 3.510319,-0.00519 0.316211,0.2968548 0.316212,0.2968548 V 12 20.345066 l -0.315815,0.296855 -0.315814,0.296855 -4.153574,0.03666 -4.153572,0.03666 v 0.97354 0.973541 H 12 11.020408 Z m 0,-9.294065 V 6.3423387 L 10.744898,5.9375303 C 10.12681,5.0293713 10.107047,5.0245329 6.8484856,4.9836104 L 3.9183674,4.9468125 v 7.0470025 7.047001 h 3.5510204 3.5510202 z m 9.061225,-0.697763 V 4.9468125 l -2.930119,0.036798 c -3.258561,0.040922 -3.278324,0.045761 -3.896412,0.9539199 l -0.27551,0.4048084 v 6.3492392 6.349238 h 3.55102 3.551021 z"
39 id="path2" />
40</svg>
diff --git a/public/assets/calendar-nb.svg b/public/assets/calendar-nb.svg
new file mode 100644
index 0000000..fbf11df
--- /dev/null
+++ b/public/assets/calendar-nb.svg
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M9 1V3H15V1H17V3H21C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H7V1H9ZM20 11H4V19H20V11ZM7 5H4V9H20V5H17V7H15V5H9V7H7V5Z"></path></svg> \ No newline at end of file
diff --git a/public/assets/calendar.svg b/public/assets/calendar.svg
new file mode 100644
index 0000000..d8a0862
--- /dev/null
+++ b/public/assets/calendar.svg
@@ -0,0 +1,40 @@
1<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2<svg
3 viewBox="0 0 24 24"
4 fill="currentColor"
5 version="1.1"
6 id="svg1"
7 sodipodi:docname="calendar.svg"
8 inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
9 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
10 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
11 xmlns="http://www.w3.org/2000/svg"
12 xmlns:svg="http://www.w3.org/2000/svg">
13 <defs
14 id="defs1" />
15 <sodipodi:namedview
16 id="namedview1"
17 pagecolor="#ffffff"
18 bordercolor="#000000"
19 borderopacity="0.25"
20 inkscape:showpageshadow="2"
21 inkscape:pageopacity="0.0"
22 inkscape:pagecheckerboard="0"
23 inkscape:deskcolor="#d1d1d1"
24 inkscape:zoom="16.333333"
25 inkscape:cx="10.867347"
26 inkscape:cy="7.0714286"
27 inkscape:window-width="1378"
28 inkscape:window-height="1032"
29 inkscape:window-x="538"
30 inkscape:window-y="0"
31 inkscape:window-maximized="1"
32 inkscape:current-layer="svg1" />
33 <path
34 d="M9 1V3H15V1H17V3H21C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H7V1H9ZM20 11H4V19H20V11ZM7 5H4V9H20V5H17V7H15V5H9V7H7V5Z"
35 id="path1" />
36 <path
37 style="fill:#dd3333;stroke-width:0.2314;fill-opacity:1"
38 d="M 2.6632967,20.900914 C 2.5791303,20.867183 2.4456452,20.779249 2.3666632,20.705506 2.0328996,20.393883 2.0510204,20.894721 2.0510204,11.981482 V 3.7221898 L 2.1887755,3.5197856 C 2.2645408,3.4084632 2.4042737,3.2597461 2.4992931,3.1893029 2.6658093,3.0658553 2.7504798,3.0605903 4.8411299,3.0436855 L 7.0102041,3.0261465 V 2.0181753 1.0102041 h 0.9795918 0.9795919 v 1.0102041 1.010204 H 12 15.030612 V 2.0204082 1.0102041 h 0.979592 0.979592 v 1.0079712 1.0079712 l 2.169074,0.017539 c 2.09065,0.016905 2.175321,0.02217 2.341837,0.1456174 0.09502,0.070443 0.234752,0.2191603 0.310518,0.3304827 L 21.94898,3.7221898 V 12 20.27781 l -0.137755,0.202405 c -0.07577,0.111322 -0.215538,0.260039 -0.310605,0.330482 l -0.17285,0.128079 -9.255722,0.01173 c -6.0661846,0.0077 -9.3084558,-0.0094 -9.4087513,-0.0496 z M 20.05102,15 V 10.989796 H 12 3.9489796 V 15 19.010204 H 12 20.05102 Z m 0,-7.9897959 V 4.9897959 H 18.520408 16.989796 V 5.9693878 6.9489796 H 16.010204 15.030612 V 5.9693878 4.9897959 H 12 8.9693878 V 5.9693878 6.9489796 H 7.9897959 7.0102041 V 5.9693878 4.9897959 H 5.4795918 3.9489796 V 7.0102041 9.0306123 H 12 20.05102 Z"
39 id="path2" />
40</svg>
diff --git a/public/assets/delete-bin-nb.svg b/public/assets/delete-bin-nb.svg
new file mode 100644
index 0000000..a71c4d8
--- /dev/null
+++ b/public/assets/delete-bin-nb.svg
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M17 6H22V8H20V21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V8H2V6H7V3C7 2.44772 7.44772 2 8 2H16C16.5523 2 17 2.44772 17 3V6ZM18 8H6V20H18V8ZM9 11H11V17H9V11ZM13 11H15V17H13V11ZM9 4V6H15V4H9Z"></path></svg> \ No newline at end of file
diff --git a/public/assets/delete-bin.svg b/public/assets/delete-bin.svg
new file mode 100644
index 0000000..d534c53
--- /dev/null
+++ b/public/assets/delete-bin.svg
@@ -0,0 +1,48 @@
1<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2<svg
3 viewBox="0 0 24 24"
4 fill="currentColor"
5 version="1.1"
6 id="svg1"
7 sodipodi:docname="delete-bin.svg"
8 inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
9 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
10 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
11 xmlns="http://www.w3.org/2000/svg"
12 xmlns:svg="http://www.w3.org/2000/svg">
13 <defs
14 id="defs1" />
15 <sodipodi:namedview
16 id="namedview1"
17 pagecolor="#ffffff"
18 bordercolor="#000000"
19 borderopacity="0.25"
20 inkscape:showpageshadow="2"
21 inkscape:pageopacity="0.0"
22 inkscape:pagecheckerboard="0"
23 inkscape:deskcolor="#d1d1d1"
24 inkscape:zoom="5.7747054"
25 inkscape:cx="8.0523589"
26 inkscape:cy="13.940105"
27 inkscape:window-width="956"
28 inkscape:window-height="1032"
29 inkscape:window-x="960"
30 inkscape:window-y="0"
31 inkscape:window-maximized="1"
32 inkscape:current-layer="svg1" />
33 <path
34 d="M17 6H22V8H20V21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V8H2V6H7V3C7 2.44772 7.44772 2 8 2H16C16.5523 2 17 2.44772 17 3V6ZM18 8H6V20H18V8ZM9 11H11V17H9V11ZM13 11H15V17H13V11ZM9 4V6H15V4H9Z"
35 id="path1" />
36 <path
37 style="fill:#dd3333;stroke:#dd3333;stroke-width:0.654497"
38 d="M 4.428179,21.633757 C 4.1901151,21.395692 4.1560562,20.517904 4.1560562,14.620412 V 7.8791899 H 3.1170421 2.0780281 V 7.0133448 6.1474998 H 4.5889787 7.0999293 V 4.4910099 c 0,-0.9110696 0.080668,-1.8072192 0.1792618,-1.9914437 0.1701493,-0.317927 0.4073212,-0.3349536 4.6657439,-0.3349536 5.369132,0 5.025628,-0.1624223 5.025628,2.3763127 v 1.6065745 h 2.51095 2.510951 v 0.865845 0.8658451 H 20.95345 19.914436 v 6.7267521 c 0,6.296846 -0.02024,6.745069 -0.316681,7.013345 -0.28412,0.257124 -1.066299,0.286592 -7.607067,0.286592 -6.396732,0 -7.3237431,-0.03336 -7.562509,-0.272122 z M 18.009577,13.940105 V 7.8791899 H 11.948662 5.8877463 v 6.0609151 6.060915 h 6.0609157 6.060915 z M 15.065704,5.0219012 V 3.8963027 H 11.948662 8.8316194 v 1.0101525 c 0,0.555584 0.051951,1.0621033 0.1154461,1.1255985 0.063495,0.063495 1.4661645,0.1154461 3.1170425,0.1154461 h 3.001596 z"
39 id="path2" />
40 <path
41 style="fill:#dd3333;stroke:#dd3333;stroke-width:0.654497"
42 d="m 9.0047884,13.940105 v -2.943873 h 0.9524295 0.9524291 v 2.943873 2.943873 H 9.9572179 9.0047884 Z"
43 id="path3" />
44 <path
45 style="fill:#dd3333;stroke:#dd3333;stroke-width:0.654497"
46 d="m 13.160845,13.940105 v -2.943873 h 0.865845 0.865845 v 2.943873 2.943873 H 14.02669 13.160845 Z"
47 id="path4" />
48</svg>
diff --git a/public/assets/edit-nb.svg b/public/assets/edit-nb.svg
new file mode 100644
index 0000000..f57a63e
--- /dev/null
+++ b/public/assets/edit-nb.svg
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M6.41421 15.89L16.5563 5.74785L15.1421 4.33363L5 14.4758V15.89H6.41421ZM7.24264 17.89H3V13.6473L14.435 2.21231C14.8256 1.82179 15.4587 1.82179 15.8492 2.21231L18.6777 5.04074C19.0682 5.43126 19.0682 6.06443 18.6777 6.45495L7.24264 17.89ZM3 19.89H21V21.89H3V19.89Z"></path></svg> \ No newline at end of file
diff --git a/public/assets/edit.svg b/public/assets/edit.svg
new file mode 100644
index 0000000..2c1fd10
--- /dev/null
+++ b/public/assets/edit.svg
@@ -0,0 +1,44 @@
1<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2<svg
3 viewBox="0 0 24 24"
4 fill="currentColor"
5 version="1.1"
6 id="svg1"
7 sodipodi:docname="edit.svg"
8 inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
9 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
10 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
11 xmlns="http://www.w3.org/2000/svg"
12 xmlns:svg="http://www.w3.org/2000/svg">
13 <defs
14 id="defs1" />
15 <sodipodi:namedview
16 id="namedview1"
17 pagecolor="#ffffff"
18 bordercolor="#000000"
19 borderopacity="0.25"
20 inkscape:showpageshadow="2"
21 inkscape:pageopacity="0.0"
22 inkscape:pagecheckerboard="0"
23 inkscape:deskcolor="#d1d1d1"
24 inkscape:zoom="8.1666667"
25 inkscape:cx="18.979592"
26 inkscape:cy="21.734694"
27 inkscape:window-width="1347"
28 inkscape:window-height="1032"
29 inkscape:window-x="569"
30 inkscape:window-y="0"
31 inkscape:window-maximized="1"
32 inkscape:current-layer="svg1" />
33 <path
34 d="M6.41421 15.89L16.5563 5.74785L15.1421 4.33363L5 14.4758V15.89H6.41421ZM7.24264 17.89H3V13.6473L14.435 2.21231C14.8256 1.82179 15.4587 1.82179 15.8492 2.21231L18.6777 5.04074C19.0682 5.43126 19.0682 6.06443 18.6777 6.45495L7.24264 17.89ZM3 19.89H21V21.89H3V19.89Z"
35 id="path1" />
36 <path
37 style="fill:#dd3333;stroke:#dd3333;stroke-width:0.462799"
38 d="M 3.0612245,20.877551 V 19.897959 H 12 20.938776 v 0.979592 0.979592 H 12 3.0612245 Z"
39 id="path2" />
40 <path
41 style="fill:#dd3333;stroke:#dd3333;stroke-width:0.462799"
42 d="M 3.0612245,15.764827 V 13.713329 L 8.9086367,7.8668687 c 5.4509273,-5.45004 5.8760063,-5.8464605 6.2691283,-5.8464605 0.378835,0 0.587344,0.1671659 2.050548,1.6439672 1.433032,1.4463495 1.62883,1.6928413 1.62883,2.050547 0,0.3731507 -0.480696,0.8873539 -5.846461,6.2539916 l -5.84646,5.847413 H 5.1127232 3.0612245 Z M 11.538908,10.869261 16.650326,5.7589307 15.888432,4.9902051 15.12654,4.2214796 10.01225,9.3346829 4.8979592,14.447886 v 0.765854 0.765852 h 0.7647659 0.7647658 z"
43 id="path3" />
44</svg>
diff --git a/public/assets/facebook-nb.svg b/public/assets/facebook-nb.svg
new file mode 100644
index 0000000..543537c
--- /dev/null
+++ b/public/assets/facebook-nb.svg
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12.001 2C6.47813 2 2.00098 6.47715 2.00098 12C2.00098 16.9913 5.65783 21.1283 10.4385 21.8785V14.8906H7.89941V12H10.4385V9.79688C10.4385 7.29063 11.9314 5.90625 14.2156 5.90625C15.3097 5.90625 16.4541 6.10156 16.4541 6.10156V8.5625H15.1931C13.9509 8.5625 13.5635 9.33334 13.5635 10.1242V12H16.3369L15.8936 14.8906H13.5635V21.8785C18.3441 21.1283 22.001 16.9913 22.001 12C22.001 6.47715 17.5238 2 12.001 2Z"></path></svg> \ No newline at end of file
diff --git a/public/assets/facebook.svg b/public/assets/facebook.svg
new file mode 100644
index 0000000..d09d296
--- /dev/null
+++ b/public/assets/facebook.svg
@@ -0,0 +1,37 @@
1<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2<svg
3 viewBox="0 0 24 24"
4 fill="currentColor"
5 version="1.1"
6 id="svg1"
7 sodipodi:docname="facebook.svg"
8 inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
9 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
10 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
11 xmlns="http://www.w3.org/2000/svg"
12 xmlns:svg="http://www.w3.org/2000/svg">
13 <defs
14 id="defs1" />
15 <sodipodi:namedview
16 id="namedview1"
17 pagecolor="#ffffff"
18 bordercolor="#000000"
19 borderopacity="0.25"
20 inkscape:showpageshadow="2"
21 inkscape:pageopacity="0.0"
22 inkscape:pagecheckerboard="0"
23 inkscape:deskcolor="#d1d1d1"
24 inkscape:zoom="6.8333333"
25 inkscape:cx="12"
26 inkscape:cy="11.926829"
27 inkscape:window-width="1374"
28 inkscape:window-height="1032"
29 inkscape:window-x="542"
30 inkscape:window-y="0"
31 inkscape:window-maximized="1"
32 inkscape:current-layer="svg1" />
33 <path
34 d="M12.001 2C6.47813 2 2.00098 6.47715 2.00098 12C2.00098 16.9913 5.65783 21.1283 10.4385 21.8785V14.8906H7.89941V12H10.4385V9.79688C10.4385 7.29063 11.9314 5.90625 14.2156 5.90625C15.3097 5.90625 16.4541 6.10156 16.4541 6.10156V8.5625H15.1931C13.9509 8.5625 13.5635 9.33334 13.5635 10.1242V12H16.3369L15.8936 14.8906H13.5635V21.8785C18.3441 21.1283 22.001 16.9913 22.001 12C22.001 6.47715 17.5238 2 12.001 2Z"
35 id="path1"
36 style="fill:#dd3333;fill-opacity:1;stroke:none;stroke-opacity:1" />
37</svg>
diff --git a/public/assets/favicon48x48.png b/public/assets/favicon48x48.png
new file mode 100644
index 0000000..9825db1
--- /dev/null
+++ b/public/assets/favicon48x48.png
Binary files differ
diff --git a/public/assets/fond-piscine.jpg b/public/assets/fond-piscine.jpg
new file mode 100644
index 0000000..239d95d
--- /dev/null
+++ b/public/assets/fond-piscine.jpg
Binary files differ
diff --git a/public/assets/home.svg b/public/assets/home.svg
new file mode 100644
index 0000000..d400c4a
--- /dev/null
+++ b/public/assets/home.svg
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M19 21H5C4.44772 21 4 20.5523 4 20V11L1 11L11.3273 1.6115C11.7087 1.26475 12.2913 1.26475 12.6727 1.6115L23 11L20 11V20C20 20.5523 19.5523 21 19 21ZM6 19H18V9.15745L12 3.7029L6 9.15745V19Z"></path></svg> \ No newline at end of file
diff --git a/public/assets/instagram-nb.svg b/public/assets/instagram-nb.svg
new file mode 100644
index 0000000..c51ee39
--- /dev/null
+++ b/public/assets/instagram-nb.svg
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12.001 9C10.3436 9 9.00098 10.3431 9.00098 12C9.00098 13.6573 10.3441 15 12.001 15C13.6583 15 15.001 13.6569 15.001 12C15.001 10.3427 13.6579 9 12.001 9ZM12.001 7C14.7614 7 17.001 9.2371 17.001 12C17.001 14.7605 14.7639 17 12.001 17C9.24051 17 7.00098 14.7629 7.00098 12C7.00098 9.23953 9.23808 7 12.001 7ZM18.501 6.74915C18.501 7.43926 17.9402 7.99917 17.251 7.99917C16.5609 7.99917 16.001 7.4384 16.001 6.74915C16.001 6.0599 16.5617 5.5 17.251 5.5C17.9393 5.49913 18.501 6.0599 18.501 6.74915ZM12.001 4C9.5265 4 9.12318 4.00655 7.97227 4.0578C7.18815 4.09461 6.66253 4.20007 6.17416 4.38967C5.74016 4.55799 5.42709 4.75898 5.09352 5.09255C4.75867 5.4274 4.55804 5.73963 4.3904 6.17383C4.20036 6.66332 4.09493 7.18811 4.05878 7.97115C4.00703 9.0752 4.00098 9.46105 4.00098 12C4.00098 14.4745 4.00753 14.8778 4.05877 16.0286C4.0956 16.8124 4.2012 17.3388 4.39034 17.826C4.5591 18.2606 4.7605 18.5744 5.09246 18.9064C5.42863 19.2421 5.74179 19.4434 6.17187 19.6094C6.66619 19.8005 7.19148 19.9061 7.97212 19.9422C9.07618 19.9939 9.46203 20 12.001 20C14.4755 20 14.8788 19.9934 16.0296 19.9422C16.8117 19.9055 17.3385 19.7996 17.827 19.6106C18.2604 19.4423 18.5752 19.2402 18.9074 18.9085C19.2436 18.5718 19.4445 18.2594 19.6107 17.8283C19.8013 17.3358 19.9071 16.8098 19.9432 16.0289C19.9949 14.9248 20.001 14.5389 20.001 12C20.001 9.52552 19.9944 9.12221 19.9432 7.97137C19.9064 7.18906 19.8005 6.66149 19.6113 6.17318C19.4434 5.74038 19.2417 5.42635 18.9084 5.09255C18.573 4.75715 18.2616 4.55693 17.8271 4.38942C17.338 4.19954 16.8124 4.09396 16.0298 4.05781C14.9258 4.00605 14.5399 4 12.001 4ZM12.001 2C14.7176 2 15.0568 2.01 16.1235 2.06C17.1876 2.10917 17.9135 2.2775 18.551 2.525C19.2101 2.77917 19.7668 3.1225 20.3226 3.67833C20.8776 4.23417 21.221 4.7925 21.476 5.45C21.7226 6.08667 21.891 6.81333 21.941 7.8775C21.9885 8.94417 22.001 9.28333 22.001 12C22.001 14.7167 21.991 15.0558 21.941 16.1225C21.8918 17.1867 21.7226 17.9125 21.476 18.55C21.2218 19.2092 20.8776 19.7658 20.3226 20.3217C19.7668 20.8767 19.2076 21.22 18.551 21.475C17.9135 21.7217 17.1876 21.89 16.1235 21.94C15.0568 21.9875 14.7176 22 12.001 22C9.28431 22 8.94514 21.99 7.87848 21.94C6.81431 21.8908 6.08931 21.7217 5.45098 21.475C4.79264 21.2208 4.23514 20.8767 3.67931 20.3217C3.12348 19.7658 2.78098 19.2067 2.52598 18.55C2.27848 17.9125 2.11098 17.1867 2.06098 16.1225C2.01348 15.0558 2.00098 14.7167 2.00098 12C2.00098 9.28333 2.01098 8.94417 2.06098 7.8775C2.11014 6.8125 2.27848 6.0875 2.52598 5.45C2.78014 4.79167 3.12348 4.23417 3.67931 3.67833C4.23514 3.1225 4.79348 2.78 5.45098 2.525C6.08848 2.2775 6.81348 2.11 7.87848 2.06C8.94514 2.0125 9.28431 2 12.001 2Z"></path></svg> \ No newline at end of file
diff --git a/public/assets/instagram.svg b/public/assets/instagram.svg
new file mode 100644
index 0000000..feb8ae8
--- /dev/null
+++ b/public/assets/instagram.svg
@@ -0,0 +1,37 @@
1<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2<svg
3 viewBox="0 0 24 24"
4 fill="currentColor"
5 version="1.1"
6 id="svg1"
7 sodipodi:docname="instagram.svg"
8 inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
9 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
10 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
11 xmlns="http://www.w3.org/2000/svg"
12 xmlns:svg="http://www.w3.org/2000/svg">
13 <defs
14 id="defs1" />
15 <sodipodi:namedview
16 id="namedview1"
17 pagecolor="#ffffff"
18 bordercolor="#000000"
19 borderopacity="0.25"
20 inkscape:showpageshadow="2"
21 inkscape:pageopacity="0.0"
22 inkscape:pagecheckerboard="0"
23 inkscape:deskcolor="#d1d1d1"
24 inkscape:zoom="20.166667"
25 inkscape:cx="12"
26 inkscape:cy="12.024793"
27 inkscape:window-width="1391"
28 inkscape:window-height="1032"
29 inkscape:window-x="525"
30 inkscape:window-y="0"
31 inkscape:window-maximized="1"
32 inkscape:current-layer="svg1" />
33 <path
34 d="M12.001 9C10.3436 9 9.00098 10.3431 9.00098 12C9.00098 13.6573 10.3441 15 12.001 15C13.6583 15 15.001 13.6569 15.001 12C15.001 10.3427 13.6579 9 12.001 9ZM12.001 7C14.7614 7 17.001 9.2371 17.001 12C17.001 14.7605 14.7639 17 12.001 17C9.24051 17 7.00098 14.7629 7.00098 12C7.00098 9.23953 9.23808 7 12.001 7ZM18.501 6.74915C18.501 7.43926 17.9402 7.99917 17.251 7.99917C16.5609 7.99917 16.001 7.4384 16.001 6.74915C16.001 6.0599 16.5617 5.5 17.251 5.5C17.9393 5.49913 18.501 6.0599 18.501 6.74915ZM12.001 4C9.5265 4 9.12318 4.00655 7.97227 4.0578C7.18815 4.09461 6.66253 4.20007 6.17416 4.38967C5.74016 4.55799 5.42709 4.75898 5.09352 5.09255C4.75867 5.4274 4.55804 5.73963 4.3904 6.17383C4.20036 6.66332 4.09493 7.18811 4.05878 7.97115C4.00703 9.0752 4.00098 9.46105 4.00098 12C4.00098 14.4745 4.00753 14.8778 4.05877 16.0286C4.0956 16.8124 4.2012 17.3388 4.39034 17.826C4.5591 18.2606 4.7605 18.5744 5.09246 18.9064C5.42863 19.2421 5.74179 19.4434 6.17187 19.6094C6.66619 19.8005 7.19148 19.9061 7.97212 19.9422C9.07618 19.9939 9.46203 20 12.001 20C14.4755 20 14.8788 19.9934 16.0296 19.9422C16.8117 19.9055 17.3385 19.7996 17.827 19.6106C18.2604 19.4423 18.5752 19.2402 18.9074 18.9085C19.2436 18.5718 19.4445 18.2594 19.6107 17.8283C19.8013 17.3358 19.9071 16.8098 19.9432 16.0289C19.9949 14.9248 20.001 14.5389 20.001 12C20.001 9.52552 19.9944 9.12221 19.9432 7.97137C19.9064 7.18906 19.8005 6.66149 19.6113 6.17318C19.4434 5.74038 19.2417 5.42635 18.9084 5.09255C18.573 4.75715 18.2616 4.55693 17.8271 4.38942C17.338 4.19954 16.8124 4.09396 16.0298 4.05781C14.9258 4.00605 14.5399 4 12.001 4ZM12.001 2C14.7176 2 15.0568 2.01 16.1235 2.06C17.1876 2.10917 17.9135 2.2775 18.551 2.525C19.2101 2.77917 19.7668 3.1225 20.3226 3.67833C20.8776 4.23417 21.221 4.7925 21.476 5.45C21.7226 6.08667 21.891 6.81333 21.941 7.8775C21.9885 8.94417 22.001 9.28333 22.001 12C22.001 14.7167 21.991 15.0558 21.941 16.1225C21.8918 17.1867 21.7226 17.9125 21.476 18.55C21.2218 19.2092 20.8776 19.7658 20.3226 20.3217C19.7668 20.8767 19.2076 21.22 18.551 21.475C17.9135 21.7217 17.1876 21.89 16.1235 21.94C15.0568 21.9875 14.7176 22 12.001 22C9.28431 22 8.94514 21.99 7.87848 21.94C6.81431 21.8908 6.08931 21.7217 5.45098 21.475C4.79264 21.2208 4.23514 20.8767 3.67931 20.3217C3.12348 19.7658 2.78098 19.2067 2.52598 18.55C2.27848 17.9125 2.11098 17.1867 2.06098 16.1225C2.01348 15.0558 2.00098 14.7167 2.00098 12C2.00098 9.28333 2.01098 8.94417 2.06098 7.8775C2.11014 6.8125 2.27848 6.0875 2.52598 5.45C2.78014 4.79167 3.12348 4.23417 3.67931 3.67833C4.23514 3.1225 4.79348 2.78 5.45098 2.525C6.08848 2.2775 6.81348 2.11 7.87848 2.06C8.94514 2.0125 9.28431 2 12.001 2Z"
35 id="path1"
36 style="fill:#dd3333;fill-opacity:1" />
37</svg>
diff --git a/public/assets/logo-120x75.jpg b/public/assets/logo-120x75.jpg
new file mode 100644
index 0000000..b58a7a6
--- /dev/null
+++ b/public/assets/logo-120x75.jpg
Binary files differ
diff --git a/public/assets/logo-150x94.jpg b/public/assets/logo-150x94.jpg
new file mode 100644
index 0000000..67ec6cc
--- /dev/null
+++ b/public/assets/logo-150x94.jpg
Binary files differ
diff --git a/public/assets/logo-fond-bleu.png b/public/assets/logo-fond-bleu.png
new file mode 100644
index 0000000..f51ac9c
--- /dev/null
+++ b/public/assets/logo-fond-bleu.png
Binary files differ
diff --git a/public/assets/logo2.jpg b/public/assets/logo2.jpg
new file mode 100644
index 0000000..39c03bd
--- /dev/null
+++ b/public/assets/logo2.jpg
Binary files differ
diff --git a/public/assets/perdu.jpg b/public/assets/perdu.jpg
new file mode 100644
index 0000000..043a97a
--- /dev/null
+++ b/public/assets/perdu.jpg
Binary files differ
diff --git a/public/assets/share-nb.svg b/public/assets/share-nb.svg
new file mode 100644
index 0000000..f58f129
--- /dev/null
+++ b/public/assets/share-nb.svg
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M13 14H11C7.54202 14 4.53953 15.9502 3.03239 18.8107C3.01093 18.5433 3 18.2729 3 18C3 12.4772 7.47715 8 13 8V2.5L23.5 11L13 19.5V14ZM11 12H15V15.3078L20.3214 11L15 6.69224V10H13C10.5795 10 8.41011 11.0749 6.94312 12.7735C8.20873 12.2714 9.58041 12 11 12Z"></path></svg> \ No newline at end of file
diff --git a/public/assets/share.svg b/public/assets/share.svg
new file mode 100644
index 0000000..e053051
--- /dev/null
+++ b/public/assets/share.svg
@@ -0,0 +1,40 @@
1<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2<svg
3 viewBox="0 0 24 24"
4 fill="currentColor"
5 version="1.1"
6 id="svg1"
7 sodipodi:docname="share-forward.svg"
8 inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
9 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
10 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
11 xmlns="http://www.w3.org/2000/svg"
12 xmlns:svg="http://www.w3.org/2000/svg">
13 <defs
14 id="defs1" />
15 <sodipodi:namedview
16 id="namedview1"
17 pagecolor="#ffffff"
18 bordercolor="#000000"
19 borderopacity="0.25"
20 inkscape:showpageshadow="2"
21 inkscape:pageopacity="0.0"
22 inkscape:pagecheckerboard="0"
23 inkscape:deskcolor="#d1d1d1"
24 inkscape:zoom="5.3333333"
25 inkscape:cx="18.46875"
26 inkscape:cy="16.03125"
27 inkscape:window-width="956"
28 inkscape:window-height="1032"
29 inkscape:window-x="960"
30 inkscape:window-y="0"
31 inkscape:window-maximized="1"
32 inkscape:current-layer="svg1" />
33 <path
34 d="M13 14H11C7.54202 14 4.53953 15.9502 3.03239 18.8107C3.01093 18.5433 3 18.2729 3 18C3 12.4772 7.47715 8 13 8V2.5L23.5 11L13 19.5V14ZM11 12H15V15.3078L20.3214 11L15 6.69224V10H13C10.5795 10 8.41011 11.0749 6.94312 12.7735C8.20873 12.2714 9.58041 12 11 12Z"
35 id="path1" />
36 <path
37 style="fill:#dd3333;stroke:#dd3333;stroke-width:0.708661"
38 d="m 13.125,16.62281 v -2.65406 l -1.734375,0.0046 c -3.1289994,0.0083 -4.7715187,0.662267 -6.84375,2.72496 L 3.1875,18.051411 3.1920836,17.275706 C 3.2079642,14.588168 5.033385,11.457683 7.5641723,9.7778419 8.8054939,8.9538996 11.118995,8.15625 12.267434,8.15625 H 13.125 V 5.5156251 c 0,-1.4523438 0.07229,-2.6125001 0.160633,-2.5781251 0.344223,0.1339326 9.936632,7.944961 9.904618,8.065257 -0.0187,0.07027 -2.143376,1.831888 -4.721501,3.914713 -2.578125,2.082826 -4.835156,3.915756 -5.015625,4.073178 C 13.142599,19.261519 13.125,19.134524 13.125,16.62281 Z m 7.253039,-5.611275 c 0.01886,-0.07509 -1.162391,-1.0943592 -2.625,-2.2650333 L 15.09375,6.6180036 15.039575,8.2308767 14.9854,9.84375 13.914575,9.844836 c -1.658785,0.00168 -3.170519,0.311395 -4.4246483,0.906488 -1.150183,0.545771 -2.7354857,1.775452 -2.5436777,1.97307 0.056375,0.05808 0.520437,-0.02888 1.03125,-0.193243 0.6245522,-0.200964 1.9267104,-0.328748 3.975626,-0.390138 L 15,12.049721 v 1.702541 1.70254 l 2.671875,-2.153366 c 1.469531,-1.184352 2.687305,-2.214808 2.706164,-2.289901 z"
39 id="path2" />
40</svg>
diff --git a/public/css/body.css b/public/css/body.css
new file mode 100644
index 0000000..87f71f2
--- /dev/null
+++ b/public/css/body.css
@@ -0,0 +1,39 @@
1body
2{
3 margin: auto;
4 max-width: 1200px;
5 /*width: 90%;*/
6 font-family: sans;
7 background-color: #0cceec;
8}
9
10#bloc_page
11{}
12
13
14.infobulle
15{
16 /*text-decoration: none;*/
17 /* évite que l'infobulle ne se retrouve dans un coin de l'écran */
18 /*position:relative;*/
19 /* block -> hauteur fixe pour vertical-align dans l'image */
20 /*display: inline-block;*/
21
22}
23/*.infobulle button
24{
25
26 display: none;
27 position: absolute;
28 bottom: 35px;
29 left: 10px;
30}
31.infobulle:hover button
32{
33 display: inline;
34
35}
36.infobulle img
37{
38 vertical-align: middle;
39}*/ \ No newline at end of file
diff --git a/public/css/foot.css b/public/css/foot.css
new file mode 100644
index 0000000..ef3291d
--- /dev/null
+++ b/public/css/foot.css
@@ -0,0 +1,120 @@
1/*-- pied de page --*/
2footer
3{
4 /*background-color: #279dc177;*/
5 /*background-color: #13aff077;*/
6 background-color: #B7E9FE;
7 /*width: 1200px;*/
8 margin: auto;
9
10}
11footer > div
12{
13 margin: auto;
14 max-width: 1200px;
15 display: flex;
16 justify-content: space-around;
17}
18.contact a
19{
20 color: unset;
21 border-bottom: 3px #13aff000 solid; /* bordure invisible */
22}
23.contact a:hover
24{
25 /*background-color: #fadb11;*/
26 text-decoration: none;
27 padding: 0 2px;
28 /*background-color: #13aff0ff;
29 border-bottom: 3px #fadb11 solid;*/
30 background-color: #ffff00;
31 /*border-bottom: 2px #00a8f3 solid;*/
32 border-bottom: 2px #ff1d04 solid;
33}
34
35
36/*-- fil d'ariane --*/
37.breadcrumb
38{
39 max-width: 45%;
40 /*margin-right: 10px;*/
41 margin: 16px 0; /* = p */
42 list-style: none; /* retirer les puces */
43 font-size: small;
44 text-align: right;
45 font-weight: bold;
46}
47.breadcrumb img
48{
49 width: 12px;
50 vertical-align: middle;
51}
52.breadcrumb a img, .breadcrumb a span
53{
54 border-bottom: 2px #13aff000 solid;
55}
56.breadcrumb a img:hover, .breadcrumb a span:hover
57{
58 /*background-color: #13aff0;
59 border-bottom: 3px #fadb11 solid;*/
60 background-color: #ffff00;
61 /*border-bottom: 2px #00a8f3 solid;*/
62 border-bottom: 2px #ff1d04 solid;
63}
64/*.breadcrumb *:not(:last-child)::after
65{
66 content: "→";
67 display: block;
68 margin: 0 3px;
69}*/
70.breadcrumb a
71{
72 color: unset; /* ne plus hériter */
73 text-decoration: none;
74}
75
76/* zone admin */
77.logged_out
78{
79 /*display: none;*/
80 justify-content: start;
81}
82.logged_out a
83{
84 color: black;
85 text-decoration: none;
86}
87.empty_admin_zone
88{
89 height: 32px;
90}
91.logged_in
92{
93 height: 32px;
94 width: 1200px;
95 margin: auto;
96 background-color: #ffff00;
97 position: fixed;
98 bottom: 0px;
99}
100.logged_in > p
101{
102 margin: 5px 0;
103 display: flex;
104 justify-content: space-around;
105}
106.logged_in button
107{
108 margin-left: 5px;
109 color: #ff1d04;
110 font-size: medium;
111 border: lightgrey 2px outset;
112 border-radius: 4px;
113 background-color: white;
114}
115.logged_in button:hover
116{
117 cursor: pointer; /* curseur qui touche du doigt */
118 background-color: #ffff00;
119 border-radius: 4px;
120} \ No newline at end of file
diff --git a/public/css/head.css b/public/css/head.css
new file mode 100644
index 0000000..b8d4c93
--- /dev/null
+++ b/public/css/head.css
@@ -0,0 +1,59 @@
1header
2{
3 text-align: center;
4 /*padding-top: 5px;*/
5 /*padding-bottom: 10px;*/
6 background-image: url('../assets/fond-piscine.jpg');
7 background-size: cover; /* largeur du bloc_page */
8}
9header > div
10{
11 /*margin: auto;*/
12 /*max-width: 1200px;*/
13 display: flex;
14 /*align-items: center;*/ /* conflit possible avec .social align-self */
15 justify-content: space-around;
16}
17.header-content
18{
19 padding: 20px 0;
20}
21.site_title
22{
23 background-color: #ffffff7f;
24 border-radius: 10px;
25}
26header h1
27{
28 font-size: x-large;
29 margin: 10px;
30}
31header h2
32{
33 font-size: medium; /* défaut = x-large */
34 margin: 10px;
35}
36header img
37{
38 vertical-align: bottom; /* supprime espace sous l'image */
39}
40header a
41{
42 color: unset; /* ne plus hériter */
43 text-decoration: none;
44}
45.social
46{
47 align-self: end;
48}
49.social img
50{
51 width: 25px;
52 background-color: #ffffffb3;
53 border-radius: 10px;
54}
55
56.social img:hover
57{
58 background-color: #ffff00;
59}
diff --git a/public/css/main.css b/public/css/main.css
new file mode 100644
index 0000000..b38f8d9
--- /dev/null
+++ b/public/css/main.css
@@ -0,0 +1,110 @@
1main
2{
3 margin: auto;
4 /*max-width: 1200px;*/
5 background-color: #E3F3FF;
6 padding: 15px 0;
7}
8section > h3
9{
10 padding: 15px;
11 margin: 0;
12 text-align: center;
13}
14section > p /* boutons spéciaux mode admin */
15{
16 /*margin-left: 15px;*/
17}
18.grid_columns
19{
20 display: grid;
21 grid-template-columns: repeat(3, 1fr);
22}
23.galery_photos
24{
25 display: flex;
26 flex-wrap: wrap;
27 justify-content: space-evenly;
28}
29.galery_photos .html_from_editor img
30{
31 max-width: 400px;
32 max-height: 250px;
33}
34article
35{
36 /*display: flex;*/
37 background-color: white;
38 margin: 15px 15px 0 15px;
39 padding: 0 15px;
40 /*min-height: 150px;*/
41}
42article .logo2
43{
44 vertical-align: middle;
45 max-width: 200px;
46 max-height: 200px;
47 margin-right: 15px;
48}
49.new_content, .article_content
50{
51 width: 100%;
52}
53.new_content_text
54{
55 max-height: 250px;
56 overflow: hidden;
57}
58
59article .action_icon
60{
61 width: 24px;
62 vertical-align: middle;
63 border: white 2px solid; /* invisible */
64}
65article .action_icon:hover
66{
67 background-color: #ffff00;
68 border-radius: 4px;
69 border: lightgrey 2px outset;
70}
71.article_title_zone
72{
73 display: flex;
74 justify-content: space-between;
75}
76.under_an_article
77{
78 display: flex;
79 justify-content: space-between;
80 font-size: small;
81}
82.under_an_article p
83{
84 margin: 5px;
85}
86.under_an_article img
87{
88 width: 24px;
89 vertical-align: middle;
90 margin-right: 5px;
91}
92.article_admin_zone
93{
94 display: flex;
95 justify-content: end;
96}
97main button
98{
99 color: #ff1d04;
100 font-size: medium;
101 border-radius: 4px;
102 background-color: white;
103 border: lightgrey 2px outset; /* rend identiques les boutons firefox et chromium */
104}
105main button:hover
106{
107 cursor: pointer; /* curseur qui pointe du doigt */
108 background-color: #ffff00;
109 border-radius: 4px;
110} \ No newline at end of file
diff --git a/public/css/nav.css b/public/css/nav.css
new file mode 100644
index 0000000..d51c58a
--- /dev/null
+++ b/public/css/nav.css
@@ -0,0 +1,95 @@
1/*-- menu principal --*/
2.empty_nav_zone
3{
4 height: 33px;
5}
6.nav_main
7{
8 position: fixed;
9 /*box-shadow: 3px 3px 5px #13aff0;*/
10 border: 2px solid #13aff0;
11 top: -2px;
12}
13.nav_main
14{
15 /*font-size: 90%;*/
16 /*margin-top: 10px;*/
17 /*margin-bottom: 10px;*/
18 text-wrap: nowrap;
19}
20.nav_main > ul
21{
22 display: flex;
23 flex-direction: row;
24 justify-content: center;
25 flex-wrap: wrap;
26}
27ul
28{
29 margin: 0;
30}
31.nav_main ul, .nav_main li
32{
33 list-style: none;
34 padding-left: 0;
35}
36.nav_main p
37{
38 padding: 7px;
39 margin: 0;
40 background-color: #ffffffe1; /* b3 = 179 = 0.7 (x 256) */
41}
42.nav_main p:hover
43{
44 background-color: white;
45}
46.current > p
47{
48 background-color: white;
49 font-weight: bold;
50}
51.drop-down > p::after
52{
53 content: ' ▼';
54 font-size: x-small;
55}
56
57.drop-down:hover > .sub-menu /* faire apparaître sub-menu */
58{
59 display: block;
60}
61.sub-menu
62{
63 display: none;
64 background-color: white;
65 border-top: 3px solid #13aff0;
66 box-shadow: 1px 1px 3px black;
67 font-size: 95%;
68}
69/*.sub-menu p:hover*/
70.nav_main p:hover
71{
72 background-color: #ffff00;
73}
74.drop-down .sub-menu .drop-down > p:after /* bricolage? */
75{
76 content: " ▶";
77 font-size: x-small;
78}
79
80/* 1er sous-menu, poitionnement pour ne pas aggrandir l'élément parent */
81nav > ul > li > ul
82{
83 position: absolute; /* retire du flux, positionnement par rapport à la fenêtre */
84}
85/* élément du menu survolé, le positionnement relatif en fait la référence du positionnement suivant */
86.drop-down .sub-menu .drop-down
87{
88 position: relative;
89}
90.drop-down .sub-menu .drop-down .sub-menu
91{
92 position: absolute; /* positionnement par rapport au précédent "position" en CSS */
93 left: 100%;
94 top: -3px; /* la bordure bleue fait 3px */
95}
diff --git a/public/css/tinymce.css b/public/css/tinymce.css
new file mode 100644
index 0000000..55639e0
--- /dev/null
+++ b/public/css/tinymce.css
@@ -0,0 +1,6 @@
1.tox-promotion{
2 display: none;
3}
4.hidden{
5 display: none;
6} \ No newline at end of file
diff --git a/public/erreur404.html b/public/erreur404.html
new file mode 100644
index 0000000..3dcd6ed
--- /dev/null
+++ b/public/erreur404.html
@@ -0,0 +1,13 @@
1<!DOCTYPE html>
2<html lang="fr">
3 <head>
4 <meta charset="utf-8">
5 <title>erreur 404</title>
6 </head>
7 <body>
8 <h1>404 - page non trouvée</h1>
9 <p>Le lien que vous avez utilisé ne mène nulle part.<br>
10 <a href="/">Revenez à la page d'accueil</a> et utilisez le menu, vous devriez trouvez votre route.</p>
11 <img style="max-width: 800px;" src="assets/perdu.jpg">
12 </body>
13</html> \ No newline at end of file
diff --git a/public/images-mini/DPpiscines.png b/public/images-mini/DPpiscines.png
new file mode 100644
index 0000000..3cb40ba
--- /dev/null
+++ b/public/images-mini/DPpiscines.png
Binary files differ
diff --git a/public/images-mini/kerne-elagage.png b/public/images-mini/kerne-elagage.png
new file mode 100644
index 0000000..5416517
--- /dev/null
+++ b/public/images-mini/kerne-elagage.png
Binary files differ
diff --git a/public/images-mini/kerne.png b/public/images-mini/kerne.png
new file mode 100644
index 0000000..eea9107
--- /dev/null
+++ b/public/images-mini/kerne.png
Binary files differ
diff --git a/public/images-mini/mako.png b/public/images-mini/mako.png
new file mode 100644
index 0000000..5ba6263
--- /dev/null
+++ b/public/images-mini/mako.png
Binary files differ
diff --git a/public/images/DPpiscines.png b/public/images/DPpiscines.png
new file mode 100644
index 0000000..3cb40ba
--- /dev/null
+++ b/public/images/DPpiscines.png
Binary files differ
diff --git a/public/images/kerne-elagage.png b/public/images/kerne-elagage.png
new file mode 100644
index 0000000..5416517
--- /dev/null
+++ b/public/images/kerne-elagage.png
Binary files differ
diff --git a/public/images/kerne.png b/public/images/kerne.png
new file mode 100644
index 0000000..eea9107
--- /dev/null
+++ b/public/images/kerne.png
Binary files differ
diff --git a/public/images/mako.png b/public/images/mako.png
new file mode 100644
index 0000000..5ba6263
--- /dev/null
+++ b/public/images/mako.png
Binary files differ
diff --git a/public/index.php b/public/index.php
new file mode 100644
index 0000000..37ebaf0
--- /dev/null
+++ b/public/index.php
@@ -0,0 +1,78 @@
1<?php
2// public/index/php
3
4/* installation de composer sur un hébergement mutualisé
51. télécharger le script d'installation:
6php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
72. installation: php composer-setup.php
83. supprimer l'installateur: php -r "unlink('composer-setup.php');"
94. utilisation: php composer.phar */
10
11declare(strict_types=1);
12
13// -- prétraitement --
14// une nouvelle classe? taper: composer dump-autoload -o
15require "../vendor/autoload.php";
16
17// configuration possible par l'utilisateur
18Config::load('../config/config.ini');
19
20// les messages d'erreur de déploiement qu'on aime
21require('../src/controller/installation.php');
22//installation(); // des mkdir et chmod, chiant en dev
23
24// $entityManager
25require '../src/model/doctrine-bootstrap.php'; // isDevMode est sur "true", DSN à adapter
26
27URL::setProtocol(Config::$protocol); // utile si port autre que 80 ou 443
28URL::setPort(Config::$port);
29URL::setHost(Config::$host);
30
31//require('controller/Session.php');
32ini_set('session.cookie_samesite', 'Strict');
33ini_set('session.cookie_httponly', 'On');
34ini_set('session.use_strict_mode', 'On');
35ini_set('session.cookie_secure', 'On');
36session_start();
37$_SESSION['admin'] = !isset($_SESSION['admin']) ? false : $_SESSION['admin']; // intialisation sur faux
38
39// login, mot de passe et captcha
40require '../src/controller/password.php';
41existUsers($entityManager);
42
43// -- navigation avec les GET --
44$current_page = 'accueil';
45if(!empty($_GET['page']))
46{
47 $current_page = htmlspecialchars($_GET['page']);
48}
49define('CURRENT_PAGE', $current_page);
50
51// -- traitement des POST (formulaires et AJAX) --
52require '../src/controller/post.php';
53
54// id des articles
55$id = '';
56if(!empty($_GET['id']))
57{
58 //$id = (int)$_GET['id']; // (int) = moyen basique d'éviter les injections
59 $id = Security::secureString($_GET['id']);
60}
61
62if(isset($_GET['action']) && $_GET['action'] === 'deconnexion')
63{
64 disconnect($entityManager);
65}
66elseif(isset($_GET['action']) && $_GET['action'] === 'modif_mdp')
67{
68 changePassword($entityManager);
69}
70
71// -- contrôleurs --
72$director = new Director($entityManager);
73$director->makeRootNode($id);
74$node = $director->getRootNode();
75
76// -- vues --
77$view_builder = new ViewBuilder($node);
78echo $view_builder->render(); // et voilà!
diff --git a/public/js/galery.js b/public/js/galery.js
new file mode 100644
index 0000000..0be63a4
--- /dev/null
+++ b/public/js/galery.js
@@ -0,0 +1,4 @@
1function enableGaleryScroller(){
2 // lancer photoswipe
3 // https://photoswipe.com/
4} \ No newline at end of file
diff --git a/public/js/main.js b/public/js/main.js
new file mode 100644
index 0000000..c86ec51
--- /dev/null
+++ b/public/js/main.js
@@ -0,0 +1,30 @@
1function newPassword(page, id = ''){
2 if(id != ''){
3 id = '&id=' + id;
4 }
5 alert('Le mot de passe a été modifié.');
6 window.setTimeout(function(){
7 location.href = "index.php?page=" + page + "&message=nouveau_mdp" + id;
8 }, 0);
9}
10
11function copyInClipBoard(link){
12 // une balise <input> avec des attributs
13 var element = document.createElement("input");
14 element.setAttribute("id", "copyMe");
15 element.setAttribute("value", link);
16
17 // placement dans la page (= le "document")
18 document.body.appendChild(element);
19 var cible = document.getElementById('copyMe');
20
21 // selection comme on le ferait à la souris
22 cible.select();
23 // copie (= Ctrl + C)
24 document.execCommand("copy");
25
26 // nettoyage
27 element.parentNode.removeChild(element);
28
29 alert('Cette adresse a été copiée dans le presse-papier:\n\n' + link);
30} \ No newline at end of file
diff --git a/public/js/tinymce.js b/public/js/tinymce.js
new file mode 100644
index 0000000..8aa9368
--- /dev/null
+++ b/public/js/tinymce.js
@@ -0,0 +1,164 @@
1let editors = {};
2
3function openEditor(articleId) {
4 // Récupérer et sauvegarder le contenu d'origine de l'article
5 const articleContent = document.getElementById(articleId).innerHTML;
6 document.getElementById(articleId).setAttribute('data-original-content', articleContent);
7
8 tinymce.init({
9 selector: `#${articleId}`,
10 language: 'fr_FR', // télécharger des paquets de langue ici: https://www.tiny.cloud/get-tiny/language-packages/
11 language_url: 'js/tinymce-langs/fr_FR.js', // ou installer tweeb/tinymce-i18n avec composer
12 license_key: 'gpl',
13 branding: false,
14 plugins: 'lists link autolink table image media autoresize help',
15 toolbar: 'undo redo newdocument print selectall styles bold italic underline strikethrough fontsizeinput forecolor backcolor fontfamily align numlist bullist outdent indent table link image media help',
16 menubar: false,
17 toolbar_mode: 'wrap',
18 statusbar: false,
19 setup: function (editor) {
20 editor.on('init', function () {
21 editors[articleId] = editor;
22
23 // Masquer le bouton "Modifier" et afficher les boutons "Annuler" et "Soumettre"
24 if(articleId != 'new')
25 {
26 document.querySelector(`#edit-${articleId}`).classList.add('hidden');
27 document.querySelector(`#delete-${articleId}`).classList.add('hidden');
28 document.querySelector(`#position_up-${articleId}`).classList.add('hidden');
29 document.querySelector(`#position_down-${articleId}`).classList.add('hidden');
30 }
31 else{
32 document.querySelector(`#new-${articleId}`).classList.add('hidden');
33 }
34 document.querySelector(`#cancel-${articleId}`).classList.remove('hidden');
35 document.querySelector(`#submit-${articleId}`).classList.remove('hidden');
36
37 });
38 },
39 // upload d'image
40 images_upload_handler: (blobInfo, progress) => new Promise((resolve, reject) => {
41 const formData = new FormData();
42 formData.append("file", blobInfo.blob());
43
44 fetch("index.php?action=upload_image", {
45 method: "POST",
46 body: formData
47 })
48 .then(response => response.json())
49 .then(data => {
50 if (data.location) {
51 resolve(data.location);
52 }
53 else {
54 reject("Erreur: Chemin d'image invalide");
55 }
56 })
57 .catch(error => {
58 reject("Erreur lors de l'upload");
59 });
60 }),
61 image_caption: true
62 });
63
64 // Remplacer le contenu de l'article par l'éditeur
65 document.getElementById(articleId).innerHTML = articleContent;
66}
67
68function deleteArticle(articleId, page = '') {
69 if (confirm('Voulez-vous vraiment supprimer cet article ?'))
70 {
71 // Envoyer une requête au serveur pour supprimer l'article
72 fetch('index.php?action=delete_article', {
73 method: 'POST',
74 headers: {
75 'Content-Type': 'application/json'
76 },
77 body: JSON.stringify({ id: articleId })
78 })
79 .then(response => response.json())
80 .then(data => {
81 if (data.success)
82 {
83 if(page == 'article'){
84 // redirection vers la page d'accueil
85 window.setTimeout(function(){
86 location.href = "index.php?page=accueil";
87 }, 0);
88 }
89 else{
90 // Supprimer l'article du DOM
91 const articleElement = document.getElementById(articleId);
92 articleElement.parentElement.parentElement.remove(); // <article> est deux niveau au dessus
93 }
94 }
95 else {
96 alert('Erreur lors de la suppression de l\'article.');
97 }
98 })
99 .catch(error => {
100 console.error('Erreur:', error);
101 });
102 }
103}
104
105function closeEditor(articleId, display_old = true)
106{
107 // Fermer l'éditeur
108 tinymce.remove(`#${articleId}`);
109 delete editors[articleId];
110
111 // Restaurer le contenu d'origine de l'article
112 if(display_old){
113 const originalContent = document.getElementById(articleId).getAttribute('data-original-content');
114 document.getElementById(articleId).innerHTML = originalContent;
115 }
116
117 // Afficher le bouton "Modifier" et masquer les boutons "Annuler" et "Soumettre"
118 if(articleId != 'new'){
119 document.querySelector(`#edit-${articleId}`).classList.remove('hidden');
120 document.querySelector(`#delete-${articleId}`).classList.remove('hidden');
121 document.querySelector(`#position_up-${articleId}`).classList.remove('hidden');
122 document.querySelector(`#position_down-${articleId}`).classList.remove('hidden');
123 }
124 else{
125 document.querySelector(`#new-${articleId}`).classList.remove('hidden');
126 }
127 document.querySelector(`#cancel-${articleId}`).classList.add('hidden');
128 document.querySelector(`#submit-${articleId}`).classList.add('hidden');
129}
130
131function submitArticle(articleId) {
132 // Récupérer l'éditeur correspondant à l'article
133 const editor = editors[articleId];
134 if (!editor) {
135 console.error('Éditeur non trouvé pour l\'article:', articleId);
136 return;
137 }
138
139 // Récupérer le contenu de l'éditeur
140 const newContent = editor.getContent();
141
142 // Envoi AJAX au serveur
143 fetch('index.php?action=editor_submit', {
144 method: 'POST',
145 headers: {
146 'Content-Type': 'application/json'
147 },
148 body: JSON.stringify({id: articleId, content: newContent})
149 })
150 .then(response => response.json())
151 .then(data => {
152 if (data.success) {
153 // Fermer l'éditeur et mettre à jour le contenu de l'article
154 closeEditor(articleId, false);
155 document.getElementById(articleId).innerHTML = newContent;
156 }
157 else {
158 alert('Erreur lors de la sauvegarde de l\'article.');
159 }
160 })
161 .catch(error => {
162 console.error('Erreur:', error);
163 });
164} \ No newline at end of file
diff --git a/src/Config.php b/src/Config.php
new file mode 100644
index 0000000..cfec876
--- /dev/null
+++ b/src/Config.php
@@ -0,0 +1,76 @@
1<?php
2// src/controller/Config.php
3
4declare(strict_types=1);
5
6class Config
7{
8 // BDD
9 static public string $db_host = 'localhost';
10 static public string $database = 'nageurs';
11 static public string $db_driver = 'pdo_mysql';
12 static public string $user = 'root';
13 static public string $password = '';
14 static public string $table_prefix = '';
15
16 // classe URL
17 static public string $protocol = 'http';
18 static public string $host = 'nageurs.localhost';
19 static public string $port = '80';
20
21 // copier dans ce tableau les variables contenant des chemins
22 static private array $path_vars = [];
23
24 static public function load(string $file_path): void
25 {
26 if(file_exists($file_path))
27 {
28 // ce serait bien de gérer aussi les fichiers corrompus?
29 $raw_data = parse_ini_file($file_path);
30 self::hydrate($raw_data);
31 }
32 else
33 {
34 echo "le fichier config.ini n'existe pas ou n'est pas lisible";
35 }
36 define('TABLE_PREFIX', self::$table_prefix);
37 }
38
39 // renseigner les variables internes de Config
40 static private function hydrate(array $raw_data): void
41 {
42 foreach($raw_data as $field => $value)
43 {
44 if($value != '') // valeur par défaut
45 {
46 if(isset(self::$$field)) // le champ existe dans Config
47 {
48 // problème du slash à la fin du nom d'un dossier
49 $value = self::slashAtEndOfPath($field, $value);
50 self::$$field = $value;
51 }
52 else
53 {
54 echo "debug: le fichier config.ini comporte une erreur, le champ: " . $field . " est incorrect,\nl'information contenue sur cette ligne ne sera pas utilisée\n";
55 }
56 }
57 /*else
58 {
59 echo "debug: le champ " . $field . " est vide, la valeur par défaut " . self::$$field . " sera utilisée.\n";
60 }*/
61 }
62 }
63
64
65 // pour que les chemins finissent toujours par un /
66 static private function slashAtEndOfPath(string $field, string $value): string
67 {
68 foreach(self::$path_vars as $item)
69 {
70 if($field === $item){
71 return !str_ends_with($value, '/') ? $value . '/' : $value;
72 }
73 }
74 return $value;
75 }
76}
diff --git a/src/controller/Director.php b/src/controller/Director.php
new file mode 100644
index 0000000..896cde1
--- /dev/null
+++ b/src/controller/Director.php
@@ -0,0 +1,101 @@
1<?php
2// src/controller/Director.php
3
4declare(strict_types=1);
5
6use Doctrine\ORM\EntityManager;
7use App\Entity\Page;
8use App\Entity\Node;
9
10class Director
11{
12 private EntityManager $entityManager;
13 static public Menu $menu_data; // pour NavBuilder
14 static public Path $page_path; // pour BreadcrumbBuilder
15 private Page $page;
16 private Node $root_node;
17
18 public function __construct(EntityManager $entityManager)
19 {
20 $this->entityManager = $entityManager;
21 self::$menu_data = new Menu($entityManager); // Menu est un modèle mais pas une entité
22 self::$page_path = new Path();
23 $this->page = self::$page_path->getLast();
24 $this->root_node = new Node; // instance mère "vide" ne possédant rien d'autre que des enfants
25 }
26
27 public function makeRootNode(string $id = ''): void
28 {
29 // on récupère toutes les entrées
30 $dql = 'SELECT n FROM App\Entity\Node n WHERE n.page = :page OR n.page IS null';
31 if($id == '')
32 {
33 $bulk_data = $this->entityManager
34 ->createQuery($dql)
35 ->setParameter('page', $this->page)
36 ->getResult();
37 }
38 else // avec $_GET['id'] dans l'URL
39 {
40 $dql .= ' OR n.article_timestamp = :id';
41 $bulk_data = $this->entityManager
42 ->createQuery($dql)
43 ->setParameter('page', $this->page)
44 ->setParameter('id', $id)
45 ->getResult();
46 }
47 $this->feedObjects($bulk_data);
48 }
49
50 public function makeArticleNode(string $id = ''): bool
51 {
52 $bulk_data = $this->entityManager
53 ->createQuery('SELECT n FROM App\Entity\Node n WHERE n.article_timestamp = :id')
54 ->setParameter('id', $id)
55 ->getResult();
56
57 if(count($bulk_data) === 0){
58 return false;
59 }
60
61 $this->root_node = $bulk_data[0];
62 return true;
63 }
64
65 public function feedObjects(array $bulk_data): void // $bulk_data = tableau de Node
66 {
67 // puis on les range
68 // (attention, risque de disfonctionnement si les noeuds de 1er niveau ne sont pas récupérés en 1er dans la BDD)
69 foreach($bulk_data as $node)
70 {
71 // premier niveau
72 if($node->getParent() == null)
73 {
74 $this->root_node->addChild($node);
75
76 // spécifique page article
77 if($node->getName() === 'main' && $this->page->getEndOfPath() == 'article'){
78 $main = $node;
79 }
80 }
81 // autres niveaux
82 else
83 {
84 $node->getParent()->addChild($node);
85
86 // spécifique page article
87 if($node->getName() === 'new' && $this->page->getEndOfPath() == 'article'){
88 $new = $node;
89 }
90 }
91 }
92 if(isset($new)){
93 $main->setTempChild($new);
94 }
95 }
96
97 public function getRootNode(): Node
98 {
99 return $this->root_node;
100 }
101}
diff --git a/src/controller/Security.php b/src/controller/Security.php
new file mode 100644
index 0000000..ab59d07
--- /dev/null
+++ b/src/controller/Security.php
@@ -0,0 +1,111 @@
1<?php
2// src/controller/Security.php
3//
4// utilise htmlawed contre les faille XSS
5
6class Security
7{
8 private static $configHtmLawed = array(
9 'safe'=>1, // protection contre les élements et attributs dangereux
10
11 // liste blanche d'éléments HTML
12 'elements'=> 'h1, h2, h3, h4, h5, h6, p, s, em, span, strong, a, ul, ol, li, sup, sub, code, blockquote, div, pre, table, caption, colgroup, col, tbody, tr, th, td, figure, img, figcaption',
13
14 // liste noire d'attributs HTML
15 'deny_attribute'=> 'id, class' // on garde 'style'
16 );
17
18 // faire qu'un certain élément puisse n'avoir que certains attributs, regarder la doc
19 private static $specHtmLawed = '';
20
21 public static function secureString(string $chaine): string
22 {
23 return trim(htmLawed($chaine, self::$configHtmLawed, self::$specHtmLawed));;
24 }
25
26 public static function secureFileName(string $chaine): string
27 {
28 // sécuriser un nom avec chemin avec basename?
29 //$chaine = basename($chaine);
30
31 /*
32 - caractères interdits sous windows / \ : * ? " < > |
33 - mac autorise les /
34 - mac interdit :
35 - linux autorise tout sauf les /
36 - imagemagick ne supporte pas les :
37
38 - 'espace' fonctionne
39 - / remplacé par firefox en :
40 - \ retire ce qui est devant le \
41 - * fonctionne
42 - ? permet le téléchargement mais pas l'affichage
43 - " ne fonctionne pas, remplacé par %22, filtrer %22
44 - < > fonctionnent
45 - | fonctionne
46 - = fonctionne, mais je filtre parce qu'on en trouve dans une URL
47 - ' ` fonctionnent
48 - % fonctionne
49 - (){}[] fonctionnent
50 - ^ fonctionne
51 - # ne fonctionne pas
52 - ~ fonctionne
53 - & fonctionne
54 - ^ pas encore testé
55 */
56
57 // => on remplace tout par des _
58 // filtrer / et \ semble inutile
59
60 $cibles = [' ', '/', '\\', ':', '*', '?', '<', '>', '|', '=', "'", '`', '"', '%22', '#'];
61 $chaine = str_replace($cibles, '_', $chaine); // nécéssite l'extension mbstring
62 $chaine = mb_strtolower($chaine);
63 return($chaine);
64
65 // les problèmes avec \ persistent !!
66 // => javascript
67 // malheureusement document.getElementById('upload').files[0].name = chaine; ne marche pas! interdit!
68 // javascript ne doit pas pouvoir accéder au système de fichiers
69 // solutions:
70 // - au lieu de fournir une chaine (le chemin du fichier), donner un objet à files[0].name
71 // - créer une copie du fichier et l'envoyer à la place
72 // - envoyer le fichier en AJAX
73 // - envoyer le nom du fichier à part puis renommer en PHP
74 }
75}
76
77// erreurs à la création des mots de passe
78function removeSpacesTabsCRLF(string $chaine): string
79{
80 $cibles = [' ', "\t", "\n", "\r"]; // doubles quotes !!
81 return(str_replace($cibles, '', $chaine));
82}
83
84// lien sans http://
85function fixLinks($data)
86{
87 // 1/
88 // si une adresse est de type "domaine.fr" sans le http:// devant, le comportement des navigateurs est de rechercher un fichier comme si mon adresse commençait par file://
89 // tomber ainsi sur une page d'erreur est parfaitement déroutant
90
91 // regex pour détecter les balises <a> et ajouter http:// au début des liens si nécessaire
92 $pattern = '#(<a[^>]+href=")((?!https?://)[^>]+>)#';
93 //$data = preg_replace($pattern, '$1http://$2', $data);
94
95 // 2/
96 // cas où la regex fait mal son boulot:
97 // l'erreur 404 est gérée par le .htaccess
98 // et le visiteur est redirigé à la page "menu"
99 // (ça ne règle pas le problème mais c'est mieux)
100
101 // 3/
102 // quand l'éditeur est ouvert (avant de valider l'article),
103 // le lien qu'on vient de créer apparaît dans l'infobulle,
104 // cliquer dessus ouvre un onglet sur une erreur 404
105 // solution partielle avec le .htaccess
106 //
107 // solution? fermer ce nouvel onglet avec echo '<SCRIPT>javascript:window.close()</SCRIPT>';
108 // comment déclencher le JS? en faisant qu'une erreur 404 causée pour cette raison soit particulière?
109
110 return($data);
111}
diff --git a/src/controller/URL.php b/src/controller/URL.php
new file mode 100644
index 0000000..956d85d
--- /dev/null
+++ b/src/controller/URL.php
@@ -0,0 +1,88 @@
1<?php
2// src/controller/URL.php
3
4declare(strict_types=1);
5
6class URL implements Stringable
7{
8 static private string $protocol = 'http://';
9 static private string $host = 'localhost';
10 static private string $port;
11 static private string $path = '/index.php';
12 private array $params;
13 private string $anchor = '';
14
15 // setters statiques
16 static public function setProtocol(string $protocol = 'http'): void
17 {
18 self::$protocol = $protocol === 'https' ? 'https://' : 'http://';
19 }
20 static public function setPort(int|string $port = 80): void
21 {
22 if((int)$port === 443){
23 self::$protocol = 'https://';
24 self::$port = '';
25 }
26 elseif((int)$port === 80){
27 self::$protocol = 'http://';
28 self::$port = '';
29 }
30 else{
31 self::$port = ':' . (string)$port;
32 }
33 }
34 static public function setHost(string $host): void
35 {
36 self::$host = $host;
37 }
38 static public function setPath(string $path): void
39 {
40 self::$path = '/' . ltrim($path, '/');
41 }
42
43 public function __construct(array $gets = [], string $anchor = ''){
44 $this->params = $gets;
45 if($anchor != ''){
46 $this->setAnchor($anchor);
47 }
48 }
49
50 //setters normaux
51 public function addParams(array $gets): void
52 {
53 // array_merge est préféré à l'opérateur d'union +, si une clé existe déjà la valeur est écrasée
54 $this->params = array_merge($this->params, $gets);
55 }
56 public function setAnchor(string $anchor = ''): void
57 {
58 if($anchor != ''){
59 $this->anchor = '#' . ltrim($anchor, '#');
60 }
61 else{
62 $this->anchor = '';
63 }
64 }
65
66 private function makeParams(): string
67 {
68 $output = '';
69 $first = true;
70
71 foreach($this->params as $key => $value) {
72 if($first){
73 $output .= '?';
74 $first = false;
75 }
76 else{
77 $output .= '&';
78 }
79 $output .= $key . '=' . $value;
80 }
81 return $output;
82 }
83
84 public function __toString(): string
85 {
86 return self::$protocol . self::$host . self::$port . self::$path . $this->makeParams() . $this->anchor;
87 }
88} \ No newline at end of file
diff --git a/src/controller/ajax.php b/src/controller/ajax.php
new file mode 100644
index 0000000..130c4c6
--- /dev/null
+++ b/src/controller/ajax.php
@@ -0,0 +1,104 @@
1<?php
2// src/controller/ajax.php
3
4declare(strict_types=1);
5
6// détection des requêtes de tinymce
7if($_SERVER['CONTENT_TYPE'] === 'application/json' && isset($_GET['action']))
8{
9 // récupération des données
10 $data = file_get_contents('php://input');
11 $json = json_decode($data, true);
12
13 if($_GET['action'] === 'editor_submit' && isset($json['id']) && isset($json['content']))
14 {
15 if(json_last_error() === JSON_ERROR_NONE)
16 {
17 $articleId = $json['id'];
18 $content = Security::secureString($json['content']);
19
20 $director = new Director($entityManager);
21 if($director->makeArticleNode($articleId)) // une entrée est trouvée
22 {
23 $node = $director->getRootNode();
24 $node->getArticle()->setContent($content);
25 $entityManager->flush();
26
27 echo json_encode(['success' => true]);
28 }
29 else{
30 echo json_encode(['success' => false, 'message' => 'Aucune entrée trouvée en BDD']);
31 }
32 }
33 else{
34 echo json_encode(['success' => false, 'message' => 'Erreur de décodage JSON']);
35 }
36 die;
37 }
38 elseif($_GET['action'] === 'delete_article' && isset($json['id']))
39 {
40 $articleId = $json['id'];
41
42 $director = new Director($entityManager);
43 $director->makeArticleNode($articleId);
44 $node = $director->getRootNode();
45 $entityManager->remove($node);
46 $entityManager->flush();
47
48 // test avec une nouvelle requête qui ne devrait rien trouver
49 if(!$director->makeArticleNode($articleId))
50 {
51 echo json_encode(['success' => true]);
52
53 // on pourrait afficher une notification "toast"
54 }
55 else{
56 http_response_code(500);
57 echo json_encode(['success' => false, 'message' => 'Erreur lors de la suppression de l\'article.']);
58 }
59 die;
60 }
61}
62
63// détection des requêtes d'upload d'image de tinymce
64if(strpos($_SERVER['CONTENT_TYPE'], 'multipart/form-data') !== false && isset($_GET['action']) && $_GET['action'] === 'upload_image'){
65 if (isset($_FILES['file'])) {
66 $file = $_FILES['file'];
67 $dest = 'images/';
68 $dest_mini = 'images-mini/';
69
70 // Vérifier si les répertoires existent, sinon les créer
71 if(!is_dir($dest)) {
72 mkdir($dest, 0700, true);
73 }
74 if(!is_dir($dest_mini)) {
75 mkdir($dest_mini, 0700, true);
76 }
77
78 $filePath = $dest . basename($file['name']);
79
80 // créer une miniature de l'image
81
82 if(move_uploaded_file($file['tmp_name'], $filePath)) {
83 $image_url = str_replace(basename($_SERVER['SCRIPT_NAME']), '', $_SERVER['SCRIPT_NAME']);
84 echo json_encode(['location' => $image_url . $filePath]); // renvoyer l'URL de l'image téléchargée
85 }
86 else{
87 http_response_code(500);
88 echo json_encode(['message' => 'Erreur 500: Internal Server Error']);
89 }
90 }
91 else{
92 http_response_code(400);
93 echo json_encode(['message' => 'Erreur 400: Bad Request']);
94 }
95 die;
96}
97
98// détection des requêtes de type XHR, pas d'utilité pour l'instant
99/*elseif(isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest'){
100 echo "requête XHR reçue par le serveur";
101 die;
102}*/
103
104
diff --git a/src/controller/installation.php b/src/controller/installation.php
new file mode 100644
index 0000000..a692618
--- /dev/null
+++ b/src/controller/installation.php
@@ -0,0 +1,144 @@
1<?php
2// src/controller/installation.php
3
4declare(strict_types=1);
5
6use App\Entity\Page;
7use App\Entity\Node;
8use App\Entity\NodeData;
9use App\Entity\Image;
10use Doctrine\Common\Collections\ArrayCollection;
11use Doctrine\ORM\EntityManager;
12
13function installation(): void
14{
15 /* -- extensions PHP -- */
16 $extensions = [];
17 foreach($extensions as $extension){
18 if(!extension_loaded($extension))
19 {
20 echo("l'extension " . $extension . ' est manquante<br>');
21 }
22 }
23 if(!extension_loaded('imagick') && !extension_loaded('gd')){
24 echo("il manque une de ces extensions au choix: imagick (de préférence) ou gd<br>");
25 }
26
27 /* -- droits des fichiers et dossiers -- */
28 $droits_dossiers = 0700;
29 $droits_fichiers = 0600;
30
31 // accès interdit en HTTP
32 if(!file_exists('../config/.htaccess')){
33 $contenu = <<< HTACCESS
34<Files "config.ini">
35 Order Allow,Deny
36 Deny from all
37</Files>
38HTACCESS;
39
40 $fichier = fopen('../config/.htaccess', 'w');
41 fputs($fichier, $contenu);
42 fclose($fichier);
43 chmod('../config/.htaccess', $droits_fichiers);
44 //echo("danger<br>pas de .htaccess dans config<br>prévenez le respondable du site");
45 //die;
46 }
47
48 // accès limité en local (600) pour config.ini
49 if(substr(sprintf('%o', fileperms('../config/config.ini')), -4) != 600){
50 chmod('../config/config.ini', $droits_fichiers);
51 }
52
53 // création de data et sous-dossiers
54 if(!file_exists('../data')){
55 mkdir('../data/');
56 chmod('../data/', $droits_dossiers);
57 }
58 if(!touch('../data')){
59 echo("dossier data non autorisé en écriture");
60 die;
61 }
62 $sous_dossiers = array('images', 'images-mini', 'videos');
63 foreach ($sous_dossiers as $sous_dossier){
64 if(!file_exists('../data/' . $sous_dossier)){
65 mkdir('../data/' . $sous_dossier);
66 chmod('../data/' . $sous_dossier, $droits_dossiers);
67 }
68 if(!touch('../data/' . $sous_dossier)){
69 echo("dossier data non autorisé en écriture");
70 die;
71 }
72 }
73}
74
75// création de la page d'accueil à la toute 1ère visite du site
76// les informations ici ne sont pas demandées à l'utilisateur pour l'instant (on verra ça plus tard)
77function makeStartPage(EntityManager $entityManager){
78 /* -- table page -- */
79 // paramètres: name_page, end_of_path, reachable, in_menu, parent
80 $accueil = new Page('Accueil', 'accueil', true, true, NULL);
81 $connection = new Page('Connexion', 'connexion', true, false, NULL);
82 $article = new Page('Article', 'article', true, false, NULL);
83 $edit_page = new Page("Modification d'une page", 'modif_page', true, false, NULL);
84 $new_page = new Page('Nouvelle page', 'nouvelle_page', true, false, NULL);
85 $edit_paths = new Page("Menu et chemins", 'menu_chemins', true, false, NULL);
86
87 /* -- table node -- */
88 // paramètres: name_node, article_timestamp, attributes, position, parent, page, article
89 $head_accueil = new Node('head', NULL, ['css_array' => ['body', 'head', 'nav', 'main', 'foot'], 'js_array' => ['main']], 1, NULL, $accueil, NULL);
90 $header = new Node('header', NULL, [], 2, NULL, NULL, NULL);
91 $nav = new Node('nav', NULL, [], 1, $header, NULL, NULL);
92 $main = new Node('main', NULL, [], 3, NULL, NULL, NULL);
93 $footer = new Node('footer', NULL, [], 4, NULL, NULL, NULL);
94 $breadcrumb = new Node('breadcrumb', NULL, [], 1, $footer, NULL, NULL);
95 $head_login = new Node('head', NULL, ["stop" => true, 'css_array' => ['body', 'head', 'nav', 'main'], 'js_array' => ['main']], 1, NULL, $connection, NULL);
96 $login = new Node('login', NULL, [], 1, $main, $connection, NULL);
97 $head_article = new Node('head', NULL, ['css_array' => ['body', 'head', 'nav', 'main', 'foot'], 'js_array' => ['main']], 1, NULL, $article, NULL);
98
99 /* -- table image -- */
100 // paramètres: file_name, file_path, file_path_mini, mime_type, alt
101 $favicon = new Image("favicon48x48.png", NULL, "assets/favicon48x48.png", "image/png", "favicon");
102 $logo = new Image("logo-120x75.jpg", NULL, "assets/logo-120x75.jpg", "image/png", "head_logo");
103 $facebook = new Image("facebook.svg", NULL, "assets/facebook.svg", "image/svg+xml", "facebook");
104 $instagram = new Image("instagram.svg", NULL, "assets/instagram.svg", "image/svg+xml", "instagram");
105 $fond_piscine = new Image("fond-piscine.jpg", "assets/fond-piscine.jpg", NULL, "images/jpg", "fond-piscine");
106
107 /* -- table node_data -- */
108 // paramètres: data, node
109 $head_accueil_data = new NodeData(["description" => "Club, École de natation et Perfectionnement", "title" => "Les Nageurs Bigoudens"], $head_accueil, new ArrayCollection([$favicon]));
110 $header_data = new NodeData(["description" => "Club, École de natation et Perfectionnement", "title" => "Les Nageurs Bigoudens", "facebook_link" => "https://www.facebook.com/nageursbigoudens29120", "instagram_link" => "https://www.instagram.com/nageursbigoudens/"], $header, new ArrayCollection([$logo, $facebook, $instagram, $fond_piscine]));
111 $footer_data = new NodeData(["adresse" => "17, rue Raymonde Folgoas Guillou, 29120 Pont-l’Abbé", "contact_nom" => "Les Nageurs Bigoudens", "e_mail" => "nb.secretariat@orange.fr"], $footer);
112 $head_login_data = new NodeData(["description" => "Connexion", "title" => "Connexion"], $head_login, new ArrayCollection([$favicon]));
113 $head_article_data = new NodeData(["description" => "", "title" => ""], $head_article, new ArrayCollection([$favicon]));
114
115 $entityManager->persist($accueil);
116 $entityManager->persist($connection);
117 $entityManager->persist($article);
118 $entityManager->persist($edit_page);
119 $entityManager->persist($new_page);
120 $entityManager->persist($edit_paths);
121 $entityManager->persist($head_accueil);
122 $entityManager->persist($header);
123 $entityManager->persist($nav);
124 $entityManager->persist($main);
125 $entityManager->persist($footer);
126 $entityManager->persist($breadcrumb);
127 $entityManager->persist($head_login);
128 $entityManager->persist($login);
129 $entityManager->persist($head_article);
130 $entityManager->persist($favicon);
131 $entityManager->persist($logo);
132 $entityManager->persist($facebook);
133 $entityManager->persist($instagram);
134 $entityManager->persist($fond_piscine);
135 $entityManager->persist($head_accueil_data);
136 $entityManager->persist($header_data);
137 $entityManager->persist($footer_data);
138 $entityManager->persist($head_login_data);
139 $entityManager->persist($head_article_data);
140 $entityManager->flush();
141
142 header('Location: ' . new URL);
143 die;
144} \ No newline at end of file
diff --git a/src/controller/password.php b/src/controller/password.php
new file mode 100644
index 0000000..d5e66ff
--- /dev/null
+++ b/src/controller/password.php
@@ -0,0 +1,357 @@
1<?php
2// src/controller/password.php
3//
4// test mot de passe et captcha
5
6declare(strict_types=1);
7
8use Doctrine\ORM\EntityManager;
9use App\Entity\User;
10
11// exécutée dans installation.php à l'ouverture de chaque page
12function existUsers(EntityManager $entityManager)
13{
14 // lecture
15 $users = $entityManager->getRepository(User::class)->findAll();
16
17 // cas particulier table vide
18 if(count($users) === 0)
19 {
20 $_GET = [];
21 $_SESSION['user'] = '';
22 $_SESSION['admin'] = false;
23
24 // création d'un utilisateur, puis rechargement de la page
25 createPassword($entityManager);
26 }
27}
28
29
30function createPassword(EntityManager $entityManager)
31{
32 // fonction exécutée à priori deux fois d'affilée: affichage puis traitement de la saisie
33
34 // II - traitement
35 unset($_SESSION['user']);
36
37 $captcha_solution = (isset($_SESSION['captcha']) && is_int($_SESSION['captcha'])) ? $_SESSION['captcha'] : 0;
38 $captcha = isset($_POST['captcha']) ? controlCaptchaInput($_POST['captcha']) : 0;
39
40 $error = '';
41 if(!isset($_POST['captcha'])) // page rechargée
42 {
43 //$error = '';
44 }
45 elseif($captcha == 0)
46 {
47 $error = 'error_non_valid_captcha';
48 }
49 elseif($captcha_solution == 0)
50 {
51 //$error = '';
52 }
53 elseif($captcha != $captcha_solution) // le test!
54 {
55 $error = 'bad_solution_captcha';
56 }
57 elseif(!isset($_POST['password']) || empty($_POST['password'])
58 || (!isset($_POST['login']) || empty($_POST['login'])))
59 {
60 $error = 'bad_login_or_password';
61 }
62 else
63 {
64 // -> caractères HTML dangereux supprimés
65 $login = Security::secureString($_POST['login']);
66 $password = Security::secureString($_POST['password']);
67
68 // -> prévenir la validation par erreur d'une chaine "vide"
69 $login = removeSpacesTabsCRLF($login);
70 $password = removeSpacesTabsCRLF($password);
71
72 // conformité
73 if(isset($password) && isset($login)
74 && $password == $_POST['password'] && $login == $_POST['login'])
75 {
76 // enregistrement et redirection
77 $password = password_hash($password, PASSWORD_DEFAULT);
78 $user = new App\Entity\User($login, $password);
79 $entityManager->persist($user);
80 $entityManager->flush();
81
82 header('Location: ' . new URL);
83 exit();
84 }
85 else
86 {
87 $error = 'bad_password';
88 }
89 }
90
91 // inséré dans $captchaHtml puis dans $formulaireNouveauMDP
92 $captcha = createCaptcha();
93 // enregistrement de la réponse du captcha pour vérification
94 $_SESSION['captcha'] = $captcha[2]; // int
95
96
97 // I - affichage
98 $title = 'Bienvenue nageur bigouden';
99 $subHeading = 'Veuillez choisir les codes que vous utiliserez pour gérer le site.';
100
101 // même vue que la fonction changerMotDePasse()
102 require('../src/view/password.php');
103
104 echo($header);
105 if($error != '')
106 {
107 sleep(1);
108 echo($error_messages[$error]);
109 }
110 echo($formulaireNouveauMDP);
111 echo($error_messages['forbidden_characters']);
112 echo($footer);
113 die;
114}
115
116
117function connect(LoginBuilder $builder, EntityManager $entityManager)
118{
119 // déjà connecté
120 if($_SESSION['admin'])
121 {
122 header('Location: ' . new URL);
123 die;
124 }
125
126 // II - traitement
127 $_SESSION['user'] = '';
128 $_SESSION['admin'] = false;
129
130 $captcha_solution = (isset($_SESSION['captcha']) && is_int($_SESSION['captcha'])) ? $_SESSION['captcha'] : 0;
131 $captcha = isset($_POST['captcha']) ? controlCaptchaInput($_POST['captcha']) : 0;
132
133 $error = '';
134 if(!isset($_POST['captcha'])) // page rechargée
135 {
136 //$error = '';
137 }
138 elseif($captcha == 0)
139 {
140 $error = 'error_non_valid_captcha';
141 }
142 elseif($captcha_solution == 0)
143 {
144 //$error = '';
145 }
146 elseif($captcha != $captcha_solution) // le test!
147 {
148 $error = 'bad_solution_captcha';
149 }
150 elseif(!isset($_POST['login']) || empty($_POST['login'])
151 || !isset($_POST['password']) || empty($_POST['password']))
152 {
153 $error = 'bad_password';
154 }
155 else // c'est OK
156 {
157 $login = $_POST['login'];
158 $password = $_POST['password'];
159 $user = getUser($login, $entityManager);
160
161 // enregistrement et redirection
162 if(password_verify($password, $user->getPassword()))
163 {
164 session_start();
165 $_SESSION['user'] = $login;
166 $_SESSION['admin'] = true;
167 $link = new URL(isset($_GET['from']) ? ['page' => $_GET['from']] : []);
168 isset($_GET['id']) ? $link->addParams(['id' => $_GET['id']]) : '';
169 header('Location: ' . $link);
170 die;
171 }
172 else
173 {
174 $error = 'bad_password';
175 }
176 }
177
178 // inséré dans $captchaHtml puis dans $formulaireNouveauMDP
179 $captcha = createCaptcha();
180 // enregistrement de la réponse du captcha pour vérification
181 $_SESSION['captcha'] = $captcha[2]; // int
182
183 // I - affichage
184 $title = "Connexion";
185 $subHeading = "Veuillez saisir votre identifiant (e-mail) et votre mot de passe.";
186
187 require('../src/view/password.php');
188
189 //$builder->addHTML($header);
190 if($error != '')
191 {
192 sleep(1);
193 $builder->addHTML($error_messages[$error]);
194 }
195 $builder->addHTML($formulaireConnexion);
196 //$builder->addHTML($warning_messages['message_cookie']);
197 $builder->addHTML($warning_messages['private_browsing']);
198 $builder->addHTML($footer);
199
200 //die;
201}
202
203
204function changePassword(EntityManager $entityManager)
205{
206 // fonction exécutée à priori deux fois d'affilée: affichage puis traitement de la saisie
207
208 // OUT !!
209 if(empty($_SESSION['user']) || !$_SESSION['admin'])
210 {
211 $_SESSION['user'] = '';
212 $_SESSION['admin'] = false;
213 header('Location: index.php');
214 die;
215 }
216
217 // II - traitement
218 $error = '';
219 $success = false;
220 if(empty($_POST)) // première fois ou page rechargée
221 {
222 //
223 }
224 elseif(!isset($_POST['login']) || empty($_POST['login'])
225 || !isset($_POST['old_password']) || empty($_POST['old_password'])
226 || !isset($_POST['new_password']) || empty($_POST['new_password']))
227 {
228 $error = 'bad_login_or_password';
229 }
230 else
231 {
232 // sécurisation de la saisie
233 $new_password = Security::secureString($_POST['new_password']);
234 $login = Security::secureString($_POST['login']);
235 $old_password = Security::secureString($_POST['old_password']);
236
237 // éviter d'enregistrer une chaîne vide
238 $new_password = removeSpacesTabsCRLF($new_password);
239
240 // tests de conformité
241 if($login != $_POST['login'] || $old_password != $_POST['old_password'] || $new_password != $_POST['new_password'])
242 {
243 $error = 'forbidden_characters';
244 }
245 else
246 {
247 $user = getUser($login, $entityManager);
248
249 if(password_verify($old_password, $user->getPassword()))
250 {
251 // enregistrement et redirection
252 $new_password = password_hash($new_password, PASSWORD_DEFAULT);
253 $user->setPassword($new_password);
254 $entityManager->flush();
255 $success = true;
256 }
257 else
258 {
259 $error = 'bad_password';
260 }
261 }
262 }
263
264
265 // I - affichage
266 $title = "Nouveau mot de passe";
267 $subHeading = "Veuillez vous identifier à nouveau puis saisir votre nouveau mot de passe.";
268
269 require('../src/view/password.php');
270
271 echo($header);
272 if($error != '')
273 {
274 sleep(1); // sécurité TRÈS insuffisante à la force brute
275 echo($error_messages[$error]);
276 }
277 elseif($success)
278 {
279 $success = false;
280 echo($alertJSNewPassword);
281 die;
282 }
283 echo($formulaireModifMDP);
284 echo($footer);
285 die;
286}
287
288
289function getUser(string $login, EntityManager $entityManager): User
290{
291 $users = $entityManager->getRepository('App\Entity\User')->findBy(['login' => $login]);
292
293 // détection d'un abus
294 if(count($users) === 0)
295 {
296 $_SESSION['user'] = '';
297 $_SESSION['admin'] = false;
298
299 header('Location: index.php'); // page création d'un mot de passe à l'attérissage
300 die;
301 }
302
303 foreach($users as $user)
304 {
305 if($user->getLogin() === $login)
306 {
307 return $user;
308 }
309 }
310 header('Location: ' . new URL);
311 die;
312}
313
314
315function disconnect(EntityManager $entityManager)
316{
317 // nettoyage complet
318 $_SESSION = []; // mémoire vive
319 session_destroy(); // fichier côté serveur
320 setcookie('PHPSESSID', '', time() - 4200, '/'); // cookie de session
321 $link = new URL(['page' => $_GET['page']]);
322 isset($_GET['id']) ? $link->addParams(['id' => $_GET['id']]) : '';
323 header('Location: ' . $link);
324 die;
325}
326
327
328function createCaptcha(): array
329{
330 $a = rand(2, 9);
331 $b = rand(2, 9);
332 return array(toLettersFrench($a), toLettersFrench($b), $a * $b);
333}
334
335function toLettersFrench(int $number): string
336{
337 return match($number)
338 {
339 2 => 'deux',
340 3 => 'trois',
341 4 => 'quatre',
342 5 => 'cinq',
343 6 => 'six',
344 7 => 'sept',
345 8 => 'huit',
346 9 => 'neuf',
347 default => '', // erreur
348 };
349}
350
351// on veut des chiffres
352function controlCaptchaInput(string $captcha = '0'): int
353{
354 // $captcha est un POST donc une chaîne, '2.3' est acceptés
355 // (int) supprime les décimales
356 return (is_numeric($captcha) && $captcha == (int) $captcha) ? (int) $captcha : 0;
357} \ No newline at end of file
diff --git a/src/controller/post.php b/src/controller/post.php
new file mode 100644
index 0000000..926a5ae
--- /dev/null
+++ b/src/controller/post.php
@@ -0,0 +1,17 @@
1<?php
2// src/controller/post.php
3
4declare(strict_types=1);
5
6if($_SERVER['REQUEST_METHOD'] === 'POST' && $_SESSION['admin'] === true)
7{
8 // requêtes AJAX
9 require '../src/controller/ajax.php';
10
11 // formulaires HTML
12 if(isset($_POST['from']) // page d'où vient la requête
13 && isset($_POST)) // données
14 {
15 echo "requête envoyée en validant un formulaire";
16 }
17}
diff --git a/src/model/Menu.php b/src/model/Menu.php
new file mode 100644
index 0000000..624a0fc
--- /dev/null
+++ b/src/model/Menu.php
@@ -0,0 +1,53 @@
1<?php
2// src/controller/Menu.php
3
4declare(strict_types=1);
5
6use Doctrine\ORM\EntityManager;
7use App\Entity\Page;
8use Doctrine\Common\Collections\ArrayCollection;
9
10class Menu extends Page
11{
12 private EntityManager $entityManager;
13 private array $other_pages = []; // pages n'apparaissant pas dans le menu
14
15 public function __construct(EntityManager $entityManager){
16 $this->children = new ArrayCollection();
17
18 $bulk_data = $entityManager
19 ->createQuery('SELECT n FROM App\Entity\Page n WHERE n.parent IS null') // :Doctrine\ORM\Query
20 ->getResult(); // :array de Page
21
22 if(count($bulk_data) === 0){
23 makeStartPage($entityManager);
24 }
25
26 foreach($bulk_data as $first_level_entries){
27 // génération du menu
28 if($first_level_entries->getInMenu()){
29 $this->addChild($first_level_entries);
30 }
31 // autres pages
32 else{
33 // attention, seul le premier élément du chemin est pris en compte
34 $this->other_pages[] = $first_level_entries;
35 }
36 }
37
38 foreach($this->getChildren() as $page){
39 $page->fillChildrenPagePath();
40 }
41
42 /*for($i = 0; $i < count($this->getChildren()[1]->getChildren()); $i++){
43 echo $this->getChildren()[1]->getChildren()[$i]->getEndOfPath() . ' - ';
44 echo $this->getChildren()[1]->getChildren()[$i]->getPageName() . '<br>';
45 }*/
46 //die;
47 }
48
49 public function getOtherPages(): array
50 {
51 return $this->other_pages;
52 }
53} \ No newline at end of file
diff --git a/src/model/Path.php b/src/model/Path.php
new file mode 100644
index 0000000..6faadfd
--- /dev/null
+++ b/src/model/Path.php
@@ -0,0 +1,84 @@
1<?php
2// src/controller/Path.php
3
4declare(strict_types=1);
5
6use Doctrine\ORM\EntityManager;
7use App\Entity\Page;
8
9class Path extends Page
10{
11 private array $current_page = []; // tableau d'objets Page
12
13 public function __construct()
14 {
15 $path_array = explode('/', CURRENT_PAGE);
16 try{
17 $this->findPage(Director::$menu_data, $path_array); // remplit $this->current_page
18 }
19 catch(Exception $e){}
20 /*echo "nb d'autres pages: " . count(Director::$menu_data->getOtherPages()) . '<br>';
21 echo 'longueur du chemin: ' . count($this->current_page) . '<br>';
22 foreach($this->current_page as $current){
23 echo $current->getEndOfPath() . ' ';
24 }
25 die;*/
26 }
27
28 // produit un tableau de Page en comparant le chemin demandé avec les données dans Menu
29 // succès => une exception est lancée pour sortir des fonctions imbriquées
30 // echec => redirection vers la page erreur 404
31 private function findPage(Page|Menu $menu, array $path_array)
32 {
33 // recherche dans les autres pages
34 if($menu instanceof Menu){
35 foreach($menu->getOtherPages() as $page)
36 {
37 if($path_array[0] === $page->getEndOfPath())
38 {
39 $this->current_page[] = $page;
40 throw new Exception();
41 }
42 }
43 }
44 // recherche dans le menu
45 foreach($menu->getChildren() as $page)
46 {
47 if($path_array[0] === $page->getEndOfPath())
48 {
49 $this->current_page[] = $page;
50 if(count($path_array) > 1)
51 {
52 array_shift($path_array); // $this->path_array n'est pas modifié, un tableau PHP est passé à une fonction par copie
53 $this->findPage($page, $path_array);
54 }
55 else{
56 throw new Exception(); // sortir de tous les findPage() en même temps
57 }
58 }
59 }
60 // rien trouvé
61 URL::setPath('erreur404.html');
62 header('Location: '. new URL);
63 die;
64 }
65
66 public function getString(): string
67 {
68 $path_string = "";
69 foreach($this->current_page as $one_page){
70 $path_string .= $one_page->getEndOfPath() . '/';
71 }
72 return rtrim($path_string, '/');
73 }
74 public function getArray(): array
75 {
76 return $this->current_page;
77 }
78
79 // c'est là qu'on est quoi
80 public function getLast(): Page
81 {
82 return $this->current_page[count($this->current_page) - 1];
83 }
84} \ No newline at end of file
diff --git a/src/model/doctrine-bootstrap.php b/src/model/doctrine-bootstrap.php
new file mode 100644
index 0000000..139f410
--- /dev/null
+++ b/src/model/doctrine-bootstrap.php
@@ -0,0 +1,31 @@
1<?php
2// src/model/doctrine-bootstrap.php
3
4use Doctrine\ORM\ORMSetup;
5use Doctrine\DBAL\DriverManager;
6use Doctrine\ORM\EntityManager;
7
8require_once "../vendor/autoload.php";
9
10// Create a simple "default" Doctrine ORM configuration for Attributes
11$config = ORMSetup::createAttributeMetadataConfiguration(
12 //paths: array(__DIR__.'/entities'),
13 paths: array('../src/model/entities'),
14 isDevMode: true,
15 // true: cache en mémoire vive
16 // false: utilisation de APCu ou redis ou memcache
17);
18
19// configuring the database connection
20$connection = DriverManager::getConnection([
21 'driver' => Config::$db_driver,
22 'user' => Config::$user,
23 'password' => Config::$password,
24 'host' => Config::$db_host,
25 'dbname' => Config::$database,
26], $config);
27
28// obtaining the entity manager
29$entityManager = new EntityManager($connection, $config);
30
31foreach($entityManager->getMetadataFactory()->getAllMetadata() as $class){} \ No newline at end of file
diff --git a/src/model/entities/Article.php b/src/model/entities/Article.php
new file mode 100644
index 0000000..3b846da
--- /dev/null
+++ b/src/model/entities/Article.php
@@ -0,0 +1,77 @@
1<?php
2// src/model/entities/Article.php
3
4declare(strict_types=1);
5
6namespace App\Entity;
7
8use Doctrine\ORM\Mapping as ORM;
9use Doctrine\Common\Collections\ArrayCollection;
10use Doctrine\Common\Collections\Collection;
11
12#[ORM\Entity]
13#[ORM\Table(name: TABLE_PREFIX . "article")]
14class Article
15{
16 #[ORM\Id]
17 #[ORM\GeneratedValue]
18 #[ORM\Column(type: "integer")]
19 private int $id_article;
20
21 // datetime_immutable permet à la base de toujours gérer cette clé primaire correctement
22 #[ORM\Column(type: 'datetime', options: ['default' => 'CURRENT_TIMESTAMP'], unique: true)]
23 private \DateTime $date_time; // le type datetime de doctrine convertit en type \DateTime de PHP
24
25 #[ORM\Column(type: "string")]
26 private string $title;
27
28 #[ORM\Column(type: "text")]
29 private string $preview; // une simple textarea
30
31 #[ORM\Column(type: "text")]
32 private string $content; // de l'éditeur html
33
34 // liaison avec table intermédiaire
35 #[ORM\ManyToMany(targetEntity: Image::class, inversedBy: "article")]
36 #[ORM\JoinTable(
37 name: "nb_article_image",
38 joinColumns: [new ORM\JoinColumn(name: "article_id", referencedColumnName: "id_article", onDelete: "CASCADE")],
39 inverseJoinColumns: [new ORM\JoinColumn(name: "image_id", referencedColumnName: "id_image", onDelete: "CASCADE")]
40 )]
41 private Collection $images;
42
43 public function __construct()
44 {
45 $this->images = new ArrayCollection(); // initialisation nécessaire
46 }
47
48 public function getDateTime(): \DateTime
49 {
50 return $this->date_time;
51 }
52 public function getTimestamp(): int
53 {
54 return $this->date_time->getTimestamp();
55 }
56 public function getTitle(): string
57 {
58 return $this->title;
59 }
60 public function getPreview(): string
61 {
62 return $this->preview;
63 }
64 public function getContent(): string
65 {
66 return $this->content;
67 }
68 public function setContent(string $data): void
69 {
70 $this->content = $data;
71 }
72
73 public function getImages(): Collection
74 {
75 return $this->images;
76 }
77}
diff --git a/src/model/entities/Image.php b/src/model/entities/Image.php
new file mode 100644
index 0000000..181c137
--- /dev/null
+++ b/src/model/entities/Image.php
@@ -0,0 +1,91 @@
1<?php
2// src/model/entities/Image.php
3
4declare(strict_types=1);
5
6namespace App\Entity;
7
8use Doctrine\ORM\Mapping as ORM;
9
10#[ORM\Entity]
11#[ORM\Table(name: TABLE_PREFIX . "image")]
12class Image
13{
14 #[ORM\Id]
15 #[ORM\GeneratedValue]
16 #[ORM\Column(type: "integer")]
17 private int $id_image;
18
19 #[ORM\Column(type: "string", length: 255, unique: true)] // nom d'image UNIQUE
20 private string $file_name;
21
22 // choisir un répertoire du genre /var/www/html/uploads/ de préférence hors de /src
23 #[ORM\Column(type: "string", length: 255, unique: true, nullable: true)]
24 private ?string $file_path;
25
26 #[ORM\Column(type: "string", length: 255, unique: true, nullable: true)]
27 private ?string $file_path_mini;
28
29 #[ORM\Column(type: "string", length: 255, nullable: true)]
30 private string $mime_type; // image/jpeg, image/png, etc
31
32 #[ORM\Column(type: "string", length: 255, nullable: true)]
33 private string $alt; // texte alternatif
34
35 // autre champs optionnels: file_size, date (default current timestamp)
36
37 /* étapes au téléchargement:
38 => Validation du type de fichier : On vérifie que le fichier est bien une image en utilisant le type MIME. On peut aussi vérifier la taille du fichier.
39 => Création d'un répertoire structuré : On génère un chemin dynamique basé sur la date (uploads/2024/12/24/) pour organiser les images.
40 => Génération d'un nom de fichier unique : On utilise uniqid() pour générer un nom unique et éviter les conflits de nom.
41 => Déplacement du fichier sur le serveur : Le fichier est déplacé depuis son emplacement temporaire vers le répertoire uploads/.
42 => Enregistrement dans la base de données : On enregistre les informations de l'image dans la base de données. */
43
44 #[ORM\ManyToMany(targetEntity: NodeData::class, mappedBy: "images")]
45 private $node_data;
46
47 #[ORM\ManyToMany(targetEntity: Article::class, mappedBy: "images")]
48 private $article;
49
50 public function __construct(string $name, ?string $path, ?string $path_mini, string $mime_type, string $alt)
51 {
52 $this->file_name = $name;
53 $this->file_path = $path;
54 $this->file_path_mini = $path_mini;
55 $this->mime_type = $mime_type;
56 $this->alt = $alt;
57 }
58
59 public function getFileName(): string
60 {
61 return $this->file_name;
62 }
63 public function getFilePath(): string
64 {
65 return $this->file_path;
66 }
67 public function getFilePathMini(): string
68 {
69 return $this->file_path_mini;
70 }
71 public function getAlt(): string
72 {
73 return $this->alt;
74 }
75
76
77 // pour ViewBuilder?
78 /*public function displayImage($imageId): void
79 {
80 //$imageId = 1; // Exemple d'ID d'image
81 $stmt = $pdo->prepare("SELECT file_path FROM images WHERE id = ?");
82 $stmt->execute([$imageId]);
83 $image = $stmt->fetch();
84
85 if ($image) {
86 echo "<img src='" . $image['file_path'] . "' alt='Image'>";
87 } else {
88 echo "Image non trouvée.";
89 }
90 }*/
91}
diff --git a/src/model/entities/Node.php b/src/model/entities/Node.php
new file mode 100644
index 0000000..49e16ba
--- /dev/null
+++ b/src/model/entities/Node.php
@@ -0,0 +1,168 @@
1<?php
2// src/model/entities/Node.php
3
4declare(strict_types=1);
5
6namespace App\Entity;
7
8use Config;
9use Doctrine\ORM\Mapping as ORM;
10
11#[ORM\Entity]
12#[ORM\Table(name: TABLE_PREFIX . "node")]
13class Node
14{
15 #[ORM\Id]
16 #[ORM\GeneratedValue]
17 #[ORM\Column(type: "integer")]
18 private int $id_node;
19
20 #[ORM\Column(type: "string", length: 255)]
21 private string $name_node;
22
23 #[ORM\Column(type: "string", length: 255, unique: true, nullable: true)]
24 private ?string $article_timestamp;
25
26 #[ORM\Column(type: "json", nullable: true)] // type: "json" crée un longtext avec mariadb
27 private ?array $attributes = null;
28
29 #[ORM\Column(type: "integer")]
30 private int $position;
31
32 #[ORM\ManyToOne(targetEntity: self::class)]
33 //#[ORM\ManyToOne(targetEntity: self::class, fetch: 'EAGER')] // À TESTER
34 #[ORM\JoinColumn(name: "parent_id", referencedColumnName: "id_node", onDelete: "SET NULL", nullable: true)]
35 private ?self $parent = null;
36
37 #[ORM\ManyToOne(targetEntity: Page::class)]
38 #[ORM\JoinColumn(name: "page_id", referencedColumnName: "id_page", onDelete: "SET DEFAULT", nullable: true)]
39 private ?Page $page;
40
41 #[ORM\ManyToOne(targetEntity: Article::class, cascade: ['persist'])]
42 #[ORM\JoinColumn(name: "article_id", referencedColumnName: "id_article", onDelete: "SET NULL", nullable: true)]
43 private ?Article $article = null;
44
45 // propriété non mappée dans la table "node", la jointure est décrite dans NodeData
46 // elle sert à persister ou supprimer des données par cascade
47 // "mappedBy" permet de cibler $node dans l'autre classe, qui elle possède un "inversedBy"
48 #[ORM\OneToOne(targetEntity: NodeData::class, mappedBy: "node", cascade: ['persist', 'remove'])]
49 private ?NodeData $node_data = null;
50
51
52 // -- fin des attributs destinés à doctrine, début du code utilisateur --
53
54 private array $children = []; // tableau de Node
55 private ?self $temp_child = null; // = "new" est l'enfant de "main" lorsque la page est "article"
56
57 public function __construct(string $name = '', ?string $article_timestamp = null, array $attributes = [], int $position = 0, ?self $parent = null, ?Page $page = null, ?Article $article = null)
58 {
59 $this->name_node = $name;
60 $this->article_timestamp = $article_timestamp;
61 $this->attributes = $attributes;
62 $this->position = $position;
63 $this->parent = $parent;
64 $this->page = $page;
65 $this->article = $article;
66 }
67
68 public function addChild(self $child): void
69 {
70 $this->children[] = $child;
71 $this->sortChildren();
72 }
73
74 // utiliser $position pour afficher les éléments dans l'ordre
75 private function sortChildren(): void
76 {
77 $iteration = count($this->children);
78 while($iteration > 1)
79 {
80 for($i = 0; $i < $iteration - 1; $i++)
81 {
82 //echo '<br>' . $this->children[$i]->getPosition() . ' - ' . $this->children[$i + 1]->getPosition();
83 if($this->children[$i]->getPosition() > $this->children[$i + 1]->getPosition())
84 {
85 $tmp = $this->children[$i];
86 $this->children[$i] = $this->children[$i + 1];
87 $this->children[$i + 1] = $tmp;
88 }
89 }
90 $iteration--;
91 }
92 }
93
94 // pfff...
95 public function getId(): int
96 {
97 return $this->id_node;
98 }
99 public function getName(): string
100 {
101 return $this->name_node;
102 }
103 /*public function setName(string $name): void
104 {
105 $this->name_node = $name;
106 }*/
107 public function getArticleTimestamp(): string
108 {
109 return $this->article_timestamp;
110 }
111 public function getAttributes(): array
112 {
113 return $this->attributes;
114 }
115 /*public function setAttributes(array $attributes): void
116 {
117 $this->attributes = $attributes;
118 }*/
119 public function getParent(): ?self
120 {
121 return $this->parent;
122 }
123 /*public function setParent(?self $parent): void
124 {
125 $this->parent = $parent;
126 }*/
127 public function getPosition(): int
128 {
129 return $this->position;
130 }
131 /*public function setPosition(int $position): void
132 {
133 $this->position = $position;
134 }*/
135 public function getPage(): Page
136 {
137 return $this->page;
138 }
139 /*public function setPage(Page $page): void
140 {
141 $this->page = $page;
142 }*/
143 public function getArticle(): Article
144 {
145 return $this->article;
146 }
147 /*public function setArticle(Article $article): void
148 {
149 $this->article = $article;
150 }*/
151 public function getNodeData(): ?NodeData
152 {
153 return $this->node_data;
154 }
155 public function getChildren(): array
156 {
157 return $this->children;
158 }
159
160 public function getTempChild(): ?self // peut renvoyer null
161 {
162 return $this->temp_child;
163 }
164 public function setTempChild(self $child): void
165 {
166 $this->temp_child = $child;
167 }
168}
diff --git a/src/model/entities/NodeData.php b/src/model/entities/NodeData.php
new file mode 100644
index 0000000..ddf6083
--- /dev/null
+++ b/src/model/entities/NodeData.php
@@ -0,0 +1,62 @@
1<?php
2// src/model/entities/NodeData.php
3
4declare(strict_types=1);
5
6namespace App\Entity;
7
8use Doctrine\ORM\Mapping as ORM;
9use Doctrine\Common\Collections\ArrayCollection; // classe
10use Doctrine\Common\Collections\Collection; // interface
11
12#[ORM\Entity]
13#[ORM\Table(name: TABLE_PREFIX . "node_data")]
14class NodeData
15{
16 #[ORM\Id]
17 #[ORM\GeneratedValue]
18 #[ORM\Column(type: "integer")]
19 private int $id_node_data;
20
21 // onDelete: "CASCADE" supprime les données si le noeud est supprimé
22 // inverseBy fait le lien avec $node_data dans Node (qui a "mappedBy")
23 #[ORM\OneToOne(targetEntity: Node::class, inversedBy: "node_data")]
24 #[ORM\JoinColumn(name: "node_id", referencedColumnName: "id_node", onDelete: "CASCADE")]
25 private Node $node;
26
27 #[ORM\Column(type: "json")]
28 private array $data;
29
30 // liaison avec table intermédiaire
31 #[ORM\ManyToMany(targetEntity: Image::class, inversedBy: "id_image")]
32 #[ORM\JoinTable(
33 name: "nb_node_image",
34 joinColumns: [new ORM\JoinColumn(name: "node_data_id", referencedColumnName: "id_node_data", onDelete: "CASCADE")],
35 inverseJoinColumns: [new ORM\JoinColumn(name: "image_id", referencedColumnName: "id_image", onDelete: "CASCADE")]
36 )]
37 private Collection $images;
38
39 public function __construct(array $data, Node $node, Collection $images = new ArrayCollection)
40 {
41 $this->data = $data;
42 $this->node = $node;
43 $this->images = $images;
44 }
45
46 public function getData(): array
47 {
48 return $this->data;
49 }
50 /*public function setData(array $data): void
51 {
52 $this->data = $data;
53 }
54 public function setNode(Node $node): void
55 {
56 $this->node = $node;
57 }*/
58 public function getImages(): Collection
59 {
60 return $this->images;
61 }
62}
diff --git a/src/model/entities/Page.php b/src/model/entities/Page.php
new file mode 100644
index 0000000..d7d8098
--- /dev/null
+++ b/src/model/entities/Page.php
@@ -0,0 +1,97 @@
1<?php
2// src/model/entities/Page.php
3
4declare(strict_types=1);
5
6namespace App\Entity;
7
8use Doctrine\ORM\Mapping as ORM;
9use Doctrine\Common\Collections\Collection;
10use Doctrine\Common\Collections\ArrayCollection;
11
12#[ORM\Entity]
13#[ORM\Table(name: TABLE_PREFIX . "page")]
14class Page
15{
16 #[ORM\Id]
17 #[ORM\GeneratedValue]
18 #[ORM\Column(type: "integer")]
19 private int $id_page;
20
21 #[ORM\Column(type: "string", length: 255)]
22 private string $name_page;
23
24 #[ORM\Column(type: "string", length: 255)]
25 private string $end_of_path; // morceau d'URL plus exactement
26
27 private string $page_path;
28
29 #[ORM\Column(type: "boolean")]
30 private bool $reachable;
31
32 #[ORM\Column(type: "boolean")]
33 private bool $in_menu;
34
35 #[ORM\ManyToOne(targetEntity: self::class)]
36 #[ORM\JoinColumn(name: "parent_id", referencedColumnName: "id_page", onDelete: "SET NULL", nullable: true)]
37 private ?self $parent = null;
38
39 #[ORM\OneToMany(targetEntity: self::class, mappedBy: 'parent')]
40 protected Collection $children;
41
42 /*#[ORM\Column(type: "json", nullable: true)]
43 private ?array $metadata = null;*/
44
45 public function __construct(string $name, string $eop, bool $reachable, bool $in_menu, ?Page $parent)
46 {
47 $this->name_page = $name;
48 $this->end_of_path = $eop;
49 $this->reachable = $reachable;
50 $this->in_menu = $in_menu;
51 $this->parent = $parent;
52 $this->children = new ArrayCollection();
53 }
54
55 // getters
56 /*public function getId(): int
57 {
58 return $this->id_page;
59 }*/
60 public function getPageName(): string
61 {
62 return $this->name_page;
63 }
64 public function getPagePath(): string
65 {
66 return $this->page_path;
67 }
68 public function getEndOfPath(): string
69 {
70 return $this->end_of_path;
71 }
72 public function getInMenu(): bool
73 {
74 return $this->in_menu;
75 }
76 public function getParent(): ?Page
77 {
78 return $this->parent;
79 }
80 public function getChildren(): Collection
81 {
82 return $this->children;
83 }
84
85 public function fillChildrenPagePath(string $parent_path = ''): void
86 {
87 $this->page_path = $parent_path != '' ? $parent_path . '/' . $this->end_of_path : $this->end_of_path;
88 foreach($this->getChildren() as $page){
89 $page->fillChildrenPagePath($this->page_path);
90 }
91 }
92
93 public function addChild(self $child): void
94 {
95 $this->children[] = $child;
96 }
97}
diff --git a/src/model/entities/User.php b/src/model/entities/User.php
new file mode 100644
index 0000000..4b1dcb8
--- /dev/null
+++ b/src/model/entities/User.php
@@ -0,0 +1,47 @@
1<?php
2// src/model/entities/User.php
3
4declare(strict_types=1);
5
6namespace App\Entity;
7
8use Doctrine\ORM\Mapping as ORM;
9
10#[ORM\Entity]
11#[ORM\Table(name: TABLE_PREFIX . "user")]
12class User
13{
14 #[ORM\Id]
15 #[ORM\GeneratedValue]
16 #[ORM\Column(type: "integer")]
17 private int $id_user;
18
19 /*#[ORM\Column(type: "string", length: 255)]
20 private string $name;*/
21
22 #[ORM\Column(type: "string", length: 255, unique: true)] // risque de modifier son mot de passe sans s'apercevoir qu'il fonctionne encore sur un autre compte
23 private string $login;
24
25 #[ORM\Column(type: "string", length: 255)]
26 private string $password;
27
28 public function __construct(string $login, string $password)
29 {
30 $this->login = $login;
31 $this->password = $password;
32 }
33
34 public function getLogin(): string
35 {
36 return $this->login;
37 }
38 public function getPassword(): string
39 {
40 return $this->password;
41 }
42
43 public function setPassword(string $password): void
44 {
45 $this->password = $password;
46 }
47} \ No newline at end of file
diff --git a/src/view/AbstractBuilder.php b/src/view/AbstractBuilder.php
new file mode 100644
index 0000000..cd2b361
--- /dev/null
+++ b/src/view/AbstractBuilder.php
@@ -0,0 +1,52 @@
1<?php
2// src/view/AbstractBuilder.php
3
4use App\Entity\Node;
5
6abstract class AbstractBuilder
7{
8 protected const VIEWS_PATH = '../src/view/templates/';
9 protected string $html = '';
10
11 protected function useChildrenBuilder(Node $node): void
12 {
13 foreach($node->getChildren() as $child_node)
14 {
15 $builder_name = $this->snakeToPascalCase($child_node->getName()) . 'Builder';
16 $builder = new $builder_name($child_node);
17 $this->html .= $builder->render();
18
19 // pages spéciales où on n'assemble pas tout
20 if($builder_name === 'HeadBuilder' && $builder->getStop())
21 {
22 foreach($node->getChildren() as $target_node){
23 if($target_node->getName() === 'main'){
24 $main_node = $target_node;
25 break;
26 }
27 }
28 // on construit <main> et on s'arrête! les autres noeuds sont ignorés
29 $builder_name = $this->snakeToPascalCase($main_node->getName()) . 'Builder';
30 $builder = new $builder_name($main_node);
31 $this->html .= "<body>\n";
32 $this->html .= $builder->render() . "\n";
33 $this->html .= "</body>\n</html>";
34 break;
35 }
36 }
37 }
38
39 protected function snakeToPascalCase(string $input): string
40 {
41 return str_replace('_', '', ucwords($input, '_'));
42 }
43
44 public function render(): string // = getHTML()
45 {
46 return $this->html;
47 }
48 public function addHTML(string $html): void
49 {
50 $this->html .= $html;
51 }
52} \ No newline at end of file
diff --git a/src/view/ArticleBuilder.php b/src/view/ArticleBuilder.php
new file mode 100644
index 0000000..989da0d
--- /dev/null
+++ b/src/view/ArticleBuilder.php
@@ -0,0 +1,60 @@
1<?php
2// src/view/ArticleBuilder.php
3
4use App\Entity\Node;
5
6class ArticleBuilder extends AbstractBuilder
7{
8 public function __construct(Node $node)
9 {
10 $viewFile = self::VIEWS_PATH . $node->getName() . '.php';
11
12 if(file_exists($viewFile))
13 {
14 // id (timestamp)
15 if(!empty($node->getAttributes()))
16 {
17 extract($node->getAttributes());
18 }
19
20 // html
21 $title = $node->getArticle()->getTitle();
22 $html = $node->getArticle()->getContent();
23 $id = $node->getArticleTimestamp();
24
25 // partage
26 $share_link = new URL(['page' => CURRENT_PAGE], $id);
27 $share_js = 'onclick="copyInClipBoard(\'' . $share_link . '\')"';
28 $share_button = '<p><a href="' . $share_link . '" ' . $share_js . '><img class="action_icon" src="assets/share.svg"></a></p>' . "\n";
29
30 // modifier un article
31 $admin_buttons = '';
32 if($_SESSION['admin'])
33 {
34 $modify_js = 'onclick="openEditor(\'' . $id . '\')"';
35 $modify_article = '<p id="edit-' . $id . '"><a href="#"><img class="action_icon" src="assets/edit.svg" ' . $modify_js . '></a></p>' . "\n";
36
37 $up_link = new URL(['page' => CURRENT_PAGE, 'id' => $id, 'action' => 'position_up']);
38 $up_button = '<p id="position_up-' . $id . '"><a href="' . $up_link . '"><img class="action_icon" src="assets/arrow-up.svg"></a></p>' . "\n";
39
40 $down_link = new URL(['page' => CURRENT_PAGE, 'id' => $id, 'action' => 'position_down']);
41 $down_button = '<p id="position_down-' . $id . '"><a href="' . $down_link . '"><img class="action_icon" src="assets/arrow-down.svg"></a></p>' . "\n";
42
43 $delete_js = 'onclick="deleteArticle(\'' . $id . '\')"';
44 $delete_article = '<p id="delete-' . $id . '"><a href="#"><img class="action_icon" src="assets/delete-bin.svg" ' . $delete_js . '></a></p>' . "\n";
45
46 $close_js = 'onclick="closeEditor(\'' . $id . '\')"';
47 $close_editor = '<p id="cancel-' . $id . '" class="hidden"><a href="#"><button ' . $close_js . '>Annuler</button></a></p>';
48
49 $submit_js = 'onclick="submitArticle(\'' . $id . '\')"';
50 $submit_article = '<p id="submit-' . $id . '" class="hidden"><a href="#"><button ' . $submit_js . '>Valider</button></a></p>';
51
52 $admin_buttons = $modify_article . $up_button . $down_button . $delete_article . $close_editor . $submit_article;
53 }
54
55 ob_start();
56 require($viewFile);
57 $this->html .= ob_get_clean();
58 }
59 }
60}
diff --git a/src/view/BlogBuilder.php b/src/view/BlogBuilder.php
new file mode 100644
index 0000000..8c2125f
--- /dev/null
+++ b/src/view/BlogBuilder.php
@@ -0,0 +1,49 @@
1<?php
2// src/view/BlogBuilder.php
3
4use App\Entity\Node;
5
6class BlogBuilder extends AbstractBuilder
7{
8 public function __construct(Node $node)
9 {
10 $viewFile = self::VIEWS_PATH . $node->getName() . '.php';
11
12 if(file_exists($viewFile))
13 {
14 if(!empty($node->getNodeData()->getData()))
15 {
16 extract($node->getNodeData()->getData());
17 }
18
19 // ajouter un article
20 $new_article = '';
21 $new_article_admin_buttons = '';
22 if($_SESSION['admin'])
23 {
24 $id = 'new';
25
26 //$link = new URL(['page' => CURRENT_PAGE, 'action' => 'open_editor']);
27 $js = 'onclick="openEditor(\'' . $id . '\')"';
28 //$new_article = '<article><a href="' . $link . '"><button>Nouvel article</button></a></article>';
29 $new_article = '<article><p id="new"></p>' . "\n" .
30 '<p id="new-' . $id . '"><a href="#"><button ' . $js . '><img class="action_icon" src="assets/edit.svg">Nouvel article</button></a></p>';
31
32 $close_js = 'onclick="closeEditor(\'' . $id . '\')"';
33 $close_editor = '<div class="article_admin_zone"><p id="cancel-' . $id . '" class="hidden"><a href="#"><button ' . $close_js . '>Annuler</button></a></p>';
34
35 $submit_js = 'onclick="submitArticle(\'' . $id . '\')"';
36 $submit_article = '<p id="submit-' . $id . '" class="hidden"><a href="#"><button ' . $submit_js . '>Valider</button></a></p></div></article>';
37
38 $new_article_admin_buttons = $close_editor . $submit_article;
39 }
40
41 $this->useChildrenBuilder($node);
42 $content = $this->html;
43
44 ob_start();
45 require $viewFile;
46 $this->html = ob_get_clean(); // pas de concaténation ici, on écrase
47 }
48 }
49} \ No newline at end of file
diff --git a/src/view/BreadcrumbBuilder.php b/src/view/BreadcrumbBuilder.php
new file mode 100644
index 0000000..f1fdddf
--- /dev/null
+++ b/src/view/BreadcrumbBuilder.php
@@ -0,0 +1,53 @@
1<?php
2// src/view/BreadcrumbBuilder.php
3
4use App\Entity\Node;
5
6class BreadcrumbBuilder extends AbstractBuilder
7{
8 public function __construct(Node $node)
9 {
10 $this->html = $this->breadcrumbHTML(false);
11 }
12
13 private function breadcrumbHTML(bool $links = false): string
14 {
15 $asset = 'assets/home.svg'; // => BDD?
16 $breadcrumb_array = Director::$page_path->getArray(); // tableau de Page
17 $html = '';
18 $nb_of_entries = count($breadcrumb_array);
19
20 if($nb_of_entries > 1)
21 {
22 // petite maison et flèche
23 $html .= '<nav class="breadcrumb" aria-label="Breadcrumb">' . "\n";
24 $html .= '<a href="' . new URL . '"><img src="' . $asset . '"></a><span class="arrow"> →</span>' . "\n";
25
26 // partie intermédiaire
27 for($i = 0; $i < ($nb_of_entries - 1); $i++)
28 {
29 // liens optionnels
30 if($links)
31 {
32 $html .= '<a href="';
33 for($j = 1; $j < $i; $j++) // chemin sans la fin
34 {
35 $html .= new URL(['page' => $breadcrumb_array[$i]->getPagePath()]);
36 }
37 $html .= '">';
38 }
39 $html .= '<span>' . $breadcrumb_array[$i]->getPageName() . '</span>';
40 if($links)
41 {
42 $html .= '</a>';
43 }
44 $html .= '<span class="arrow"> →</span>' . "\n";
45 }
46
47 // fin du chemin
48 $html .= '<span aria-current="page">' . $breadcrumb_array[$nb_of_entries - 1]->getPageName() . "</span>\n";
49 $html .= "</nav>\n";
50 }
51 return $html;
52 }
53} \ No newline at end of file
diff --git a/src/view/FooterBuilder.php b/src/view/FooterBuilder.php
new file mode 100644
index 0000000..49da71c
--- /dev/null
+++ b/src/view/FooterBuilder.php
@@ -0,0 +1,64 @@
1<?php
2// src/view/FooterBuilder.php
3
4use App\Entity\Node;
5
6class FooterBuilder extends AbstractBuilder
7{
8 public function __construct(Node $node)
9 {
10 $viewFile = self::VIEWS_PATH . $node->getName() . '.php';
11
12 if(file_exists($viewFile))
13 {
14 // $adresses postale et e-mail
15 if(!empty($node->getNodeData()->getData()))
16 {
17 extract($node->getNodeData()->getData());
18 }
19
20 $this->useChildrenBuilder($node);
21 $breadcrumb = $this->html;
22
23 // zone admin
24 $empty_admin_zone = '';
25 //$zone_admin = '';
26 if($_SESSION['admin'])
27 {
28 $div_admin = 'logged_in';
29 $empty_admin_zone = 'empty_admin_zone';
30 $link_edit_page = new URL(['page' => CURRENT_PAGE, 'action' => 'modif_page']);
31 $link_new_page = new URL(['from' => CURRENT_PAGE, 'page' => 'nouvelle_page']);
32 $link_change_paths = new URL(['from' => CURRENT_PAGE, 'page' => 'menu_chemins']);
33
34 $link_change_password = new URL(['from' => CURRENT_PAGE, 'action' => 'modif_mdp']);
35 isset($_GET['id']) ? $link_change_password->addParams(['id' => $_GET['id']]) : '';
36
37 $link_logout = new URL(['page' => CURRENT_PAGE, 'action' => 'deconnexion']);
38 isset($_GET['id']) ? $link_logout->addParams(['id' => $_GET['id']]) : '';
39
40 $zone_admin = '<p>Vous êtes en mode administrateur.' . "\n" .
41 '<a href="' . $link_edit_page . '"><button>Modifier la page</button></a>' . "\n" .
42 '<a href="' . $link_new_page . '"><button>Nouvelle page</button></a>' . "\n" .
43 '<a href="' . $link_change_paths . '"><button>Menu et chemins</button></a>' . "\n" .
44 '<a href="' . $link_change_password . '"><button>Changer de mot de passe</button></a>' . "\n" .
45 '<a href="' . $link_logout . '"><button>Déconnexion</button></a></p>' . "\n";
46 }
47 else
48 {
49 $div_admin = 'logged_out';
50 $zone_admin = '';
51 if(Director::$page_path->getLast()->getEndOfPath() === 'article' && isset($_GET['id'])){
52 $zone_admin = '<button><a href="' . new URL(['page' => 'connexion', 'from' => CURRENT_PAGE, 'id' => $_GET['id']]) . '">Mode admin</a></button>';
53 }
54 else{
55 $zone_admin = '<button><a href="' . new URL(['page' => 'connexion', 'from' => CURRENT_PAGE]) . '">Mode admin</a></button>';
56 }
57 }
58
59 ob_start();
60 require $viewFile;
61 $this->html = ob_get_clean();
62 }
63 }
64} \ No newline at end of file
diff --git a/src/view/GaleryBuilder.php b/src/view/GaleryBuilder.php
new file mode 100644
index 0000000..89be2b1
--- /dev/null
+++ b/src/view/GaleryBuilder.php
@@ -0,0 +1,49 @@
1<?php
2// src/view/GaleryBuilder.php
3
4use App\Entity\Node;
5
6class GaleryBuilder extends AbstractBuilder
7{
8 public function __construct(Node $node)
9 {
10 $viewFile = self::VIEWS_PATH . $node->getName() . '.php';
11
12 if(file_exists($viewFile))
13 {
14 if(!empty($node->getNodeData()->getData()))
15 {
16 extract($node->getNodeData()->getData());
17 }
18
19 // ajouter un article
20 $new_article = '';
21 $new_article_admin_buttons = '';
22 if($_SESSION['admin'])
23 {
24 $id = 'new';
25
26 //$link = new URL(['page' => CURRENT_PAGE, 'action' => 'open_editor']);
27 $js = 'onclick="openEditor(\'' . $id . '\')"';
28 //$new_article = '<article><a href="' . $link . '"><button>Nouvel article</button></a></article>';
29 $new_article = '<article><p id="new"></p>' . "\n" .
30 '<p id="new-' . $id . '"><a href="#"><button ' . $js . '><img class="action_icon" src="assets/edit.svg">Nouvel article</button></a></p>';
31
32 $close_js = 'onclick="closeEditor(\'' . $id . '\')"';
33 $close_editor = '<div class="article_admin_zone"><p id="cancel-' . $id . '" class="hidden"><a href="#"><button ' . $close_js . '>Annuler</button></a></p>';
34
35 $submit_js = 'onclick="submitArticle(\'' . $id . '\')"';
36 $submit_article = '<p id="submit-' . $id . '" class="hidden"><a href="#"><button ' . $submit_js . '>Valider</button></a></p></div></article>';
37
38 $new_article_admin_buttons = $close_editor . $submit_article;
39 }
40
41 $this->useChildrenBuilder($node);
42 $content = $this->html;
43
44 ob_start();
45 require $viewFile;
46 $this->html = ob_get_clean(); // pas de concaténation ici, on écrase
47 }
48 }
49} \ No newline at end of file
diff --git a/src/view/GridBuilder.php b/src/view/GridBuilder.php
new file mode 100644
index 0000000..6e16d46
--- /dev/null
+++ b/src/view/GridBuilder.php
@@ -0,0 +1,55 @@
1<?php
2// src/view/GridBuilder.php
3
4use App\Entity\Node;
5
6class GridBuilder extends AbstractBuilder
7{
8 public function __construct(Node $node)
9 {
10 $viewFile = self::VIEWS_PATH . $node->getName() . '.php';
11
12 if(file_exists($viewFile))
13 {
14 if(!empty($node->getNodeData()->getData()))
15 {
16 extract($node->getNodeData()->getData());
17 }
18
19 // ajouter un article
20 $new_article = '';
21 $new_article_admin_buttons = '';
22 if($_SESSION['admin'])
23 {
24 $id = 'new';
25 $js = 'onclick="openEditor(\'' . $id . '\')"';
26
27 if(Director::$page_path->getLast()->getEndOfPath() === 'accueil')
28 {
29 $new_article = '<article><p id="new"></p>' . "\n" .
30 '<p><a href="' . new URL(['page' => 'article']) . '"><button><img class="action_icon" src="assets/edit.svg">Nouvel article</button></a></p>';
31 }
32 else
33 {
34 $new_article = '<article><p id="new"></p>' . "\n" .
35 '<p id="new-' . $id . '"><a href="#"><button ' . $js . '><img class="action_icon" src="assets/edit.svg">Nouvel article</button></a></p>';
36 }
37
38 $close_js = 'onclick="closeEditor(\'' . $id . '\')"';
39 $close_editor = '<div class="article_admin_zone"><p id="cancel-' . $id . '" class="hidden"><a href="#"><button ' . $close_js . '>Annuler</button></a></p>';
40
41 $submit_js = 'onclick="submitArticle(\'' . $id . '\')"';
42 $submit_article = '<p id="submit-' . $id . '" class="hidden"><a href="#"><button ' . $submit_js . '>Valider</button></a></p></div></article>';
43
44 $new_article_admin_buttons = $close_editor . $submit_article;
45 }
46
47 $this->useChildrenBuilder($node);
48 $content = $this->html;
49
50 ob_start();
51 require $viewFile;
52 $this->html = ob_get_clean(); // pas de concaténation ici, on écrase
53 }
54 }
55} \ No newline at end of file
diff --git a/src/view/HeadBuilder.php b/src/view/HeadBuilder.php
new file mode 100644
index 0000000..c31c930
--- /dev/null
+++ b/src/view/HeadBuilder.php
@@ -0,0 +1,68 @@
1<?php
2// src/view/HeadBuilder.php
3
4use App\Entity\Node;
5
6class HeadBuilder extends AbstractBuilder
7{
8 private bool $stop = false;
9
10 public function __construct(Node $node)
11 {
12 $viewFile = self::VIEWS_PATH . $node->getName() . '.php';
13
14 if(file_exists($viewFile))
15 {
16 // css et js
17 if(!empty($node->getAttributes()))
18 {
19 extract($node->getAttributes());
20 }
21
22 // pages spéciales où on n'assemble pas tout
23 $this->stop = isset($stop) ? $stop : false;
24 $css = '';
25 foreach($css_array as $name)
26 {
27 $css .= '<link rel="stylesheet" href="css/' . $name . '.css">' . "\n";
28 }
29 $js = '';
30 foreach($js_array as $name)
31 {
32 $js .= '<script src="js/' . $name . '.js"></script>' . "\n";
33 }
34
35 // tinymce, nécéssite un script de copie dans composer.json
36 if($_SESSION['admin']){
37 $css .= '<link rel="stylesheet" href="css/tinymce.css">' . "\n";
38 $js .= '<script src="js/tinymce/tinymce.min.js"></script>' . "\n";
39 $js .= '<script src="js/tinymce.js"></script>' . "\n";
40 }
41
42 // titre et description
43 if(!empty($node->getNodeData()->getData()))
44 {
45 extract($node->getNodeData()->getData());
46 }
47
48 // favicon
49 foreach($node->getNodeData()->getImages() as $image)
50 {
51 if(str_contains($image->getFileName(), 'favicon'))
52 {
53 $favicon = rtrim($image->getFilePathMini(), '/');
54 $alt = $image->getAlt();
55 }
56 }
57
58 ob_start();
59 require $viewFile;
60 $this->html .= ob_get_clean();
61 }
62 }
63
64 public function getStop(): bool
65 {
66 return $this->stop;
67 }
68}
diff --git a/src/view/HeaderBuilder.php b/src/view/HeaderBuilder.php
new file mode 100644
index 0000000..252958a
--- /dev/null
+++ b/src/view/HeaderBuilder.php
@@ -0,0 +1,64 @@
1<?php
2// src/view/HeaderBuilder.php
3
4use App\Entity\Node;
5
6class HeaderBuilder extends AbstractBuilder
7{
8 private ?Node $nav = null;
9
10 public function __construct(Node $node)
11 {
12 // nav
13 // n'utilise pas useChildrenBuilder, il faudrait peut-être
14 $children = $node->getChildren();
15 foreach($children as $child)
16 {
17 if($child->getName() === 'nav')
18 {
19 $this->nav = $child;
20 $nav_builder = new NavBuilder($this->nav);
21 $nav = $nav_builder->render();
22 }
23 }
24
25 $viewFile = self::VIEWS_PATH . $node->getName() . '.php';
26
27 if(file_exists($viewFile))
28 {
29 // titre et description
30 if(!empty($node->getNodeData()->getData()))
31 {
32 extract($node->getNodeData()->getData());
33 }
34
35 // attributs, aucun pour l'instant
36 if(!empty($node->getAttributes()))
37 {
38 extract($node->getAttributes());
39 }
40
41 // header logo + réseaux sociaux
42 $targets = ['logo', 'facebook', 'instagram', 'fond_piscine'];
43 $i = 0;
44 foreach($node->getNodeData()->getImages() as $image)
45 {
46 if(str_contains($image->getFileName(), $targets[$i]))
47 {
48 $var = $targets[$i];
49 $$var = rtrim($image->getFilePathMini(), '/');
50 $var .= '_alt'; // ex: logo_alt
51 $$var = $image->getAlt();
52 }
53 $i++;
54 }
55
56 // générer HTML réseaux sociaux
57 //
58
59 ob_start();
60 require $viewFile;
61 $this->html .= ob_get_clean();
62 }
63 }
64} \ No newline at end of file
diff --git a/src/view/LoginBuilder.php b/src/view/LoginBuilder.php
new file mode 100644
index 0000000..ac9910f
--- /dev/null
+++ b/src/view/LoginBuilder.php
@@ -0,0 +1,15 @@
1<?php
2// src/view/LoginBuilder.php
3
4use App\Entity\Node;
5
6class LoginBuilder extends AbstractBuilder
7{
8 public function __construct(Node $node)
9 {
10 global $entityManager;
11
12 // une classe Password ce serait pas mal!!
13 connect($this, $entityManager);
14 }
15}
diff --git a/src/view/MainBuilder.php b/src/view/MainBuilder.php
new file mode 100644
index 0000000..a44570b
--- /dev/null
+++ b/src/view/MainBuilder.php
@@ -0,0 +1,30 @@
1<?php
2// src/view/MainBuilder.php
3
4use App\Entity\Node;
5
6class MainBuilder extends AbstractBuilder
7{
8 public function __construct(Node $node)
9 {
10 $this->html .= "<main>\n";
11
12 if(Director::$page_path->getLast()->getEndOfPath() === 'article'){
13 if($node->getTempChild() == null){
14 $new = new Node;
15 }
16 else{
17 $new = $node->getTempChild();
18 }
19 //$builder_name = $this->snakeToPascalCase($new->getName()) . 'Builder';
20 $builder_name = 'NewBuilder';
21 $builder = new $builder_name($new);
22 $this->html .= $builder->render();
23 }
24 else{
25 $this->useChildrenBuilder($node);
26 }
27
28 $this->html .= "</main>\n";
29 }
30}
diff --git a/src/view/NavBuilder.php b/src/view/NavBuilder.php
new file mode 100644
index 0000000..e7254b1
--- /dev/null
+++ b/src/view/NavBuilder.php
@@ -0,0 +1,61 @@
1<?php
2// src/view/NavBuilder.php
3
4declare(strict_types=1);
5
6use App\Entity\Node;
7use App\Entity\Page;
8
9class NavBuilder extends AbstractBuilder
10{
11 public function __construct(Node $node)
12 {
13 $this->html .= '<nav class="nav_main"><ul>';
14 $this->html .= $this->navMainHTML(Director::$menu_data, Director::$page_path->getArray());
15 $this->html .= '</ul></nav>';
16 }
17
18 private function navMainHTML(Page $nav_data, array $current): string
19 {
20 $nav_html = '';
21 static $level = 0;
22
23 foreach($nav_data->getChildren() as $data)
24 {
25 $class = '';
26 if(isset($current[$level]) && $data->getEndOfPath() === $current[$level]->getEndOfPath()){
27 $class = ' current';
28 }
29
30 if(count($data->getChildren()) > 0) // titre de catégorie
31 {
32 $nav_html .= '<li class="drop-down'. $class . '"><p>' . $data->getPageName() . '</p><ul class="sub-menu">' . "\n";
33 $level++;
34 $nav_html .= $this->navMainHTML($data, $current);
35 $level--;
36 $nav_html .= '</ul></li>' . "\n";
37 }
38 else
39 {
40 $target = '';
41 if(str_starts_with($data->getEndOfPath(), 'http')) // lien vers autre site
42 {
43 $link = $data->getEndOfPath(); // $link = chaine
44 $target = ' target="_blank"';
45 }
46 elseif($data->getEndOfPath() != '') // lien relatif
47 {
48 $link = new URL(['page' => $data->getPagePath()]); // $link = objet
49 }
50 /*else
51 {
52 echo "else page d'accueil" . '<br>';
53 $link = new URL; // page d'accueil
54 }*/
55
56 $nav_html .= '<a href="' . $link . '"' . $target . '><li class="'. $class . '"><p>' . $data->getPageName() . '</p></li></a>' . "\n";
57 }
58 }
59 return $nav_html;
60 }
61} \ No newline at end of file
diff --git a/src/view/NewBuilder.php b/src/view/NewBuilder.php
new file mode 100644
index 0000000..605c174
--- /dev/null
+++ b/src/view/NewBuilder.php
@@ -0,0 +1,93 @@
1<?php
2// src/view/NewBuilder.php
3
4use App\Entity\Node;
5
6class NewBuilder extends AbstractBuilder
7{
8 public function __construct(Node $node)
9 {
10 $viewFile = self::VIEWS_PATH . $node->getName() . '.php';
11
12 if(file_exists($viewFile))
13 {
14 // id (timestamp)
15 if(!empty($node->getAttributes()))
16 {
17 extract($node->getAttributes());
18 }
19
20 // html, date
21 $title = $node->getArticle()->getTitle();
22 $preview = $node->getArticle()->getPreview();
23 $id = $node->getArticleTimestamp();
24 $content = '';
25
26 // page article unique
27 if(Director::$page_path->getLast()->getEndOfPath() === 'article')
28 {
29 $content = $node->getArticle()->getContent();
30 $from_to_button = '<p><a class="link_to_article" href="' . new URL(['page' => 'accueil']) . '"><button>Retour page d\'accueil</button></a></p>';
31 }
32 // page d'accueil (avec des news)
33 else
34 {
35 $from_to_button = '<p><a class="link_to_article" href="' . new URL(['page' => 'article', 'id' => $id]) . '"><button><img class="action_icon" src="assets/book-open.svg">Lire la suite</button></a></p>';
36 }
37
38
39 $date_object = $node->getArticle()->getDateTime(); // class DateTime
40 $date = 'le ' . str_replace(':', 'h', $date_object->format('d-m-Y à H:i'));
41
42 // partage
43 $share_link = new URL(['page' => CURRENT_PAGE], $id);
44 isset($_GET['id']) ? $share_link->addParams(['id' => $_GET['id']]) : '';
45 $share_js = 'onclick="copyInClipBoard(\'' . $share_link . '\')"';
46 $share_button = '<p><a href="' . $share_link . '" ' . $share_js . '><img class="action_icon" src="assets/share.svg"></a></p>' . "\n";
47
48 // modifier un article
49 $admin_buttons = '';
50 if($_SESSION['admin'])
51 {
52 if(Director::$page_path->getLast()->getEndOfPath() === 'article'){
53 $modify_js = 'onclick="openEditor(\'' . $id . '\')"';
54 $modify_article = '<p id="edit-' . $id . '"><a href="#"><img class="action_icon" src="assets/edit.svg" ' . $modify_js . '></a></p>' . "\n";
55
56 $up_button = '<p id="position_up-' . $id . '"></p>' . "\n";
57 $down_button = '<p id="position_down-' . $id . '"></p>' . "\n";
58
59 $delete_js = 'onclick="deleteArticle(\'' . $id . '\', \'' . CURRENT_PAGE . '\')"';
60 $delete_article = '<p id="delete-' . $id . '"><a href="#"><img class="action_icon" src="assets/delete-bin.svg" ' . $delete_js . '></a></p>' . "\n";
61
62 $close_js = 'onclick="closeEditor(\'' . $id . '\')"';
63 $close_editor = '<p id="cancel-' . $id . '" class="hidden"><a href="#"><button ' . $close_js . '>Annuler</button></a></p>';
64
65 $submit_js = 'onclick="submitArticle(\'' . $id . '\')"';
66 $submit_article = '<p id="submit-' . $id . '" class="hidden"><a href="#"><button ' . $submit_js . '>Valider</button></a></p>';
67 }
68 else{
69 $modify_article = '<p id="edit-' . $id . '"></p>' . "\n";
70
71 $up_link = new URL(['page' => CURRENT_PAGE, 'id' => $id, 'action' => 'position_up']);
72 $up_button = '<p id="position_up-' . $id . '"><a href="' . $up_link . '"><img class="action_icon" src="assets/arrow-up.svg"></a></p>' . "\n";
73
74 $down_link = new URL(['page' => CURRENT_PAGE, 'id' => $id, 'action' => 'position_down']);
75 $down_button = '<p id="position_down-' . $id . '"><a href="' . $down_link . '"><img class="action_icon" src="assets/arrow-down.svg"></a></p>' . "\n";
76
77 $delete_js = 'onclick="deleteArticle(\'' . $id . '\')"';
78 $delete_article = '<p id="delete-' . $id . '"><a href="#"><img class="action_icon" src="assets/delete-bin.svg" ' . $delete_js . '></a></p>' . "\n";
79
80 $close_editor = '<p id="cancel-' . $id . '" class="hidden"></p>';
81 $submit_article = '<p id="submit-' . $id . '" class="hidden"></p>';
82
83 $submit_article = '<p id="submit-' . $id . '" class="hidden"></p>';
84 }
85 $admin_buttons = $modify_article . $up_button . $down_button . $delete_article . $close_editor . $submit_article;
86 }
87
88 ob_start();
89 require($viewFile);
90 $this->html .= ob_get_clean();
91 }
92 }
93}
diff --git a/src/view/ViewBuilder.php b/src/view/ViewBuilder.php
new file mode 100644
index 0000000..acac972
--- /dev/null
+++ b/src/view/ViewBuilder.php
@@ -0,0 +1,16 @@
1<?php
2// src/view/ViewBuilder.php
3//
4// appelle les autres Builder
5
6declare(strict_types=1);
7
8use App\Entity\Node;
9
10class ViewBuilder extends AbstractBuilder
11{
12 public function __construct(Node $root_node)
13 {
14 $this->useChildrenBuilder($root_node);
15 }
16}
diff --git a/src/view/password.php b/src/view/password.php
new file mode 100644
index 0000000..0ff717c
--- /dev/null
+++ b/src/view/password.php
@@ -0,0 +1,152 @@
1<?php
2// src/view/password.php
3//
4// ce fichier contient le HTML de deux pages du site:
5// - connexion au mode admin
6// - changement de mot de passe
7//
8// rajouter la page "créationn du mot de passe"?
9
10// insertion du captcha
11ob_start();
12?>
13 <p>Montrez que vous n'êtes pas un robot.<br>
14 <label for="captcha" >Combien font <?= $captcha[0] ?> fois <?= $captcha[1] ?>?</label>
15 <input required type="text" id="captcha" name="captcha" autocomplete="off" size="1">
16 </p>
17<?php
18$captchaHtml = ob_get_clean();
19
20
21// formulaire connexion
22$link = new URL(['page' => 'connexion']);
23isset($_GET['from']) ? $link->addParams(['from' => $_GET['from']]) : '';
24isset($_GET['id']) ? $link->addParams(['id' => $_GET['id']]) : '';
25ob_start();
26?>
27 <form class="connexionFormulaire" method="post" action="<?= $link ?>" >
28 <p><label for="login" >Identifiant (E-mail):</label>
29 <input id="login" type="text" name="login" autofocus required></p>
30 <p><label for="password" >Mot de passe:</label>
31 <input id="password" type="password" name="password" required></p>
32
33 <?= $captchaHtml ?>
34
35 <input type="submit" value="Valider">
36 </form>
37<?php
38$formulaireConnexion = ob_get_clean();
39
40// formulaire création du mot de passe
41ob_start();
42?>
43 <form class="connexionFormulaire" method="post" action="index.php" >
44 <p><label for="login" >Identifiant (e-mail):</label>
45 <input id="login" type="text" name="login" autofocus required></p>
46 <p><label for="password" >Mot de passe:</label>
47 <input id="password" type="password" name="password" required></p>
48
49 <?= $captchaHtml ?>
50
51 <input type="submit" value="Valider">
52 </form>
53<?php
54$formulaireNouveauMDP = ob_get_clean();
55
56// formulaire changement de mot de passe
57$link = new URL(['action' => 'modif_mdp']);
58isset($_GET['from']) ? $link->addParams(['from' => $_GET['from']]) : '';
59isset($_GET['id']) ? $link->addParams(['id' => $_GET['id']]) : '';
60ob_start();
61?>
62 <form class="connexionFormulaire" method="post" action="<?= $link ?>" >
63 <label for="login" >Identifiant (e-mail):</label>
64 <input id="login" type="login" name="login" autofocus required ><br><br>
65 <label for="old_password" >Ancien mot de passe:</label>
66 <input id="old_password" type="password" name="old_password" required ><br><br>
67 <label for="new_password" >Nouveau mot de passe:</label>
68 <input id="new_password" type="password" name="new_password" required autocomplete="off">
69 <br><br>
70 <input type="submit" value="Valider" >
71 <!-- <a href="index.php<?= $from ?>" >
72 <input type="button" value="Annuler">
73 </a> -->
74 </form>
75<?php
76$formulaireModifMDP = ob_get_clean();
77
78// en-tête
79ob_start();
80?>
81<!DOCTYPE html>
82
83<html lang="fr">
84 <head>
85 <meta charset="utf-8">
86 <title><?= $title ?></title>
87
88 <link rel="icon" type="image/png" href="assets/favicon48x48.png">
89 <script src="js/main.js" ></script>
90 <meta name="viewport" content="width=device-width, initial-scale=1.0">
91 <style>
92 body{background-color: #E3F3FF;}
93 #bloc_page{text-align: center;}
94 .avertissement{color: red;}
95 </style>
96 </head>
97
98 <body>
99 <div id="bloc_page" >
100 <h2 class="connexionTitre" ><?= $title ?></h2>
101 <p class="connexionP" ><?= $subHeading ?></p>
102<?php
103$header = ob_get_clean();
104
105
106$error_messages = [
107 'error_non_valid_captcha' => '<p class="avertissement" >Erreur au test anti-robot, veuillez saisir un nombre entier.</p>',
108 'bad_solution_captcha' => '<p class="avertissement" >Erreur au test anti-robot, veuillez réessayer.</p>',
109 'bad_login_or_password' => '<p class="avertissement" >Saisir un Identifiant (e-mail) et un mot de passe.</p>',
110 'bad_password' => '<p class="avertissement" >Mauvais mot de passe, veuillez réessayer.</p>',
111 'forbidden_characters' => '<p class="avertissement" >Caractères interdits: espaces, tabulations, sauts CR/LF.</p>'
112];
113
114$warning_messages = [
115 'message_disconnect' => "<p class='connexionP' ><i>N'oubliez de cliquer sur 'déconnexion' quand vous aurez fini.</i></p>",
116 //'message_cookie' => "<p class='connexionP' style='color: red;'>Ce site utilise un cookie « obligatoire » lorsque vous êtes connecté ainsi que sur cette page.<br>Il sera supprimé à votre déconnexion ou dès que vous aurez quitté le site.</p>",
117 'private_browsing' =>"<p class='connexionP' >Au fait? Vous n'utilisez pas votre propre ordinateur ou téléphone?<br/>
118 Utilisez la navigation privée.</p>"
119];
120
121
122// confirmation modification du mot de passe
123$page = isset($_GET['from']) ? $_GET['from'] : 'accueil';
124$id = isset($_GET['id']) ? ', \'' . $_GET['id'] . '\'' : '';
125$js = "newPassword('" . $page . "'" . $id . ");";
126ob_start();
127?>
128<script><?= $js ?></script>
129<noscript>
130 <p class="avertissement" >Le mot de passe a été modifié<br>
131 <a href="<?= $link ?>" ><button>Retour au site.</button></a></p>
132</noscript>
133<?php
134$alertJSNewPassword = ob_get_clean();
135
136
137// bas de la page
138$link = new URL();
139isset($_GET['from']) ? $link->addParams(['page' => $_GET['from'] ]) : '';
140isset($_GET['id']) ? $link->addParams(['id' => $_GET['id']]) : '';
141ob_start();
142if(isset($_GET['from'])) // exclue la "création du mot de passe"
143{
144?>
145 <p class="connexionP connexionFooter" >
146 <a href="<?= $link ?>" >
147 <button>Retour au site.</button>
148 </a>
149 </p>
150<?php
151}
152$footer = ob_get_clean(); \ No newline at end of file
diff --git a/src/view/templates/article.php b/src/view/templates/article.php
new file mode 100644
index 0000000..d34697e
--- /dev/null
+++ b/src/view/templates/article.php
@@ -0,0 +1,15 @@
1<article>
2 <div class="article_content">
3 <div class="article_title_zone">
4 <h4><?= $title ?></h4>
5 <?= $share_button ?>
6 </div>
7 <div id="<?= $id ?>" class="html_from_editor">
8 <?= $html ?>
9 </div>
10 <div class="article_admin_zone">
11 <?= $admin_buttons ?>
12 </div>
13 </div>
14
15</article> \ No newline at end of file
diff --git a/src/view/templates/blog.php b/src/view/templates/blog.php
new file mode 100644
index 0000000..35cac8b
--- /dev/null
+++ b/src/view/templates/blog.php
@@ -0,0 +1,6 @@
1<section class="blog">
2 <h3><?= $title ?></h3>
3<?= $new_article ?>
4<?= $new_article_admin_buttons ?>
5<?= $content ?>
6</section> \ No newline at end of file
diff --git a/src/view/templates/footer.php b/src/view/templates/footer.php
new file mode 100644
index 0000000..5af7924
--- /dev/null
+++ b/src/view/templates/footer.php
@@ -0,0 +1,15 @@
1 <footer>
2 <div>
3 <p class="contact"><?= $contact_nom ?><br>
4 <?= $adresse ?><br>
5 <a href="mailto:<?= $e_mail ?>"><?= $e_mail ?></a></p>
6<?= $breadcrumb ?>
7 </div>
8 <div class="<?= $empty_admin_zone ?>"></div>
9 <div class="<?= $div_admin ?>">
10<?= $zone_admin ?>
11 </div>
12 </footer>
13 </div>
14 </body>
15</html> \ No newline at end of file
diff --git a/src/view/templates/galery.php b/src/view/templates/galery.php
new file mode 100644
index 0000000..306526f
--- /dev/null
+++ b/src/view/templates/galery.php
@@ -0,0 +1,10 @@
1<section class="galery">
2 <h3><?= $title ?></h3>
3<?= $new_article ?>
4<?= $new_article_admin_buttons ?>
5 <div class="galery_photos">
6 <a><p></p></a>
7<?= $content ?>
8 </div>
9 <script>enableGaleryScroller();</script>
10</section> \ No newline at end of file
diff --git a/src/view/templates/grid.php b/src/view/templates/grid.php
new file mode 100644
index 0000000..a09ed40
--- /dev/null
+++ b/src/view/templates/grid.php
@@ -0,0 +1,8 @@
1<section class="grid">
2 <h3><?= $title ?></h3>
3<?= $new_article ?>
4<?= $new_article_admin_buttons ?>
5 <div class="grid_columns">
6<?= $content ?>
7 </div>
8</section> \ No newline at end of file
diff --git a/src/view/templates/head.php b/src/view/templates/head.php
new file mode 100644
index 0000000..1ebb17e
--- /dev/null
+++ b/src/view/templates/head.php
@@ -0,0 +1,11 @@
1<!DOCTYPE html>
2<html lang="fr">
3 <head>
4 <meta charset="utf-8">
5 <title><?= $title ?></title>
6 <link rel="icon" type="image/png" href="<?= $favicon ?>" alt="<?= $alt ?>">
7 <meta name="description" content="<?= $description ?>">
8 <meta name="viewport" content="width=device-width">
9 <?= $css ?>
10 <?= $js ?>
11 </head> \ No newline at end of file
diff --git a/src/view/templates/header.php b/src/view/templates/header.php
new file mode 100644
index 0000000..fa55cea
--- /dev/null
+++ b/src/view/templates/header.php
@@ -0,0 +1,23 @@
1<body>
2 <div>
3 <header>
4 <div class="empty_nav_zone">
5 <?= $nav ?>
6 </div>
7
8 <div class="header-content">
9 <div class="head_logo">
10 <a href="<?= new URL ?>"><img src="<?= $logo ?>" alt="<?= $logo_alt ?>"></a>
11 </div>
12 <div class="site_title">
13 <a href="<?= new URL ?>"><h1><?= $title ?></h1></a>
14 <h2><?= $description ?></h2>
15 </div>
16 <div class="social">
17 <a href="<?= $facebook_link ?>" target="_blank" rel="noopener noreferrer">
18 <img src="<?= $facebook ?>" alt="<?= $facebook_alt ?>"></a>
19 <a href="<?= $instagram_link ?>" target="_blank" rel="noopener noreferrer">
20 <img src="<?= $instagram ?>" alt="<?= $instagram_alt ?>"></a>
21 </div>
22 </div>
23 </header> \ No newline at end of file
diff --git a/src/view/templates/new.php b/src/view/templates/new.php
new file mode 100644
index 0000000..9dd8969
--- /dev/null
+++ b/src/view/templates/new.php
@@ -0,0 +1,21 @@
1<article>
2 <div class="new_content">
3 <div class="article_title_zone">
4 <h4><?= $title ?></h4>
5 <?= $share_button ?>
6 </div>
7 <div class="new_content_text">
8 <?= $preview ?>
9 </div>
10 <div id="<?= $id ?>" class="article_content_text">
11 <?= $content ?>
12 </div>
13 <div class="under_an_article">
14 <p><img src="assets/calendar.svg"><?= $date ?></p>
15 <?= $from_to_button ?>
16 </div>
17 <div class="article_admin_zone">
18 <?= $admin_buttons ?>
19 </div>
20 </div>
21</article> \ No newline at end of file