diff options
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 @@ | |||
1 | vendor/ | ||
2 | node_modules/ | ||
3 | public/js/tinymce/ | ||
4 | public/js/tinymce-langs/ | ||
5 | config/config.ini | ||
6 | data/ | ||
7 | bdd_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 @@ | |||
1 | Les Nageurs Bigoudens - Club, Ecole de natation et Perfectionnement | ||
2 | https://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 | |||
6 | function 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 | |||
25 | copyDirectory($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 | |||
5 | use Doctrine\ORM\Tools\Console\ConsoleRunner; | ||
6 | use Doctrine\ORM\Tools\Console\EntityManagerProvider\SingleManagerProvider; | ||
7 | |||
8 | chdir('bin'); // pour être au même niveau que l'appli dans /public | ||
9 | |||
10 | require('../src/Config.php'); | ||
11 | Config::load('../config/config.ini'); | ||
12 | |||
13 | require '../src/model/doctrine-bootstrap.php'; // relatif | ||
14 | //require __DIR__ . '/../src/model/doctrine-bootstrap.php'; // absolu | ||
15 | |||
16 | ConsoleRunner::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 | |||
13 | setupEnvironment(); | ||
14 | process(is_array($argv) ? $argv : array()); | ||
15 | |||
16 | /** | ||
17 | * Initializes various values | ||
18 | * | ||
19 | * @throws RuntimeException If uopz extension prevents exit calls | ||
20 | */ | ||
21 | function 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 | */ | ||
48 | function 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 | */ | ||
114 | function displayHelp() | ||
115 | { | ||
116 | echo <<<EOF | ||
117 | Composer Installer | ||
118 | ------------------ | ||
119 | Options | ||
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 | |||
137 | EOF; | ||
138 | } | ||
139 | |||
140 | /** | ||
141 | * Sets the USE_ANSI define for colorizing output | ||
142 | * | ||
143 | * @param array $argv Command-line arguments | ||
144 | */ | ||
145 | function 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 | */ | ||
162 | function 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 | */ | ||
202 | function 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 | */ | ||
229 | function 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 | */ | ||
262 | function 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 | */ | ||
295 | function 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 | */ | ||
505 | function 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 | */ | ||
518 | function 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 | */ | ||
532 | function 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 | */ | ||
543 | function 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 | */ | ||
569 | function 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 | */ | ||
613 | function 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 | */ | ||
628 | function 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 | |||
643 | function 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 | |||
658 | class 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----- | ||
1206 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAnBDHjZS6e0ZMoK3xTD7f | ||
1207 | FNCzlXjX/Aie2dit8QXA03pSrOTbaMnxON3hUL47Lz3g1SC6YJEMVHr0zYq4elWi | ||
1208 | i3ecFEgzLcj+pZM5X6qWu2Ozz4vWx3JYo1/a/HYdOuW9e3lwS8VtS0AVJA+U8X0A | ||
1209 | hZnBmGpltHhO8hPKHgkJtkTUxCheTcbqn4wGHl8Z2SediDcPTLwqezWKUfrYzu1f | ||
1210 | o/j3WFwFs6GtK4wdYtiXr+yspBZHO3y1udf8eFFGcb2V3EaLOrtfur6XQVizjOuk | ||
1211 | 8lw5zzse1Qp/klHqbDRsjSzJ6iL6F4aynBc6Euqt/8ccNAIz0rLjLhOraeyj4eNn | ||
1212 | 8iokwMKiXpcrQLTKH+RH1JCuOVxQ436bJwbSsp1VwiqftPQieN+tzqy+EiHJJmGf | ||
1213 | TBAbWcncicCk9q2md+AmhNbvHO4PWbbz9TzC7HJb460jyWeuMEvw3gNIpEo2jYa9 | ||
1214 | pMV6cVqnSa+wOc0D7pC9a6bne0bvLcm3S+w6I5iDB3lZsb3A9UtRiSP7aGSo7D72 | ||
1215 | 8tC8+cIgZcI7k9vjvOqH+d7sdOU2yPCnRY6wFh62/g8bDnUpr56nZN1G89GwM4d4 | ||
1216 | r/TU7BQQIzsZgAiqOGXvVklIgAMiV0iucgf3rNBLjjeNEwNSTTG9F0CtQ+7JLwaE | ||
1217 | wSEuAuRm+pRqi8BRnQ/GKUcCAwEAAQ== | ||
1218 | -----END PUBLIC KEY----- | ||
1219 | PKDEV; | ||
1220 | } | ||
1221 | |||
1222 | public static function getPKTags() | ||
1223 | { | ||
1224 | return <<<PKTAGS | ||
1225 | -----BEGIN PUBLIC KEY----- | ||
1226 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0Vi/2K6apCVj76nCnCl2 | ||
1227 | MQUPdK+A9eqkYBacXo2wQBYmyVlXm2/n/ZsX6pCLYPQTHyr5jXbkQzBw8SKqPdlh | ||
1228 | vA7NpbMeNCz7wP/AobvUXM8xQuXKbMDTY2uZ4O7sM+PfGbptKPBGLe8Z8d2sUnTO | ||
1229 | bXtX6Lrj13wkRto7st/w/Yp33RHe9SlqkiiS4MsH1jBkcIkEHsRaveZzedUaxY0M | ||
1230 | mba0uPhGUInpPzEHwrYqBBEtWvP97t2vtfx8I5qv28kh0Y6t+jnjL1Urid2iuQZf | ||
1231 | noCMFIOu4vksK5HxJxxrN0GOmGmwVQjOOtxkwikNiotZGPR4KsVj8NnBrLX7oGuM | ||
1232 | nQvGciiu+KoC2r3HDBrpDeBVdOWxDzT5R4iI0KoLzFh2pKqwbY+obNPS2bj+2dgJ | ||
1233 | rV3V5Jjry42QOCBN3c88wU1PKftOLj2ECpewY6vnE478IipiEu7EAdK8Zwj2LmTr | ||
1234 | RKQUSa9k7ggBkYZWAeO/2Ag0ey3g2bg7eqk+sHEq5ynIXd5lhv6tC5PBdHlWipDK | ||
1235 | tl2IxiEnejnOmAzGVivE1YGduYBjN+mjxDVy8KGBrjnz1JPgAvgdwJ2dYw4Rsc/e | ||
1236 | TzCFWGk/HM6a4f0IzBWbJ5ot0PIi4amk07IotBXDWwqDiQTwyuGCym5EqWQ2BD95 | ||
1237 | RGv89BPD+2DLnJysngsvVaUCAwEAAQ== | ||
1238 | -----END PUBLIC KEY----- | ||
1239 | PKTAGS; | ||
1240 | } | ||
1241 | } | ||
1242 | |||
1243 | class 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 | |||
1290 | class 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 | |||
1338 | class 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 | |||
1696 | ISRG Root X1 | ||
1697 | ============ | ||
1698 | -----BEGIN CERTIFICATE----- | ||
1699 | MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw | ||
1700 | TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh | ||
1701 | cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 | ||
1702 | WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu | ||
1703 | ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY | ||
1704 | MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc | ||
1705 | h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ | ||
1706 | 0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U | ||
1707 | A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW | ||
1708 | T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH | ||
1709 | B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC | ||
1710 | B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv | ||
1711 | KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn | ||
1712 | OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn | ||
1713 | jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw | ||
1714 | qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI | ||
1715 | rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV | ||
1716 | HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq | ||
1717 | hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL | ||
1718 | ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ | ||
1719 | 3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK | ||
1720 | NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 | ||
1721 | ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur | ||
1722 | TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC | ||
1723 | jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc | ||
1724 | oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq | ||
1725 | 4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA | ||
1726 | mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d | ||
1727 | emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= | ||
1728 | -----END CERTIFICATE----- | ||
1729 | |||
1730 | ISRG Root X2 | ||
1731 | ============ | ||
1732 | -----BEGIN CERTIFICATE----- | ||
1733 | MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw | ||
1734 | CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg | ||
1735 | R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00 | ||
1736 | MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT | ||
1737 | ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw | ||
1738 | EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW | ||
1739 | +1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9 | ||
1740 | ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T | ||
1741 | AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI | ||
1742 | zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW | ||
1743 | tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1 | ||
1744 | /q4AaOeMSQ+2b1tbFfLn | ||
1745 | -----END CERTIFICATE----- | ||
1746 | CACERT; | ||
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 @@ | |||
1 | déploiement | ||
2 | |||
3 | => procédure pour un hébergement "normal" avec un accès SSH, | ||
4 | ne 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 | ||
9 | ou l'ajouter au PATH en éditant .bash_profile (facile avec filezilla): | ||
10 | trouver la ligne PATH=$PATH:$HOME/bin | ||
11 | et 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; | ||
19 | exec($commande, $sortie, $code_retour); | ||
20 | |||
21 | |||
22 | 1/ se connecter au serveur en ssh et télécharger les fichiers: | ||
23 | cd /var/www | ||
24 | git clone git@ordipolo.fr:nageurs | ||
25 | |||
26 | 2/ renommer le dossier du site pour apache | ||
27 | mv nageurs dossier_du_site | ||
28 | |||
29 | 3/ côté client, envoyer les fichier config.ini et bdd_nageurs.sql en FTP | ||
30 | placer config.ini à la racine du site | ||
31 | |||
32 | retour côté serveur en ssh | ||
33 | |||
34 | 4/ mieux vaut utiliser composer et la console doctrine sans être root, | ||
35 | changer les permissions: | ||
36 | chown -R www-data www-data dossier_du_site | ||
37 | |||
38 | 5/ télécharger les dépendances PHP (dont doctrine) en lisant le composer.json: | ||
39 | sudo -u www-data php composer.phar update | ||
40 | |||
41 | 6/ générer l'autoloader | ||
42 | composer dump-autoload -o | ||
43 | |||
44 | 7/ se connecter à la BDD et créer la base: | ||
45 | mysql/mariadb -u root -p | ||
46 | CREATE DATABASES nageurs CHARACTER SET utf8mb4; | ||
47 | l'utilisateur doit avoir les droits SELECT, INSERT, UPDATE, DELETE, CREATE, DROP et ALTER | ||
48 | |||
49 | 8/ créer les tables: | ||
50 | sudo -u www-data php bin/doctrine orm:schema-tool:create | ||
51 | et remplir la base de données: | ||
52 | mariadb -D nageurs -u root -p < bdd_nageurs.sql | ||
53 | |||
54 | 9/ le 1er chargement d'une page est long, | ||
55 | le cache de symfony inclu fait ensuite son job | ||
56 | |||
57 | sinon, regarder ici: | ||
58 | https://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 @@ | |||
1 | body | ||
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 --*/ | ||
2 | footer | ||
3 | { | ||
4 | /*background-color: #279dc177;*/ | ||
5 | /*background-color: #13aff077;*/ | ||
6 | background-color: #B7E9FE; | ||
7 | /*width: 1200px;*/ | ||
8 | margin: auto; | ||
9 | |||
10 | } | ||
11 | footer > 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 @@ | |||
1 | header | ||
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 | } | ||
9 | header > 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 | } | ||
26 | header h1 | ||
27 | { | ||
28 | font-size: x-large; | ||
29 | margin: 10px; | ||
30 | } | ||
31 | header h2 | ||
32 | { | ||
33 | font-size: medium; /* défaut = x-large */ | ||
34 | margin: 10px; | ||
35 | } | ||
36 | header img | ||
37 | { | ||
38 | vertical-align: bottom; /* supprime espace sous l'image */ | ||
39 | } | ||
40 | header 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 @@ | |||
1 | main | ||
2 | { | ||
3 | margin: auto; | ||
4 | /*max-width: 1200px;*/ | ||
5 | background-color: #E3F3FF; | ||
6 | padding: 15px 0; | ||
7 | } | ||
8 | section > h3 | ||
9 | { | ||
10 | padding: 15px; | ||
11 | margin: 0; | ||
12 | text-align: center; | ||
13 | } | ||
14 | section > 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 | } | ||
34 | article | ||
35 | { | ||
36 | /*display: flex;*/ | ||
37 | background-color: white; | ||
38 | margin: 15px 15px 0 15px; | ||
39 | padding: 0 15px; | ||
40 | /*min-height: 150px;*/ | ||
41 | } | ||
42 | article .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 | |||
59 | article .action_icon | ||
60 | { | ||
61 | width: 24px; | ||
62 | vertical-align: middle; | ||
63 | border: white 2px solid; /* invisible */ | ||
64 | } | ||
65 | article .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 | } | ||
97 | main 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 | } | ||
105 | main 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 | } | ||
27 | ul | ||
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 */ | ||
81 | nav > 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é | ||
5 | 1. télécharger le script d'installation: | ||
6 | php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" | ||
7 | 2. installation: php composer-setup.php | ||
8 | 3. supprimer l'installateur: php -r "unlink('composer-setup.php');" | ||
9 | 4. utilisation: php composer.phar */ | ||
10 | |||
11 | declare(strict_types=1); | ||
12 | |||
13 | // -- prétraitement -- | ||
14 | // une nouvelle classe? taper: composer dump-autoload -o | ||
15 | require "../vendor/autoload.php"; | ||
16 | |||
17 | // configuration possible par l'utilisateur | ||
18 | Config::load('../config/config.ini'); | ||
19 | |||
20 | // les messages d'erreur de déploiement qu'on aime | ||
21 | require('../src/controller/installation.php'); | ||
22 | //installation(); // des mkdir et chmod, chiant en dev | ||
23 | |||
24 | // $entityManager | ||
25 | require '../src/model/doctrine-bootstrap.php'; // isDevMode est sur "true", DSN à adapter | ||
26 | |||
27 | URL::setProtocol(Config::$protocol); // utile si port autre que 80 ou 443 | ||
28 | URL::setPort(Config::$port); | ||
29 | URL::setHost(Config::$host); | ||
30 | |||
31 | //require('controller/Session.php'); | ||
32 | ini_set('session.cookie_samesite', 'Strict'); | ||
33 | ini_set('session.cookie_httponly', 'On'); | ||
34 | ini_set('session.use_strict_mode', 'On'); | ||
35 | ini_set('session.cookie_secure', 'On'); | ||
36 | session_start(); | ||
37 | $_SESSION['admin'] = !isset($_SESSION['admin']) ? false : $_SESSION['admin']; // intialisation sur faux | ||
38 | |||
39 | // login, mot de passe et captcha | ||
40 | require '../src/controller/password.php'; | ||
41 | existUsers($entityManager); | ||
42 | |||
43 | // -- navigation avec les GET -- | ||
44 | $current_page = 'accueil'; | ||
45 | if(!empty($_GET['page'])) | ||
46 | { | ||
47 | $current_page = htmlspecialchars($_GET['page']); | ||
48 | } | ||
49 | define('CURRENT_PAGE', $current_page); | ||
50 | |||
51 | // -- traitement des POST (formulaires et AJAX) -- | ||
52 | require '../src/controller/post.php'; | ||
53 | |||
54 | // id des articles | ||
55 | $id = ''; | ||
56 | if(!empty($_GET['id'])) | ||
57 | { | ||
58 | //$id = (int)$_GET['id']; // (int) = moyen basique d'éviter les injections | ||
59 | $id = Security::secureString($_GET['id']); | ||
60 | } | ||
61 | |||
62 | if(isset($_GET['action']) && $_GET['action'] === 'deconnexion') | ||
63 | { | ||
64 | disconnect($entityManager); | ||
65 | } | ||
66 | elseif(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); | ||
78 | echo $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 @@ | |||
1 | function 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 @@ | |||
1 | function 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 | |||
11 | function 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 @@ | |||
1 | let editors = {}; | ||
2 | |||
3 | function 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 | |||
68 | function 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 | |||
105 | function 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 | |||
131 | function 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 | |||
4 | declare(strict_types=1); | ||
5 | |||
6 | class 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 | |||
4 | declare(strict_types=1); | ||
5 | |||
6 | use Doctrine\ORM\EntityManager; | ||
7 | use App\Entity\Page; | ||
8 | use App\Entity\Node; | ||
9 | |||
10 | class 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 | |||
6 | class 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 | ||
78 | function removeSpacesTabsCRLF(string $chaine): string | ||
79 | { | ||
80 | $cibles = [' ', "\t", "\n", "\r"]; // doubles quotes !! | ||
81 | return(str_replace($cibles, '', $chaine)); | ||
82 | } | ||
83 | |||
84 | // lien sans http:// | ||
85 | function 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 | |||
4 | declare(strict_types=1); | ||
5 | |||
6 | class 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 | |||
4 | declare(strict_types=1); | ||
5 | |||
6 | // détection des requêtes de tinymce | ||
7 | if($_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 | ||
64 | if(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 | |||
4 | declare(strict_types=1); | ||
5 | |||
6 | use App\Entity\Page; | ||
7 | use App\Entity\Node; | ||
8 | use App\Entity\NodeData; | ||
9 | use App\Entity\Image; | ||
10 | use Doctrine\Common\Collections\ArrayCollection; | ||
11 | use Doctrine\ORM\EntityManager; | ||
12 | |||
13 | function 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> | ||
38 | HTACCESS; | ||
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) | ||
77 | function 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 | |||
6 | declare(strict_types=1); | ||
7 | |||
8 | use Doctrine\ORM\EntityManager; | ||
9 | use App\Entity\User; | ||
10 | |||
11 | // exécutée dans installation.php à l'ouverture de chaque page | ||
12 | function 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 | |||
30 | function 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 | |||
117 | function 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 | |||
204 | function 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 | |||
289 | function 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 | |||
315 | function 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 | |||
328 | function createCaptcha(): array | ||
329 | { | ||
330 | $a = rand(2, 9); | ||
331 | $b = rand(2, 9); | ||
332 | return array(toLettersFrench($a), toLettersFrench($b), $a * $b); | ||
333 | } | ||
334 | |||
335 | function 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 | ||
352 | function 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 | |||
4 | declare(strict_types=1); | ||
5 | |||
6 | if($_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 | |||
4 | declare(strict_types=1); | ||
5 | |||
6 | use Doctrine\ORM\EntityManager; | ||
7 | use App\Entity\Page; | ||
8 | use Doctrine\Common\Collections\ArrayCollection; | ||
9 | |||
10 | class 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 | |||
4 | declare(strict_types=1); | ||
5 | |||
6 | use Doctrine\ORM\EntityManager; | ||
7 | use App\Entity\Page; | ||
8 | |||
9 | class 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 | |||
4 | use Doctrine\ORM\ORMSetup; | ||
5 | use Doctrine\DBAL\DriverManager; | ||
6 | use Doctrine\ORM\EntityManager; | ||
7 | |||
8 | require_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 | |||
31 | foreach($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 | |||
4 | declare(strict_types=1); | ||
5 | |||
6 | namespace App\Entity; | ||
7 | |||
8 | use Doctrine\ORM\Mapping as ORM; | ||
9 | use Doctrine\Common\Collections\ArrayCollection; | ||
10 | use Doctrine\Common\Collections\Collection; | ||
11 | |||
12 | #[ORM\Entity] | ||
13 | #[ORM\Table(name: TABLE_PREFIX . "article")] | ||
14 | class 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 | |||
4 | declare(strict_types=1); | ||
5 | |||
6 | namespace App\Entity; | ||
7 | |||
8 | use Doctrine\ORM\Mapping as ORM; | ||
9 | |||
10 | #[ORM\Entity] | ||
11 | #[ORM\Table(name: TABLE_PREFIX . "image")] | ||
12 | class 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 | |||
4 | declare(strict_types=1); | ||
5 | |||
6 | namespace App\Entity; | ||
7 | |||
8 | use Config; | ||
9 | use Doctrine\ORM\Mapping as ORM; | ||
10 | |||
11 | #[ORM\Entity] | ||
12 | #[ORM\Table(name: TABLE_PREFIX . "node")] | ||
13 | class 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 | |||
4 | declare(strict_types=1); | ||
5 | |||
6 | namespace App\Entity; | ||
7 | |||
8 | use Doctrine\ORM\Mapping as ORM; | ||
9 | use Doctrine\Common\Collections\ArrayCollection; // classe | ||
10 | use Doctrine\Common\Collections\Collection; // interface | ||
11 | |||
12 | #[ORM\Entity] | ||
13 | #[ORM\Table(name: TABLE_PREFIX . "node_data")] | ||
14 | class 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 | |||
4 | declare(strict_types=1); | ||
5 | |||
6 | namespace App\Entity; | ||
7 | |||
8 | use Doctrine\ORM\Mapping as ORM; | ||
9 | use Doctrine\Common\Collections\Collection; | ||
10 | use Doctrine\Common\Collections\ArrayCollection; | ||
11 | |||
12 | #[ORM\Entity] | ||
13 | #[ORM\Table(name: TABLE_PREFIX . "page")] | ||
14 | class 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 | |||
4 | declare(strict_types=1); | ||
5 | |||
6 | namespace App\Entity; | ||
7 | |||
8 | use Doctrine\ORM\Mapping as ORM; | ||
9 | |||
10 | #[ORM\Entity] | ||
11 | #[ORM\Table(name: TABLE_PREFIX . "user")] | ||
12 | class 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 | |||
4 | use App\Entity\Node; | ||
5 | |||
6 | abstract 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 | |||
4 | use App\Entity\Node; | ||
5 | |||
6 | class 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 | |||
4 | use App\Entity\Node; | ||
5 | |||
6 | class 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 | |||
4 | use App\Entity\Node; | ||
5 | |||
6 | class 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 | |||
4 | use App\Entity\Node; | ||
5 | |||
6 | class 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 | |||
4 | use App\Entity\Node; | ||
5 | |||
6 | class 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 | |||
4 | use App\Entity\Node; | ||
5 | |||
6 | class 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 | |||
4 | use App\Entity\Node; | ||
5 | |||
6 | class 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 | |||
4 | use App\Entity\Node; | ||
5 | |||
6 | class 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 | |||
4 | use App\Entity\Node; | ||
5 | |||
6 | class 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 | |||
4 | use App\Entity\Node; | ||
5 | |||
6 | class 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 | |||
4 | declare(strict_types=1); | ||
5 | |||
6 | use App\Entity\Node; | ||
7 | use App\Entity\Page; | ||
8 | |||
9 | class 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 | |||
4 | use App\Entity\Node; | ||
5 | |||
6 | class 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 | |||
6 | declare(strict_types=1); | ||
7 | |||
8 | use App\Entity\Node; | ||
9 | |||
10 | class 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 | ||
11 | ob_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']); | ||
23 | isset($_GET['from']) ? $link->addParams(['from' => $_GET['from']]) : ''; | ||
24 | isset($_GET['id']) ? $link->addParams(['id' => $_GET['id']]) : ''; | ||
25 | ob_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 | ||
41 | ob_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']); | ||
58 | isset($_GET['from']) ? $link->addParams(['from' => $_GET['from']]) : ''; | ||
59 | isset($_GET['id']) ? $link->addParams(['id' => $_GET['id']]) : ''; | ||
60 | ob_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 | ||
79 | ob_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 . ");"; | ||
126 | ob_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(); | ||
139 | isset($_GET['from']) ? $link->addParams(['page' => $_GET['from'] ]) : ''; | ||
140 | isset($_GET['id']) ? $link->addParams(['id' => $_GET['id']]) : ''; | ||
141 | ob_start(); | ||
142 | if(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 | ||