From df3612ed7e6691530503f79483d2fdbc032d01b8 Mon Sep 17 00:00:00 2001 From: polo-pc-greta Date: Thu, 27 Mar 2025 10:13:03 +0100 Subject: mise en ligne github --- .gitignore | 7 + README.md | 2 + bin/copy_directory.php | 25 + bin/doctrine | 18 + composer-setup.php | 1748 ++++++++++++++++++++++++++++++++++ composer.json | 32 + config/.htaccess | 4 + deploiement.txt | 58 ++ public/assets/arrow-down-nb.svg | 36 + public/assets/arrow-down.svg | 40 + public/assets/arrow-up-nb.svg | 1 + public/assets/arrow-up.svg | 40 + public/assets/book-open-nb.svg | 1 + public/assets/book-open.svg | 40 + public/assets/calendar-nb.svg | 1 + public/assets/calendar.svg | 40 + public/assets/delete-bin-nb.svg | 1 + public/assets/delete-bin.svg | 48 + public/assets/edit-nb.svg | 1 + public/assets/edit.svg | 44 + public/assets/facebook-nb.svg | 1 + public/assets/facebook.svg | 37 + public/assets/favicon48x48.png | Bin 0 -> 3067 bytes public/assets/fond-piscine.jpg | Bin 0 -> 24757 bytes public/assets/home.svg | 1 + public/assets/instagram-nb.svg | 1 + public/assets/instagram.svg | 37 + public/assets/logo-120x75.jpg | Bin 0 -> 12240 bytes public/assets/logo-150x94.jpg | Bin 0 -> 13506 bytes public/assets/logo-fond-bleu.png | Bin 0 -> 61236 bytes public/assets/logo2.jpg | Bin 0 -> 36899 bytes public/assets/perdu.jpg | Bin 0 -> 107765 bytes public/assets/share-nb.svg | 1 + public/assets/share.svg | 40 + public/css/body.css | 39 + public/css/foot.css | 120 +++ public/css/head.css | 59 ++ public/css/main.css | 110 +++ public/css/nav.css | 95 ++ public/css/tinymce.css | 6 + public/erreur404.html | 13 + public/images-mini/DPpiscines.png | Bin 0 -> 15359 bytes public/images-mini/kerne-elagage.png | Bin 0 -> 31766 bytes public/images-mini/kerne.png | Bin 0 -> 13272 bytes public/images-mini/mako.png | Bin 0 -> 3011 bytes public/images/DPpiscines.png | Bin 0 -> 15359 bytes public/images/kerne-elagage.png | Bin 0 -> 31766 bytes public/images/kerne.png | Bin 0 -> 13272 bytes public/images/mako.png | Bin 0 -> 3011 bytes public/index.php | 78 ++ public/js/galery.js | 4 + public/js/main.js | 30 + public/js/tinymce.js | 164 ++++ src/Config.php | 76 ++ src/controller/Director.php | 101 ++ src/controller/Security.php | 111 +++ src/controller/URL.php | 88 ++ src/controller/ajax.php | 104 ++ src/controller/installation.php | 144 +++ src/controller/password.php | 357 +++++++ src/controller/post.php | 17 + src/model/Menu.php | 53 ++ src/model/Path.php | 84 ++ src/model/doctrine-bootstrap.php | 31 + src/model/entities/Article.php | 77 ++ src/model/entities/Image.php | 91 ++ src/model/entities/Node.php | 168 ++++ src/model/entities/NodeData.php | 62 ++ src/model/entities/Page.php | 97 ++ src/model/entities/User.php | 47 + src/view/AbstractBuilder.php | 52 + src/view/ArticleBuilder.php | 60 ++ src/view/BlogBuilder.php | 49 + src/view/BreadcrumbBuilder.php | 53 ++ src/view/FooterBuilder.php | 64 ++ src/view/GaleryBuilder.php | 49 + src/view/GridBuilder.php | 55 ++ src/view/HeadBuilder.php | 68 ++ src/view/HeaderBuilder.php | 64 ++ src/view/LoginBuilder.php | 15 + src/view/MainBuilder.php | 30 + src/view/NavBuilder.php | 61 ++ src/view/NewBuilder.php | 93 ++ src/view/ViewBuilder.php | 16 + src/view/password.php | 152 +++ src/view/templates/article.php | 15 + src/view/templates/blog.php | 6 + src/view/templates/footer.php | 15 + src/view/templates/galery.php | 10 + src/view/templates/grid.php | 8 + src/view/templates/head.php | 11 + src/view/templates/header.php | 23 + src/view/templates/new.php | 21 + 93 files changed, 5721 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 bin/copy_directory.php create mode 100644 bin/doctrine create mode 100644 composer-setup.php create mode 100644 composer.json create mode 100644 config/.htaccess create mode 100644 deploiement.txt create mode 100644 public/assets/arrow-down-nb.svg create mode 100644 public/assets/arrow-down.svg create mode 100644 public/assets/arrow-up-nb.svg create mode 100644 public/assets/arrow-up.svg create mode 100644 public/assets/book-open-nb.svg create mode 100644 public/assets/book-open.svg create mode 100644 public/assets/calendar-nb.svg create mode 100644 public/assets/calendar.svg create mode 100644 public/assets/delete-bin-nb.svg create mode 100644 public/assets/delete-bin.svg create mode 100644 public/assets/edit-nb.svg create mode 100644 public/assets/edit.svg create mode 100644 public/assets/facebook-nb.svg create mode 100644 public/assets/facebook.svg create mode 100644 public/assets/favicon48x48.png create mode 100644 public/assets/fond-piscine.jpg create mode 100644 public/assets/home.svg create mode 100644 public/assets/instagram-nb.svg create mode 100644 public/assets/instagram.svg create mode 100644 public/assets/logo-120x75.jpg create mode 100644 public/assets/logo-150x94.jpg create mode 100644 public/assets/logo-fond-bleu.png create mode 100644 public/assets/logo2.jpg create mode 100644 public/assets/perdu.jpg create mode 100644 public/assets/share-nb.svg create mode 100644 public/assets/share.svg create mode 100644 public/css/body.css create mode 100644 public/css/foot.css create mode 100644 public/css/head.css create mode 100644 public/css/main.css create mode 100644 public/css/nav.css create mode 100644 public/css/tinymce.css create mode 100644 public/erreur404.html create mode 100644 public/images-mini/DPpiscines.png create mode 100644 public/images-mini/kerne-elagage.png create mode 100644 public/images-mini/kerne.png create mode 100644 public/images-mini/mako.png create mode 100644 public/images/DPpiscines.png create mode 100644 public/images/kerne-elagage.png create mode 100644 public/images/kerne.png create mode 100644 public/images/mako.png create mode 100644 public/index.php create mode 100644 public/js/galery.js create mode 100644 public/js/main.js create mode 100644 public/js/tinymce.js create mode 100644 src/Config.php create mode 100644 src/controller/Director.php create mode 100644 src/controller/Security.php create mode 100644 src/controller/URL.php create mode 100644 src/controller/ajax.php create mode 100644 src/controller/installation.php create mode 100644 src/controller/password.php create mode 100644 src/controller/post.php create mode 100644 src/model/Menu.php create mode 100644 src/model/Path.php create mode 100644 src/model/doctrine-bootstrap.php create mode 100644 src/model/entities/Article.php create mode 100644 src/model/entities/Image.php create mode 100644 src/model/entities/Node.php create mode 100644 src/model/entities/NodeData.php create mode 100644 src/model/entities/Page.php create mode 100644 src/model/entities/User.php create mode 100644 src/view/AbstractBuilder.php create mode 100644 src/view/ArticleBuilder.php create mode 100644 src/view/BlogBuilder.php create mode 100644 src/view/BreadcrumbBuilder.php create mode 100644 src/view/FooterBuilder.php create mode 100644 src/view/GaleryBuilder.php create mode 100644 src/view/GridBuilder.php create mode 100644 src/view/HeadBuilder.php create mode 100644 src/view/HeaderBuilder.php create mode 100644 src/view/LoginBuilder.php create mode 100644 src/view/MainBuilder.php create mode 100644 src/view/NavBuilder.php create mode 100644 src/view/NewBuilder.php create mode 100644 src/view/ViewBuilder.php create mode 100644 src/view/password.php create mode 100644 src/view/templates/article.php create mode 100644 src/view/templates/blog.php create mode 100644 src/view/templates/footer.php create mode 100644 src/view/templates/galery.php create mode 100644 src/view/templates/grid.php create mode 100644 src/view/templates/head.php create mode 100644 src/view/templates/header.php create mode 100644 src/view/templates/new.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..631f78b --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +vendor/ +node_modules/ +public/js/tinymce/ +public/js/tinymce-langs/ +config/config.ini +data/ +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 @@ +Les Nageurs Bigoudens - Club, Ecole de natation et Perfectionnement +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 @@ +isDir()) { + mkdir($destination . DIRECTORY_SEPARATOR . $iterator->getSubPathName()); + } else { + copy($item, $destination . DIRECTORY_SEPARATOR . $iterator->getSubPathName()); + } + } +} + +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 @@ +#!/usr/bin/env php + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +setupEnvironment(); +process(is_array($argv) ? $argv : array()); + +/** + * Initializes various values + * + * @throws RuntimeException If uopz extension prevents exit calls + */ +function setupEnvironment() +{ + ini_set('display_errors', 1); + + if (extension_loaded('uopz') && !(ini_get('uopz.disable') || ini_get('uopz.exit'))) { + // uopz works at opcode level and disables exit calls + if (function_exists('uopz_allow_exit')) { + @uopz_allow_exit(true); + } else { + throw new RuntimeException('The uopz extension ignores exit calls and breaks this installer.'); + } + } + + $installer = 'ComposerInstaller'; + + if (defined('PHP_WINDOWS_VERSION_MAJOR')) { + if ($version = getenv('COMPOSERSETUP')) { + $installer = sprintf('Composer-Setup.exe/%s', $version); + } + } + + define('COMPOSER_INSTALLER', $installer); +} + +/** + * Processes the installer + */ +function process($argv) +{ + // Determine ANSI output from --ansi and --no-ansi flags + setUseAnsi($argv); + + $help = in_array('--help', $argv) || in_array('-h', $argv); + if ($help) { + displayHelp(); + exit(0); + } + + $check = in_array('--check', $argv); + $force = in_array('--force', $argv); + $quiet = in_array('--quiet', $argv); + $channel = 'stable'; + if (in_array('--snapshot', $argv)) { + $channel = 'snapshot'; + } elseif (in_array('--preview', $argv)) { + $channel = 'preview'; + } elseif (in_array('--1', $argv)) { + $channel = '1'; + } elseif (in_array('--2', $argv)) { + $channel = '2'; + } elseif (in_array('--2.2', $argv)) { + $channel = '2.2'; + } + $disableTls = in_array('--disable-tls', $argv); + $installDir = getOptValue('--install-dir', $argv, false); + $version = getOptValue('--version', $argv, false); + $filename = getOptValue('--filename', $argv, 'composer.phar'); + $cafile = getOptValue('--cafile', $argv, false); + + if (!checkParams($installDir, $version, $cafile)) { + exit(1); + } + + $ok = checkPlatform($warnings, $quiet, $disableTls, true); + + if ($check) { + // Only show warnings if we haven't output any errors + if ($ok) { + showWarnings($warnings); + showSecurityWarning($disableTls); + } + exit($ok ? 0 : 1); + } + + if ($ok || $force) { + if ($channel === '1' && !$quiet) { + 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'); + } + + $installer = new Installer($quiet, $disableTls, $cafile); + if ($installer->run($version, $installDir, $filename, $channel)) { + showWarnings($warnings); + showSecurityWarning($disableTls); + exit(0); + } + } + + exit(1); +} + +/** + * Displays the help + */ +function displayHelp() +{ + echo << $value) { + $next = $key + 1; + if (0 === strpos($value, $opt)) { + if ($optLength === strlen($value) && isset($argv[$next])) { + return trim($argv[$next]); + } else { + return trim(substr($value, $optLength + 1)); + } + } + } + + return $default; +} + +/** + * Checks that user-supplied params are valid + * + * @param mixed $installDir The required istallation directory + * @param mixed $version The required composer version to install + * @param mixed $cafile Certificate Authority file + * + * @return bool True if the supplied params are okay + */ +function checkParams($installDir, $version, $cafile) +{ + $result = true; + + if (false !== $installDir && !is_dir($installDir)) { + out("The defined install dir ({$installDir}) does not exist.", 'info'); + $result = false; + } + + if (false !== $version && 1 !== preg_match('/^\d+\.\d+\.\d+(\-(alpha|beta|RC)\d*)*$/', $version)) { + out("The defined install version ({$version}) does not match release pattern.", 'info'); + $result = false; + } + + if (false !== $cafile && (!file_exists($cafile) || !is_readable($cafile))) { + out("The defined Certificate Authority (CA) cert file ({$cafile}) does not exist or is not readable.", 'info'); + $result = false; + } + return $result; +} + +/** + * Checks the platform for possible issues running Composer + * + * Errors are written to the output, warnings are saved for later display. + * + * @param array $warnings Populated by method, to be shown later + * @param bool $quiet Quiet mode + * @param bool $disableTls Bypass tls + * @param bool $install If we are installing, rather than diagnosing + * + * @return bool True if there are no errors + */ +function checkPlatform(&$warnings, $quiet, $disableTls, $install) +{ + getPlatformIssues($errors, $warnings, $install); + + // Make openssl warning an error if tls has not been specifically disabled + if (isset($warnings['openssl']) && !$disableTls) { + $errors['openssl'] = $warnings['openssl']; + unset($warnings['openssl']); + } + + if (!empty($errors)) { + // Composer-Setup.exe uses "Some settings" to flag platform errors + out('Some settings on your machine make Composer unable to work properly.', 'error'); + out('Make sure that you fix the issues listed below and run this script again:', 'error'); + outputIssues($errors); + return false; + } + + if (empty($warnings) && !$quiet) { + out('All settings correct for using Composer', 'success'); + } + return true; +} + +/** + * Checks platform configuration for common incompatibility issues + * + * @param array $errors Populated by method + * @param array $warnings Populated by method + * @param bool $install If we are installing, rather than diagnosing + * + * @return bool If any errors or warnings have been found + */ +function getPlatformIssues(&$errors, &$warnings, $install) +{ + $errors = array(); + $warnings = array(); + + if ($iniPath = php_ini_loaded_file()) { + $iniMessage = PHP_EOL.'The php.ini used by your command-line PHP is: ' . $iniPath; + } else { + $iniMessage = PHP_EOL.'A php.ini file does not exist. You will have to create one.'; + } + $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.'; + + if (ini_get('detect_unicode')) { + $errors['unicode'] = array( + 'The detect_unicode setting must be disabled.', + 'Add the following to the end of your `php.ini`:', + ' detect_unicode = Off', + $iniMessage + ); + } + + if (extension_loaded('suhosin')) { + $suhosin = ini_get('suhosin.executor.include.whitelist'); + $suhosinBlacklist = ini_get('suhosin.executor.include.blacklist'); + if (false === stripos($suhosin, 'phar') && (!$suhosinBlacklist || false !== stripos($suhosinBlacklist, 'phar'))) { + $errors['suhosin'] = array( + 'The suhosin.executor.include.whitelist setting is incorrect.', + 'Add the following to the end of your `php.ini` or suhosin.ini (Example path [for Debian]: /etc/php5/cli/conf.d/suhosin.ini):', + ' suhosin.executor.include.whitelist = phar '.$suhosin, + $iniMessage + ); + } + } + + if (!function_exists('json_decode')) { + $errors['json'] = array( + 'The json extension is missing.', + 'Install it or recompile php without --disable-json' + ); + } + + if (!extension_loaded('Phar')) { + $errors['phar'] = array( + 'The phar extension is missing.', + 'Install it or recompile php without --disable-phar' + ); + } + + if (!extension_loaded('filter')) { + $errors['filter'] = array( + 'The filter extension is missing.', + 'Install it or recompile php without --disable-filter' + ); + } + + if (!extension_loaded('hash')) { + $errors['hash'] = array( + 'The hash extension is missing.', + 'Install it or recompile php without --disable-hash' + ); + } + + if (!extension_loaded('iconv') && !extension_loaded('mbstring')) { + $errors['iconv_mbstring'] = array( + 'The iconv OR mbstring extension is required and both are missing.', + 'Install either of them or recompile php without --disable-iconv' + ); + } + + if (!ini_get('allow_url_fopen')) { + $errors['allow_url_fopen'] = array( + 'The allow_url_fopen setting is incorrect.', + 'Add the following to the end of your `php.ini`:', + ' allow_url_fopen = On', + $iniMessage + ); + } + + if (extension_loaded('ionCube Loader') && ioncube_loader_iversion() < 40009) { + $ioncube = ioncube_loader_version(); + $errors['ioncube'] = array( + 'Your ionCube Loader extension ('.$ioncube.') is incompatible with Phar files.', + 'Upgrade to ionCube 4.0.9 or higher or remove this line (path may be different) from your `php.ini` to disable it:', + ' zend_extension = /usr/lib/php5/20090626+lfs/ioncube_loader_lin_5.3.so', + $iniMessage + ); + } + + if (version_compare(PHP_VERSION, '5.3.2', '<')) { + $errors['php'] = array( + 'Your PHP ('.PHP_VERSION.') is too old, you must upgrade to PHP 5.3.2 or higher.' + ); + } + + if (version_compare(PHP_VERSION, '5.3.4', '<')) { + $warnings['php'] = array( + 'Your PHP ('.PHP_VERSION.') is quite old, upgrading to PHP 5.3.4 or higher is recommended.', + 'Composer works with 5.3.2+ for most people, but there might be edge case issues.' + ); + } + + if (!extension_loaded('openssl')) { + $warnings['openssl'] = array( + 'The openssl extension is missing, which means that secure HTTPS transfers are impossible.', + 'If possible you should enable it or recompile php with --with-openssl' + ); + } + + if (extension_loaded('openssl') && OPENSSL_VERSION_NUMBER < 0x1000100f) { + // Attempt to parse version number out, fallback to whole string value. + $opensslVersion = trim(strstr(OPENSSL_VERSION_TEXT, ' ')); + $opensslVersion = substr($opensslVersion, 0, strpos($opensslVersion, ' ')); + $opensslVersion = $opensslVersion ? $opensslVersion : OPENSSL_VERSION_TEXT; + + $warnings['openssl_version'] = array( + 'The OpenSSL library ('.$opensslVersion.') used by PHP does not support TLSv1.2 or TLSv1.1.', + 'If possible you should upgrade OpenSSL to version 1.0.1 or above.' + ); + } + + if (!defined('HHVM_VERSION') && !extension_loaded('apcu') && ini_get('apc.enable_cli')) { + $warnings['apc_cli'] = array( + 'The apc.enable_cli setting is incorrect.', + 'Add the following to the end of your `php.ini`:', + ' apc.enable_cli = Off', + $iniMessage + ); + } + + if (!$install && extension_loaded('xdebug')) { + $warnings['xdebug_loaded'] = array( + 'The xdebug extension is loaded, this can slow down Composer a little.', + 'Disabling it when using Composer is recommended.' + ); + + if (ini_get('xdebug.profiler_enabled')) { + $warnings['xdebug_profile'] = array( + 'The xdebug.profiler_enabled setting is enabled, this can slow down Composer a lot.', + 'Add the following to the end of your `php.ini` to disable it:', + ' xdebug.profiler_enabled = 0', + $iniMessage + ); + } + } + + if (!extension_loaded('zlib')) { + $warnings['zlib'] = array( + 'The zlib extension is not loaded, this can slow down Composer a lot.', + 'If possible, install it or recompile php with --with-zlib', + $iniMessage + ); + } + + if (defined('PHP_WINDOWS_VERSION_BUILD') + && (version_compare(PHP_VERSION, '7.2.23', '<') + || (version_compare(PHP_VERSION, '7.3.0', '>=') + && version_compare(PHP_VERSION, '7.3.10', '<')))) { + $warnings['onedrive'] = array( + 'The Windows OneDrive folder is not supported on PHP versions below 7.2.23 and 7.3.10.', + 'Upgrade your PHP ('.PHP_VERSION.') to use this location with Composer.' + ); + } + + if (extension_loaded('uopz') && !(ini_get('uopz.disable') || ini_get('uopz.exit'))) { + $warnings['uopz'] = array( + 'The uopz extension ignores exit calls and may not work with all Composer commands.', + 'Disabling it when using Composer is recommended.' + ); + } + + ob_start(); + phpinfo(INFO_GENERAL); + $phpinfo = ob_get_clean(); + if (preg_match('{Configure Command(?: *| *=> *)(.*?)(?:|$)}m', $phpinfo, $match)) { + $configure = $match[1]; + + if (false !== strpos($configure, '--enable-sigchild')) { + $warnings['sigchild'] = array( + 'PHP was compiled with --enable-sigchild which can cause issues on some platforms.', + 'Recompile it without this flag if possible, see also:', + ' https://bugs.php.net/bug.php?id=22999' + ); + } + + if (false !== strpos($configure, '--with-curlwrappers')) { + $warnings['curlwrappers'] = array( + 'PHP was compiled with --with-curlwrappers which will cause issues with HTTP authentication and GitHub.', + 'Recompile it without this flag if possible' + ); + } + } + + // Stringify the message arrays + foreach ($errors as $key => $value) { + $errors[$key] = PHP_EOL.implode(PHP_EOL, $value); + } + + foreach ($warnings as $key => $value) { + $warnings[$key] = PHP_EOL.implode(PHP_EOL, $value); + } + + return !empty($errors) || !empty($warnings); +} + + +/** + * Outputs an array of issues + * + * @param array $issues + */ +function outputIssues($issues) +{ + foreach ($issues as $issue) { + out($issue, 'info'); + } + out(''); +} + +/** + * Outputs any warnings found + * + * @param array $warnings + */ +function showWarnings($warnings) +{ + if (!empty($warnings)) { + out('Some settings on your machine may cause stability issues with Composer.', 'error'); + out('If you encounter issues, try to change the following:', 'error'); + outputIssues($warnings); + } +} + +/** + * Outputs an end of process warning if tls has been bypassed + * + * @param bool $disableTls Bypass tls + */ +function showSecurityWarning($disableTls) +{ + if ($disableTls) { + out('You have instructed the Installer not to enforce SSL/TLS security on remote HTTPS requests.', 'info'); + out('This will leave all downloads during installation vulnerable to Man-In-The-Middle (MITM) attacks', 'info'); + } +} + +/** + * colorize output + */ +function out($text, $color = null, $newLine = true) +{ + $styles = array( + 'success' => "\033[0;32m%s\033[0m", + 'error' => "\033[31;31m%s\033[0m", + 'info' => "\033[33;33m%s\033[0m" + ); + + $format = '%s'; + + if (isset($styles[$color]) && USE_ANSI) { + $format = $styles[$color]; + } + + if ($newLine) { + $format .= PHP_EOL; + } + + printf($format, $text); +} + +/** + * Returns the system-dependent Composer home location, which may not exist + * + * @return string + */ +function getHomeDir() +{ + $home = getenv('COMPOSER_HOME'); + if ($home) { + return $home; + } + + $userDir = getUserDir(); + + if (defined('PHP_WINDOWS_VERSION_MAJOR')) { + return $userDir.'/Composer'; + } + + $dirs = array(); + + if (useXdg()) { + // XDG Base Directory Specifications + $xdgConfig = getenv('XDG_CONFIG_HOME'); + if (!$xdgConfig) { + $xdgConfig = $userDir . '/.config'; + } + + $dirs[] = $xdgConfig . '/composer'; + } + + $dirs[] = $userDir . '/.composer'; + + // select first dir which exists of: $XDG_CONFIG_HOME/composer or ~/.composer + foreach ($dirs as $dir) { + if (is_dir($dir)) { + return $dir; + } + } + + // if none exists, we default to first defined one (XDG one if system uses it, or ~/.composer otherwise) + return $dirs[0]; +} + +/** + * Returns the location of the user directory from the environment + * @throws RuntimeException If the environment value does not exists + * + * @return string + */ +function getUserDir() +{ + $userEnv = defined('PHP_WINDOWS_VERSION_MAJOR') ? 'APPDATA' : 'HOME'; + $userDir = getenv($userEnv); + + if (!$userDir) { + throw new RuntimeException('The '.$userEnv.' or COMPOSER_HOME environment variable must be set for composer to run correctly'); + } + + return rtrim(strtr($userDir, '\\', '/'), '/'); +} + +/** + * @return bool + */ +function useXdg() +{ + foreach (array_keys($_SERVER) as $key) { + if (strpos($key, 'XDG_') === 0) { + return true; + } + } + + if (is_dir('/etc/xdg')) { + return true; + } + + return false; +} + +function validateCaFile($contents) +{ + // assume the CA is valid if php is vulnerable to + // https://www.sektioneins.de/advisories/advisory-012013-php-openssl_x509_parse-memory-corruption-vulnerability.html + if ( + PHP_VERSION_ID <= 50327 + || (PHP_VERSION_ID >= 50400 && PHP_VERSION_ID < 50422) + || (PHP_VERSION_ID >= 50500 && PHP_VERSION_ID < 50506) + ) { + return !empty($contents); + } + + return (bool) openssl_x509_parse($contents); +} + +class Installer +{ + private $quiet; + private $disableTls; + private $cafile; + private $displayPath; + private $target; + private $tmpFile; + private $tmpCafile; + private $baseUrl; + private $algo; + private $errHandler; + private $httpClient; + private $pubKeys = array(); + private $installs = array(); + + /** + * Constructor - must not do anything that throws an exception + * + * @param bool $quiet Quiet mode + * @param bool $disableTls Bypass tls + * @param mixed $cafile Path to CA bundle, or false + */ + public function __construct($quiet, $disableTls, $caFile) + { + if (($this->quiet = $quiet)) { + ob_start(); + } + $this->disableTls = $disableTls; + $this->cafile = $caFile; + $this->errHandler = new ErrorHandler(); + } + + /** + * Runs the installer + * + * @param mixed $version Specific version to install, or false + * @param mixed $installDir Specific installation directory, or false + * @param string $filename Specific filename to save to, or composer.phar + * @param string $channel Specific version channel to use + * @throws Exception If anything other than a RuntimeException is caught + * + * @return bool If the installation succeeded + */ + public function run($version, $installDir, $filename, $channel) + { + try { + $this->initTargets($installDir, $filename); + $this->initTls(); + $this->httpClient = new HttpClient($this->disableTls, $this->cafile); + $result = $this->install($version, $channel); + + // in case --1 or --2 is passed, we leave the default channel for next self-update to stable + if (1 === preg_match('{^\d+$}D', $channel)) { + $channel = 'stable'; + } + + if ($result && $channel !== 'stable' && !$version && defined('PHP_BINARY')) { + $null = (defined('PHP_WINDOWS_VERSION_MAJOR') ? 'NUL' : '/dev/null'); + @exec(escapeshellarg(PHP_BINARY) .' '.escapeshellarg($this->target).' self-update --'.$channel.' --set-channel-only -q > '.$null.' 2> '.$null, $output); + } + } catch (Exception $e) { + $result = false; + } + + // Always clean up + $this->cleanUp($result); + + if (isset($e)) { + // Rethrow anything that is not a RuntimeException + if (!$e instanceof RuntimeException) { + throw $e; + } + out($e->getMessage(), 'error'); + } + return $result; + } + + /** + * Initialization methods to set the required filenames and composer url + * + * @param mixed $installDir Specific installation directory, or false + * @param string $filename Specific filename to save to, or composer.phar + * @throws RuntimeException If the installation directory is not writable + */ + protected function initTargets($installDir, $filename) + { + $this->displayPath = ($installDir ? rtrim($installDir, '/').'/' : '').$filename; + $installDir = $installDir ? realpath($installDir) : getcwd(); + + if (!is_writeable($installDir)) { + throw new RuntimeException('The installation directory "'.$installDir.'" is not writable'); + } + + $this->target = $installDir.DIRECTORY_SEPARATOR.$filename; + $this->tmpFile = $installDir.DIRECTORY_SEPARATOR.basename($this->target, '.phar').'-temp.phar'; + + $uriScheme = $this->disableTls ? 'http' : 'https'; + $this->baseUrl = $uriScheme.'://getcomposer.org'; + } + + /** + * A wrapper around methods to check tls and write public keys + * @throws RuntimeException If SHA384 is not supported + */ + protected function initTls() + { + if ($this->disableTls) { + return; + } + + if (!in_array('sha384', array_map('strtolower', openssl_get_md_methods()))) { + throw new RuntimeException('SHA384 is not supported by your openssl extension'); + } + + $this->algo = defined('OPENSSL_ALGO_SHA384') ? OPENSSL_ALGO_SHA384 : 'SHA384'; + $home = $this->getComposerHome(); + + $this->pubKeys = array( + 'dev' => $this->installKey(self::getPKDev(), $home, 'keys.dev.pub'), + 'tags' => $this->installKey(self::getPKTags(), $home, 'keys.tags.pub') + ); + + if (empty($this->cafile) && !HttpClient::getSystemCaRootBundlePath()) { + $this->cafile = $this->tmpCafile = $this->installKey(HttpClient::getPackagedCaFile(), $home, 'cacert-temp.pem'); + } + } + + /** + * Returns the Composer home directory, creating it if required + * @throws RuntimeException If the directory cannot be created + * + * @return string + */ + protected function getComposerHome() + { + $home = getHomeDir(); + + if (!is_dir($home)) { + $this->errHandler->start(); + + if (!mkdir($home, 0777, true)) { + throw new RuntimeException(sprintf( + 'Unable to create Composer home directory "%s": %s', + $home, + $this->errHandler->message + )); + } + $this->installs[] = $home; + $this->errHandler->stop(); + } + return $home; + } + + /** + * Writes public key data to disc + * + * @param string $data The public key(s) in pem format + * @param string $path The directory to write to + * @param string $filename The name of the file + * @throws RuntimeException If the file cannot be written + * + * @return string The path to the saved data + */ + protected function installKey($data, $path, $filename) + { + $this->errHandler->start(); + + $target = $path.DIRECTORY_SEPARATOR.$filename; + $installed = file_exists($target); + $write = file_put_contents($target, $data, LOCK_EX); + @chmod($target, 0644); + + $this->errHandler->stop(); + + if (!$write) { + throw new RuntimeException(sprintf('Unable to write %s to: %s', $filename, $path)); + } + + if (!$installed) { + $this->installs[] = $target; + } + + return $target; + } + + /** + * The main install function + * + * @param mixed $version Specific version to install, or false + * @param string $channel Version channel to use + * + * @return bool If the installation succeeded + */ + protected function install($version, $channel) + { + $retries = 3; + $result = false; + $infoMsg = 'Downloading...'; + $infoType = 'info'; + + while ($retries--) { + if (!$this->quiet) { + out($infoMsg, $infoType); + $infoMsg = 'Retrying...'; + $infoType = 'error'; + } + + if (!$this->getVersion($channel, $version, $url, $error)) { + out($error, 'error'); + continue; + } + + if (!$this->downloadToTmp($url, $signature, $error)) { + out($error, 'error'); + continue; + } + + if (!$this->verifyAndSave($version, $signature, $error)) { + out($error, 'error'); + continue; + } + + $result = true; + break; + } + + if (!$this->quiet) { + if ($result) { + out(PHP_EOL."Composer (version {$version}) successfully installed to: {$this->target}", 'success'); + out("Use it: php {$this->displayPath}", 'info'); + out(''); + } else { + out('The download failed repeatedly, aborting.', 'error'); + } + } + return $result; + } + + /** + * Sets the version url, downloading version data if required + * + * @param string $channel Version channel to use + * @param false|string $version Version to install, or set by method + * @param null|string $url The versioned url, set by method + * @param null|string $error Set by method on failure + * + * @return bool If the operation succeeded + */ + protected function getVersion($channel, &$version, &$url, &$error) + { + $error = ''; + + if ($version) { + if (empty($url)) { + $url = $this->baseUrl."/download/{$version}/composer.phar"; + } + return true; + } + + $this->errHandler->start(); + + if ($this->downloadVersionData($data, $error)) { + $this->parseVersionData($data, $channel, $version, $url); + } + + $this->errHandler->stop(); + return empty($error); + } + + /** + * Downloads and json-decodes version data + * + * @param null|array $data Downloaded version data, set by method + * @param null|string $error Set by method on failure + * + * @return bool If the operation succeeded + */ + protected function downloadVersionData(&$data, &$error) + { + $url = $this->baseUrl.'/versions'; + $errFmt = 'The "%s" file could not be %s: %s'; + + if (!$json = $this->httpClient->get($url)) { + $error = sprintf($errFmt, $url, 'downloaded', $this->errHandler->message); + return false; + } + + if (!$data = json_decode($json, true)) { + $error = sprintf($errFmt, $url, 'json-decoded', $this->getJsonError()); + return false; + } + return true; + } + + /** + * A wrapper around the methods needed to download and save the phar + * + * @param string $url The versioned download url + * @param null|string $signature Set by method on successful download + * @param null|string $error Set by method on failure + * + * @return bool If the operation succeeded + */ + protected function downloadToTmp($url, &$signature, &$error) + { + $error = ''; + $errFmt = 'The "%s" file could not be downloaded: %s'; + $sigUrl = $url.'.sig'; + $this->errHandler->start(); + + if (!$fh = fopen($this->tmpFile, 'w')) { + $error = sprintf('Could not create file "%s": %s', $this->tmpFile, $this->errHandler->message); + + } elseif (!$this->getSignature($sigUrl, $signature)) { + $error = sprintf($errFmt, $sigUrl, $this->errHandler->message); + + } elseif (!fwrite($fh, $this->httpClient->get($url))) { + $error = sprintf($errFmt, $url, $this->errHandler->message); + } + + if (is_resource($fh)) { + fclose($fh); + } + $this->errHandler->stop(); + return empty($error); + } + + /** + * Verifies the downloaded file and saves it to the target location + * + * @param string $version The composer version downloaded + * @param string $signature The digital signature to check + * @param null|string $error Set by method on failure + * + * @return bool If the operation succeeded + */ + protected function verifyAndSave($version, $signature, &$error) + { + $error = ''; + + if (!$this->validatePhar($this->tmpFile, $pharError)) { + $error = 'The download is corrupt: '.$pharError; + + } elseif (!$this->verifySignature($version, $signature, $this->tmpFile)) { + $error = 'Signature mismatch, could not verify the phar file integrity'; + + } else { + $this->errHandler->start(); + + if (!rename($this->tmpFile, $this->target)) { + $error = sprintf('Could not write to file "%s": %s', $this->target, $this->errHandler->message); + } + chmod($this->target, 0755); + $this->errHandler->stop(); + } + + return empty($error); + } + + /** + * Parses an array of version data to match the required channel + * + * @param array $data Downloaded version data + * @param mixed $channel Version channel to use + * @param false|string $version Set by method + * @param mixed $url The versioned url, set by method + */ + protected function parseVersionData(array $data, $channel, &$version, &$url) + { + foreach ($data[$channel] as $candidate) { + if ($candidate['min-php'] <= PHP_VERSION_ID) { + $version = $candidate['version']; + $url = $this->baseUrl.$candidate['path']; + break; + } + } + + if (!$version) { + $error = sprintf( + 'None of the %d %s version(s) of Composer matches your PHP version (%s / ID: %d)', + count($data[$channel]), + $channel, + PHP_VERSION, + PHP_VERSION_ID + ); + throw new RuntimeException($error); + } + } + + /** + * Downloads the digital signature of required phar file + * + * @param string $url The signature url + * @param null|string $signature Set by method on success + * + * @return bool If the download succeeded + */ + protected function getSignature($url, &$signature) + { + if (!$result = $this->disableTls) { + $signature = $this->httpClient->get($url); + + if ($signature) { + $signature = json_decode($signature, true); + $signature = base64_decode($signature['sha384']); + $result = true; + } + } + + return $result; + } + + /** + * Verifies the signature of the downloaded phar + * + * @param string $version The composer versione + * @param string $signature The downloaded digital signature + * @param string $file The temp phar file + * + * @return bool If the operation succeeded + */ + protected function verifySignature($version, $signature, $file) + { + if (!$result = $this->disableTls) { + $path = preg_match('{^[0-9a-f]{40}$}', $version) ? $this->pubKeys['dev'] : $this->pubKeys['tags']; + $pubkeyid = openssl_pkey_get_public('file://'.$path); + + $result = 1 === openssl_verify( + file_get_contents($file), + $signature, + $pubkeyid, + $this->algo + ); + + // PHP 8 automatically frees the key instance and deprecates the function + if (PHP_VERSION_ID < 80000) { + openssl_free_key($pubkeyid); + } + } + + return $result; + } + + /** + * Validates the downloaded phar file + * + * @param string $pharFile The temp phar file + * @param null|string $error Set by method on failure + * + * @return bool If the operation succeeded + */ + protected function validatePhar($pharFile, &$error) + { + if (ini_get('phar.readonly')) { + return true; + } + + try { + // Test the phar validity + $phar = new Phar($pharFile); + // Free the variable to unlock the file + unset($phar); + $result = true; + + } catch (Exception $e) { + if (!$e instanceof UnexpectedValueException && !$e instanceof PharException) { + throw $e; + } + $error = $e->getMessage(); + $result = false; + } + return $result; + } + + /** + * Returns a string representation of the last json error + * + * @return string The error string or code + */ + protected function getJsonError() + { + if (function_exists('json_last_error_msg')) { + return json_last_error_msg(); + } else { + return 'json_last_error = '.json_last_error(); + } + } + + /** + * Cleans up resources at the end of the installation + * + * @param bool $result If the installation succeeded + */ + protected function cleanUp($result) + { + if (!$result) { + // Output buffered errors + if ($this->quiet) { + $this->outputErrors(); + } + // Clean up stuff we created + $this->uninstall(); + } elseif ($this->tmpCafile) { + @unlink($this->tmpCafile); + } + } + + /** + * Outputs unique errors when in quiet mode + * + */ + protected function outputErrors() + { + $errors = explode(PHP_EOL, ob_get_clean()); + $shown = array(); + + foreach ($errors as $error) { + if ($error && !in_array($error, $shown)) { + out($error, 'error'); + $shown[] = $error; + } + } + } + + /** + * Uninstalls newly-created files and directories on failure + * + */ + protected function uninstall() + { + foreach (array_reverse($this->installs) as $target) { + if (is_file($target)) { + @unlink($target); + } elseif (is_dir($target)) { + @rmdir($target); + } + } + + if ($this->tmpFile !== null && file_exists($this->tmpFile)) { + @unlink($this->tmpFile); + } + } + + public static function getPKDev() + { + return <<message) { + $this->message .= PHP_EOL; + } + $this->message .= preg_replace('{^file_get_contents\(.*?\): }', '', $msg); + } + + /** + * Starts error-handling if not already active + * + * Any message is cleared + */ + public function start() + { + if (!$this->active) { + set_error_handler(array($this, 'handleError')); + $this->active = true; + } + $this->message = ''; + } + + /** + * Stops error-handling if active + * + * Any message is preserved until the next call to start() + */ + public function stop() + { + if ($this->active) { + restore_error_handler(); + $this->active = false; + } + } +} + +class NoProxyPattern +{ + private $composerInNoProxy = false; + private $rulePorts = array(); + + public function __construct($pattern) + { + $rules = preg_split('{[\s,]+}', $pattern, null, PREG_SPLIT_NO_EMPTY); + + if ($matches = preg_grep('{getcomposer\.org(?::\d+)?}i', $rules)) { + $this->composerInNoProxy = true; + + foreach ($matches as $match) { + if (strpos($match, ':') !== false) { + list(, $port) = explode(':', $match); + $this->rulePorts[] = (int) $port; + } + } + } + } + + /** + * Returns true if NO_PROXY contains getcomposer.org + * + * @param string $url http(s)://getcomposer.org + * + * @return bool + */ + public function test($url) + { + if (!$this->composerInNoProxy) { + return false; + } + + if (empty($this->rulePorts)) { + return true; + } + + if (strpos($url, 'http://') === 0) { + $port = 80; + } else { + $port = 443; + } + + return in_array($port, $this->rulePorts); + } +} + +class HttpClient { + + /** @var null|string */ + private static $caPath; + + private $options = array('http' => array()); + private $disableTls = false; + + public function __construct($disableTls = false, $cafile = false) + { + $this->disableTls = $disableTls; + if ($this->disableTls === false) { + if (!empty($cafile) && !is_dir($cafile)) { + if (!is_readable($cafile) || !validateCaFile(file_get_contents($cafile))) { + throw new RuntimeException('The configured cafile (' .$cafile. ') was not valid or could not be read.'); + } + } + $options = $this->getTlsStreamContextDefaults($cafile); + $this->options = array_replace_recursive($this->options, $options); + } + } + + public function get($url) + { + $context = $this->getStreamContext($url); + $result = file_get_contents($url, false, $context); + + if ($result && extension_loaded('zlib')) { + $decode = false; + foreach ($http_response_header as $header) { + if (preg_match('{^content-encoding: *gzip *$}i', $header)) { + $decode = true; + continue; + } elseif (preg_match('{^HTTP/}i', $header)) { + $decode = false; + } + } + + if ($decode) { + if (version_compare(PHP_VERSION, '5.4.0', '>=')) { + $result = zlib_decode($result); + } else { + // work around issue with gzuncompress & co that do not work with all gzip checksums + $result = file_get_contents('compress.zlib://data:application/octet-stream;base64,'.base64_encode($result)); + } + + if (!$result) { + throw new RuntimeException('Failed to decode zlib stream'); + } + } + } + + return $result; + } + + protected function getStreamContext($url) + { + if ($this->disableTls === false) { + if (PHP_VERSION_ID < 50600) { + $this->options['ssl']['SNI_server_name'] = parse_url($url, PHP_URL_HOST); + } + } + // Keeping the above mostly isolated from the code copied from Composer. + return $this->getMergedStreamContext($url); + } + + protected function getTlsStreamContextDefaults($cafile) + { + $ciphers = implode(':', array( + 'ECDHE-RSA-AES128-GCM-SHA256', + 'ECDHE-ECDSA-AES128-GCM-SHA256', + 'ECDHE-RSA-AES256-GCM-SHA384', + 'ECDHE-ECDSA-AES256-GCM-SHA384', + 'DHE-RSA-AES128-GCM-SHA256', + 'DHE-DSS-AES128-GCM-SHA256', + 'kEDH+AESGCM', + 'ECDHE-RSA-AES128-SHA256', + 'ECDHE-ECDSA-AES128-SHA256', + 'ECDHE-RSA-AES128-SHA', + 'ECDHE-ECDSA-AES128-SHA', + 'ECDHE-RSA-AES256-SHA384', + 'ECDHE-ECDSA-AES256-SHA384', + 'ECDHE-RSA-AES256-SHA', + 'ECDHE-ECDSA-AES256-SHA', + 'DHE-RSA-AES128-SHA256', + 'DHE-RSA-AES128-SHA', + 'DHE-DSS-AES128-SHA256', + 'DHE-RSA-AES256-SHA256', + 'DHE-DSS-AES256-SHA', + 'DHE-RSA-AES256-SHA', + 'AES128-GCM-SHA256', + 'AES256-GCM-SHA384', + 'AES128-SHA256', + 'AES256-SHA256', + 'AES128-SHA', + 'AES256-SHA', + 'AES', + 'CAMELLIA', + 'DES-CBC3-SHA', + '!aNULL', + '!eNULL', + '!EXPORT', + '!DES', + '!RC4', + '!MD5', + '!PSK', + '!aECDH', + '!EDH-DSS-DES-CBC3-SHA', + '!EDH-RSA-DES-CBC3-SHA', + '!KRB5-DES-CBC3-SHA', + )); + + /** + * CN_match and SNI_server_name are only known once a URL is passed. + * They will be set in the getOptionsForUrl() method which receives a URL. + * + * cafile or capath can be overridden by passing in those options to constructor. + */ + $options = array( + 'ssl' => array( + 'ciphers' => $ciphers, + 'verify_peer' => true, + 'verify_depth' => 7, + 'SNI_enabled' => true, + ) + ); + + /** + * Attempt to find a local cafile or throw an exception. + * The user may go download one if this occurs. + */ + if (!$cafile) { + $cafile = self::getSystemCaRootBundlePath(); + } + if (is_dir($cafile)) { + $options['ssl']['capath'] = $cafile; + } elseif ($cafile) { + $options['ssl']['cafile'] = $cafile; + } else { + throw new RuntimeException('A valid cafile could not be located automatically.'); + } + + /** + * Disable TLS compression to prevent CRIME attacks where supported. + */ + if (version_compare(PHP_VERSION, '5.4.13') >= 0) { + $options['ssl']['disable_compression'] = true; + } + + return $options; + } + + /** + * function copied from Composer\Util\StreamContextFactory::initOptions + * + * Any changes should be applied there as well, or backported here. + * + * @param string $url URL the context is to be used for + * @return resource Default context + * @throws \RuntimeException if https proxy required and OpenSSL uninstalled + */ + protected function getMergedStreamContext($url) + { + $options = $this->options; + + // Handle HTTP_PROXY/http_proxy on CLI only for security reasons + if ((PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') && (!empty($_SERVER['HTTP_PROXY']) || !empty($_SERVER['http_proxy']))) { + $proxy = parse_url(!empty($_SERVER['http_proxy']) ? $_SERVER['http_proxy'] : $_SERVER['HTTP_PROXY']); + } + + // Prefer CGI_HTTP_PROXY if available + if (!empty($_SERVER['CGI_HTTP_PROXY'])) { + $proxy = parse_url($_SERVER['CGI_HTTP_PROXY']); + } + + // Override with HTTPS proxy if present and URL is https + if (preg_match('{^https://}i', $url) && (!empty($_SERVER['HTTPS_PROXY']) || !empty($_SERVER['https_proxy']))) { + $proxy = parse_url(!empty($_SERVER['https_proxy']) ? $_SERVER['https_proxy'] : $_SERVER['HTTPS_PROXY']); + } + + // Remove proxy if URL matches no_proxy directive + if (!empty($_SERVER['NO_PROXY']) || !empty($_SERVER['no_proxy']) && parse_url($url, PHP_URL_HOST)) { + $pattern = new NoProxyPattern(!empty($_SERVER['no_proxy']) ? $_SERVER['no_proxy'] : $_SERVER['NO_PROXY']); + if ($pattern->test($url)) { + unset($proxy); + } + } + + if (!empty($proxy)) { + $proxyURL = isset($proxy['scheme']) ? $proxy['scheme'] . '://' : ''; + $proxyURL .= isset($proxy['host']) ? $proxy['host'] : ''; + + if (isset($proxy['port'])) { + $proxyURL .= ":" . $proxy['port']; + } elseif (strpos($proxyURL, 'http://') === 0) { + $proxyURL .= ":80"; + } elseif (strpos($proxyURL, 'https://') === 0) { + $proxyURL .= ":443"; + } + + // check for a secure proxy + if (strpos($proxyURL, 'https://') === 0) { + if (!extension_loaded('openssl')) { + throw new RuntimeException('You must enable the openssl extension to use a secure proxy.'); + } + if (strpos($url, 'https://') === 0) { + throw new RuntimeException('PHP does not support https requests through a secure proxy.'); + } + } + + // http(s):// is not supported in proxy + $proxyURL = str_replace(array('http://', 'https://'), array('tcp://', 'ssl://'), $proxyURL); + + $options['http'] = array( + 'proxy' => $proxyURL, + ); + + // add request_fulluri for http requests + if ('http' === parse_url($url, PHP_URL_SCHEME)) { + $options['http']['request_fulluri'] = true; + } + + // handle proxy auth if present + if (isset($proxy['user'])) { + $auth = rawurldecode($proxy['user']); + if (isset($proxy['pass'])) { + $auth .= ':' . rawurldecode($proxy['pass']); + } + $auth = base64_encode($auth); + + $options['http']['header'] = "Proxy-Authorization: Basic {$auth}\r\n"; + } + } + + if (isset($options['http']['header'])) { + $options['http']['header'] .= "Connection: close\r\n"; + } else { + $options['http']['header'] = "Connection: close\r\n"; + } + if (extension_loaded('zlib')) { + $options['http']['header'] .= "Accept-Encoding: gzip\r\n"; + } + $options['http']['header'] .= "User-Agent: ".COMPOSER_INSTALLER."\r\n"; + $options['http']['protocol_version'] = 1.1; + $options['http']['timeout'] = 600; + + return stream_context_create($options); + } + + /** + * This method was adapted from Sslurp. + * https://github.com/EvanDotPro/Sslurp + * + * (c) Evan Coury + * + * For the full copyright and license information, please see below: + * + * Copyright (c) 2013, Evan Coury + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + public static function getSystemCaRootBundlePath() + { + if (self::$caPath !== null) { + return self::$caPath; + } + + // If SSL_CERT_FILE env variable points to a valid certificate/bundle, use that. + // This mimics how OpenSSL uses the SSL_CERT_FILE env variable. + $envCertFile = getenv('SSL_CERT_FILE'); + if ($envCertFile && is_readable($envCertFile) && validateCaFile(file_get_contents($envCertFile))) { + return self::$caPath = $envCertFile; + } + + // If SSL_CERT_DIR env variable points to a valid certificate/bundle, use that. + // This mimics how OpenSSL uses the SSL_CERT_FILE env variable. + $envCertDir = getenv('SSL_CERT_DIR'); + if ($envCertDir && is_dir($envCertDir) && is_readable($envCertDir)) { + return self::$caPath = $envCertDir; + } + + $configured = ini_get('openssl.cafile'); + if ($configured && strlen($configured) > 0 && is_readable($configured) && validateCaFile(file_get_contents($configured))) { + return self::$caPath = $configured; + } + + $configured = ini_get('openssl.capath'); + if ($configured && is_dir($configured) && is_readable($configured)) { + return self::$caPath = $configured; + } + + $caBundlePaths = array( + '/etc/pki/tls/certs/ca-bundle.crt', // Fedora, RHEL, CentOS (ca-certificates package) + '/etc/ssl/certs/ca-certificates.crt', // Debian, Ubuntu, Gentoo, Arch Linux (ca-certificates package) + '/etc/ssl/ca-bundle.pem', // SUSE, openSUSE (ca-certificates package) + '/usr/local/share/certs/ca-root-nss.crt', // FreeBSD (ca_root_nss_package) + '/usr/ssl/certs/ca-bundle.crt', // Cygwin + '/opt/local/share/curl/curl-ca-bundle.crt', // OS X macports, curl-ca-bundle package + '/usr/local/share/curl/curl-ca-bundle.crt', // Default cURL CA bunde path (without --with-ca-bundle option) + '/usr/share/ssl/certs/ca-bundle.crt', // Really old RedHat? + '/etc/ssl/cert.pem', // OpenBSD + '/usr/local/etc/ssl/cert.pem', // FreeBSD 10.x + '/usr/local/etc/openssl/cert.pem', // OS X homebrew, openssl package + '/usr/local/etc/openssl@1.1/cert.pem', // OS X homebrew, openssl@1.1 package + '/opt/homebrew/etc/openssl@3/cert.pem', // macOS silicon homebrew, openssl@3 package + '/opt/homebrew/etc/openssl@1.1/cert.pem', // macOS silicon homebrew, openssl@1.1 package + ); + + foreach ($caBundlePaths as $caBundle) { + if (@is_readable($caBundle) && validateCaFile(file_get_contents($caBundle))) { + return self::$caPath = $caBundle; + } + } + + foreach ($caBundlePaths as $caBundle) { + $caBundle = dirname($caBundle); + if (is_dir($caBundle) && glob($caBundle.'/*')) { + return self::$caPath = $caBundle; + } + } + + return self::$caPath = false; + } + + public static function getPackagedCaFile() + { + return << + Order Allow,Deny + Deny from all + \ 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 @@ +déploiement + +=> procédure pour un hébergement "normal" avec un accès SSH, +ne marche pas sans la formule pro d'OVH +- peu de manips (le renommage) peuvent être faites avec filezilla +- pour le reste il faut contourner avec des scripts PHP faisant des exec() + +=> pour utiliser PHP-cli il faut utiliser le chemin de PHP /usr/local/php8.3/bin +ou l'ajouter au PATH en éditant .bash_profile (facile avec filezilla): +trouver la ligne PATH=$PATH:$HOME/bin +et y coller :$HOME/usr/local/php8.3/bin + +=> utiliser PHP-cli depuis un script web: +$param = '-r "echo \'hello world\';"'; +$commande = 'php ' . $param; // OU +$commande = '/usr/local/php8.3/bin/php ' . $param; +$sortie = null; +$code_retour = null; +exec($commande, $sortie, $code_retour); + + +1/ se connecter au serveur en ssh et télécharger les fichiers: +cd /var/www +git clone git@ordipolo.fr:nageurs + +2/ renommer le dossier du site pour apache +mv nageurs dossier_du_site + +3/ côté client, envoyer les fichier config.ini et bdd_nageurs.sql en FTP +placer config.ini à la racine du site + +retour côté serveur en ssh + +4/ mieux vaut utiliser composer et la console doctrine sans être root, +changer les permissions: +chown -R www-data www-data dossier_du_site + +5/ télécharger les dépendances PHP (dont doctrine) en lisant le composer.json: +sudo -u www-data php composer.phar update + +6/ générer l'autoloader +composer dump-autoload -o + +7/ se connecter à la BDD et créer la base: +mysql/mariadb -u root -p +CREATE DATABASES nageurs CHARACTER SET utf8mb4; +l'utilisateur doit avoir les droits SELECT, INSERT, UPDATE, DELETE, CREATE, DROP et ALTER + +8/ créer les tables: +sudo -u www-data php bin/doctrine orm:schema-tool:create +et remplir la base de données: +mariadb -D nageurs -u root -p < bdd_nageurs.sql + +9/ le 1er chargement d'une page est long, +le cache de symfony inclu fait ensuite son job + +sinon, regarder ici: +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 @@ + + + + + + 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 @@ + + + + + + + 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 @@ + \ 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 @@ + + + + + + + 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 @@ + \ 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 @@ + + + + + + + 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 @@ + \ 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 @@ + + + + + + + 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 @@ + \ 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 @@ + + + + + + + + + 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 @@ + \ 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 @@ + + + + + + + + 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 @@ + \ 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 @@ + + + + + + diff --git a/public/assets/favicon48x48.png b/public/assets/favicon48x48.png new file mode 100644 index 0000000..9825db1 Binary files /dev/null and b/public/assets/favicon48x48.png differ diff --git a/public/assets/fond-piscine.jpg b/public/assets/fond-piscine.jpg new file mode 100644 index 0000000..239d95d Binary files /dev/null and b/public/assets/fond-piscine.jpg 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 @@ + \ 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 @@ + \ 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 @@ + + + + + + diff --git a/public/assets/logo-120x75.jpg b/public/assets/logo-120x75.jpg new file mode 100644 index 0000000..b58a7a6 Binary files /dev/null and b/public/assets/logo-120x75.jpg differ diff --git a/public/assets/logo-150x94.jpg b/public/assets/logo-150x94.jpg new file mode 100644 index 0000000..67ec6cc Binary files /dev/null and b/public/assets/logo-150x94.jpg differ diff --git a/public/assets/logo-fond-bleu.png b/public/assets/logo-fond-bleu.png new file mode 100644 index 0000000..f51ac9c Binary files /dev/null and b/public/assets/logo-fond-bleu.png differ diff --git a/public/assets/logo2.jpg b/public/assets/logo2.jpg new file mode 100644 index 0000000..39c03bd Binary files /dev/null and b/public/assets/logo2.jpg differ diff --git a/public/assets/perdu.jpg b/public/assets/perdu.jpg new file mode 100644 index 0000000..043a97a Binary files /dev/null and b/public/assets/perdu.jpg 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 @@ + \ 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 @@ + + + + + + + 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 @@ +body +{ + margin: auto; + max-width: 1200px; + /*width: 90%;*/ + font-family: sans; + background-color: #0cceec; +} + +#bloc_page +{} + + +.infobulle +{ + /*text-decoration: none;*/ + /* évite que l'infobulle ne se retrouve dans un coin de l'écran */ + /*position:relative;*/ + /* block -> hauteur fixe pour vertical-align dans l'image */ + /*display: inline-block;*/ + +} +/*.infobulle button +{ + + display: none; + position: absolute; + bottom: 35px; + left: 10px; +} +.infobulle:hover button +{ + display: inline; + +} +.infobulle img +{ + vertical-align: middle; +}*/ \ 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 @@ +/*-- pied de page --*/ +footer +{ + /*background-color: #279dc177;*/ + /*background-color: #13aff077;*/ + background-color: #B7E9FE; + /*width: 1200px;*/ + margin: auto; + +} +footer > div +{ + margin: auto; + max-width: 1200px; + display: flex; + justify-content: space-around; +} +.contact a +{ + color: unset; + border-bottom: 3px #13aff000 solid; /* bordure invisible */ +} +.contact a:hover +{ + /*background-color: #fadb11;*/ + text-decoration: none; + padding: 0 2px; + /*background-color: #13aff0ff; + border-bottom: 3px #fadb11 solid;*/ + background-color: #ffff00; + /*border-bottom: 2px #00a8f3 solid;*/ + border-bottom: 2px #ff1d04 solid; +} + + +/*-- fil d'ariane --*/ +.breadcrumb +{ + max-width: 45%; + /*margin-right: 10px;*/ + margin: 16px 0; /* = p */ + list-style: none; /* retirer les puces */ + font-size: small; + text-align: right; + font-weight: bold; +} +.breadcrumb img +{ + width: 12px; + vertical-align: middle; +} +.breadcrumb a img, .breadcrumb a span +{ + border-bottom: 2px #13aff000 solid; +} +.breadcrumb a img:hover, .breadcrumb a span:hover +{ + /*background-color: #13aff0; + border-bottom: 3px #fadb11 solid;*/ + background-color: #ffff00; + /*border-bottom: 2px #00a8f3 solid;*/ + border-bottom: 2px #ff1d04 solid; +} +/*.breadcrumb *:not(:last-child)::after +{ + content: "→"; + display: block; + margin: 0 3px; +}*/ +.breadcrumb a +{ + color: unset; /* ne plus hériter */ + text-decoration: none; +} + +/* zone admin */ +.logged_out +{ + /*display: none;*/ + justify-content: start; +} +.logged_out a +{ + color: black; + text-decoration: none; +} +.empty_admin_zone +{ + height: 32px; +} +.logged_in +{ + height: 32px; + width: 1200px; + margin: auto; + background-color: #ffff00; + position: fixed; + bottom: 0px; +} +.logged_in > p +{ + margin: 5px 0; + display: flex; + justify-content: space-around; +} +.logged_in button +{ + margin-left: 5px; + color: #ff1d04; + font-size: medium; + border: lightgrey 2px outset; + border-radius: 4px; + background-color: white; +} +.logged_in button:hover +{ + cursor: pointer; /* curseur qui touche du doigt */ + background-color: #ffff00; + border-radius: 4px; +} \ 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 @@ +header +{ + text-align: center; + /*padding-top: 5px;*/ + /*padding-bottom: 10px;*/ + background-image: url('../assets/fond-piscine.jpg'); + background-size: cover; /* largeur du bloc_page */ +} +header > div +{ + /*margin: auto;*/ + /*max-width: 1200px;*/ + display: flex; + /*align-items: center;*/ /* conflit possible avec .social align-self */ + justify-content: space-around; +} +.header-content +{ + padding: 20px 0; +} +.site_title +{ + background-color: #ffffff7f; + border-radius: 10px; +} +header h1 +{ + font-size: x-large; + margin: 10px; +} +header h2 +{ + font-size: medium; /* défaut = x-large */ + margin: 10px; +} +header img +{ + vertical-align: bottom; /* supprime espace sous l'image */ +} +header a +{ + color: unset; /* ne plus hériter */ + text-decoration: none; +} +.social +{ + align-self: end; +} +.social img +{ + width: 25px; + background-color: #ffffffb3; + border-radius: 10px; +} + +.social img:hover +{ + background-color: #ffff00; +} 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 @@ +main +{ + margin: auto; + /*max-width: 1200px;*/ + background-color: #E3F3FF; + padding: 15px 0; +} +section > h3 +{ + padding: 15px; + margin: 0; + text-align: center; +} +section > p /* boutons spéciaux mode admin */ +{ + /*margin-left: 15px;*/ +} +.grid_columns +{ + display: grid; + grid-template-columns: repeat(3, 1fr); +} +.galery_photos +{ + display: flex; + flex-wrap: wrap; + justify-content: space-evenly; +} +.galery_photos .html_from_editor img +{ + max-width: 400px; + max-height: 250px; +} +article +{ + /*display: flex;*/ + background-color: white; + margin: 15px 15px 0 15px; + padding: 0 15px; + /*min-height: 150px;*/ +} +article .logo2 +{ + vertical-align: middle; + max-width: 200px; + max-height: 200px; + margin-right: 15px; +} +.new_content, .article_content +{ + width: 100%; +} +.new_content_text +{ + max-height: 250px; + overflow: hidden; +} + +article .action_icon +{ + width: 24px; + vertical-align: middle; + border: white 2px solid; /* invisible */ +} +article .action_icon:hover +{ + background-color: #ffff00; + border-radius: 4px; + border: lightgrey 2px outset; +} +.article_title_zone +{ + display: flex; + justify-content: space-between; +} +.under_an_article +{ + display: flex; + justify-content: space-between; + font-size: small; +} +.under_an_article p +{ + margin: 5px; +} +.under_an_article img +{ + width: 24px; + vertical-align: middle; + margin-right: 5px; +} +.article_admin_zone +{ + display: flex; + justify-content: end; +} +main button +{ + color: #ff1d04; + font-size: medium; + border-radius: 4px; + background-color: white; + border: lightgrey 2px outset; /* rend identiques les boutons firefox et chromium */ +} +main button:hover +{ + cursor: pointer; /* curseur qui pointe du doigt */ + background-color: #ffff00; + border-radius: 4px; +} \ 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 @@ +/*-- menu principal --*/ +.empty_nav_zone +{ + height: 33px; +} +.nav_main +{ + position: fixed; + /*box-shadow: 3px 3px 5px #13aff0;*/ + border: 2px solid #13aff0; + top: -2px; +} +.nav_main +{ + /*font-size: 90%;*/ + /*margin-top: 10px;*/ + /*margin-bottom: 10px;*/ + text-wrap: nowrap; +} +.nav_main > ul +{ + display: flex; + flex-direction: row; + justify-content: center; + flex-wrap: wrap; +} +ul +{ + margin: 0; +} +.nav_main ul, .nav_main li +{ + list-style: none; + padding-left: 0; +} +.nav_main p +{ + padding: 7px; + margin: 0; + background-color: #ffffffe1; /* b3 = 179 = 0.7 (x 256) */ +} +.nav_main p:hover +{ + background-color: white; +} +.current > p +{ + background-color: white; + font-weight: bold; +} +.drop-down > p::after +{ + content: ' ▼'; + font-size: x-small; +} + +.drop-down:hover > .sub-menu /* faire apparaître sub-menu */ +{ + display: block; +} +.sub-menu +{ + display: none; + background-color: white; + border-top: 3px solid #13aff0; + box-shadow: 1px 1px 3px black; + font-size: 95%; +} +/*.sub-menu p:hover*/ +.nav_main p:hover +{ + background-color: #ffff00; +} +.drop-down .sub-menu .drop-down > p:after /* bricolage? */ +{ + content: " ▶"; + font-size: x-small; +} + +/* 1er sous-menu, poitionnement pour ne pas aggrandir l'élément parent */ +nav > ul > li > ul +{ + position: absolute; /* retire du flux, positionnement par rapport à la fenêtre */ +} +/* élément du menu survolé, le positionnement relatif en fait la référence du positionnement suivant */ +.drop-down .sub-menu .drop-down +{ + position: relative; +} +.drop-down .sub-menu .drop-down .sub-menu +{ + position: absolute; /* positionnement par rapport au précédent "position" en CSS */ + left: 100%; + top: -3px; /* la bordure bleue fait 3px */ +} 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 @@ +.tox-promotion{ + display: none; +} +.hidden{ + display: none; +} \ 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 @@ + + + + + erreur 404 + + +

404 - page non trouvée

+

Le lien que vous avez utilisé ne mène nulle part.
+ Revenez à la page d'accueil et utilisez le menu, vous devriez trouvez votre route.

+ + + \ 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 Binary files /dev/null and b/public/images-mini/DPpiscines.png differ diff --git a/public/images-mini/kerne-elagage.png b/public/images-mini/kerne-elagage.png new file mode 100644 index 0000000..5416517 Binary files /dev/null and b/public/images-mini/kerne-elagage.png differ diff --git a/public/images-mini/kerne.png b/public/images-mini/kerne.png new file mode 100644 index 0000000..eea9107 Binary files /dev/null and b/public/images-mini/kerne.png differ diff --git a/public/images-mini/mako.png b/public/images-mini/mako.png new file mode 100644 index 0000000..5ba6263 Binary files /dev/null and b/public/images-mini/mako.png differ diff --git a/public/images/DPpiscines.png b/public/images/DPpiscines.png new file mode 100644 index 0000000..3cb40ba Binary files /dev/null and b/public/images/DPpiscines.png differ diff --git a/public/images/kerne-elagage.png b/public/images/kerne-elagage.png new file mode 100644 index 0000000..5416517 Binary files /dev/null and b/public/images/kerne-elagage.png differ diff --git a/public/images/kerne.png b/public/images/kerne.png new file mode 100644 index 0000000..eea9107 Binary files /dev/null and b/public/images/kerne.png differ diff --git a/public/images/mako.png b/public/images/mako.png new file mode 100644 index 0000000..5ba6263 Binary files /dev/null and b/public/images/mako.png 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 @@ +makeRootNode($id); +$node = $director->getRootNode(); + +// -- vues -- +$view_builder = new ViewBuilder($node); +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 @@ +function enableGaleryScroller(){ + // lancer photoswipe + // https://photoswipe.com/ +} \ 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 @@ +function newPassword(page, id = ''){ + if(id != ''){ + id = '&id=' + id; + } + alert('Le mot de passe a été modifié.'); + window.setTimeout(function(){ + location.href = "index.php?page=" + page + "&message=nouveau_mdp" + id; + }, 0); +} + +function copyInClipBoard(link){ + // une balise avec des attributs + var element = document.createElement("input"); + element.setAttribute("id", "copyMe"); + element.setAttribute("value", link); + + // placement dans la page (= le "document") + document.body.appendChild(element); + var cible = document.getElementById('copyMe'); + + // selection comme on le ferait à la souris + cible.select(); + // copie (= Ctrl + C) + document.execCommand("copy"); + + // nettoyage + element.parentNode.removeChild(element); + + alert('Cette adresse a été copiée dans le presse-papier:\n\n' + link); +} \ 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 @@ +let editors = {}; + +function openEditor(articleId) { + // Récupérer et sauvegarder le contenu d'origine de l'article + const articleContent = document.getElementById(articleId).innerHTML; + document.getElementById(articleId).setAttribute('data-original-content', articleContent); + + tinymce.init({ + selector: `#${articleId}`, + language: 'fr_FR', // télécharger des paquets de langue ici: https://www.tiny.cloud/get-tiny/language-packages/ + language_url: 'js/tinymce-langs/fr_FR.js', // ou installer tweeb/tinymce-i18n avec composer + license_key: 'gpl', + branding: false, + plugins: 'lists link autolink table image media autoresize help', + 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', + menubar: false, + toolbar_mode: 'wrap', + statusbar: false, + setup: function (editor) { + editor.on('init', function () { + editors[articleId] = editor; + + // Masquer le bouton "Modifier" et afficher les boutons "Annuler" et "Soumettre" + if(articleId != 'new') + { + document.querySelector(`#edit-${articleId}`).classList.add('hidden'); + document.querySelector(`#delete-${articleId}`).classList.add('hidden'); + document.querySelector(`#position_up-${articleId}`).classList.add('hidden'); + document.querySelector(`#position_down-${articleId}`).classList.add('hidden'); + } + else{ + document.querySelector(`#new-${articleId}`).classList.add('hidden'); + } + document.querySelector(`#cancel-${articleId}`).classList.remove('hidden'); + document.querySelector(`#submit-${articleId}`).classList.remove('hidden'); + + }); + }, + // upload d'image + images_upload_handler: (blobInfo, progress) => new Promise((resolve, reject) => { + const formData = new FormData(); + formData.append("file", blobInfo.blob()); + + fetch("index.php?action=upload_image", { + method: "POST", + body: formData + }) + .then(response => response.json()) + .then(data => { + if (data.location) { + resolve(data.location); + } + else { + reject("Erreur: Chemin d'image invalide"); + } + }) + .catch(error => { + reject("Erreur lors de l'upload"); + }); + }), + image_caption: true + }); + + // Remplacer le contenu de l'article par l'éditeur + document.getElementById(articleId).innerHTML = articleContent; +} + +function deleteArticle(articleId, page = '') { + if (confirm('Voulez-vous vraiment supprimer cet article ?')) + { + // Envoyer une requête au serveur pour supprimer l'article + fetch('index.php?action=delete_article', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ id: articleId }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) + { + if(page == 'article'){ + // redirection vers la page d'accueil + window.setTimeout(function(){ + location.href = "index.php?page=accueil"; + }, 0); + } + else{ + // Supprimer l'article du DOM + const articleElement = document.getElementById(articleId); + articleElement.parentElement.parentElement.remove(); //
est deux niveau au dessus + } + } + else { + alert('Erreur lors de la suppression de l\'article.'); + } + }) + .catch(error => { + console.error('Erreur:', error); + }); + } +} + +function closeEditor(articleId, display_old = true) +{ + // Fermer l'éditeur + tinymce.remove(`#${articleId}`); + delete editors[articleId]; + + // Restaurer le contenu d'origine de l'article + if(display_old){ + const originalContent = document.getElementById(articleId).getAttribute('data-original-content'); + document.getElementById(articleId).innerHTML = originalContent; + } + + // Afficher le bouton "Modifier" et masquer les boutons "Annuler" et "Soumettre" + if(articleId != 'new'){ + document.querySelector(`#edit-${articleId}`).classList.remove('hidden'); + document.querySelector(`#delete-${articleId}`).classList.remove('hidden'); + document.querySelector(`#position_up-${articleId}`).classList.remove('hidden'); + document.querySelector(`#position_down-${articleId}`).classList.remove('hidden'); + } + else{ + document.querySelector(`#new-${articleId}`).classList.remove('hidden'); + } + document.querySelector(`#cancel-${articleId}`).classList.add('hidden'); + document.querySelector(`#submit-${articleId}`).classList.add('hidden'); +} + +function submitArticle(articleId) { + // Récupérer l'éditeur correspondant à l'article + const editor = editors[articleId]; + if (!editor) { + console.error('Éditeur non trouvé pour l\'article:', articleId); + return; + } + + // Récupérer le contenu de l'éditeur + const newContent = editor.getContent(); + + // Envoi AJAX au serveur + fetch('index.php?action=editor_submit', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({id: articleId, content: newContent}) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + // Fermer l'éditeur et mettre à jour le contenu de l'article + closeEditor(articleId, false); + document.getElementById(articleId).innerHTML = newContent; + } + else { + alert('Erreur lors de la sauvegarde de l\'article.'); + } + }) + .catch(error => { + console.error('Erreur:', error); + }); +} \ 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 @@ + $value) + { + if($value != '') // valeur par défaut + { + if(isset(self::$$field)) // le champ existe dans Config + { + // problème du slash à la fin du nom d'un dossier + $value = self::slashAtEndOfPath($field, $value); + self::$$field = $value; + } + else + { + 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"; + } + } + /*else + { + echo "debug: le champ " . $field . " est vide, la valeur par défaut " . self::$$field . " sera utilisée.\n"; + }*/ + } + } + + + // pour que les chemins finissent toujours par un / + static private function slashAtEndOfPath(string $field, string $value): string + { + foreach(self::$path_vars as $item) + { + if($field === $item){ + return !str_ends_with($value, '/') ? $value . '/' : $value; + } + } + return $value; + } +} 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 @@ +entityManager = $entityManager; + self::$menu_data = new Menu($entityManager); // Menu est un modèle mais pas une entité + self::$page_path = new Path(); + $this->page = self::$page_path->getLast(); + $this->root_node = new Node; // instance mère "vide" ne possédant rien d'autre que des enfants + } + + public function makeRootNode(string $id = ''): void + { + // on récupère toutes les entrées + $dql = 'SELECT n FROM App\Entity\Node n WHERE n.page = :page OR n.page IS null'; + if($id == '') + { + $bulk_data = $this->entityManager + ->createQuery($dql) + ->setParameter('page', $this->page) + ->getResult(); + } + else // avec $_GET['id'] dans l'URL + { + $dql .= ' OR n.article_timestamp = :id'; + $bulk_data = $this->entityManager + ->createQuery($dql) + ->setParameter('page', $this->page) + ->setParameter('id', $id) + ->getResult(); + } + $this->feedObjects($bulk_data); + } + + public function makeArticleNode(string $id = ''): bool + { + $bulk_data = $this->entityManager + ->createQuery('SELECT n FROM App\Entity\Node n WHERE n.article_timestamp = :id') + ->setParameter('id', $id) + ->getResult(); + + if(count($bulk_data) === 0){ + return false; + } + + $this->root_node = $bulk_data[0]; + return true; + } + + public function feedObjects(array $bulk_data): void // $bulk_data = tableau de Node + { + // puis on les range + // (attention, risque de disfonctionnement si les noeuds de 1er niveau ne sont pas récupérés en 1er dans la BDD) + foreach($bulk_data as $node) + { + // premier niveau + if($node->getParent() == null) + { + $this->root_node->addChild($node); + + // spécifique page article + if($node->getName() === 'main' && $this->page->getEndOfPath() == 'article'){ + $main = $node; + } + } + // autres niveaux + else + { + $node->getParent()->addChild($node); + + // spécifique page article + if($node->getName() === 'new' && $this->page->getEndOfPath() == 'article'){ + $new = $node; + } + } + } + if(isset($new)){ + $main->setTempChild($new); + } + } + + public function getRootNode(): Node + { + return $this->root_node; + } +} 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, // protection contre les élements et attributs dangereux + + // liste blanche d'éléments HTML + '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', + + // liste noire d'attributs HTML + 'deny_attribute'=> 'id, class' // on garde 'style' + ); + + // faire qu'un certain élément puisse n'avoir que certains attributs, regarder la doc + private static $specHtmLawed = ''; + + public static function secureString(string $chaine): string + { + return trim(htmLawed($chaine, self::$configHtmLawed, self::$specHtmLawed));; + } + + public static function secureFileName(string $chaine): string + { + // sécuriser un nom avec chemin avec basename? + //$chaine = basename($chaine); + + /* + - caractères interdits sous windows / \ : * ? " < > | + - mac autorise les / + - mac interdit : + - linux autorise tout sauf les / + - imagemagick ne supporte pas les : + + - 'espace' fonctionne + - / remplacé par firefox en : + - \ retire ce qui est devant le \ + - * fonctionne + - ? permet le téléchargement mais pas l'affichage + - " ne fonctionne pas, remplacé par %22, filtrer %22 + - < > fonctionnent + - | fonctionne + - = fonctionne, mais je filtre parce qu'on en trouve dans une URL + - ' ` fonctionnent + - % fonctionne + - (){}[] fonctionnent + - ^ fonctionne + - # ne fonctionne pas + - ~ fonctionne + - & fonctionne + - ^ pas encore testé + */ + + // => on remplace tout par des _ + // filtrer / et \ semble inutile + + $cibles = [' ', '/', '\\', ':', '*', '?', '<', '>', '|', '=', "'", '`', '"', '%22', '#']; + $chaine = str_replace($cibles, '_', $chaine); // nécéssite l'extension mbstring + $chaine = mb_strtolower($chaine); + return($chaine); + + // les problèmes avec \ persistent !! + // => javascript + // malheureusement document.getElementById('upload').files[0].name = chaine; ne marche pas! interdit! + // javascript ne doit pas pouvoir accéder au système de fichiers + // solutions: + // - au lieu de fournir une chaine (le chemin du fichier), donner un objet à files[0].name + // - créer une copie du fichier et l'envoyer à la place + // - envoyer le fichier en AJAX + // - envoyer le nom du fichier à part puis renommer en PHP + } +} + +// erreurs à la création des mots de passe +function removeSpacesTabsCRLF(string $chaine): string +{ + $cibles = [' ', "\t", "\n", "\r"]; // doubles quotes !! + return(str_replace($cibles, '', $chaine)); +} + +// lien sans http:// +function fixLinks($data) +{ + // 1/ + // 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:// + // tomber ainsi sur une page d'erreur est parfaitement déroutant + + // regex pour détecter les balises et ajouter http:// au début des liens si nécessaire + $pattern = '#(]+href=")((?!https?://)[^>]+>)#'; + //$data = preg_replace($pattern, '$1http://$2', $data); + + // 2/ + // cas où la regex fait mal son boulot: + // l'erreur 404 est gérée par le .htaccess + // et le visiteur est redirigé à la page "menu" + // (ça ne règle pas le problème mais c'est mieux) + + // 3/ + // quand l'éditeur est ouvert (avant de valider l'article), + // le lien qu'on vient de créer apparaît dans l'infobulle, + // cliquer dessus ouvre un onglet sur une erreur 404 + // solution partielle avec le .htaccess + // + // solution? fermer ce nouvel onglet avec echo ''; + // comment déclencher le JS? en faisant qu'une erreur 404 causée pour cette raison soit particulière? + + return($data); +} 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 @@ +params = $gets; + if($anchor != ''){ + $this->setAnchor($anchor); + } + } + + //setters normaux + public function addParams(array $gets): void + { + // array_merge est préféré à l'opérateur d'union +, si une clé existe déjà la valeur est écrasée + $this->params = array_merge($this->params, $gets); + } + public function setAnchor(string $anchor = ''): void + { + if($anchor != ''){ + $this->anchor = '#' . ltrim($anchor, '#'); + } + else{ + $this->anchor = ''; + } + } + + private function makeParams(): string + { + $output = ''; + $first = true; + + foreach($this->params as $key => $value) { + if($first){ + $output .= '?'; + $first = false; + } + else{ + $output .= '&'; + } + $output .= $key . '=' . $value; + } + return $output; + } + + public function __toString(): string + { + return self::$protocol . self::$host . self::$port . self::$path . $this->makeParams() . $this->anchor; + } +} \ 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 @@ +makeArticleNode($articleId)) // une entrée est trouvée + { + $node = $director->getRootNode(); + $node->getArticle()->setContent($content); + $entityManager->flush(); + + echo json_encode(['success' => true]); + } + else{ + echo json_encode(['success' => false, 'message' => 'Aucune entrée trouvée en BDD']); + } + } + else{ + echo json_encode(['success' => false, 'message' => 'Erreur de décodage JSON']); + } + die; + } + elseif($_GET['action'] === 'delete_article' && isset($json['id'])) + { + $articleId = $json['id']; + + $director = new Director($entityManager); + $director->makeArticleNode($articleId); + $node = $director->getRootNode(); + $entityManager->remove($node); + $entityManager->flush(); + + // test avec une nouvelle requête qui ne devrait rien trouver + if(!$director->makeArticleNode($articleId)) + { + echo json_encode(['success' => true]); + + // on pourrait afficher une notification "toast" + } + else{ + http_response_code(500); + echo json_encode(['success' => false, 'message' => 'Erreur lors de la suppression de l\'article.']); + } + die; + } +} + +// détection des requêtes d'upload d'image de tinymce +if(strpos($_SERVER['CONTENT_TYPE'], 'multipart/form-data') !== false && isset($_GET['action']) && $_GET['action'] === 'upload_image'){ + if (isset($_FILES['file'])) { + $file = $_FILES['file']; + $dest = 'images/'; + $dest_mini = 'images-mini/'; + + // Vérifier si les répertoires existent, sinon les créer + if(!is_dir($dest)) { + mkdir($dest, 0700, true); + } + if(!is_dir($dest_mini)) { + mkdir($dest_mini, 0700, true); + } + + $filePath = $dest . basename($file['name']); + + // créer une miniature de l'image + + if(move_uploaded_file($file['tmp_name'], $filePath)) { + $image_url = str_replace(basename($_SERVER['SCRIPT_NAME']), '', $_SERVER['SCRIPT_NAME']); + echo json_encode(['location' => $image_url . $filePath]); // renvoyer l'URL de l'image téléchargée + } + else{ + http_response_code(500); + echo json_encode(['message' => 'Erreur 500: Internal Server Error']); + } + } + else{ + http_response_code(400); + echo json_encode(['message' => 'Erreur 400: Bad Request']); + } + die; +} + +// détection des requêtes de type XHR, pas d'utilité pour l'instant +/*elseif(isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest'){ + echo "requête XHR reçue par le serveur"; + die; +}*/ + + 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 @@ +'); + } + } + if(!extension_loaded('imagick') && !extension_loaded('gd')){ + echo("il manque une de ces extensions au choix: imagick (de préférence) ou gd
"); + } + + /* -- droits des fichiers et dossiers -- */ + $droits_dossiers = 0700; + $droits_fichiers = 0600; + + // accès interdit en HTTP + if(!file_exists('../config/.htaccess')){ + $contenu = <<< HTACCESS + + Order Allow,Deny + Deny from all + +HTACCESS; + + $fichier = fopen('../config/.htaccess', 'w'); + fputs($fichier, $contenu); + fclose($fichier); + chmod('../config/.htaccess', $droits_fichiers); + //echo("danger
pas de .htaccess dans config
prévenez le respondable du site"); + //die; + } + + // accès limité en local (600) pour config.ini + if(substr(sprintf('%o', fileperms('../config/config.ini')), -4) != 600){ + chmod('../config/config.ini', $droits_fichiers); + } + + // création de data et sous-dossiers + if(!file_exists('../data')){ + mkdir('../data/'); + chmod('../data/', $droits_dossiers); + } + if(!touch('../data')){ + echo("dossier data non autorisé en écriture"); + die; + } + $sous_dossiers = array('images', 'images-mini', 'videos'); + foreach ($sous_dossiers as $sous_dossier){ + if(!file_exists('../data/' . $sous_dossier)){ + mkdir('../data/' . $sous_dossier); + chmod('../data/' . $sous_dossier, $droits_dossiers); + } + if(!touch('../data/' . $sous_dossier)){ + echo("dossier data non autorisé en écriture"); + die; + } + } +} + +// création de la page d'accueil à la toute 1ère visite du site +// les informations ici ne sont pas demandées à l'utilisateur pour l'instant (on verra ça plus tard) +function makeStartPage(EntityManager $entityManager){ + /* -- table page -- */ + // paramètres: name_page, end_of_path, reachable, in_menu, parent + $accueil = new Page('Accueil', 'accueil', true, true, NULL); + $connection = new Page('Connexion', 'connexion', true, false, NULL); + $article = new Page('Article', 'article', true, false, NULL); + $edit_page = new Page("Modification d'une page", 'modif_page', true, false, NULL); + $new_page = new Page('Nouvelle page', 'nouvelle_page', true, false, NULL); + $edit_paths = new Page("Menu et chemins", 'menu_chemins', true, false, NULL); + + /* -- table node -- */ + // paramètres: name_node, article_timestamp, attributes, position, parent, page, article + $head_accueil = new Node('head', NULL, ['css_array' => ['body', 'head', 'nav', 'main', 'foot'], 'js_array' => ['main']], 1, NULL, $accueil, NULL); + $header = new Node('header', NULL, [], 2, NULL, NULL, NULL); + $nav = new Node('nav', NULL, [], 1, $header, NULL, NULL); + $main = new Node('main', NULL, [], 3, NULL, NULL, NULL); + $footer = new Node('footer', NULL, [], 4, NULL, NULL, NULL); + $breadcrumb = new Node('breadcrumb', NULL, [], 1, $footer, NULL, NULL); + $head_login = new Node('head', NULL, ["stop" => true, 'css_array' => ['body', 'head', 'nav', 'main'], 'js_array' => ['main']], 1, NULL, $connection, NULL); + $login = new Node('login', NULL, [], 1, $main, $connection, NULL); + $head_article = new Node('head', NULL, ['css_array' => ['body', 'head', 'nav', 'main', 'foot'], 'js_array' => ['main']], 1, NULL, $article, NULL); + + /* -- table image -- */ + // paramètres: file_name, file_path, file_path_mini, mime_type, alt + $favicon = new Image("favicon48x48.png", NULL, "assets/favicon48x48.png", "image/png", "favicon"); + $logo = new Image("logo-120x75.jpg", NULL, "assets/logo-120x75.jpg", "image/png", "head_logo"); + $facebook = new Image("facebook.svg", NULL, "assets/facebook.svg", "image/svg+xml", "facebook"); + $instagram = new Image("instagram.svg", NULL, "assets/instagram.svg", "image/svg+xml", "instagram"); + $fond_piscine = new Image("fond-piscine.jpg", "assets/fond-piscine.jpg", NULL, "images/jpg", "fond-piscine"); + + /* -- table node_data -- */ + // paramètres: data, node + $head_accueil_data = new NodeData(["description" => "Club, École de natation et Perfectionnement", "title" => "Les Nageurs Bigoudens"], $head_accueil, new ArrayCollection([$favicon])); + $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])); + $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); + $head_login_data = new NodeData(["description" => "Connexion", "title" => "Connexion"], $head_login, new ArrayCollection([$favicon])); + $head_article_data = new NodeData(["description" => "", "title" => ""], $head_article, new ArrayCollection([$favicon])); + + $entityManager->persist($accueil); + $entityManager->persist($connection); + $entityManager->persist($article); + $entityManager->persist($edit_page); + $entityManager->persist($new_page); + $entityManager->persist($edit_paths); + $entityManager->persist($head_accueil); + $entityManager->persist($header); + $entityManager->persist($nav); + $entityManager->persist($main); + $entityManager->persist($footer); + $entityManager->persist($breadcrumb); + $entityManager->persist($head_login); + $entityManager->persist($login); + $entityManager->persist($head_article); + $entityManager->persist($favicon); + $entityManager->persist($logo); + $entityManager->persist($facebook); + $entityManager->persist($instagram); + $entityManager->persist($fond_piscine); + $entityManager->persist($head_accueil_data); + $entityManager->persist($header_data); + $entityManager->persist($footer_data); + $entityManager->persist($head_login_data); + $entityManager->persist($head_article_data); + $entityManager->flush(); + + header('Location: ' . new URL); + die; +} \ 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 @@ +getRepository(User::class)->findAll(); + + // cas particulier table vide + if(count($users) === 0) + { + $_GET = []; + $_SESSION['user'] = ''; + $_SESSION['admin'] = false; + + // création d'un utilisateur, puis rechargement de la page + createPassword($entityManager); + } +} + + +function createPassword(EntityManager $entityManager) +{ + // fonction exécutée à priori deux fois d'affilée: affichage puis traitement de la saisie + + // II - traitement + unset($_SESSION['user']); + + $captcha_solution = (isset($_SESSION['captcha']) && is_int($_SESSION['captcha'])) ? $_SESSION['captcha'] : 0; + $captcha = isset($_POST['captcha']) ? controlCaptchaInput($_POST['captcha']) : 0; + + $error = ''; + if(!isset($_POST['captcha'])) // page rechargée + { + //$error = ''; + } + elseif($captcha == 0) + { + $error = 'error_non_valid_captcha'; + } + elseif($captcha_solution == 0) + { + //$error = ''; + } + elseif($captcha != $captcha_solution) // le test! + { + $error = 'bad_solution_captcha'; + } + elseif(!isset($_POST['password']) || empty($_POST['password']) + || (!isset($_POST['login']) || empty($_POST['login']))) + { + $error = 'bad_login_or_password'; + } + else + { + // -> caractères HTML dangereux supprimés + $login = Security::secureString($_POST['login']); + $password = Security::secureString($_POST['password']); + + // -> prévenir la validation par erreur d'une chaine "vide" + $login = removeSpacesTabsCRLF($login); + $password = removeSpacesTabsCRLF($password); + + // conformité + if(isset($password) && isset($login) + && $password == $_POST['password'] && $login == $_POST['login']) + { + // enregistrement et redirection + $password = password_hash($password, PASSWORD_DEFAULT); + $user = new App\Entity\User($login, $password); + $entityManager->persist($user); + $entityManager->flush(); + + header('Location: ' . new URL); + exit(); + } + else + { + $error = 'bad_password'; + } + } + + // inséré dans $captchaHtml puis dans $formulaireNouveauMDP + $captcha = createCaptcha(); + // enregistrement de la réponse du captcha pour vérification + $_SESSION['captcha'] = $captcha[2]; // int + + + // I - affichage + $title = 'Bienvenue nageur bigouden'; + $subHeading = 'Veuillez choisir les codes que vous utiliserez pour gérer le site.'; + + // même vue que la fonction changerMotDePasse() + require('../src/view/password.php'); + + echo($header); + if($error != '') + { + sleep(1); + echo($error_messages[$error]); + } + echo($formulaireNouveauMDP); + echo($error_messages['forbidden_characters']); + echo($footer); + die; +} + + +function connect(LoginBuilder $builder, EntityManager $entityManager) +{ + // déjà connecté + if($_SESSION['admin']) + { + header('Location: ' . new URL); + die; + } + + // II - traitement + $_SESSION['user'] = ''; + $_SESSION['admin'] = false; + + $captcha_solution = (isset($_SESSION['captcha']) && is_int($_SESSION['captcha'])) ? $_SESSION['captcha'] : 0; + $captcha = isset($_POST['captcha']) ? controlCaptchaInput($_POST['captcha']) : 0; + + $error = ''; + if(!isset($_POST['captcha'])) // page rechargée + { + //$error = ''; + } + elseif($captcha == 0) + { + $error = 'error_non_valid_captcha'; + } + elseif($captcha_solution == 0) + { + //$error = ''; + } + elseif($captcha != $captcha_solution) // le test! + { + $error = 'bad_solution_captcha'; + } + elseif(!isset($_POST['login']) || empty($_POST['login']) + || !isset($_POST['password']) || empty($_POST['password'])) + { + $error = 'bad_password'; + } + else // c'est OK + { + $login = $_POST['login']; + $password = $_POST['password']; + $user = getUser($login, $entityManager); + + // enregistrement et redirection + if(password_verify($password, $user->getPassword())) + { + session_start(); + $_SESSION['user'] = $login; + $_SESSION['admin'] = true; + $link = new URL(isset($_GET['from']) ? ['page' => $_GET['from']] : []); + isset($_GET['id']) ? $link->addParams(['id' => $_GET['id']]) : ''; + header('Location: ' . $link); + die; + } + else + { + $error = 'bad_password'; + } + } + + // inséré dans $captchaHtml puis dans $formulaireNouveauMDP + $captcha = createCaptcha(); + // enregistrement de la réponse du captcha pour vérification + $_SESSION['captcha'] = $captcha[2]; // int + + // I - affichage + $title = "Connexion"; + $subHeading = "Veuillez saisir votre identifiant (e-mail) et votre mot de passe."; + + require('../src/view/password.php'); + + //$builder->addHTML($header); + if($error != '') + { + sleep(1); + $builder->addHTML($error_messages[$error]); + } + $builder->addHTML($formulaireConnexion); + //$builder->addHTML($warning_messages['message_cookie']); + $builder->addHTML($warning_messages['private_browsing']); + $builder->addHTML($footer); + + //die; +} + + +function changePassword(EntityManager $entityManager) +{ + // fonction exécutée à priori deux fois d'affilée: affichage puis traitement de la saisie + + // OUT !! + if(empty($_SESSION['user']) || !$_SESSION['admin']) + { + $_SESSION['user'] = ''; + $_SESSION['admin'] = false; + header('Location: index.php'); + die; + } + + // II - traitement + $error = ''; + $success = false; + if(empty($_POST)) // première fois ou page rechargée + { + // + } + elseif(!isset($_POST['login']) || empty($_POST['login']) + || !isset($_POST['old_password']) || empty($_POST['old_password']) + || !isset($_POST['new_password']) || empty($_POST['new_password'])) + { + $error = 'bad_login_or_password'; + } + else + { + // sécurisation de la saisie + $new_password = Security::secureString($_POST['new_password']); + $login = Security::secureString($_POST['login']); + $old_password = Security::secureString($_POST['old_password']); + + // éviter d'enregistrer une chaîne vide + $new_password = removeSpacesTabsCRLF($new_password); + + // tests de conformité + if($login != $_POST['login'] || $old_password != $_POST['old_password'] || $new_password != $_POST['new_password']) + { + $error = 'forbidden_characters'; + } + else + { + $user = getUser($login, $entityManager); + + if(password_verify($old_password, $user->getPassword())) + { + // enregistrement et redirection + $new_password = password_hash($new_password, PASSWORD_DEFAULT); + $user->setPassword($new_password); + $entityManager->flush(); + $success = true; + } + else + { + $error = 'bad_password'; + } + } + } + + + // I - affichage + $title = "Nouveau mot de passe"; + $subHeading = "Veuillez vous identifier à nouveau puis saisir votre nouveau mot de passe."; + + require('../src/view/password.php'); + + echo($header); + if($error != '') + { + sleep(1); // sécurité TRÈS insuffisante à la force brute + echo($error_messages[$error]); + } + elseif($success) + { + $success = false; + echo($alertJSNewPassword); + die; + } + echo($formulaireModifMDP); + echo($footer); + die; +} + + +function getUser(string $login, EntityManager $entityManager): User +{ + $users = $entityManager->getRepository('App\Entity\User')->findBy(['login' => $login]); + + // détection d'un abus + if(count($users) === 0) + { + $_SESSION['user'] = ''; + $_SESSION['admin'] = false; + + header('Location: index.php'); // page création d'un mot de passe à l'attérissage + die; + } + + foreach($users as $user) + { + if($user->getLogin() === $login) + { + return $user; + } + } + header('Location: ' . new URL); + die; +} + + +function disconnect(EntityManager $entityManager) +{ + // nettoyage complet + $_SESSION = []; // mémoire vive + session_destroy(); // fichier côté serveur + setcookie('PHPSESSID', '', time() - 4200, '/'); // cookie de session + $link = new URL(['page' => $_GET['page']]); + isset($_GET['id']) ? $link->addParams(['id' => $_GET['id']]) : ''; + header('Location: ' . $link); + die; +} + + +function createCaptcha(): array +{ + $a = rand(2, 9); + $b = rand(2, 9); + return array(toLettersFrench($a), toLettersFrench($b), $a * $b); +} + +function toLettersFrench(int $number): string +{ + return match($number) + { + 2 => 'deux', + 3 => 'trois', + 4 => 'quatre', + 5 => 'cinq', + 6 => 'six', + 7 => 'sept', + 8 => 'huit', + 9 => 'neuf', + default => '', // erreur + }; +} + +// on veut des chiffres +function controlCaptchaInput(string $captcha = '0'): int +{ + // $captcha est un POST donc une chaîne, '2.3' est acceptés + // (int) supprime les décimales + return (is_numeric($captcha) && $captcha == (int) $captcha) ? (int) $captcha : 0; +} \ 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 @@ +children = new ArrayCollection(); + + $bulk_data = $entityManager + ->createQuery('SELECT n FROM App\Entity\Page n WHERE n.parent IS null') // :Doctrine\ORM\Query + ->getResult(); // :array de Page + + if(count($bulk_data) === 0){ + makeStartPage($entityManager); + } + + foreach($bulk_data as $first_level_entries){ + // génération du menu + if($first_level_entries->getInMenu()){ + $this->addChild($first_level_entries); + } + // autres pages + else{ + // attention, seul le premier élément du chemin est pris en compte + $this->other_pages[] = $first_level_entries; + } + } + + foreach($this->getChildren() as $page){ + $page->fillChildrenPagePath(); + } + + /*for($i = 0; $i < count($this->getChildren()[1]->getChildren()); $i++){ + echo $this->getChildren()[1]->getChildren()[$i]->getEndOfPath() . ' - '; + echo $this->getChildren()[1]->getChildren()[$i]->getPageName() . '
'; + }*/ + //die; + } + + public function getOtherPages(): array + { + return $this->other_pages; + } +} \ 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 @@ +findPage(Director::$menu_data, $path_array); // remplit $this->current_page + } + catch(Exception $e){} + /*echo "nb d'autres pages: " . count(Director::$menu_data->getOtherPages()) . '
'; + echo 'longueur du chemin: ' . count($this->current_page) . '
'; + foreach($this->current_page as $current){ + echo $current->getEndOfPath() . ' '; + } + die;*/ + } + + // produit un tableau de Page en comparant le chemin demandé avec les données dans Menu + // succès => une exception est lancée pour sortir des fonctions imbriquées + // echec => redirection vers la page erreur 404 + private function findPage(Page|Menu $menu, array $path_array) + { + // recherche dans les autres pages + if($menu instanceof Menu){ + foreach($menu->getOtherPages() as $page) + { + if($path_array[0] === $page->getEndOfPath()) + { + $this->current_page[] = $page; + throw new Exception(); + } + } + } + // recherche dans le menu + foreach($menu->getChildren() as $page) + { + if($path_array[0] === $page->getEndOfPath()) + { + $this->current_page[] = $page; + if(count($path_array) > 1) + { + array_shift($path_array); // $this->path_array n'est pas modifié, un tableau PHP est passé à une fonction par copie + $this->findPage($page, $path_array); + } + else{ + throw new Exception(); // sortir de tous les findPage() en même temps + } + } + } + // rien trouvé + URL::setPath('erreur404.html'); + header('Location: '. new URL); + die; + } + + public function getString(): string + { + $path_string = ""; + foreach($this->current_page as $one_page){ + $path_string .= $one_page->getEndOfPath() . '/'; + } + return rtrim($path_string, '/'); + } + public function getArray(): array + { + return $this->current_page; + } + + // c'est là qu'on est quoi + public function getLast(): Page + { + return $this->current_page[count($this->current_page) - 1]; + } +} \ 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 @@ + Config::$db_driver, + 'user' => Config::$user, + 'password' => Config::$password, + 'host' => Config::$db_host, + 'dbname' => Config::$database, +], $config); + +// obtaining the entity manager +$entityManager = new EntityManager($connection, $config); + +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 @@ + 'CURRENT_TIMESTAMP'], unique: true)] + private \DateTime $date_time; // le type datetime de doctrine convertit en type \DateTime de PHP + + #[ORM\Column(type: "string")] + private string $title; + + #[ORM\Column(type: "text")] + private string $preview; // une simple textarea + + #[ORM\Column(type: "text")] + private string $content; // de l'éditeur html + + // liaison avec table intermédiaire + #[ORM\ManyToMany(targetEntity: Image::class, inversedBy: "article")] + #[ORM\JoinTable( + name: "nb_article_image", + joinColumns: [new ORM\JoinColumn(name: "article_id", referencedColumnName: "id_article", onDelete: "CASCADE")], + inverseJoinColumns: [new ORM\JoinColumn(name: "image_id", referencedColumnName: "id_image", onDelete: "CASCADE")] + )] + private Collection $images; + + public function __construct() + { + $this->images = new ArrayCollection(); // initialisation nécessaire + } + + public function getDateTime(): \DateTime + { + return $this->date_time; + } + public function getTimestamp(): int + { + return $this->date_time->getTimestamp(); + } + public function getTitle(): string + { + return $this->title; + } + public function getPreview(): string + { + return $this->preview; + } + public function getContent(): string + { + return $this->content; + } + public function setContent(string $data): void + { + $this->content = $data; + } + + public function getImages(): Collection + { + return $this->images; + } +} 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 @@ + 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. + => 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. + => 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. + => Déplacement du fichier sur le serveur : Le fichier est déplacé depuis son emplacement temporaire vers le répertoire uploads/. + => Enregistrement dans la base de données : On enregistre les informations de l'image dans la base de données. */ + + #[ORM\ManyToMany(targetEntity: NodeData::class, mappedBy: "images")] + private $node_data; + + #[ORM\ManyToMany(targetEntity: Article::class, mappedBy: "images")] + private $article; + + public function __construct(string $name, ?string $path, ?string $path_mini, string $mime_type, string $alt) + { + $this->file_name = $name; + $this->file_path = $path; + $this->file_path_mini = $path_mini; + $this->mime_type = $mime_type; + $this->alt = $alt; + } + + public function getFileName(): string + { + return $this->file_name; + } + public function getFilePath(): string + { + return $this->file_path; + } + public function getFilePathMini(): string + { + return $this->file_path_mini; + } + public function getAlt(): string + { + return $this->alt; + } + + + // pour ViewBuilder? + /*public function displayImage($imageId): void + { + //$imageId = 1; // Exemple d'ID d'image + $stmt = $pdo->prepare("SELECT file_path FROM images WHERE id = ?"); + $stmt->execute([$imageId]); + $image = $stmt->fetch(); + + if ($image) { + echo "Image"; + } else { + echo "Image non trouvée."; + } + }*/ +} 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 @@ +name_node = $name; + $this->article_timestamp = $article_timestamp; + $this->attributes = $attributes; + $this->position = $position; + $this->parent = $parent; + $this->page = $page; + $this->article = $article; + } + + public function addChild(self $child): void + { + $this->children[] = $child; + $this->sortChildren(); + } + + // utiliser $position pour afficher les éléments dans l'ordre + private function sortChildren(): void + { + $iteration = count($this->children); + while($iteration > 1) + { + for($i = 0; $i < $iteration - 1; $i++) + { + //echo '
' . $this->children[$i]->getPosition() . ' - ' . $this->children[$i + 1]->getPosition(); + if($this->children[$i]->getPosition() > $this->children[$i + 1]->getPosition()) + { + $tmp = $this->children[$i]; + $this->children[$i] = $this->children[$i + 1]; + $this->children[$i + 1] = $tmp; + } + } + $iteration--; + } + } + + // pfff... + public function getId(): int + { + return $this->id_node; + } + public function getName(): string + { + return $this->name_node; + } + /*public function setName(string $name): void + { + $this->name_node = $name; + }*/ + public function getArticleTimestamp(): string + { + return $this->article_timestamp; + } + public function getAttributes(): array + { + return $this->attributes; + } + /*public function setAttributes(array $attributes): void + { + $this->attributes = $attributes; + }*/ + public function getParent(): ?self + { + return $this->parent; + } + /*public function setParent(?self $parent): void + { + $this->parent = $parent; + }*/ + public function getPosition(): int + { + return $this->position; + } + /*public function setPosition(int $position): void + { + $this->position = $position; + }*/ + public function getPage(): Page + { + return $this->page; + } + /*public function setPage(Page $page): void + { + $this->page = $page; + }*/ + public function getArticle(): Article + { + return $this->article; + } + /*public function setArticle(Article $article): void + { + $this->article = $article; + }*/ + public function getNodeData(): ?NodeData + { + return $this->node_data; + } + public function getChildren(): array + { + return $this->children; + } + + public function getTempChild(): ?self // peut renvoyer null + { + return $this->temp_child; + } + public function setTempChild(self $child): void + { + $this->temp_child = $child; + } +} 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 @@ +data = $data; + $this->node = $node; + $this->images = $images; + } + + public function getData(): array + { + return $this->data; + } + /*public function setData(array $data): void + { + $this->data = $data; + } + public function setNode(Node $node): void + { + $this->node = $node; + }*/ + public function getImages(): Collection + { + return $this->images; + } +} 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 @@ +name_page = $name; + $this->end_of_path = $eop; + $this->reachable = $reachable; + $this->in_menu = $in_menu; + $this->parent = $parent; + $this->children = new ArrayCollection(); + } + + // getters + /*public function getId(): int + { + return $this->id_page; + }*/ + public function getPageName(): string + { + return $this->name_page; + } + public function getPagePath(): string + { + return $this->page_path; + } + public function getEndOfPath(): string + { + return $this->end_of_path; + } + public function getInMenu(): bool + { + return $this->in_menu; + } + public function getParent(): ?Page + { + return $this->parent; + } + public function getChildren(): Collection + { + return $this->children; + } + + public function fillChildrenPagePath(string $parent_path = ''): void + { + $this->page_path = $parent_path != '' ? $parent_path . '/' . $this->end_of_path : $this->end_of_path; + foreach($this->getChildren() as $page){ + $page->fillChildrenPagePath($this->page_path); + } + } + + public function addChild(self $child): void + { + $this->children[] = $child; + } +} 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 @@ +login = $login; + $this->password = $password; + } + + public function getLogin(): string + { + return $this->login; + } + public function getPassword(): string + { + return $this->password; + } + + public function setPassword(string $password): void + { + $this->password = $password; + } +} \ 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 @@ +getChildren() as $child_node) + { + $builder_name = $this->snakeToPascalCase($child_node->getName()) . 'Builder'; + $builder = new $builder_name($child_node); + $this->html .= $builder->render(); + + // pages spéciales où on n'assemble pas tout + if($builder_name === 'HeadBuilder' && $builder->getStop()) + { + foreach($node->getChildren() as $target_node){ + if($target_node->getName() === 'main'){ + $main_node = $target_node; + break; + } + } + // on construit
et on s'arrête! les autres noeuds sont ignorés + $builder_name = $this->snakeToPascalCase($main_node->getName()) . 'Builder'; + $builder = new $builder_name($main_node); + $this->html .= "\n"; + $this->html .= $builder->render() . "\n"; + $this->html .= "\n"; + break; + } + } + } + + protected function snakeToPascalCase(string $input): string + { + return str_replace('_', '', ucwords($input, '_')); + } + + public function render(): string // = getHTML() + { + return $this->html; + } + public function addHTML(string $html): void + { + $this->html .= $html; + } +} \ 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 @@ +getName() . '.php'; + + if(file_exists($viewFile)) + { + // id (timestamp) + if(!empty($node->getAttributes())) + { + extract($node->getAttributes()); + } + + // html + $title = $node->getArticle()->getTitle(); + $html = $node->getArticle()->getContent(); + $id = $node->getArticleTimestamp(); + + // partage + $share_link = new URL(['page' => CURRENT_PAGE], $id); + $share_js = 'onclick="copyInClipBoard(\'' . $share_link . '\')"'; + $share_button = '

' . "\n"; + + // modifier un article + $admin_buttons = ''; + if($_SESSION['admin']) + { + $modify_js = 'onclick="openEditor(\'' . $id . '\')"'; + $modify_article = '

' . "\n"; + + $up_link = new URL(['page' => CURRENT_PAGE, 'id' => $id, 'action' => 'position_up']); + $up_button = '

' . "\n"; + + $down_link = new URL(['page' => CURRENT_PAGE, 'id' => $id, 'action' => 'position_down']); + $down_button = '

' . "\n"; + + $delete_js = 'onclick="deleteArticle(\'' . $id . '\')"'; + $delete_article = '

' . "\n"; + + $close_js = 'onclick="closeEditor(\'' . $id . '\')"'; + $close_editor = ''; + + $submit_js = 'onclick="submitArticle(\'' . $id . '\')"'; + $submit_article = ''; + + $admin_buttons = $modify_article . $up_button . $down_button . $delete_article . $close_editor . $submit_article; + } + + ob_start(); + require($viewFile); + $this->html .= ob_get_clean(); + } + } +} 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 @@ +getName() . '.php'; + + if(file_exists($viewFile)) + { + if(!empty($node->getNodeData()->getData())) + { + extract($node->getNodeData()->getData()); + } + + // ajouter un article + $new_article = ''; + $new_article_admin_buttons = ''; + if($_SESSION['admin']) + { + $id = 'new'; + + //$link = new URL(['page' => CURRENT_PAGE, 'action' => 'open_editor']); + $js = 'onclick="openEditor(\'' . $id . '\')"'; + //$new_article = ''; + $new_article = '

' . "\n" . + '

'; + + $close_js = 'onclick="closeEditor(\'' . $id . '\')"'; + $close_editor = '
'; + + $submit_js = 'onclick="submitArticle(\'' . $id . '\')"'; + $submit_article = '
'; + + $new_article_admin_buttons = $close_editor . $submit_article; + } + + $this->useChildrenBuilder($node); + $content = $this->html; + + ob_start(); + require $viewFile; + $this->html = ob_get_clean(); // pas de concaténation ici, on écrase + } + } +} \ 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 @@ +html = $this->breadcrumbHTML(false); + } + + private function breadcrumbHTML(bool $links = false): string + { + $asset = 'assets/home.svg'; // => BDD? + $breadcrumb_array = Director::$page_path->getArray(); // tableau de Page + $html = ''; + $nb_of_entries = count($breadcrumb_array); + + if($nb_of_entries > 1) + { + // petite maison et flèche + $html .= '\n"; + } + return $html; + } +} \ 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 @@ +getName() . '.php'; + + if(file_exists($viewFile)) + { + // $adresses postale et e-mail + if(!empty($node->getNodeData()->getData())) + { + extract($node->getNodeData()->getData()); + } + + $this->useChildrenBuilder($node); + $breadcrumb = $this->html; + + // zone admin + $empty_admin_zone = ''; + //$zone_admin = ''; + if($_SESSION['admin']) + { + $div_admin = 'logged_in'; + $empty_admin_zone = 'empty_admin_zone'; + $link_edit_page = new URL(['page' => CURRENT_PAGE, 'action' => 'modif_page']); + $link_new_page = new URL(['from' => CURRENT_PAGE, 'page' => 'nouvelle_page']); + $link_change_paths = new URL(['from' => CURRENT_PAGE, 'page' => 'menu_chemins']); + + $link_change_password = new URL(['from' => CURRENT_PAGE, 'action' => 'modif_mdp']); + isset($_GET['id']) ? $link_change_password->addParams(['id' => $_GET['id']]) : ''; + + $link_logout = new URL(['page' => CURRENT_PAGE, 'action' => 'deconnexion']); + isset($_GET['id']) ? $link_logout->addParams(['id' => $_GET['id']]) : ''; + + $zone_admin = '

Vous êtes en mode administrateur.' . "\n" . + '' . "\n" . + '' . "\n" . + '' . "\n" . + '' . "\n" . + '

' . "\n"; + } + else + { + $div_admin = 'logged_out'; + $zone_admin = ''; + if(Director::$page_path->getLast()->getEndOfPath() === 'article' && isset($_GET['id'])){ + $zone_admin = ''; + } + else{ + $zone_admin = ''; + } + } + + ob_start(); + require $viewFile; + $this->html = ob_get_clean(); + } + } +} \ 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 @@ +getName() . '.php'; + + if(file_exists($viewFile)) + { + if(!empty($node->getNodeData()->getData())) + { + extract($node->getNodeData()->getData()); + } + + // ajouter un article + $new_article = ''; + $new_article_admin_buttons = ''; + if($_SESSION['admin']) + { + $id = 'new'; + + //$link = new URL(['page' => CURRENT_PAGE, 'action' => 'open_editor']); + $js = 'onclick="openEditor(\'' . $id . '\')"'; + //$new_article = ''; + $new_article = '

' . "\n" . + '

'; + + $close_js = 'onclick="closeEditor(\'' . $id . '\')"'; + $close_editor = '
'; + + $submit_js = 'onclick="submitArticle(\'' . $id . '\')"'; + $submit_article = '
'; + + $new_article_admin_buttons = $close_editor . $submit_article; + } + + $this->useChildrenBuilder($node); + $content = $this->html; + + ob_start(); + require $viewFile; + $this->html = ob_get_clean(); // pas de concaténation ici, on écrase + } + } +} \ 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 @@ +getName() . '.php'; + + if(file_exists($viewFile)) + { + if(!empty($node->getNodeData()->getData())) + { + extract($node->getNodeData()->getData()); + } + + // ajouter un article + $new_article = ''; + $new_article_admin_buttons = ''; + if($_SESSION['admin']) + { + $id = 'new'; + $js = 'onclick="openEditor(\'' . $id . '\')"'; + + if(Director::$page_path->getLast()->getEndOfPath() === 'accueil') + { + $new_article = '

' . "\n" . + '

'; + } + else + { + $new_article = '

' . "\n" . + '

'; + } + + $close_js = 'onclick="closeEditor(\'' . $id . '\')"'; + $close_editor = '
'; + + $submit_js = 'onclick="submitArticle(\'' . $id . '\')"'; + $submit_article = '
'; + + $new_article_admin_buttons = $close_editor . $submit_article; + } + + $this->useChildrenBuilder($node); + $content = $this->html; + + ob_start(); + require $viewFile; + $this->html = ob_get_clean(); // pas de concaténation ici, on écrase + } + } +} \ 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 @@ +getName() . '.php'; + + if(file_exists($viewFile)) + { + // css et js + if(!empty($node->getAttributes())) + { + extract($node->getAttributes()); + } + + // pages spéciales où on n'assemble pas tout + $this->stop = isset($stop) ? $stop : false; + $css = ''; + foreach($css_array as $name) + { + $css .= '' . "\n"; + } + $js = ''; + foreach($js_array as $name) + { + $js .= '' . "\n"; + } + + // tinymce, nécéssite un script de copie dans composer.json + if($_SESSION['admin']){ + $css .= '' . "\n"; + $js .= '' . "\n"; + $js .= '' . "\n"; + } + + // titre et description + if(!empty($node->getNodeData()->getData())) + { + extract($node->getNodeData()->getData()); + } + + // favicon + foreach($node->getNodeData()->getImages() as $image) + { + if(str_contains($image->getFileName(), 'favicon')) + { + $favicon = rtrim($image->getFilePathMini(), '/'); + $alt = $image->getAlt(); + } + } + + ob_start(); + require $viewFile; + $this->html .= ob_get_clean(); + } + } + + public function getStop(): bool + { + return $this->stop; + } +} 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 @@ +getChildren(); + foreach($children as $child) + { + if($child->getName() === 'nav') + { + $this->nav = $child; + $nav_builder = new NavBuilder($this->nav); + $nav = $nav_builder->render(); + } + } + + $viewFile = self::VIEWS_PATH . $node->getName() . '.php'; + + if(file_exists($viewFile)) + { + // titre et description + if(!empty($node->getNodeData()->getData())) + { + extract($node->getNodeData()->getData()); + } + + // attributs, aucun pour l'instant + if(!empty($node->getAttributes())) + { + extract($node->getAttributes()); + } + + // header logo + réseaux sociaux + $targets = ['logo', 'facebook', 'instagram', 'fond_piscine']; + $i = 0; + foreach($node->getNodeData()->getImages() as $image) + { + if(str_contains($image->getFileName(), $targets[$i])) + { + $var = $targets[$i]; + $$var = rtrim($image->getFilePathMini(), '/'); + $var .= '_alt'; // ex: logo_alt + $$var = $image->getAlt(); + } + $i++; + } + + // générer HTML réseaux sociaux + // + + ob_start(); + require $viewFile; + $this->html .= ob_get_clean(); + } + } +} \ 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 @@ +html .= "
\n"; + + if(Director::$page_path->getLast()->getEndOfPath() === 'article'){ + if($node->getTempChild() == null){ + $new = new Node; + } + else{ + $new = $node->getTempChild(); + } + //$builder_name = $this->snakeToPascalCase($new->getName()) . 'Builder'; + $builder_name = 'NewBuilder'; + $builder = new $builder_name($new); + $this->html .= $builder->render(); + } + else{ + $this->useChildrenBuilder($node); + } + + $this->html .= "
\n"; + } +} 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 @@ +html .= ''; + } + + private function navMainHTML(Page $nav_data, array $current): string + { + $nav_html = ''; + static $level = 0; + + foreach($nav_data->getChildren() as $data) + { + $class = ''; + if(isset($current[$level]) && $data->getEndOfPath() === $current[$level]->getEndOfPath()){ + $class = ' current'; + } + + if(count($data->getChildren()) > 0) // titre de catégorie + { + $nav_html .= '' . "\n"; + } + else + { + $target = ''; + if(str_starts_with($data->getEndOfPath(), 'http')) // lien vers autre site + { + $link = $data->getEndOfPath(); // $link = chaine + $target = ' target="_blank"'; + } + elseif($data->getEndOfPath() != '') // lien relatif + { + $link = new URL(['page' => $data->getPagePath()]); // $link = objet + } + /*else + { + echo "else page d'accueil" . '
'; + $link = new URL; // page d'accueil + }*/ + + $nav_html .= '
  • ' . $data->getPageName() . '

  • ' . "\n"; + } + } + return $nav_html; + } +} \ 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 @@ +getName() . '.php'; + + if(file_exists($viewFile)) + { + // id (timestamp) + if(!empty($node->getAttributes())) + { + extract($node->getAttributes()); + } + + // html, date + $title = $node->getArticle()->getTitle(); + $preview = $node->getArticle()->getPreview(); + $id = $node->getArticleTimestamp(); + $content = ''; + + // page article unique + if(Director::$page_path->getLast()->getEndOfPath() === 'article') + { + $content = $node->getArticle()->getContent(); + $from_to_button = '

    '; + } + // page d'accueil (avec des news) + else + { + $from_to_button = '

    '; + } + + + $date_object = $node->getArticle()->getDateTime(); // class DateTime + $date = 'le ' . str_replace(':', 'h', $date_object->format('d-m-Y à H:i')); + + // partage + $share_link = new URL(['page' => CURRENT_PAGE], $id); + isset($_GET['id']) ? $share_link->addParams(['id' => $_GET['id']]) : ''; + $share_js = 'onclick="copyInClipBoard(\'' . $share_link . '\')"'; + $share_button = '

    ' . "\n"; + + // modifier un article + $admin_buttons = ''; + if($_SESSION['admin']) + { + if(Director::$page_path->getLast()->getEndOfPath() === 'article'){ + $modify_js = 'onclick="openEditor(\'' . $id . '\')"'; + $modify_article = '

    ' . "\n"; + + $up_button = '

    ' . "\n"; + $down_button = '

    ' . "\n"; + + $delete_js = 'onclick="deleteArticle(\'' . $id . '\', \'' . CURRENT_PAGE . '\')"'; + $delete_article = '

    ' . "\n"; + + $close_js = 'onclick="closeEditor(\'' . $id . '\')"'; + $close_editor = ''; + + $submit_js = 'onclick="submitArticle(\'' . $id . '\')"'; + $submit_article = ''; + } + else{ + $modify_article = '

    ' . "\n"; + + $up_link = new URL(['page' => CURRENT_PAGE, 'id' => $id, 'action' => 'position_up']); + $up_button = '

    ' . "\n"; + + $down_link = new URL(['page' => CURRENT_PAGE, 'id' => $id, 'action' => 'position_down']); + $down_button = '

    ' . "\n"; + + $delete_js = 'onclick="deleteArticle(\'' . $id . '\')"'; + $delete_article = '

    ' . "\n"; + + $close_editor = ''; + $submit_article = ''; + + $submit_article = ''; + } + $admin_buttons = $modify_article . $up_button . $down_button . $delete_article . $close_editor . $submit_article; + } + + ob_start(); + require($viewFile); + $this->html .= ob_get_clean(); + } + } +} 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 @@ +useChildrenBuilder($root_node); + } +} 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 @@ + +

    Montrez que vous n'êtes pas un robot.
    + + +

    + 'connexion']); +isset($_GET['from']) ? $link->addParams(['from' => $_GET['from']]) : ''; +isset($_GET['id']) ? $link->addParams(['id' => $_GET['id']]) : ''; +ob_start(); +?> +
    +

    +

    +

    +

    + + + + +
    + +
    +

    +

    +

    +

    + + + + +
    + 'modif_mdp']); +isset($_GET['from']) ? $link->addParams(['from' => $_GET['from']]) : ''; +isset($_GET['id']) ? $link->addParams(['id' => $_GET['id']]) : ''; +ob_start(); +?> +
    + +

    + +

    + + +

    + + +
    + + + + + + + <?= $title ?> + + + + + + + + +
    +

    +

    + '

    Erreur au test anti-robot, veuillez saisir un nombre entier.

    ', + 'bad_solution_captcha' => '

    Erreur au test anti-robot, veuillez réessayer.

    ', + 'bad_login_or_password' => '

    Saisir un Identifiant (e-mail) et un mot de passe.

    ', + 'bad_password' => '

    Mauvais mot de passe, veuillez réessayer.

    ', + 'forbidden_characters' => '

    Caractères interdits: espaces, tabulations, sauts CR/LF.

    ' +]; + +$warning_messages = [ + 'message_disconnect' => "

    N'oubliez de cliquer sur 'déconnexion' quand vous aurez fini.

    ", + //'message_cookie' => "

    Ce site utilise un cookie « obligatoire » lorsque vous êtes connecté ainsi que sur cette page.
    Il sera supprimé à votre déconnexion ou dès que vous aurez quitté le site.

    ", + 'private_browsing' =>"

    Au fait? Vous n'utilisez pas votre propre ordinateur ou téléphone?
    + Utilisez la navigation privée.

    " +]; + + +// confirmation modification du mot de passe +$page = isset($_GET['from']) ? $_GET['from'] : 'accueil'; +$id = isset($_GET['id']) ? ', \'' . $_GET['id'] . '\'' : ''; +$js = "newPassword('" . $page . "'" . $id . ");"; +ob_start(); +?> + + +addParams(['page' => $_GET['from'] ]) : ''; +isset($_GET['id']) ? $link->addParams(['id' => $_GET['id']]) : ''; +ob_start(); +if(isset($_GET['from'])) // exclue la "création du mot de passe" +{ +?> +

    + + + +

    + +
    +
    +

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

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


    +
    +

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

    + + +
    +

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

    + + +
    + +
    +
    \ 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 @@ + + + + + <?= $title ?> + + + + + + \ 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 @@ + +
    +
    +
    + +
    + +
    + +
    +

    +

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

    + +
    +
    + +
    +
    + +
    +
    +

    + +
    +
    + +
    +
    +
    \ No newline at end of file -- cgit v1.2.3