From 8a85c15372a3e65adf616cc64eeeeb9a0a81540d Mon Sep 17 00:00:00 2001 From: Cedric Abonnel Date: Wed, 13 May 2026 23:41:58 +0200 Subject: [PATCH] fix #29 : envoyer le lien magique par email (envoyer_mail_smtp) --- .gitignore | 14 + .php-cs-fixer.cache | 1 + .php-cs-fixer.dist.php | 12 + CHANGELOG.md | 140 + LICENSE | 21 + bootstrap.php | 19 + composer.json | 26 + composer.lock | 3304 +++++++++++++++++ config/config.php | 40 + data/.gitkeep | 0 data/site/about.json | 18 + data/site/about.md | 39 + data/site/legal.json | 18 + data/site/legal.md | 43 + data/site/licenses.json | 18 + data/site/licenses.md | 38 + database/interactions_create.sql | 30 + database/migrate-init.php | 47 + database/migrate.php | 83 + database/migration_001_roles_ratings.sql | 39 + database/migration_002_profile_url.sql | 1 + database/migration_003_profile_slug.sql | 2 + database/migration_004_profile_bio.sql | 1 + database/migration_005_rss_feeds.sql | 8 + database/migration_006_profile_links.sql | 9 + .../migration_007_comment_verify_token.sql | 6 + database/tables_create.sql | 17 + docs/architecture-notes.md | 49 + docs/auth-magic-link.md | 98 + docs/cache-architecture.md | 184 + docs/notes-dev.md | 39 + phpstan-baseline.neon | 6 + phpstan-bootstrap.php | 5 + phpstan.neon | 11 + public/.htaccess | 83 + public/LICENSE | 21 + public/assets/css/LICENSE-Bootstrap.txt | 21 + public/assets/css/bootstrap.min.css | 6 + public/assets/css/style.css | 1809 +++++++++ public/assets/favicon.svg | 4 + public/assets/fonts/LICENSE-Inter.txt | 92 + .../assets/fonts/inter-italic-latin-ext.woff2 | Bin 0 -> 37592 bytes public/assets/fonts/inter-italic-latin.woff2 | Bin 0 -> 25040 bytes .../assets/fonts/inter-normal-latin-ext.woff2 | Bin 0 -> 85068 bytes public/assets/fonts/inter-normal-latin.woff2 | Bin 0 -> 48256 bytes public/assets/js/add_files.js | 118 + public/assets/js/admin.js | 48 + public/assets/js/app.js | 462 +++ public/assets/js/bio-toggle.js | 14 + public/assets/js/bootstrap.bundle.min.js | 6 + public/assets/js/links-sortable.js | 29 + public/assets/js/post_confirm.js | 62 + public/assets/js/reactions.js | 45 + public/assets/js/toc.js | 37 + public/feed.php | 110 + public/file.php | 35 + public/index.php | 3140 ++++++++++++++++ public/login/config.php | 136 + public/login/index.php | 216 ++ public/login/magic.php | 80 + public/login/oidc.php | 6 + public/logout.php | 32 + public/oidc/callback.php | 200 + public/oidc/me.php | 194 + public/oidc/start.php | 72 + public/robots.txt | 16 + public/route.php | 7 + public/sitemap.php | 49 + scripts/fetch-network-info.sh | 16 + src/ArticleManager.php | 1213 ++++++ src/CommentManager.php | 202 + src/ConfigRepo.php | 114 + src/Domain/User.php | 16 + src/FeedFetcher.php | 185 + src/FileManager.php | 85 + src/Http/Csrf.php | 24 + src/Infrastructure/Database.php | 94 + src/Infrastructure/DbAdapter.php | 36 + src/Infrastructure/Session.php | 42 + src/Parsedown.php | 1828 +++++++++ src/PostManager.php | 69 + src/RatingManager.php | 46 + src/ReactionManager.php | 63 + src/Repository/DictionnaryRepository.php | 54 + src/Repository/ProfileRepository.php | 158 + src/Repository/UserRepository.php | 129 + src/SearchEngine.php | 276 ++ src/SearchLogParser.php | 127 + src/Service/AuthService.php | 105 + src/Service/MailQueue.php | 218 ++ src/Service/MailService.php | 287 ++ src/Service/UiFormRenderer.php | 76 + src/Service/Validator.php | 79 + src/SiteSettings.php | 84 + src/SmtpSettings.php | 53 + src/TagSuggester.php | 186 + src/auth.php | 285 ++ src/db.php | 18 + src/helpers.php | 135 + src/mailer.php | 181 + templates/about.php | 10 + templates/add_files.php | 128 + templates/admin.php | 1067 ++++++ templates/admin_role_edit.php | 73 + templates/author_articles.php | 97 + templates/author_profile.php | 84 + templates/categories.php | 158 + templates/comments_section.php | 138 + templates/contact.php | 145 + templates/copyright_ack.php | 113 + templates/diff.php | 78 + templates/edit_tags.php | 119 + templates/flux.php | 48 + templates/footer.php | 10 + templates/header.php | 49 + templates/import_image.php | 41 + templates/import_image_step2.php | 212 ++ templates/layout.php | 139 + templates/legal.php | 10 + templates/licenses.php | 10 + templates/liens.php | 49 + templates/post_confirm.php | 268 ++ templates/post_form.php | 515 +++ templates/post_list.php | 330 ++ templates/post_view.php | 354 ++ templates/profile.php | 227 ++ templates/search.php | 78 + templates/sources.php | 242 ++ versions.php | 6 + 129 files changed, 22818 insertions(+) create mode 100644 .gitignore create mode 100644 .php-cs-fixer.cache create mode 100644 .php-cs-fixer.dist.php create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 bootstrap.php create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 config/config.php create mode 100644 data/.gitkeep create mode 100644 data/site/about.json create mode 100644 data/site/about.md create mode 100644 data/site/legal.json create mode 100644 data/site/legal.md create mode 100644 data/site/licenses.json create mode 100644 data/site/licenses.md create mode 100644 database/interactions_create.sql create mode 100644 database/migrate-init.php create mode 100644 database/migrate.php create mode 100644 database/migration_001_roles_ratings.sql create mode 100644 database/migration_002_profile_url.sql create mode 100644 database/migration_003_profile_slug.sql create mode 100644 database/migration_004_profile_bio.sql create mode 100644 database/migration_005_rss_feeds.sql create mode 100644 database/migration_006_profile_links.sql create mode 100644 database/migration_007_comment_verify_token.sql create mode 100644 database/tables_create.sql create mode 100644 docs/architecture-notes.md create mode 100644 docs/auth-magic-link.md create mode 100644 docs/cache-architecture.md create mode 100644 docs/notes-dev.md create mode 100644 phpstan-baseline.neon create mode 100644 phpstan-bootstrap.php create mode 100644 phpstan.neon create mode 100644 public/.htaccess create mode 100644 public/LICENSE create mode 100644 public/assets/css/LICENSE-Bootstrap.txt create mode 100644 public/assets/css/bootstrap.min.css create mode 100644 public/assets/css/style.css create mode 100644 public/assets/favicon.svg create mode 100644 public/assets/fonts/LICENSE-Inter.txt create mode 100644 public/assets/fonts/inter-italic-latin-ext.woff2 create mode 100644 public/assets/fonts/inter-italic-latin.woff2 create mode 100644 public/assets/fonts/inter-normal-latin-ext.woff2 create mode 100644 public/assets/fonts/inter-normal-latin.woff2 create mode 100644 public/assets/js/add_files.js create mode 100644 public/assets/js/admin.js create mode 100644 public/assets/js/app.js create mode 100644 public/assets/js/bio-toggle.js create mode 100644 public/assets/js/bootstrap.bundle.min.js create mode 100644 public/assets/js/links-sortable.js create mode 100644 public/assets/js/post_confirm.js create mode 100644 public/assets/js/reactions.js create mode 100644 public/assets/js/toc.js create mode 100644 public/feed.php create mode 100644 public/file.php create mode 100644 public/index.php create mode 100644 public/login/config.php create mode 100644 public/login/index.php create mode 100644 public/login/magic.php create mode 100644 public/login/oidc.php create mode 100644 public/logout.php create mode 100644 public/oidc/callback.php create mode 100644 public/oidc/me.php create mode 100644 public/oidc/start.php create mode 100644 public/robots.txt create mode 100644 public/route.php create mode 100644 public/sitemap.php create mode 100644 scripts/fetch-network-info.sh create mode 100644 src/ArticleManager.php create mode 100644 src/CommentManager.php create mode 100644 src/ConfigRepo.php create mode 100644 src/Domain/User.php create mode 100644 src/FeedFetcher.php create mode 100644 src/FileManager.php create mode 100644 src/Http/Csrf.php create mode 100644 src/Infrastructure/Database.php create mode 100644 src/Infrastructure/DbAdapter.php create mode 100644 src/Infrastructure/Session.php create mode 100644 src/Parsedown.php create mode 100644 src/PostManager.php create mode 100644 src/RatingManager.php create mode 100644 src/ReactionManager.php create mode 100644 src/Repository/DictionnaryRepository.php create mode 100644 src/Repository/ProfileRepository.php create mode 100644 src/Repository/UserRepository.php create mode 100644 src/SearchEngine.php create mode 100644 src/SearchLogParser.php create mode 100644 src/Service/AuthService.php create mode 100644 src/Service/MailQueue.php create mode 100644 src/Service/MailService.php create mode 100644 src/Service/UiFormRenderer.php create mode 100644 src/Service/Validator.php create mode 100644 src/SiteSettings.php create mode 100644 src/SmtpSettings.php create mode 100644 src/TagSuggester.php create mode 100644 src/auth.php create mode 100644 src/db.php create mode 100644 src/helpers.php create mode 100644 src/mailer.php create mode 100644 templates/about.php create mode 100644 templates/add_files.php create mode 100644 templates/admin.php create mode 100644 templates/admin_role_edit.php create mode 100644 templates/author_articles.php create mode 100644 templates/author_profile.php create mode 100644 templates/categories.php create mode 100644 templates/comments_section.php create mode 100644 templates/contact.php create mode 100644 templates/copyright_ack.php create mode 100644 templates/diff.php create mode 100644 templates/edit_tags.php create mode 100644 templates/flux.php create mode 100644 templates/footer.php create mode 100644 templates/header.php create mode 100644 templates/import_image.php create mode 100644 templates/import_image_step2.php create mode 100644 templates/layout.php create mode 100644 templates/legal.php create mode 100644 templates/licenses.php create mode 100644 templates/liens.php create mode 100644 templates/post_confirm.php create mode 100644 templates/post_form.php create mode 100644 templates/post_list.php create mode 100644 templates/post_view.php create mode 100644 templates/profile.php create mode 100644 templates/search.php create mode 100644 templates/sources.php create mode 100644 versions.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bef216d --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# Credentials +.env + +# Composer dependencies +vendor/ + +# OS +.DS_Store +Thumbs.db + +# Fichiers uploadés et cache générés (propriété www-data) +data/*/files/ +data/_cache/ +_cache/ diff --git a/.php-cs-fixer.cache b/.php-cs-fixer.cache new file mode 100644 index 0000000..08840a1 --- /dev/null +++ b/.php-cs-fixer.cache @@ -0,0 +1 @@ +{"php":"8.3.6","version":"3.89.1:v3.89.1#f34967da2866ace090a2b447de1f357356474573","indent":" ","lineEnding":"\n","rules":{"binary_operator_spaces":{"default":"at_least_single_space"},"blank_line_after_opening_tag":true,"blank_line_between_import_groups":true,"blank_lines_before_namespace":true,"braces_position":{"allow_single_line_empty_anonymous_classes":true},"class_definition":{"inline_constructor_arguments":false,"space_before_parenthesis":true},"compact_nullable_type_declaration":true,"declare_equal_normalize":true,"lowercase_cast":true,"lowercase_static_reference":true,"modifier_keywords":true,"new_with_parentheses":{"anonymous_class":true},"no_blank_lines_after_class_opening":true,"no_extra_blank_lines":{"tokens":["use"]},"no_leading_import_slash":true,"no_whitespace_in_blank_line":true,"ordered_class_elements":{"order":["use_trait"]},"ordered_imports":{"imports_order":["class","function","const"],"sort_algorithm":"none"},"return_type_declaration":true,"short_scalar_cast":true,"single_import_per_statement":{"group_to_single_imports":false},"single_space_around_construct":{"constructs_followed_by_a_single_space":["abstract","as","case","catch","class","const_import","do","else","elseif","final","finally","for","foreach","function","function_import","if","insteadof","interface","namespace","new","private","protected","public","static","switch","trait","try","use","use_lambda","while"],"constructs_preceded_by_a_single_space":["as","else","elseif","use_lambda"]},"single_trait_insert_per_statement":true,"ternary_operator_spaces":true,"unary_operator_spaces":{"only_dec_inc":true},"blank_line_after_namespace":true,"constant_case":true,"control_structure_braces":true,"control_structure_continuation_position":true,"elseif":true,"function_declaration":{"closure_fn_spacing":"one"},"indentation_type":true,"line_ending":true,"lowercase_keywords":true,"method_argument_space":{"after_heredoc":false,"attribute_placement":"ignore","on_multiline":"ensure_fully_multiline"},"no_break_comment":true,"no_closing_tag":true,"no_multiple_statements_per_line":true,"no_space_around_double_colon":true,"no_spaces_after_function_name":true,"no_trailing_whitespace":true,"no_trailing_whitespace_in_comment":true,"single_blank_line_at_eof":true,"single_class_element_per_statement":{"elements":["property"]},"single_line_after_imports":true,"spaces_inside_parentheses":true,"statement_indentation":true,"switch_case_semicolon_to_colon":true,"switch_case_space":true,"encoding":true,"full_opening_tag":true,"strict_param":true,"declare_strict_types":true,"no_unused_imports":true,"single_quote":true},"hashes":{"templates\/header.php":"f4b64c4ecb4dadec166cb7935309096a","templates\/footer.php":"3111b4701ea698ba11c3423260657e28","public\/login\/oidc.php":"8ec86d6f3af33f64d586109ec17f817d","public\/login\/config.php":"5b7b3e2937b349c76a2fd239c3ae06f8","src\/Infrastructure\/DbAdapter.php":"3899a835130c146e2d30dbcca88d8f33","src\/Infrastructure\/Database.php":"6f2848ed70b29d9c2e2d259be611b9b0","src\/Infrastructure\/Session.php":"3538a1147cc81678c470d45ea8574a95","src\/Domain\/User.php":"02213454f7edf43f4afae3f2f81aaf01","src\/Http\/Csrf.php":"55631812cab4b1192f8e30c5d35fd5eb","src\/FileManager.php":"a51dda44f293f238aea295fd56b2fa99","src\/PostManager.php":"25f0179c4d96e9aa04218d54bf45a029","src\/helpers.php":"3a83a4872b1e3e3c58898b54f51e72b4","src\/db.php":"8888b7fbc9740eb3c60dd2374d0cb5d6","src\/ConfigRepo.php":"c2dcee160a272d27725d480a90e76dcf","src\/Parsedown.php":"85da2b47eca1a703fdfe44753bf912df","src\/Service\/Validator.php":"7c267b8b9f3f1bac0f2520dd10364831","src\/Service\/UiFormRenderer.php":"065617191c6d680ce97588f4fa159688","src\/mailer.php":"17e6b19103c880cc9a6c6634486506c2","src\/Repository\/DictionnaryRepository.php":"f937e98cf0f27b59ae00e430b52a586d","src\/Repository\/ProfileRepository.php":"b1cd483652500ee4e2aaaa9e0330ff1d","versions.php":"51a72261e1a507d3435b4a24e5f5fc09","config\/config.php":"a8b7698b01ab9b40eea655e8fcc194fc","phpstan-bootstrap.php":"d74864c2f107b740523f070d077d715e","src\/Service\/MailQueue.php":"20db418b83dcf426b7c6ad6787644cde","src\/Service\/AuthService.php":"f95a9ab097dcfc4ac6cbcf908cf4cd90","src\/Repository\/UserRepository.php":"d0ccc80374b54a5c4f20cb04c00fb083","src\/Service\/MailService.php":"7bff5df8cac3274a1a4ab8fe137514f2","templates\/licenses.php":"c1b8db32773de42bb4392063c50155e2","bootstrap.php":"23e657accae3b860a9ffc227daa30d27","public\/file.php":"4e9de9cbe565e895e7bd809028754cb9","public\/logout.php":"68bc31b06a9e23aa7a43ca7642365bcd","src\/ArticleManager.php":"43e0061b20b22037596aeac8c96de3ae","templates\/post_view.php":"73d3588e0b4c3470e4ce5b6182dfb2fa","templates\/post_list.php":"5ad535f5bdb6de8a6cf9630cefc48e58","templates\/layout.php":"5a9c8a96723eabafc186d6c23dd5cfa3","templates\/contact.php":"9d2026fd4250e0cef3ff736812cd2b12","templates\/post_form.php":"b444d7937eaa5d3b068e25d5d14bc2f7","templates\/legal.php":"b8eaf0c118bb06c92171aaecca091550","templates\/about.php":"022d078aaa40d2067352c81509144182","public\/login\/magic.php":"3d9447ed551e6401c3d43429b1600247","public\/login\/index.php":"7a27b066b232e943e0a98850f0993c52","public\/route.php":"8a478892d21a95352cae93b85169f424","public\/oidc\/callback.php":"be3e7a4e00b40d956c6a54de38fcfe50","public\/oidc\/start.php":"0ecd2496ed9b591e177abe7354a7809f","public\/oidc\/me.php":"d979853cfab834a645ed0c6df5297ed7","public\/index.php":"d48d76494172e7f172ef82010ea8ce93","src\/auth.php":"73d056745086cd0e63053eab808d2b5e"}} \ No newline at end of file diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..53b4e00 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,12 @@ +in(__DIR__); +return (new PhpCsFixer\Config()) + ->setRiskyAllowed(true) + ->setRules([ + '@PSR12' => true, + 'strict_param' => true, + 'declare_strict_types' => true, + 'no_unused_imports' => true, + 'single_quote' => true, + ]) + ->setFinder($finder); diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ab1a914 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,140 @@ +# Changelog — varlog + +## [Unreleased] — 2026-05-13 + +### Performances + +- **Cache multi-niveaux pour les vues d'articles** : temps de chargement réduit + de +5 s à ~0,4 s sur 1 062 articles. + - Mémoïsation de `getAll()` et `getSearchIndex()` dans la requête PHP + (`$allCache`, `$searchIndexCache`) — évite les appels répétés. + - Cache disque par article (`_cache/articles/{uuid}.json`) avec invalidation + par comparaison `mtime` — 1 lecture au lieu de 2 par article. + - Slug index (`_cache/slug_index.json`) : `getBySlug()` en O(1) sans scanner + tous les articles ; construit depuis `search_index.json` en un seul fichier. + - `getCategories()` et `$_allPublished` chargés depuis `search_index.json` + au lieu de `getAll()` — 1 fichier lu quelle que soit la taille du catalogue. + - `search_index.json` enrichi avec `cover`, `created_at`, `author` ; rebuild + automatique si le format est obsolète. + - `SearchEngine::scorePool()` : tokenise chaque article une seule fois pour + N mots de titre (vs N passes séparées qui retokenisaient chaque article + N fois et calculaient la similarité trigramme sur le contenu). + - Le nombre de lectures de fichiers par vue d'article est désormais constant + (~4), indépendamment du nombre total d'articles. + - Documentation : `docs/cache-architecture.md`. + +### Corrigé + +- **Upload de fichiers (#48)** : les fichiers > 8 Mo étaient rejetés silencieusement. + Le serveur utilise `mod_php` (non PHP-FPM) ; les limites ont été corrigées dans + `/etc/php/8.3/apache2/php.ini` : `upload_max_filesize = 500M`, `post_max_size = 2048M`. + Le handler `add_files` détecte désormais le dépassement et affiche un message + d'erreur explicite au lieu de rediriger sans rien faire. + +### Fonctionnalités + +- **Réactions visiteurs** : trois boutons (👍 Utile / 🔥 Important / 🤔 À creuser) + affichés sous chaque article. Toggle : recliquer retire la réaction. Accessible sans + compte via un cookie UUID (`vl_vid`, 1 an, `HttpOnly`). Comportement async fetch avec + fallback formulaire natif (compatible CSP `script-src 'self'`). Routes : + `POST /react`. Table BDD : `article_reactions`. + +- **Commentaires avec vérification email** : formulaire nom + email (non publié) + + texte (2 000 caractères max). Protection honeypot + CSRF en session. Un code à + 6 chiffres est envoyé par email (expire 24 h) ; le commentaire est auto-publié au clic + sur le lien de confirmation. Routes : `POST /comment`, + `GET /verify-comment/<6chiffres>`. Table BDD : `comments`. + +- **Modération commentaires** : onglet **Commentaires** dans `/admin/comments` listant + tous les commentaires avec statut (vérifié / publié) et actions masquer/republier. + Route : `POST /comment-moderate`. + +- **Page de confirmation à l'enregistrement** : cliquer sur "Enregistrer" affiche une + page intermédiaire avec le diff du contenu, le slug (déplacé ici depuis le formulaire, + avec suggestion auto si le titre a changé), un commentaire de révision pré-rempli + d'après les modifications détectées, et un aperçu SEO (snippet Google). La + sauvegarde effective n'a lieu qu'après confirmation. + +- **URLs propres** : toutes les routes internes migrent vers des chemins lisibles. + Les anciennes URLs `/?action=…` restent fonctionnelles (compatibilité). + | Ancienne URL | Nouvelle URL | + |---|---| + | `/?action=edit&uuid=` | `/edit/` | + | `/?action=sources&uuid=` | `/sources/` | + | `/?action=diff&uuid=&rev=` | `/diff//` | + | `/?action=create` | `/new` | + | `/?action=admin[&tab=]` | `/admin[/]` | + | `/?action=categories` | `/categories` | + | `/?action=profile` | `/profile` | + | `/?action=about\|legal\|licenses\|contact` | `/about`, `/legal`… | + | `/?action=regen_thumbs` | `/admin/regen-thumbs` | + | `/?action=add_files&uuid=` | `/files//add` | + | `/?action=import_image&uuid=` | `/import/` | + | `/?cat=` | `/categorie/` | + | `/?cursor=` | `/page/` | + +- **Moteur de recherche** : index trigram+substring pré-construit (`search_index.json`, + reconstruit à chaque écriture), accessible depuis la navbar. + +### Corrections + +- **Métadonnées fichiers (sources)** : `addFileMeta()` ne sauvegardait pas l'auteur et + l'URL source en raison d'un guard `file_exists()` trop strict — supprimé. +- **Authentification OIDC** (`State invalide.`) : `session_start()` était appelé avant + `bootstrap.php` dans les fichiers OIDC, écrasant les paramètres de cookie + (`SameSite=Lax`, `Secure`, `HttpOnly`) — corrigé dans `start.php`, `callback.php` + et `me.php`. +- **Sidebar droite de l'article** : classe Bootstrap `flex-nowrap-lg` inexistante, + remplacée par `flex-lg-nowrap` — la sidebar ne tombe plus en bas de page. +- **Date d'affichage en liste** : `created_at` affiché à la place de `published_at` + — corrigé avec fallback approprié. +- **Formulaire d'édition** : "Fichiers existants" déplacé dans la colonne de droite ; + attribution auteur/source étendue à tous les types de fichiers (pas seulement images). +- **Historique des révisions** : plus de révision créée si le contenu et le titre + sont inchangés. Ajout des boutons de suppression par révision et suppression globale. +- **Canonical URL catégorie** : passe de `/?cat=…` à `/categorie/…`. +- **Flux RSS** : `/rss` et `/rss.xml` redirigent en 301 vers `/feed` (URL + canonique) ; les articles des catégories privées sont exclus du flux ; + la description est convertie depuis Markdown en texte brut. + +--- + +## 2026-05-09 + +### Fonctionnalités + +- **SEO** : balises canonical, `sitemap.xml`, `robots.txt`, JSON-LD (`BlogPosting` / + `WebSite`), `noindex` sur les pages d'administration. +- **Recherche** : page de résultats avec score de pertinence, mise en évidence des + termes, lien vers la catégorie depuis les résultats. +- **Support HEIC/HEIF** : conversion automatique en JPEG à l'upload. +- **Support SVG** : upload autorisé, servi avec Content-Type correct. +- **Avant-première** : article visible en liste mais verrouillé avant sa date de + publication. +- **Pagination curseur** : navigation par UUID de dernier article vu, sans offset SQL. +- **Layout article 3 colonnes** : sidebar gauche (catégorie), contenu central, + sidebar droite (pièces jointes, liens externes, articles liés). +- **Import depuis URL** : téléchargement de fichiers distants avec extraction + automatique des métadonnées (EXIF, OpenGraph, PDF). +- **Gestion des pièces jointes** dans le formulaire d'édition, avec attribution + auteur/source affichée dans la vue article. + +### Corrections + +- Login intégré dans `layout.php`, chemins CSS en absolu. +- Redéclaration de `url()` dans `config.php` — fatal error corrigée. +- Correction permissions `www-data` sur `data/`. + +--- + +## 2026-04 et antérieur + +- Flux RSS paginé (`/feed`, `/rss`, `/rss.xml`) avec autodiscovery. +- Stockage des articles en fichiers Markdown (migration depuis base de données). +- SSO via Keycloak/OIDC avec PKCE. +- Images de couverture (liste, vue article, `og:image`). +- Brouillons visibles uniquement par l'auteur. +- Formulaire de contact (CSRF, honeypot, rate-limit). +- Pages : mentions légales (LCEN/RGPD), licences, à propos. +- Auto-hébergement Bootstrap 5, police Inter, favicon SVG. +- Headers HTTP de sécurité, CSP stricte. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9e1275a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024–2026 Cédric Abonnel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/bootstrap.php b/bootstrap.php new file mode 100644 index 0000000..c123030 --- /dev/null +++ b/bootstrap.php @@ -0,0 +1,19 @@ + 0, + 'path' => '/', + 'secure' => $isHttps, + 'httponly' => true, + 'samesite' => 'Lax', + ]); + session_start(); +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..11782bb --- /dev/null +++ b/composer.json @@ -0,0 +1,26 @@ +{ + "name": "varlog/folio", + "description": "Folio — moteur de blog PHP minimaliste", + "type": "project", + "license": "MIT", + "require": { + "ext-pdo": "*", + "php": ">=8.2", + "vlucas/phpdotenv": "^5.6", + "phpmailer/phpmailer": "^6.11", + "jumbojett/openid-connect-php": "^1.0" + }, + "autoload": { + "psr-4": { + "App\\": "app/" + } + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "friendsofphp/php-cs-fixer": "^3.64" + }, + "scripts": { + "fix": "php-cs-fixer fix --config=.php-cs-fixer.dist.php", + "stan": "phpstan analyse" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..7e7fffd --- /dev/null +++ b/composer.lock @@ -0,0 +1,3304 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "66835353a1516d406913941be6780955", + "packages": [ + { + "name": "graham-campbell/result-type", + "version": "v1.1.3", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "3ba905c11371512af9d9bdd27d99b782216b6945" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/3ba905c11371512af9d9bdd27d99b782216b6945", + "reference": "3ba905c11371512af9d9bdd27d99b782216b6945", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.3" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2024-07-20T21:45:45+00:00" + }, + { + "name": "jumbojett/openid-connect-php", + "version": "v1.0.2", + "source": { + "type": "git", + "url": "https://github.com/jumbojett/OpenID-Connect-PHP.git", + "reference": "f327e7eb0626d55ddb6abc7b7c9e6ad3af4e5d51" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jumbojett/OpenID-Connect-PHP/zipball/f327e7eb0626d55ddb6abc7b7c9e6ad3af4e5d51", + "reference": "f327e7eb0626d55ddb6abc7b7c9e6ad3af4e5d51", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "php": ">=7.0", + "phpseclib/phpseclib": "^3.0.7" + }, + "require-dev": { + "phpunit/phpunit": "<10", + "roave/security-advisories": "dev-latest", + "yoast/phpunit-polyfills": "^2.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "description": "Bare-bones OpenID Connect client", + "support": { + "issues": "https://github.com/jumbojett/OpenID-Connect-PHP/issues", + "source": "https://github.com/jumbojett/OpenID-Connect-PHP/tree/v1.0.2" + }, + "time": "2024-09-13T07:08:11+00:00" + }, + { + "name": "paragonie/constant_time_encoding", + "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", + "shasum": "" + }, + "require": { + "php": "^8" + }, + "require-dev": { + "infection/infection": "^0", + "nikic/php-fuzzer": "^0", + "phpunit/phpunit": "^9|^10|^11", + "vimeo/psalm": "^4|^5|^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\ConstantTime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" + }, + { + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" + } + ], + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", + "keywords": [ + "base16", + "base32", + "base32_decode", + "base32_encode", + "base64", + "base64_decode", + "base64_encode", + "bin2hex", + "encoding", + "hex", + "hex2bin", + "rfc4648" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/constant_time_encoding/issues", + "source": "https://github.com/paragonie/constant_time_encoding" + }, + "time": "2025-09-24T15:06:41+00:00" + }, + { + "name": "paragonie/random_compat", + "version": "v9.99.100", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", + "shasum": "" + }, + "require": { + "php": ">= 7" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/random_compat/issues", + "source": "https://github.com/paragonie/random_compat" + }, + "time": "2020-10-15T08:29:30+00:00" + }, + { + "name": "phpmailer/phpmailer", + "version": "v6.12.0", + "source": { + "type": "git", + "url": "https://github.com/PHPMailer/PHPMailer.git", + "reference": "d1ac35d784bf9f5e61b424901d5a014967f15b12" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/d1ac35d784bf9f5e61b424901d5a014967f15b12", + "reference": "d1ac35d784bf9f5e61b424901d5a014967f15b12", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-filter": "*", + "ext-hash": "*", + "php": ">=5.5.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "doctrine/annotations": "^1.2.6 || ^1.13.3", + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.3.2", + "phpcompatibility/php-compatibility": "^9.3.5", + "roave/security-advisories": "dev-latest", + "squizlabs/php_codesniffer": "^3.7.2", + "yoast/phpunit-polyfills": "^1.0.4" + }, + "suggest": { + "decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication", + "ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses", + "ext-openssl": "Needed for secure SMTP sending and DKIM signing", + "greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication", + "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication", + "league/oauth2-google": "Needed for Google XOAUTH2 authentication", + "psr/log": "For optional PSR-3 debug logging", + "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)", + "thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPMailer\\PHPMailer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-only" + ], + "authors": [ + { + "name": "Marcus Bointon", + "email": "phpmailer@synchromedia.co.uk" + }, + { + "name": "Jim Jagielski", + "email": "jimjag@gmail.com" + }, + { + "name": "Andy Prevost", + "email": "codeworxtech@users.sourceforge.net" + }, + { + "name": "Brent R. Matzelle" + } + ], + "description": "PHPMailer is a full-featured email creation and transfer class for PHP", + "support": { + "issues": "https://github.com/PHPMailer/PHPMailer/issues", + "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.12.0" + }, + "funding": [ + { + "url": "https://github.com/Synchro", + "type": "github" + } + ], + "time": "2025-10-15T16:49:08+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.9.4", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", + "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.9.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2025-08-21T11:53:16+00:00" + }, + { + "name": "phpseclib/phpseclib", + "version": "3.0.47", + "source": { + "type": "git", + "url": "https://github.com/phpseclib/phpseclib.git", + "reference": "9d6ca36a6c2dd434765b1071b2644a1c683b385d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/9d6ca36a6c2dd434765b1071b2644a1c683b385d", + "reference": "9d6ca36a6c2dd434765b1071b2644a1c683b385d", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^1|^2|^3", + "paragonie/random_compat": "^1.4|^2.0|^9.99.99", + "php": ">=5.6.1" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "suggest": { + "ext-dom": "Install the DOM extension to load XML formatted public keys.", + "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", + "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", + "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", + "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." + }, + "type": "library", + "autoload": { + "files": [ + "phpseclib/bootstrap.php" + ], + "psr-4": { + "phpseclib3\\": "phpseclib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jim Wigginton", + "email": "terrafrost@php.net", + "role": "Lead Developer" + }, + { + "name": "Patrick Monnerat", + "email": "pm@datasphere.ch", + "role": "Developer" + }, + { + "name": "Andreas Fischer", + "email": "bantu@phpbb.com", + "role": "Developer" + }, + { + "name": "Hans-Jürgen Petrich", + "email": "petrich@tronic-media.com", + "role": "Developer" + }, + { + "name": "Graham Campbell", + "email": "graham@alt-three.com", + "role": "Developer" + } + ], + "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", + "homepage": "http://phpseclib.sourceforge.net", + "keywords": [ + "BigInteger", + "aes", + "asn.1", + "asn1", + "blowfish", + "crypto", + "cryptography", + "encryption", + "rsa", + "security", + "sftp", + "signature", + "signing", + "ssh", + "twofish", + "x.509", + "x509" + ], + "support": { + "issues": "https://github.com/phpseclib/phpseclib/issues", + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.47" + }, + "funding": [ + { + "url": "https://github.com/terrafrost", + "type": "github" + }, + { + "url": "https://www.patreon.com/phpseclib", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib", + "type": "tidelift" + } + ], + "time": "2025-10-06T01:07:24+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-02T08:10:11+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.6.2", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/24ac4c74f91ee2c193fa1aaa5c249cb0822809af", + "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.1.3", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.3", + "symfony/polyfill-ctype": "^1.24", + "symfony/polyfill-mbstring": "^1.24", + "symfony/polyfill-php80": "^1.24" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-filter": "*", + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "5.6-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2025-04-30T23:37:27+00:00" + } + ], + "packages-dev": [ + { + "name": "clue/ndjson-react", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/clue/reactphp-ndjson.git", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/392dc165fce93b5bb5c637b67e59619223c931b0", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "react/stream": "^1.2" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", + "react/event-loop": "^1.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Clue\\React\\NDJson\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "description": "Streaming newline-delimited JSON (NDJSON) parser and encoder for ReactPHP.", + "homepage": "https://github.com/clue/reactphp-ndjson", + "keywords": [ + "NDJSON", + "json", + "jsonlines", + "newline", + "reactphp", + "streaming" + ], + "support": { + "issues": "https://github.com/clue/reactphp-ndjson/issues", + "source": "https://github.com/clue/reactphp-ndjson/tree/v1.3.0" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2022-12-23T10:58:28+00:00" + }, + { + "name": "composer/pcre", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.2" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-11-12T16:29:46+00:00" + }, + { + "name": "composer/semver", + "version": "3.4.4", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.4" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2025-08-20T19:15:30+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.5", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", + "shasum": "" + }, + "require": { + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-05-06T16:37:16+00:00" + }, + { + "name": "evenement/evenement", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/igorw/evenement.git", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^9 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Evenement\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + } + ], + "description": "Événement is a very simple event dispatching library for PHP", + "keywords": [ + "event-dispatcher", + "event-emitter" + ], + "support": { + "issues": "https://github.com/igorw/evenement/issues", + "source": "https://github.com/igorw/evenement/tree/v3.0.2" + }, + "time": "2023-08-08T05:53:35+00:00" + }, + { + "name": "fidry/cpu-core-counter", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "fidry/makefile": "^0.2.0", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "webmozarts/strict-phpunit": "^7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Fidry\\CpuCoreCounter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Tiny utility to get the number of CPU cores.", + "keywords": [ + "CPU", + "core" + ], + "support": { + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.3.0" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2025-08-14T07:29:31+00:00" + }, + { + "name": "friendsofphp/php-cs-fixer", + "version": "v3.89.1", + "source": { + "type": "git", + "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", + "reference": "f34967da2866ace090a2b447de1f357356474573" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/f34967da2866ace090a2b447de1f357356474573", + "reference": "f34967da2866ace090a2b447de1f357356474573", + "shasum": "" + }, + "require": { + "clue/ndjson-react": "^1.3", + "composer/semver": "^3.4", + "composer/xdebug-handler": "^3.0.5", + "ext-filter": "*", + "ext-hash": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "fidry/cpu-core-counter": "^1.3", + "php": "^7.4 || ^8.0", + "react/child-process": "^0.6.6", + "react/event-loop": "^1.5", + "react/socket": "^1.16", + "react/stream": "^1.4", + "sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0", + "symfony/console": "^5.4.47 || ^6.4.24 || ^7.0", + "symfony/event-dispatcher": "^5.4.45 || ^6.4.24 || ^7.0", + "symfony/filesystem": "^5.4.45 || ^6.4.24 || ^7.0", + "symfony/finder": "^5.4.45 || ^6.4.24 || ^7.0", + "symfony/options-resolver": "^5.4.45 || ^6.4.24 || ^7.0", + "symfony/polyfill-mbstring": "^1.33", + "symfony/polyfill-php80": "^1.33", + "symfony/polyfill-php81": "^1.33", + "symfony/polyfill-php84": "^1.33", + "symfony/process": "^5.4.47 || ^6.4.24 || ^7.2", + "symfony/stopwatch": "^5.4.45 || ^6.4.24 || ^7.0" + }, + "require-dev": { + "facile-it/paraunit": "^1.3.1 || ^2.7", + "infection/infection": "^0.31.0", + "justinrainbow/json-schema": "^6.5", + "keradus/cli-executor": "^2.2", + "mikey179/vfsstream": "^1.6.12", + "php-coveralls/php-coveralls": "^2.8", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.6", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.6", + "phpunit/phpunit": "^9.6.25 || ^10.5.53 || ^11.5.34", + "symfony/var-dumper": "^5.4.48 || ^6.4.24 || ^7.3.2", + "symfony/yaml": "^5.4.45 || ^6.4.24 || ^7.3.2" + }, + "suggest": { + "ext-dom": "For handling output formats in XML", + "ext-mbstring": "For handling non-UTF8 characters." + }, + "bin": [ + "php-cs-fixer" + ], + "type": "application", + "autoload": { + "psr-4": { + "PhpCsFixer\\": "src/" + }, + "exclude-from-classmap": [ + "src/Fixer/Internal/*" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Dariusz Rumiński", + "email": "dariusz.ruminski@gmail.com" + } + ], + "description": "A tool to automatically fix PHP code style", + "keywords": [ + "Static code analysis", + "fixer", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.89.1" + }, + "funding": [ + { + "url": "https://github.com/keradus", + "type": "github" + } + ], + "time": "2025-10-24T12:05:10+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "1.12.32", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/2770dcdf5078d0b0d53f94317e06affe88419aa8", + "reference": "2770dcdf5078d0b0d53f94317e06affe88419aa8", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2025-09-30T10:16:31+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "react/cache", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/cache.git", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/promise": "^3.0 || ^2.0 || ^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, Promise-based cache interface for ReactPHP", + "keywords": [ + "cache", + "caching", + "promise", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/cache/issues", + "source": "https://github.com/reactphp/cache/tree/v1.2.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2022-11-30T15:59:55+00:00" + }, + { + "name": "react/child-process", + "version": "v0.6.6", + "source": { + "type": "git", + "url": "https://github.com/reactphp/child-process.git", + "reference": "1721e2b93d89b745664353b9cfc8f155ba8a6159" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/child-process/zipball/1721e2b93d89b745664353b9cfc8f155ba8a6159", + "reference": "1721e2b93d89b745664353b9cfc8f155ba8a6159", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/event-loop": "^1.2", + "react/stream": "^1.4" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/socket": "^1.16", + "sebastian/environment": "^5.0 || ^3.0 || ^2.0 || ^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\ChildProcess\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven library for executing child processes with ReactPHP.", + "keywords": [ + "event-driven", + "process", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/child-process/issues", + "source": "https://github.com/reactphp/child-process/tree/v0.6.6" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-01-01T16:37:48+00:00" + }, + { + "name": "react/dns", + "version": "v1.13.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/dns.git", + "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/dns/zipball/eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", + "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/cache": "^1.0 || ^0.6 || ^0.5", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.7 || ^1.2.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3 || ^2", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Dns\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async DNS resolver for ReactPHP", + "keywords": [ + "async", + "dns", + "dns-resolver", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/dns/issues", + "source": "https://github.com/reactphp/dns/tree/v1.13.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-13T14:18:03+00:00" + }, + { + "name": "react/event-loop", + "version": "v1.5.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/event-loop.git", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "suggest": { + "ext-pcntl": "For signal handling support when using the StreamSelectLoop" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\EventLoop\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", + "keywords": [ + "asynchronous", + "event-loop" + ], + "support": { + "issues": "https://github.com/reactphp/event-loop/issues", + "source": "https://github.com/reactphp/event-loop/tree/v1.5.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2023-11-13T13:48:05+00:00" + }, + { + "name": "react/promise", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/23444f53a813a3296c1368bb104793ce8d88f04a", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpstan/phpstan": "1.12.28 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "support": { + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v3.3.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-08-19T18:57:03+00:00" + }, + { + "name": "react/socket", + "version": "v1.16.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/socket.git", + "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/socket/zipball/23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", + "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/dns": "^1.13", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.6 || ^1.2.1", + "react/stream": "^1.4" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3.3 || ^2", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Socket\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", + "keywords": [ + "Connection", + "Socket", + "async", + "reactphp", + "stream" + ], + "support": { + "issues": "https://github.com/reactphp/socket/issues", + "source": "https://github.com/reactphp/socket/tree/v1.16.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-07-26T10:38:09+00:00" + }, + { + "name": "react/stream", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/stream.git", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.8", + "react/event-loop": "^1.2" + }, + "require-dev": { + "clue/stream-filter": "~1.2", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Stream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", + "keywords": [ + "event-driven", + "io", + "non-blocking", + "pipe", + "reactphp", + "readable", + "stream", + "writable" + ], + "support": { + "issues": "https://github.com/reactphp/stream/issues", + "source": "https://github.com/reactphp/stream/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-11T12:45:25+00:00" + }, + { + "name": "sebastian/diff", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0", + "symfony/process": "^7.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:55:46+00:00" + }, + { + "name": "symfony/console", + "version": "v7.3.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "cdb80fa5869653c83cfe1a9084a673b6daf57ea7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/cdb80fa5869653c83cfe1a9084a673b6daf57ea7", + "reference": "cdb80fa5869653c83cfe1a9084a673b6daf57ea7", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.2" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v7.3.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-10-14T15:46:26+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v7.3.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b7dc69e71de420ac04bc9ab830cf3ffebba48191", + "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/error-handler": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-08-13T11:49:31+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v7.3.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "edcbb768a186b5c3f25d0643159a787d3e63b7fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/edcbb768a186b5c3f25d0643159a787d3e63b7fd", + "reference": "edcbb768a186b5c3f25d0643159a787d3e63b7fd", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v7.3.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-07T08:17:47+00:00" + }, + { + "name": "symfony/finder", + "version": "v7.3.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "9f696d2f1e340484b4683f7853b273abff94421f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/9f696d2f1e340484b4683f7853b273abff94421f", + "reference": "9f696d2f1e340484b4683f7853b273abff94421f", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v7.3.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-10-15T18:45:57+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v7.3.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/0ff2f5c3df08a395232bbc3c2eb7e84912df911d", + "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v7.3.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-08-05T10:16:07+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-27T09:58:17+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php81", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php81\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php81/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-24T13:30:11+00:00" + }, + { + "name": "symfony/process", + "version": "v7.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/f24f8f316367b30810810d4eb30c543d7003ff3b", + "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v7.3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-11T10:12:26+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-04-25T09:37:31+00:00" + }, + { + "name": "symfony/stopwatch", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/stopwatch.git", + "reference": "5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd", + "reference": "5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/service-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Stopwatch\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a way to profile code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/stopwatch/tree/v7.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-24T10:49:57+00:00" + }, + { + "name": "symfony/string", + "version": "v7.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "f96476035142921000338bad71e5247fbc138872" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872", + "reference": "f96476035142921000338bad71e5247fbc138872", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.1", + "symfony/http-client": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v7.3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-11T14:36:48+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=8.2" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/config/config.php b/config/config.php new file mode 100644 index 0000000..0971b05 --- /dev/null +++ b/config/config.php @@ -0,0 +1,40 @@ +load(); + +if (!$_ENV['APP_URL']) { + http_response_code(500); + echo 'Configuration manquante : définis APP_URL ou APP_URL dans le .env'; + exit; +} + +// Normalise: toujours un trailing slash unique +define('APP_URL', rtrim($_ENV['APP_URL'], '/') . '/'); + +// (Optionnel) Expose dans $_ENV si besoin +$_ENV['APP_URL'] = APP_URL; + +/** + * Helper pour construire des liens absolus propres. + * url('ressources/user/login.php') + * url('api/items', ['page'=>2]) + */ +if (!function_exists('url')) { + function url(string $path = '', array $qs = []): string + { + $u = APP_URL . ltrim($path, '/'); + if ($qs) { + $u .= (str_contains($u, '?') ? '&' : '?') . http_build_query($qs); + } + return $u; + } +} diff --git a/data/.gitkeep b/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/data/site/about.json b/data/site/about.json new file mode 100644 index 0000000..3aa3cd5 --- /dev/null +++ b/data/site/about.json @@ -0,0 +1,18 @@ +{ + "uuid": "a3d8f2c1-7b4e-4f9a-8c3d-2e5a9b6f1d4c", + "slug": "about", + "title": "À propos", + "author": "cedric@abonnel.fr", + "published": true, + "published_at": "2021-01-16 04:02:40", + "created_at": "2021-01-16 04:02:40", + "updated_at": "2026-05-13 00:00:00", + "revisions": [], + "cover": "", + "files_meta": [], + "external_links": [], + "seo_title": "", + "seo_description": "", + "og_image": "", + "category": "" +} diff --git a/data/site/about.md b/data/site/about.md new file mode 100644 index 0000000..fc749f8 --- /dev/null +++ b/data/site/about.md @@ -0,0 +1,39 @@ +# À propos + +Qui se cache derrière varlog ? + +Je m'appelle **Cédric**. Passionné d'informatique depuis longtemps, je gère un **HomeLab** à la maison — un petit laboratoire personnel où je fais tourner des serveurs, expérimente des configs réseau et casse des choses pour mieux les comprendre. + +varlog est mon carnet de bord technique. J'y documente ce que je fais, ce que j'apprends, et parfois ce qui tourne mal — les incidents sont souvent les meilleures leçons. + +Le blog a été lancé publiquement aux **JDLL 2025** (Journées Du Logiciel Libre), à Lyon. + +## Ce dont je parle ici + +### HomeLab & infrastructure + +Proxmox, virtualisation, domotique (Zigbee, MQTT, Home Assistant), supervision avec Uptime Kuma, auto-hébergement de services (Gitea, Keycloak…), incidents réseau et leurs post-mortems. + +### Réseaux & télécom + +Passionné par les réseaux mobiles (3G/4G/5G/6G), la fibre optique (50G-PON), les stratégies des opérateurs et les infrastructures qui font fonctionner tout ça sans qu'on y pense. + +### Linux & développement + +Debian au quotidien, scripts, administration système, et un peu de PHP — dont ce blog lui-même, développé maison sous le nom de code *Folio*. + +### Numérique & société + +Souveraineté numérique, données personnelles, IA et plateformes qui monétisent nos contenus — des sujets qui m'intéressent autant qu'ils m'inquiètent. + +### Le reste + +Bricolage, travaux, anecdotes techniques, lectures, liseuses Kobo, et quelques billets qui n'entrent dans aucune case. La vie ne se range pas en catégories. + +## Contact + +Vous pouvez me joindre via le [formulaire de contact](/contact). Je lis tous les messages, même si je ne réponds pas toujours vite. + +--- + +Le contenu de ce blog est publié sous licence [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) sauf mention contraire. Le moteur *Folio* est distribué sous [licence MIT](/LICENSE). diff --git a/data/site/legal.json b/data/site/legal.json new file mode 100644 index 0000000..e59bdfe --- /dev/null +++ b/data/site/legal.json @@ -0,0 +1,18 @@ +{ + "uuid": "b2c7e1f4-4a3d-4e8b-9f2a-1d6c8e3f5a7b", + "slug": "legal", + "title": "Mentions légales", + "author": "cedric@abonnel.fr", + "published": true, + "published_at": "2021-01-16 04:02:40", + "created_at": "2021-01-16 04:02:40", + "updated_at": "2026-05-13 00:00:00", + "revisions": [], + "cover": "", + "files_meta": [], + "external_links": [], + "seo_title": "", + "seo_description": "", + "og_image": "", + "category": "" +} diff --git a/data/site/legal.md b/data/site/legal.md new file mode 100644 index 0000000..48c8597 --- /dev/null +++ b/data/site/legal.md @@ -0,0 +1,43 @@ +# Mentions légales + +Conformément à la loi n° 2004-575 du 21 juin 2004 pour la confiance dans l'économie numérique (LCEN). + +## Éditeur du site + +**Responsable de publication :** Cédric Abonnel +**Qualité :** Particulier — site personnel non commercial +**Contact :** [formulaire de contact](/contact) + +## Hébergement + +**Type :** Auto-hébergement sur infrastructure personnelle (HomeLab) +**Exploitant :** Cédric Abonnel +**Fournisseur d'accès à internet :** Infrastructure personnelle auto-hébergée + +## Propriété intellectuelle + +Le **contenu éditorial** de ce site (articles, textes, images produites par l'auteur) est publié sous licence [Creative Commons Attribution 4.0 International (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/), sauf mention contraire. + +Le **moteur du site** (*Folio*) est un logiciel libre distribué sous [licence MIT](/LICENSE). + +Les composants tiers (Bootstrap, PHPMailer, police Inter…) sont soumis à leurs licences respectives, détaillées sur la [page des licences](/licenses). + +## Données personnelles (RGPD) + +Ce site est un blog personnel **sans publicité, sans pistage, sans système de commentaires** ni inscription publique. + +Les seules données traitées automatiquement sont les **journaux de connexion du serveur web** (adresse IP, horodatage, page demandée), conservés conformément aux obligations légales (article L34-1 du Code des postes et des communications électroniques — durée maximale : 1 an). + +Ces données ne sont ni vendues, ni transmises à des tiers, ni utilisées à des fins commerciales. + +Conformément au RGPD (règlement UE 2016/679), vous disposez d'un droit d'accès, de rectification et de suppression des données vous concernant. Pour exercer ces droits : [formulaire de contact](/contact). + +## Cookies + +Ce site utilise uniquement un **cookie de session technique**, nécessaire au fonctionnement de l'authentification. Il n'est déposé que lors d'une connexion au compte d'administration et n'est pas utilisé à des fins de suivi ou de profilage. Aucun cookie tiers n'est déposé. + +## Responsabilité + +L'éditeur s'efforce de maintenir les informations publiées à jour et exactes, mais ne peut garantir l'exhaustivité ou l'absence d'erreurs du contenu. + +Les liens vers des sites tiers sont fournis à titre informatif. L'éditeur n'est pas responsable du contenu de ces sites externes. diff --git a/data/site/licenses.json b/data/site/licenses.json new file mode 100644 index 0000000..26eab7d --- /dev/null +++ b/data/site/licenses.json @@ -0,0 +1,18 @@ +{ + "uuid": "fdff8ad3-d369-4bd7-bbb9-e14d433868d7", + "slug": "licenses", + "title": "Licences", + "author": "cedric@abonnel.fr", + "published": true, + "published_at": "2021-01-16 04:02:40", + "created_at": "2021-01-16 04:02:40", + "updated_at": "2021-01-16 04:02:40", + "revisions": [], + "cover": "", + "files_meta": [], + "external_links": [], + "seo_title": "", + "seo_description": "", + "og_image": "", + "category": "" +} diff --git a/data/site/licenses.md b/data/site/licenses.md new file mode 100644 index 0000000..e3ea570 --- /dev/null +++ b/data/site/licenses.md @@ -0,0 +1,38 @@ +# Licences + +Composants logiciels utilisés par ce site et leurs licences. + +## Ce site + +| Composant | Licence | Usage | +|-----------|---------|-------| +| **Folio** — moteur de blog PHP | MIT | Moteur de ce blog — par Cédric Abonnel ([voir la licence](/LICENSE)) | +| **Contenu éditorial** | CC BY 4.0 | Articles et textes du blog — [Creative Commons Attribution 4.0](https://creativecommons.org/licenses/by/4.0/) | + +## Bibliothèques (production) + +| Composant | Version | Licence | Usage | +|-----------|---------|---------|-------| +| **Bootstrap** | 5.3.3 | MIT | Framework CSS/JS — auto-hébergé ([voir la licence](/assets/css/LICENSE-Bootstrap.txt)) | +| **PHPMailer** | 6.12.0 | LGPL-2.1 | Envoi d'e-mails SMTP | +| **phpdotenv** | 5.6.2 | BSD-3-Clause | Variables d'environnement | +| **openid-connect-php** | 1.0.2 | Apache-2.0 | Authentification SSO (OIDC) | +| **Police Inter** | v20 | OFL-1.1 | Typographie — auto-hébergée ([voir la licence](/assets/fonts/LICENSE-Inter.txt)) | + +## Outils de développement + +| Composant | Version | Licence | Usage | +|-----------|---------|---------|-------| +| **PHPStan** | 1.12.32 | MIT | Analyse statique PHP | +| **PHP-CS-Fixer** | 3.89.1 | MIT | Formatage du code | +| **Claude Code CLI** | — | Commercial | Outil de développement (Anthropic) — [Conditions d'utilisation](https://www.anthropic.com/legal/aup) | + +## Infrastructure + +| Composant | Licence | Usage | +|-----------|---------|-------| +| **PHP 8.3** | PHP License v3.01 | Langage côté serveur | +| **PostgreSQL** | PostgreSQL License | Base de données relationnelle | +| **Apache HTTP Server** | Apache-2.0 | Serveur web | + + diff --git a/database/interactions_create.sql b/database/interactions_create.sql new file mode 100644 index 0000000..6287690 --- /dev/null +++ b/database/interactions_create.sql @@ -0,0 +1,30 @@ +-- Réactions visiteurs (cookie anti-doublon) +CREATE TABLE article_reactions ( + id SERIAL PRIMARY KEY, + article_uuid TEXT NOT NULL, + reaction_type TEXT NOT NULL CHECK (reaction_type IN ('useful', 'important', 'interesting')), + visitor_hash TEXT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + UNIQUE (article_uuid, reaction_type, visitor_hash) +); +CREATE INDEX ON article_reactions (article_uuid); + +-- Commentaires avec vérification par email +CREATE TABLE comments ( + id SERIAL PRIMARY KEY, + article_uuid TEXT NOT NULL, + author_name TEXT NOT NULL, + author_email TEXT NOT NULL, + content TEXT NOT NULL CHECK (LENGTH(content) <= 2000), + verify_token TEXT, + verification_code TEXT, + verify_attempts INTEGER NOT NULL DEFAULT 0, + verified BOOLEAN NOT NULL DEFAULT FALSE, + published BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + ip_address TEXT, + user_agent TEXT +); +CREATE INDEX ON comments (article_uuid, verified, published); +CREATE INDEX ON comments (verify_token) + WHERE verified = FALSE AND verify_token IS NOT NULL; diff --git a/database/migrate-init.php b/database/migrate-init.php new file mode 100644 index 0000000..431c702 --- /dev/null +++ b/database/migrate-init.php @@ -0,0 +1,47 @@ + PDO::ERRMODE_EXCEPTION, +]); + +$pdo->exec(' + CREATE TABLE IF NOT EXISTS schema_migrations ( + name TEXT NOT NULL PRIMARY KEY, + applied_at TIMESTAMP NOT NULL DEFAULT NOW() + ) +'); + +// Migrations déjà appliquées avant la mise en place de ce système +$alreadyApplied = [ + 'migration_001_roles_ratings.sql', + 'migration_002_profile_url.sql', + 'migration_003_profile_slug.sql', + 'migration_004_profile_bio.sql', + 'migration_005_rss_feeds.sql', + 'migration_006_profile_links.sql', +]; + +$stmt = $pdo->prepare('INSERT INTO schema_migrations (name) VALUES (?) ON CONFLICT DO NOTHING'); +foreach ($alreadyApplied as $name) { + $stmt->execute([$name]); + echo " ✓ marquée : $name\n"; +} + +echo "\nInitialisation terminée. Vous pouvez supprimer ce fichier.\n"; diff --git a/database/migrate.php b/database/migrate.php new file mode 100644 index 0000000..6486fd4 --- /dev/null +++ b/database/migrate.php @@ -0,0 +1,83 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, +]); + +// Crée la table de suivi si elle n'existe pas encore +$pdo->exec(' + CREATE TABLE IF NOT EXISTS schema_migrations ( + name TEXT NOT NULL PRIMARY KEY, + applied_at TIMESTAMP NOT NULL DEFAULT NOW() + ) +'); + +// Migrations déjà appliquées +$applied = array_flip( + $pdo->query('SELECT name FROM schema_migrations ORDER BY name') + ->fetchAll(PDO::FETCH_COLUMN) +); + +// Fichiers de migration dans le même dossier, triés par nom +$files = glob(__DIR__ . '/migration_*.sql') ?: []; +sort($files); + +$count = 0; +foreach ($files as $file) { + $name = basename($file); + if (isset($applied[$name])) { + continue; + } + + $sql = file_get_contents($file); + if ($sql === false || trim($sql) === '') { + echo " ⚠ $name : fichier vide ou illisible, ignoré\n"; + continue; + } + + echo " → $name ... "; + $pdo->exec($sql); + $pdo->prepare('INSERT INTO schema_migrations (name) VALUES (?)') + ->execute([$name]); + echo "✓\n"; + $count++; +} + +if ($count === 0) { + echo " (aucune migration en attente)\n"; +} else { + echo " $count migration(s) appliquée(s).\n"; +} diff --git a/database/migration_001_roles_ratings.sql b/database/migration_001_roles_ratings.sql new file mode 100644 index 0000000..1437249 --- /dev/null +++ b/database/migration_001_roles_ratings.sql @@ -0,0 +1,39 @@ +-- Migration 001 : Système de rôles et notes d'articles +-- À exécuter une seule fois sur le serveur PostgreSQL + +-- Rôles disponibles +CREATE TABLE IF NOT EXISTS roles ( + id SERIAL PRIMARY KEY, + name VARCHAR(50) UNIQUE NOT NULL, + label TEXT NOT NULL +); + +INSERT INTO roles (name, label) VALUES + ('admin', 'Administrateur'), + ('editor', 'Rédacteur'), + ('reader', 'Lecteur') +ON CONFLICT (name) DO NOTHING; + +-- Association utilisateur ↔ rôle (clé : email, pour compatibilité OIDC sans FK) +CREATE TABLE IF NOT EXISTS user_roles ( + user_email TEXT NOT NULL, + role_id INTEGER NOT NULL REFERENCES roles(id) ON DELETE CASCADE, + granted_at TIMESTAMP DEFAULT NOW(), + granted_by TEXT, + PRIMARY KEY (user_email, role_id) +); + +-- Seed : cedric@abonnel.fr → admin +INSERT INTO user_roles (user_email, role_id, granted_by) +SELECT 'cedric@abonnel.fr', id, 'migration' +FROM roles WHERE name = 'admin' +ON CONFLICT DO NOTHING; + +-- Notes d'articles (1-5 étoiles, une note par utilisateur par article) +CREATE TABLE IF NOT EXISTS article_ratings ( + article_uuid VARCHAR(36) NOT NULL, + user_email TEXT NOT NULL, + rating SMALLINT NOT NULL CHECK (rating BETWEEN 1 AND 5), + rated_at TIMESTAMP DEFAULT NOW(), + PRIMARY KEY (article_uuid, user_email) +); diff --git a/database/migration_002_profile_url.sql b/database/migration_002_profile_url.sql new file mode 100644 index 0000000..b9b4421 --- /dev/null +++ b/database/migration_002_profile_url.sql @@ -0,0 +1 @@ +ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS profile_url TEXT NOT NULL DEFAULT ''; diff --git a/database/migration_003_profile_slug.sql b/database/migration_003_profile_slug.sql new file mode 100644 index 0000000..e8f9911 --- /dev/null +++ b/database/migration_003_profile_slug.sql @@ -0,0 +1,2 @@ +ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS profile_slug TEXT NOT NULL DEFAULT ''; +CREATE UNIQUE INDEX IF NOT EXISTS user_profiles_profile_slug_idx ON user_profiles (profile_slug) WHERE profile_slug <> ''; diff --git a/database/migration_004_profile_bio.sql b/database/migration_004_profile_bio.sql new file mode 100644 index 0000000..0b96126 --- /dev/null +++ b/database/migration_004_profile_bio.sql @@ -0,0 +1 @@ +ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS bio TEXT NOT NULL DEFAULT ''; diff --git a/database/migration_005_rss_feeds.sql b/database/migration_005_rss_feeds.sql new file mode 100644 index 0000000..c07adaa --- /dev/null +++ b/database/migration_005_rss_feeds.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS rss_feeds ( + id SERIAL PRIMARY KEY, + user_email TEXT NOT NULL, + feed_url TEXT NOT NULL, + label TEXT NOT NULL DEFAULT '', + created_at TIMESTAMP DEFAULT now(), + UNIQUE (user_email, feed_url) +); diff --git a/database/migration_006_profile_links.sql b/database/migration_006_profile_links.sql new file mode 100644 index 0000000..219837d --- /dev/null +++ b/database/migration_006_profile_links.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS profile_links ( + id SERIAL PRIMARY KEY, + user_email TEXT NOT NULL, + url TEXT NOT NULL, + title TEXT NOT NULL DEFAULT '', + description TEXT NOT NULL DEFAULT '', + position INT NOT NULL DEFAULT 0, + created_at TIMESTAMP DEFAULT now() +); diff --git a/database/migration_007_comment_verify_token.sql b/database/migration_007_comment_verify_token.sql new file mode 100644 index 0000000..d927614 --- /dev/null +++ b/database/migration_007_comment_verify_token.sql @@ -0,0 +1,6 @@ +-- Ajout du token UUID dans l'URL de vérification et du compteur de tentatives +ALTER TABLE comments ADD COLUMN IF NOT EXISTS verify_token TEXT; +ALTER TABLE comments ADD COLUMN IF NOT EXISTS verify_attempts INTEGER NOT NULL DEFAULT 0; + +CREATE INDEX IF NOT EXISTS idx_comments_verify_token ON comments (verify_token) + WHERE verified = FALSE AND verify_token IS NOT NULL; diff --git a/database/tables_create.sql b/database/tables_create.sql new file mode 100644 index 0000000..a180afb --- /dev/null +++ b/database/tables_create.sql @@ -0,0 +1,17 @@ +CREATE TABLE posts ( + id SERIAL PRIMARY KEY, + title TEXT NOT NULL, + content TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP, + is_published BOOLEAN DEFAULT FALSE +); + +CREATE TABLE post_files ( + id SERIAL PRIMARY KEY, + post_id INTEGER REFERENCES posts(id) ON DELETE CASCADE, + file_type TEXT, + file_path TEXT, + original_name TEXT, + uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); diff --git a/docs/architecture-notes.md b/docs/architecture-notes.md new file mode 100644 index 0000000..7959da0 --- /dev/null +++ b/docs/architecture-notes.md @@ -0,0 +1,49 @@ +logique complète en PHP framework maison), avec un système de routing clair, base postgres et extensible pour gérer : + +* les posts (CRUD + publication + masquage (au lieu de suppression)), +* les commentaires (publier, masquer, privé), +* les pièces jointes (upload, masquage (au lieu de supprimer), inutile de réuploadé si le fichier à déjà été poussé dans un autre poste par exemple.). + +architecture MVC, avec un routeur maison et des contrôleurs structurés. +Tout sera modulaire et facile à maintenir. + +--- + +## 🏗️ Structure du projet + +``` +project/ +│ +├─ public/ +│ ├─ index.php # Point d'entrée (router) +│ └─ uploads/ # Dossier des fichiers uploadés +│ +├─ app/ +│ ├─ Core/ +│ │ ├─ Router.php # Routeur maison +| │ ├─ Model.php +| │ ├─ View.php +│ │ └─ Controller.php # Classe de base pour les contrôleurs +│ │ +│ ├─ Controllers/ +│ │ ├─ PostController.php +│ │ ├─ CommentController.php +│ │ └─ AttachmentController.php +│ │ +│ ├─ Models/ +│ │ ├─ Post.php +│ │ ├─ Comment.php +│ │ └─ Attachment.php +│ │ +│ ├── Views/ +│ │ ├── posts/ +│ │ │ ├── index.php +│ │ │ ├── show.php +│ │ │ └── form.php +│ │ ├── comments/ +│ │ └── attachments/ +│ │ +│ └─ config.php # Configuration (DB, etc.) +│ +└─ composer.json +``` diff --git a/docs/auth-magic-link.md b/docs/auth-magic-link.md new file mode 100644 index 0000000..e909cdb --- /dev/null +++ b/docs/auth-magic-link.md @@ -0,0 +1,98 @@ +# Authentification — Lien magique + +Mécanisme d'authentification par email sans mot de passe. L'utilisateur reçoit un lien à usage unique, valide un temps limité, qui ouvre une session PHP. + +## Fichiers concernés + +| Fichier | Rôle | +|---|---| +| `public/login/index.php` | Formulaire de demande + génération du token | +| `public/login/magic.php` | Consommation du token + ouverture de session | +| Table BDD `auth_magic_links` | Persistance des tokens | + +## Configuration (`.env`) + +| Variable | Défaut | Description | +|---|---|---| +| `MAGIC_LINK_TTL_MINUTES` | `30` | Durée de validité du lien | +| `MAGIC_COOLDOWN_MINUTES` | `5` | Délai minimal entre deux demandes pour le même email | +| `MAGIC_WINDOW_HOURS` | `12` | Fenêtre glissante pour le plafond | +| `MAGIC_MAX_PER_WINDOW` | `5` | Nombre maximal de liens émis par fenêtre | + +## Schéma BDD — `auth_magic_links` + +```sql +id UUID PRIMARY KEY (gen_random_uuid()) +email TEXT NOT NULL +token TEXT NOT NULL UNIQUE +created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +expires_at TIMESTAMPTZ NOT NULL +consumed_at TIMESTAMPTZ NULL -- NULL = non consommé +ip TEXT +user_agent TEXT +return_to TEXT NULL -- chemin relatif de redirection post-login +``` + +## Phase 1 — Demande du lien (`index.php`, POST) + +``` +[Formulaire email] → POST /login + │ + ├─ Validation CSRF + ├─ Validation syntaxe email + │ + ├─ Purge BDD : DELETE liens expirés ou consommés pour cet email + │ + ├─ Cooldown : un lien < MAGIC_COOLDOWN_MINUTES → refus + ├─ Plafond : >= MAGIC_MAX_PER_WINDOW liens dans MAGIC_WINDOW_HOURS → refus + │ + ├─ Génération token : random_bytes(32) → base64url (43 chars, URL-safe) + ├─ INSERT auth_magic_links (expires_at = NOW() + TTL) + │ + └─ [stub] Envoi email : /login/magic.php?token= +``` + +Le token est un base64url sans padding (`+/` remplacés par `-_`, `=` supprimés) — il ne contient que des caractères `[A-Za-z0-9\-_]`. + +L'envoi de l'email est actuellement un **stub** : le code construit `$magicUrl` mais l'appel SMTP n'est pas implémenté (`index.php:142`). + +## Phase 2 — Consommation du lien (`magic.php`, GET) + +``` +[Clic sur le lien] → GET /login/magic.php?token= + │ + ├─ Validation format token (regex [A-Za-z0-9\-\_]) + │ + ├─ BEGIN TRANSACTION + │ ├─ SELECT ... FOR UPDATE (verrou anti double-consommation) + │ ├─ Token inconnu → 400 + │ ├─ consumed_at non null → "Lien déjà utilisé" + │ ├─ expires_at dépassé → "Lien expiré" + │ ├─ UPDATE consumed_at = NOW() + │ └─ COMMIT + │ + ├─ session_start() + session_regenerate_id(true) + ├─ $_SESSION['user_email'] = email + │ + └─ Redirection 303 → return_to (validé : doit commencer par /) +``` + +Le `FOR UPDATE` garantit qu'un token ne peut pas être consommé deux fois en cas de double-clic ou de requêtes concurrentes. + +## Sécurités notables + +- **CSRF** sur le formulaire de demande +- **Rate limiting** double (cooldown + plafond glissant) par email +- **Token URL-safe** généré par CSPRNG (`random_bytes`) +- **Verrou transactionnel** (`FOR UPDATE`) à la consommation +- **`session_regenerate_id(true)`** — prévient la fixation de session +- **`return_to` filtré** : seuls les chemins relatifs (commençant par `/`) sont acceptés + +## État actuel — ce qui reste à câbler + +L'envoi SMTP n'est pas implémenté. Le token est correctement généré et persisté en BDD, mais le mail n'est pas envoyé. Voir `public/login/index.php:142` : + +```php +$magicUrl = url('/login/magic.php') . '?token=' . urlencode($token); +/* envoyer_mail_smtp(...) ou mail(...) */ +``` diff --git a/docs/cache-architecture.md b/docs/cache-architecture.md new file mode 100644 index 0000000..6b5087f --- /dev/null +++ b/docs/cache-architecture.md @@ -0,0 +1,184 @@ +# Architecture du cache — Moteur Folio + +## Contexte et problème initial + +Le moteur stocke chaque article dans un sous-répertoire `data/{uuid}/` contenant deux fichiers : +- `meta.json` — métadonnées (titre, slug, catégorie, cover, dates…) +- `index.md` — contenu Markdown + +Avec 1 000+ articles, chaque vue de page déclenchait **3 appels à `getAll()`** (via `getBySlug()`, `getCategories()` et directement pour les articles liés), ce qui représentait ~6 000 lectures de fichiers par requête. La page mettait **+5 secondes** à charger. + +--- + +## Les quatre niveaux de cache + +### 1. Cache mémoire de requête — `$allCache` et `$searchIndexCache` + +**Scope** : durée de vie d'une requête PHP (in-process). + +`ArticleManager` mémoïse deux tableaux en propriétés privées : + +- `$allCache` — résultat de `loadAll()` (tous les articles avec contenu) +- `$searchIndexCache` — contenu de `search_index.json` + +```php +// Premier appel : scan disque + construction du tableau +$this->allCache = $this->loadAll(); + +// Appels suivants dans la même requête : tableau déjà en mémoire +return $this->allCache; +``` + +**Invalidation** : `writeMeta()` et `delete()` mettent les deux propriétés à `null`. + +--- + +### 2. Cache disque par article — `_cache/articles/{uuid}.json` + +**Scope** : persistant entre les requêtes, jusqu'à modification de l'article. + +`loadArticle()` vérifie si le cache est plus récent que `meta.json` avant de lire les sources : + +``` +_cache/articles/{uuid}.json <-- filemtime >= meta.json ? → utiliser le cache + → lire meta.json + index.md, écrire le cache +``` + +Le fichier cache contient toutes les données de l'article (métadonnées + contenu), ce qui réduit les lectures de **2 fichiers à 1** par article chargé. + +**Invalidation** : `writeMeta()` supprime `_cache/articles/{uuid}.json` avant d'écrire le nouveau `meta.json`. `delete()` le supprime aussi. + +--- + +### 3. Index slug → UUID — `_cache/slug_index.json` + +**Scope** : persistant, mis à jour incrémentalement. + +Permet à `getBySlug()` de trouver un article en **O(1)** (lecture de l'index + lecture du cache article) au lieu de parcourir tous les articles. + +``` +slug_index.json : {"mon-article": "uuid-xxxx", "autre-article": "uuid-yyyy", ...} +``` + +**Construction** : à la première utilisation, `buildSlugIndex()` lit le `search_index.json` (un seul fichier) pour construire la correspondance. Si le search_index n'existe pas encore, il tombe en repli sur `loadAll()`. + +**Invalidation** : `writeMeta()` supprime le fichier (reconstruction automatique à la prochaine requête). `delete()` fait de même. + +> Suppression plutôt que mise à jour incrémentale : la reconstruction depuis `search_index.json` est quasi instantanée (lecture d'un seul fichier JSON), donc il n'y a pas d'intérêt à maintenir des mises à jour partielles. + +--- + +### 4. Index de recherche — `search_index.json` + +**Scope** : persistant, reconstruit après chaque modification d'article. + +Fichier JSON plat contenant un tableau de tous les articles avec leurs champs essentiels et leur texte brut pré-calculé (`plain`, sans syntaxe Markdown). Utilisé pour : + +- La recherche plein-texte (`SearchEngine`) +- La liste des articles publiés pour les articles liés/similaires (évite `getAll()`) +- Les catégories (`getCategories()`) +- La construction du slug index + +**Champs stockés** : + +```json +{ + "uuid": "...", + "slug": "...", + "title": "...", + "category": "...", + "author": "...", + "cover": "...", + "published": true, + "published_at": "2026-01-15 10:00:00", + "created_at": "2026-01-14 09:30:00", + "updated_at": "2026-01-15 10:00:00", + "plain": "texte brut de l'article sans markdown..." +} +``` + +**Rebuild automatique** : si le fichier ne contient pas le champ `cover` (format antérieur à la v2 du cache), `getSearchIndex()` déclenche automatiquement un rebuild. + +**Invalidation** : `rebuildSearchIndex()` (appelé par `create()`, `update()`, `delete()`). + +--- + +## Chemin d'une requête de vue d'article après optimisation + +``` +GET /post/{slug} +│ +├── getBySlug(slug) +│ ├── Lire slug_index.json [1 lecture] +│ ├── → UUID trouvé +│ └── getByUuid(uuid) +│ └── Lire _cache/articles/{uuid}.json [1 lecture] +│ +├── getCategories() +│ └── getSearchIndex() [lecture de search_index.json, mise en cache mémoire] +│ +├── $_allPublished (articles liés + similaires) +│ └── getSearchIndex() [déjà en cache mémoire → 0 lecture] +│ +├── scorePool(mots_du_titre, $_allPublished) +│ └── Tokenisation unique par article, pas de re-calcul par mot +│ +└── getBacklinks(slug) + └── Lire _cache/backlinks.json [1 lecture] + +Total : ~4 lectures de fichiers, indépendamment du nombre total d'articles. +``` + +Avant optimisation (1 062 articles) : ~6 300 lectures de fichiers. + +--- + +## Performances mesurées + +| Scénario | Avant | Après | +|---|---|---| +| Cold cache (aucun cache disque) | +5 s | ~0,6 s | +| Warm cache (cache disque présent) | +5 s | ~0,4 s | + +--- + +## Scalabilité + +| Volume d'articles | Lectures de fichiers par vue (après) | +|---|---| +| 1 000 | ~4 | +| 100 000 | ~4 | +| 500 000 | ~4 | + +Le nombre de lectures est **constant** : le chemin de vue ne dépend plus du nombre total d'articles, seulement de la présence des fichiers de cache. + +La seule opération encore en O(N) est `rebuildSearchIndex()`, mais elle n'est déclenchée que sur écriture (création, modification, suppression d'article), jamais sur lecture. + +--- + +## Invalidation — résumé + +| Événement | Caches invalidés | +|---|---| +| `writeMeta()` (toute écriture d'article) | `$allCache`, `$searchIndexCache`, cache article (`{uuid}.json`), slug index | +| `delete()` | idem + suppression physique du cache article | +| `rebuildSearchIndex()` | `$searchIndexCache` (remplacé par les nouvelles données) | + +--- + +## Maintenance + +### Vider manuellement les caches disque + +En cas de besoin (migration, incohérence) : + +```bash +ssh varlog "sudo rm -rf /var/www/lan.acegrp.varlog/data/_cache/articles/" +ssh varlog "sudo rm /var/www/lan.acegrp.varlog/data/_cache/slug_index.json" +``` + +Les caches se reconstruisent automatiquement à la première requête suivante. + +### Forcer un rebuild du search_index + +Modifier et sauvegarder n'importe quel article depuis l'interface admin déclenche un rebuild complet. Il n'existe pas de commande CLI dédiée pour l'instant. diff --git a/docs/notes-dev.md b/docs/notes-dev.md new file mode 100644 index 0000000..89f5d7d --- /dev/null +++ b/docs/notes-dev.md @@ -0,0 +1,39 @@ +# Notes de développement + +## Structure du projet (serveur) + +``` +/var/www/lan.acegrp.varlog/ +├── public/ +│ ├── index.php # Point d'entrée +│ ├── route.php # Routeur (actions GET/POST) +│ └── assets/ # CSS, JS, uploads +├── templates/ # Vues PHP (incluses via extract() + include) +│ ├── layout.php +│ ├── post_form.php +│ └── post_view.php +├── src/ +│ ├── db.php # Connexion PDO PostgreSQL +│ ├── PostManager.php +│ └── FileManager.php +├── config/ +│ └── config.php # Charge .env, définit les constantes DB +└── docs/ +``` + +## Conventions templates + +Les templates reçoivent leurs variables via `extract()` depuis `route.php`. Toute variable optionnelle (non transmise dans tous les contextes) doit utiliser `??` pour éviter un `Undefined variable` warning : + +```php +// Bon +$dateValue = $published_at ?? date('Y-m-d\TH:i'); + + +// À éviter + // Warning si create (pas d'édition) +``` + +## Permissions serveur + +PHP-FPM tourne en `www-data`. Les fichiers sensibles (`.env`) appartiennent à `cedrix:www-data 640`. Voir `PROJET.md` § Permissions serveur. diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..659ec56 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,6 @@ +parameters: + ignoreErrors: + - + message: "#^Unreachable statement \\- code above always terminates\\.$#" + count: 1 + path: src/Repository/ProfileRepository.php diff --git a/phpstan-bootstrap.php b/phpstan-bootstrap.php new file mode 100644 index 0000000..647c135 --- /dev/null +++ b/phpstan-bootstrap.php @@ -0,0 +1,5 @@ + +RewriteRule ^post/([a-z0-9][a-z0-9-]*)/?$ /index.php?action=view&slug=$1 [L,QSA] + +# Filtre par catégorie : /categorie/ +RewriteRule ^categorie/(.+?)/?$ /index.php?cat=$1 [L,QSA,B] + +# Pagination par curseur : /page/ +RewriteRule ^page/([0-9a-f-]{36})/?$ /index.php?cursor=$1 [L,QSA] + +# Édition / création +RewriteRule ^edit/([0-9a-f-]{36})/tags/(.+?)/?$ /index.php?action=edit_tags&uuid=$1&tag_type=$2 [L,QSA,B] +RewriteRule ^edit/([0-9a-f-]{36})/?$ /index.php?action=edit&uuid=$1 [L,QSA] +RewriteRule ^new/?$ /index.php?action=create [L,QSA] +RewriteRule ^delete/([0-9a-f-]{36})/?$ /index.php?action=delete&uuid=$1 [L,QSA] + +# Sources et diff +RewriteRule ^sources/([0-9a-f-]{36})/?$ /index.php?action=sources&uuid=$1 [L,QSA] +RewriteRule ^diff/([0-9a-f-]{36})/(\d+)/?$ /index.php?action=diff&uuid=$1&rev=$2 [L,QSA] + +# Fichiers / import +RewriteRule ^files/([0-9a-f-]{36})/add/?$ /index.php?action=add_files&uuid=$1 [L,QSA] +RewriteRule ^import/([0-9a-f-]{36})/?$ /index.php?action=import_image&uuid=$1 [L,QSA] + +# Admin (regen-thumbs et role/ avant la règle générique admin/) +RewriteRule ^admin/regen-thumbs/?$ /index.php?action=regen_thumbs [L,QSA] +RewriteRule ^admin/role/([a-z0-9_-]+)/?$ /index.php?action=admin_role_edit&role_name=$1 [L,QSA] +RewriteRule ^admin/([a-z0-9-]+)/?$ /index.php?action=admin&tab=$1 [L,QSA] +RewriteRule ^admin/?$ /index.php?action=admin [L,QSA] + +# Réactions et commentaires +RewriteRule ^react/?$ /index.php?action=react [L,QSA] +RewriteRule ^comment/?$ /index.php?action=comment [L,QSA] +RewriteRule ^comment-moderate/?$ /index.php?action=comment_moderate [L,QSA] +RewriteRule ^verify-comment/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/?$ /index.php?action=verify_comment&token=$1 [L,QSA] + +# Pages de gestion +RewriteRule ^categories/?$ /index.php?action=categories [L,QSA] +RewriteRule ^profile/?$ /index.php?action=profile [L,QSA] +RewriteRule ^search/?$ /index.php?action=search [L,QSA] +RewriteRule ^flux/?$ /index.php?action=flux [L,QSA] +RewriteRule ^feed/add/?$ /index.php?action=add_feed [L,QSA] +RewriteRule ^feed/delete/?$ /index.php?action=delete_feed [L,QSA] + +# Profil public auteur + page liens +RewriteRule ^profil/([a-z0-9][a-z0-9-]*)/article/cursor/([0-9a-f-]{36})/?$ /index.php?action=author_articles&slug=$1&cursor=$2 [L,QSA] +RewriteRule ^profil/([a-z0-9][a-z0-9-]*)/article/?$ /index.php?action=author_articles&slug=$1 [L,QSA] +RewriteRule ^profil/([a-z0-9][a-z0-9-]*)/?$ /index.php?action=author&slug=$1 [L,QSA] +RewriteRule ^liens/([a-z0-9][a-z0-9-]*)/?$ /index.php?action=liens&slug=$1 [L,QSA] +RewriteRule ^link/add/?$ /index.php?action=add_link [L,QSA] +RewriteRule ^link/delete/?$ /index.php?action=delete_link [L,QSA] +RewriteRule ^link/reorder/?$ /index.php?action=reorder_links [L,QSA] + +# Pages statiques +RewriteRule ^about/?$ /index.php?action=about [L,QSA] +RewriteRule ^legal/?$ /index.php?action=legal [L,QSA] +RewriteRule ^licenses/?$ /index.php?action=licenses [L,QSA] +RewriteRule ^contact/?$ /index.php?action=contact [L,QSA] + +# Flux RSS — /feed est canonique, /rss et /rss.xml redirigent en 301 +RewriteRule ^rss/?$ /feed [R=301,L] +RewriteRule ^rss\.xml$ /feed [R=301,L] +RewriteRule ^feed/([0-9a-f-]{36})/?$ /feed.php?after=$1 [L,QSA] +RewriteRule ^feed/?$ /feed.php [L,QSA] + +# Sitemap +RewriteRule ^sitemap\.xml$ /sitemap.php [L] + +# Ajoute .php si le fichier correspondant existe +RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI}.php -f +RewriteRule ^(.+?)/?$ /$1.php [L,QSA] + +# 404 intelligent : redirige vers l'article le plus proche +ErrorDocument 404 /index.php?action=not_found diff --git a/public/LICENSE b/public/LICENSE new file mode 100644 index 0000000..9e1275a --- /dev/null +++ b/public/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024–2026 Cédric Abonnel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/public/assets/css/LICENSE-Bootstrap.txt b/public/assets/css/LICENSE-Bootstrap.txt new file mode 100644 index 0000000..2a703f5 --- /dev/null +++ b/public/assets/css/LICENSE-Bootstrap.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2011-2024 The Bootstrap Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/public/assets/css/bootstrap.min.css b/public/assets/css/bootstrap.min.css new file mode 100644 index 0000000..3993414 --- /dev/null +++ b/public/assets/css/bootstrap.min.css @@ -0,0 +1,6 @@ +@charset "UTF-8";/*! + * Bootstrap v5.3.3 (https://getbootstrap.com/) + * Copyright 2011-2024 The Bootstrap Authors + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */:root,[data-bs-theme=light]{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-black:#000;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-primary-text-emphasis:#052c65;--bs-secondary-text-emphasis:#2b2f32;--bs-success-text-emphasis:#0a3622;--bs-info-text-emphasis:#055160;--bs-warning-text-emphasis:#664d03;--bs-danger-text-emphasis:#58151c;--bs-light-text-emphasis:#495057;--bs-dark-text-emphasis:#495057;--bs-primary-bg-subtle:#cfe2ff;--bs-secondary-bg-subtle:#e2e3e5;--bs-success-bg-subtle:#d1e7dd;--bs-info-bg-subtle:#cff4fc;--bs-warning-bg-subtle:#fff3cd;--bs-danger-bg-subtle:#f8d7da;--bs-light-bg-subtle:#fcfcfd;--bs-dark-bg-subtle:#ced4da;--bs-primary-border-subtle:#9ec5fe;--bs-secondary-border-subtle:#c4c8cb;--bs-success-border-subtle:#a3cfbb;--bs-info-border-subtle:#9eeaf9;--bs-warning-border-subtle:#ffe69c;--bs-danger-border-subtle:#f1aeb5;--bs-light-border-subtle:#e9ecef;--bs-dark-border-subtle:#adb5bd;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-color-rgb:33,37,41;--bs-body-bg:#fff;--bs-body-bg-rgb:255,255,255;--bs-emphasis-color:#000;--bs-emphasis-color-rgb:0,0,0;--bs-secondary-color:rgba(33, 37, 41, 0.75);--bs-secondary-color-rgb:33,37,41;--bs-secondary-bg:#e9ecef;--bs-secondary-bg-rgb:233,236,239;--bs-tertiary-color:rgba(33, 37, 41, 0.5);--bs-tertiary-color-rgb:33,37,41;--bs-tertiary-bg:#f8f9fa;--bs-tertiary-bg-rgb:248,249,250;--bs-heading-color:inherit;--bs-link-color:#0d6efd;--bs-link-color-rgb:13,110,253;--bs-link-decoration:underline;--bs-link-hover-color:#0a58ca;--bs-link-hover-color-rgb:10,88,202;--bs-code-color:#d63384;--bs-highlight-color:#212529;--bs-highlight-bg:#fff3cd;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.375rem;--bs-border-radius-sm:0.25rem;--bs-border-radius-lg:0.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-xxl:2rem;--bs-border-radius-2xl:var(--bs-border-radius-xxl);--bs-border-radius-pill:50rem;--bs-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-box-shadow-sm:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-box-shadow-lg:0 1rem 3rem rgba(0, 0, 0, 0.175);--bs-box-shadow-inset:inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-focus-ring-width:0.25rem;--bs-focus-ring-opacity:0.25;--bs-focus-ring-color:rgba(13, 110, 253, 0.25);--bs-form-valid-color:#198754;--bs-form-valid-border-color:#198754;--bs-form-invalid-color:#dc3545;--bs-form-invalid-border-color:#dc3545}[data-bs-theme=dark]{color-scheme:dark;--bs-body-color:#dee2e6;--bs-body-color-rgb:222,226,230;--bs-body-bg:#212529;--bs-body-bg-rgb:33,37,41;--bs-emphasis-color:#fff;--bs-emphasis-color-rgb:255,255,255;--bs-secondary-color:rgba(222, 226, 230, 0.75);--bs-secondary-color-rgb:222,226,230;--bs-secondary-bg:#343a40;--bs-secondary-bg-rgb:52,58,64;--bs-tertiary-color:rgba(222, 226, 230, 0.5);--bs-tertiary-color-rgb:222,226,230;--bs-tertiary-bg:#2b3035;--bs-tertiary-bg-rgb:43,48,53;--bs-primary-text-emphasis:#6ea8fe;--bs-secondary-text-emphasis:#a7acb1;--bs-success-text-emphasis:#75b798;--bs-info-text-emphasis:#6edff6;--bs-warning-text-emphasis:#ffda6a;--bs-danger-text-emphasis:#ea868f;--bs-light-text-emphasis:#f8f9fa;--bs-dark-text-emphasis:#dee2e6;--bs-primary-bg-subtle:#031633;--bs-secondary-bg-subtle:#161719;--bs-success-bg-subtle:#051b11;--bs-info-bg-subtle:#032830;--bs-warning-bg-subtle:#332701;--bs-danger-bg-subtle:#2c0b0e;--bs-light-bg-subtle:#343a40;--bs-dark-bg-subtle:#1a1d20;--bs-primary-border-subtle:#084298;--bs-secondary-border-subtle:#41464b;--bs-success-border-subtle:#0f5132;--bs-info-border-subtle:#087990;--bs-warning-border-subtle:#997404;--bs-danger-border-subtle:#842029;--bs-light-border-subtle:#495057;--bs-dark-border-subtle:#343a40;--bs-heading-color:inherit;--bs-link-color:#6ea8fe;--bs-link-hover-color:#8bb9fe;--bs-link-color-rgb:110,168,254;--bs-link-hover-color-rgb:139,185,254;--bs-code-color:#e685b5;--bs-highlight-color:#dee2e6;--bs-highlight-bg:#664d03;--bs-border-color:#495057;--bs-border-color-translucent:rgba(255, 255, 255, 0.15);--bs-form-valid-color:#75b798;--bs-form-valid-border-color:#75b798;--bs-form-invalid-color:#ea868f;--bs-form-invalid-border-color:#ea868f}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;border:0;border-top:var(--bs-border-width) solid;opacity:.25}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2;color:var(--bs-heading-color)}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.1875em;color:var(--bs-highlight-color);background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,1));text-decoration:underline}a:hover{--bs-link-color-rgb:var(--bs-link-hover-color-rgb)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:.25rem}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-secondary-color);text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:var(--bs-body-bg);border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:var(--bs-secondary-color)}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{--bs-gutter-x:1.5rem;--bs-gutter-y:0;width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}:root{--bs-breakpoint-xs:0;--bs-breakpoint-sm:576px;--bs-breakpoint-md:768px;--bs-breakpoint-lg:992px;--bs-breakpoint-xl:1200px;--bs-breakpoint-xxl:1400px}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.66666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.66666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.66666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.66666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.66666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.66666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-color-type:initial;--bs-table-bg-type:initial;--bs-table-color-state:initial;--bs-table-bg-state:initial;--bs-table-color:var(--bs-emphasis-color);--bs-table-bg:var(--bs-body-bg);--bs-table-border-color:var(--bs-border-color);--bs-table-accent-bg:transparent;--bs-table-striped-color:var(--bs-emphasis-color);--bs-table-striped-bg:rgba(var(--bs-emphasis-color-rgb), 0.05);--bs-table-active-color:var(--bs-emphasis-color);--bs-table-active-bg:rgba(var(--bs-emphasis-color-rgb), 0.1);--bs-table-hover-color:var(--bs-emphasis-color);--bs-table-hover-bg:rgba(var(--bs-emphasis-color-rgb), 0.075);width:100%;margin-bottom:1rem;vertical-align:top;border-color:var(--bs-table-border-color)}.table>:not(caption)>*>*{padding:.5rem .5rem;color:var(--bs-table-color-state,var(--bs-table-color-type,var(--bs-table-color)));background-color:var(--bs-table-bg);border-bottom-width:var(--bs-border-width);box-shadow:inset 0 0 0 9999px var(--bs-table-bg-state,var(--bs-table-bg-type,var(--bs-table-accent-bg)))}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table-group-divider{border-top:calc(var(--bs-border-width) * 2) solid currentcolor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:var(--bs-border-width) 0}.table-bordered>:not(caption)>*>*{border-width:0 var(--bs-border-width)}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-color-type:var(--bs-table-striped-color);--bs-table-bg-type:var(--bs-table-striped-bg)}.table-striped-columns>:not(caption)>tr>:nth-child(2n){--bs-table-color-type:var(--bs-table-striped-color);--bs-table-bg-type:var(--bs-table-striped-bg)}.table-active{--bs-table-color-state:var(--bs-table-active-color);--bs-table-bg-state:var(--bs-table-active-bg)}.table-hover>tbody>tr:hover>*{--bs-table-color-state:var(--bs-table-hover-color);--bs-table-bg-state:var(--bs-table-hover-bg)}.table-primary{--bs-table-color:#000;--bs-table-bg:#cfe2ff;--bs-table-border-color:#a6b5cc;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-secondary{--bs-table-color:#000;--bs-table-bg:#e2e3e5;--bs-table-border-color:#b5b6b7;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-success{--bs-table-color:#000;--bs-table-bg:#d1e7dd;--bs-table-border-color:#a7b9b1;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-info{--bs-table-color:#000;--bs-table-bg:#cff4fc;--bs-table-border-color:#a6c3ca;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-warning{--bs-table-color:#000;--bs-table-bg:#fff3cd;--bs-table-border-color:#ccc2a4;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-danger{--bs-table-color:#000;--bs-table-bg:#f8d7da;--bs-table-border-color:#c6acae;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-light{--bs-table-color:#000;--bs-table-bg:#f8f9fa;--bs-table-border-color:#c6c7c8;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-dark{--bs-table-color:#fff;--bs-table-bg:#212529;--bs-table-border-color:#4d5154;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + var(--bs-border-width));padding-bottom:calc(.375rem + var(--bs-border-width));margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + var(--bs-border-width));padding-bottom:calc(.5rem + var(--bs-border-width));font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + var(--bs-border-width));padding-bottom:calc(.25rem + var(--bs-border-width));font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:var(--bs-secondary-color)}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-body-bg);background-clip:padding-box;border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:var(--bs-body-color);background-color:var(--bs-body-bg);border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{min-width:85px;height:1.5em;margin:0}.form-control::-webkit-datetime-edit{display:block;padding:0}.form-control::-moz-placeholder{color:var(--bs-secondary-color);opacity:1}.form-control::placeholder{color:var(--bs-secondary-color);opacity:1}.form-control:disabled{background-color:var(--bs-secondary-bg);opacity:1}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:var(--bs-body-color);background-color:var(--bs-tertiary-bg);pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:var(--bs-border-width);border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:var(--bs-body-color);background-color:var(--bs-tertiary-bg);pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:var(--bs-border-width);border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:var(--bs-secondary-bg)}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:var(--bs-secondary-bg)}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:var(--bs-body-color);background-color:transparent;border:solid transparent;border-width:var(--bs-border-width) 0}.form-control-plaintext:focus{outline:0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2));padding:.25rem .5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2));padding:.5rem 1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + calc(var(--bs-border-width) * 2))}textarea.form-control-sm{min-height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2))}textarea.form-control-lg{min-height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2))}.form-control-color{width:3rem;height:calc(1.5em + .75rem + calc(var(--bs-border-width) * 2));padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{border:0!important;border-radius:var(--bs-border-radius)}.form-control-color::-webkit-color-swatch{border:0!important;border-radius:var(--bs-border-radius)}.form-control-color.form-control-sm{height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2))}.form-control-color.form-control-lg{height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2))}.form-select{--bs-form-select-bg-img:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-body-bg);background-image:var(--bs-form-select-bg-img),var(--bs-form-select-bg-icon,none);background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:var(--bs-secondary-bg)}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 var(--bs-body-color)}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}[data-bs-theme=dark] .form-select{--bs-form-select-bg-img:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23dee2e6' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e")}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-reverse{padding-right:1.5em;padding-left:0;text-align:right}.form-check-reverse .form-check-input{float:right;margin-right:-1.5em;margin-left:0}.form-check-input{--bs-form-check-bg:var(--bs-body-bg);flex-shrink:0;width:1em;height:1em;margin-top:.25em;vertical-align:top;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-form-check-bg);background-image:var(--bs-form-check-bg-image);background-repeat:no-repeat;background-position:center;background-size:contain;border:var(--bs-border-width) solid var(--bs-border-color);-webkit-print-color-adjust:exact;color-adjust:exact;print-color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{cursor:default;opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");width:2em;margin-left:-2.5em;background-image:var(--bs-form-switch-bg);background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-switch.form-check-reverse{padding-right:2.5em;padding-left:0}.form-switch.form-check-reverse .form-check-input{margin-right:-2.5em;margin-left:0}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}[data-bs-theme=dark] .form-switch .form-check-input:not(:checked):not(:focus){--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.25%29'/%3e%3c/svg%3e")}.form-range{width:100%;height:1.5rem;padding:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;-webkit-appearance:none;appearance:none;background-color:#0d6efd;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:var(--bs-secondary-bg);border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;-moz-appearance:none;appearance:none;background-color:#0d6efd;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:var(--bs-secondary-bg);border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:var(--bs-secondary-color)}.form-range:disabled::-moz-range-thumb{background-color:var(--bs-secondary-color)}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-control-plaintext,.form-floating>.form-select{height:calc(3.5rem + calc(var(--bs-border-width) * 2));min-height:calc(3.5rem + calc(var(--bs-border-width) * 2));line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;z-index:2;height:100%;padding:1rem .75rem;overflow:hidden;text-align:start;text-overflow:ellipsis;white-space:nowrap;pointer-events:none;border:var(--bs-border-width) solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control,.form-floating>.form-control-plaintext{padding:1rem .75rem}.form-floating>.form-control-plaintext::-moz-placeholder,.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control-plaintext::placeholder,.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control-plaintext:not(:-moz-placeholder-shown),.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:focus,.form-floating>.form-control-plaintext:not(:placeholder-shown),.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:-webkit-autofill,.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{color:rgba(var(--bs-body-color-rgb),.65);transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control-plaintext~label,.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{color:rgba(var(--bs-body-color-rgb),.65);transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:not(:-moz-placeholder-shown)~label::after{position:absolute;inset:1rem 0.375rem;z-index:-1;height:1.5em;content:"";background-color:var(--bs-body-bg);border-radius:var(--bs-border-radius)}.form-floating>.form-control-plaintext~label::after,.form-floating>.form-control:focus~label::after,.form-floating>.form-control:not(:placeholder-shown)~label::after,.form-floating>.form-select~label::after{position:absolute;inset:1rem 0.375rem;z-index:-1;height:1.5em;content:"";background-color:var(--bs-body-bg);border-radius:var(--bs-border-radius)}.form-floating>.form-control:-webkit-autofill~label{color:rgba(var(--bs-body-color-rgb),.65);transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control-plaintext~label{border-width:var(--bs-border-width) 0}.form-floating>.form-control:disabled~label,.form-floating>:disabled~label{color:#6c757d}.form-floating>.form-control:disabled~label::after,.form-floating>:disabled~label::after{background-color:var(--bs-secondary-bg)}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-floating,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-floating:focus-within,.input-group>.form-select:focus{z-index:5}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:5}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);text-align:center;white-space:nowrap;background-color:var(--bs-tertiary-bg);border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius)}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-control,.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-select,.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-control,.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-select,.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:calc(var(--bs-border-width) * -1);border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.form-floating:not(:first-child)>.form-control,.input-group>.form-floating:not(:first-child)>.form-select{border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:var(--bs-form-valid-color)}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:var(--bs-success);border-radius:var(--bs-border-radius)}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:var(--bs-form-valid-border-color);padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:var(--bs-form-valid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:var(--bs-form-valid-border-color)}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{--bs-form-select-bg-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");padding-right:4.125rem;background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:var(--bs-form-valid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.form-control-color.is-valid,.was-validated .form-control-color:valid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:var(--bs-form-valid-border-color)}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:var(--bs-form-valid-color)}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:var(--bs-form-valid-color)}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-valid,.input-group>.form-floating:not(:focus-within).is-valid,.input-group>.form-select:not(:focus).is-valid,.was-validated .input-group>.form-control:not(:focus):valid,.was-validated .input-group>.form-floating:not(:focus-within):valid,.was-validated .input-group>.form-select:not(:focus):valid{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:var(--bs-form-invalid-color)}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:var(--bs-danger);border-radius:var(--bs-border-radius)}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:var(--bs-form-invalid-border-color);padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:var(--bs-form-invalid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:var(--bs-form-invalid-border-color)}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{--bs-form-select-bg-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");padding-right:4.125rem;background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:var(--bs-form-invalid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.form-control-color.is-invalid,.was-validated .form-control-color:invalid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:var(--bs-form-invalid-border-color)}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:var(--bs-form-invalid-color)}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:var(--bs-form-invalid-color)}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-invalid,.input-group>.form-floating:not(:focus-within).is-invalid,.input-group>.form-select:not(:focus).is-invalid,.was-validated .input-group>.form-control:not(:focus):invalid,.was-validated .input-group>.form-floating:not(:focus-within):invalid,.was-validated .input-group>.form-select:not(:focus):invalid{z-index:4}.btn{--bs-btn-padding-x:0.75rem;--bs-btn-padding-y:0.375rem;--bs-btn-font-family: ;--bs-btn-font-size:1rem;--bs-btn-font-weight:400;--bs-btn-line-height:1.5;--bs-btn-color:var(--bs-body-color);--bs-btn-bg:transparent;--bs-btn-border-width:var(--bs-border-width);--bs-btn-border-color:transparent;--bs-btn-border-radius:var(--bs-border-radius);--bs-btn-hover-border-color:transparent;--bs-btn-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.15),0 1px 1px rgba(0, 0, 0, 0.075);--bs-btn-disabled-opacity:0.65;--bs-btn-focus-box-shadow:0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5);display:inline-block;padding:var(--bs-btn-padding-y) var(--bs-btn-padding-x);font-family:var(--bs-btn-font-family);font-size:var(--bs-btn-font-size);font-weight:var(--bs-btn-font-weight);line-height:var(--bs-btn-line-height);color:var(--bs-btn-color);text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;border:var(--bs-btn-border-width) solid var(--bs-btn-border-color);border-radius:var(--bs-btn-border-radius);background-color:var(--bs-btn-bg);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color)}.btn-check+.btn:hover{color:var(--bs-btn-color);background-color:var(--bs-btn-bg);border-color:var(--bs-btn-border-color)}.btn:focus-visible{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:focus-visible+.btn{border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked+.btn,.btn.active,.btn.show,.btn:first-child:active,:not(.btn-check)+.btn:active{color:var(--bs-btn-active-color);background-color:var(--bs-btn-active-bg);border-color:var(--bs-btn-active-border-color)}.btn-check:checked+.btn:focus-visible,.btn.active:focus-visible,.btn.show:focus-visible,.btn:first-child:active:focus-visible,:not(.btn-check)+.btn:active:focus-visible{box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked:focus-visible+.btn{box-shadow:var(--bs-btn-focus-box-shadow)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{color:var(--bs-btn-disabled-color);pointer-events:none;background-color:var(--bs-btn-disabled-bg);border-color:var(--bs-btn-disabled-border-color);opacity:var(--bs-btn-disabled-opacity)}.btn-primary{--bs-btn-color:#fff;--bs-btn-bg:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#0b5ed7;--bs-btn-hover-border-color:#0a58ca;--bs-btn-focus-shadow-rgb:49,132,253;--bs-btn-active-color:#fff;--bs-btn-active-bg:#0a58ca;--bs-btn-active-border-color:#0a53be;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#0d6efd;--bs-btn-disabled-border-color:#0d6efd}.btn-secondary{--bs-btn-color:#fff;--bs-btn-bg:#6c757d;--bs-btn-border-color:#6c757d;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#5c636a;--bs-btn-hover-border-color:#565e64;--bs-btn-focus-shadow-rgb:130,138,145;--bs-btn-active-color:#fff;--bs-btn-active-bg:#565e64;--bs-btn-active-border-color:#51585e;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#6c757d;--bs-btn-disabled-border-color:#6c757d}.btn-success{--bs-btn-color:#fff;--bs-btn-bg:#198754;--bs-btn-border-color:#198754;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#157347;--bs-btn-hover-border-color:#146c43;--bs-btn-focus-shadow-rgb:60,153,110;--bs-btn-active-color:#fff;--bs-btn-active-bg:#146c43;--bs-btn-active-border-color:#13653f;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#198754;--bs-btn-disabled-border-color:#198754}.btn-info{--bs-btn-color:#000;--bs-btn-bg:#0dcaf0;--bs-btn-border-color:#0dcaf0;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#31d2f2;--bs-btn-hover-border-color:#25cff2;--bs-btn-focus-shadow-rgb:11,172,204;--bs-btn-active-color:#000;--bs-btn-active-bg:#3dd5f3;--bs-btn-active-border-color:#25cff2;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#0dcaf0;--bs-btn-disabled-border-color:#0dcaf0}.btn-warning{--bs-btn-color:#000;--bs-btn-bg:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#ffca2c;--bs-btn-hover-border-color:#ffc720;--bs-btn-focus-shadow-rgb:217,164,6;--bs-btn-active-color:#000;--bs-btn-active-bg:#ffcd39;--bs-btn-active-border-color:#ffc720;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#ffc107;--bs-btn-disabled-border-color:#ffc107}.btn-danger{--bs-btn-color:#fff;--bs-btn-bg:#dc3545;--bs-btn-border-color:#dc3545;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#bb2d3b;--bs-btn-hover-border-color:#b02a37;--bs-btn-focus-shadow-rgb:225,83,97;--bs-btn-active-color:#fff;--bs-btn-active-bg:#b02a37;--bs-btn-active-border-color:#a52834;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#dc3545;--bs-btn-disabled-border-color:#dc3545}.btn-light{--bs-btn-color:#000;--bs-btn-bg:#f8f9fa;--bs-btn-border-color:#f8f9fa;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#d3d4d5;--bs-btn-hover-border-color:#c6c7c8;--bs-btn-focus-shadow-rgb:211,212,213;--bs-btn-active-color:#000;--bs-btn-active-bg:#c6c7c8;--bs-btn-active-border-color:#babbbc;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#f8f9fa;--bs-btn-disabled-border-color:#f8f9fa}.btn-dark{--bs-btn-color:#fff;--bs-btn-bg:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#424649;--bs-btn-hover-border-color:#373b3e;--bs-btn-focus-shadow-rgb:66,70,73;--bs-btn-active-color:#fff;--bs-btn-active-bg:#4d5154;--bs-btn-active-border-color:#373b3e;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#212529;--bs-btn-disabled-border-color:#212529}.btn-outline-primary{--bs-btn-color:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#0d6efd;--bs-btn-hover-border-color:#0d6efd;--bs-btn-focus-shadow-rgb:13,110,253;--bs-btn-active-color:#fff;--bs-btn-active-bg:#0d6efd;--bs-btn-active-border-color:#0d6efd;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#0d6efd;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#0d6efd;--bs-gradient:none}.btn-outline-secondary{--bs-btn-color:#6c757d;--bs-btn-border-color:#6c757d;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#6c757d;--bs-btn-hover-border-color:#6c757d;--bs-btn-focus-shadow-rgb:108,117,125;--bs-btn-active-color:#fff;--bs-btn-active-bg:#6c757d;--bs-btn-active-border-color:#6c757d;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#6c757d;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#6c757d;--bs-gradient:none}.btn-outline-success{--bs-btn-color:#198754;--bs-btn-border-color:#198754;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#198754;--bs-btn-hover-border-color:#198754;--bs-btn-focus-shadow-rgb:25,135,84;--bs-btn-active-color:#fff;--bs-btn-active-bg:#198754;--bs-btn-active-border-color:#198754;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#198754;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#198754;--bs-gradient:none}.btn-outline-info{--bs-btn-color:#0dcaf0;--bs-btn-border-color:#0dcaf0;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#0dcaf0;--bs-btn-hover-border-color:#0dcaf0;--bs-btn-focus-shadow-rgb:13,202,240;--bs-btn-active-color:#000;--bs-btn-active-bg:#0dcaf0;--bs-btn-active-border-color:#0dcaf0;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#0dcaf0;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#0dcaf0;--bs-gradient:none}.btn-outline-warning{--bs-btn-color:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#ffc107;--bs-btn-hover-border-color:#ffc107;--bs-btn-focus-shadow-rgb:255,193,7;--bs-btn-active-color:#000;--bs-btn-active-bg:#ffc107;--bs-btn-active-border-color:#ffc107;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#ffc107;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#ffc107;--bs-gradient:none}.btn-outline-danger{--bs-btn-color:#dc3545;--bs-btn-border-color:#dc3545;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#dc3545;--bs-btn-hover-border-color:#dc3545;--bs-btn-focus-shadow-rgb:220,53,69;--bs-btn-active-color:#fff;--bs-btn-active-bg:#dc3545;--bs-btn-active-border-color:#dc3545;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#dc3545;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#dc3545;--bs-gradient:none}.btn-outline-light{--bs-btn-color:#f8f9fa;--bs-btn-border-color:#f8f9fa;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#f8f9fa;--bs-btn-hover-border-color:#f8f9fa;--bs-btn-focus-shadow-rgb:248,249,250;--bs-btn-active-color:#000;--bs-btn-active-bg:#f8f9fa;--bs-btn-active-border-color:#f8f9fa;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#f8f9fa;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#f8f9fa;--bs-gradient:none}.btn-outline-dark{--bs-btn-color:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#212529;--bs-btn-hover-border-color:#212529;--bs-btn-focus-shadow-rgb:33,37,41;--bs-btn-active-color:#fff;--bs-btn-active-bg:#212529;--bs-btn-active-border-color:#212529;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#212529;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#212529;--bs-gradient:none}.btn-link{--bs-btn-font-weight:400;--bs-btn-color:var(--bs-link-color);--bs-btn-bg:transparent;--bs-btn-border-color:transparent;--bs-btn-hover-color:var(--bs-link-hover-color);--bs-btn-hover-border-color:transparent;--bs-btn-active-color:var(--bs-link-hover-color);--bs-btn-active-border-color:transparent;--bs-btn-disabled-color:#6c757d;--bs-btn-disabled-border-color:transparent;--bs-btn-box-shadow:0 0 0 #000;--bs-btn-focus-shadow-rgb:49,132,253;text-decoration:underline}.btn-link:focus-visible{color:var(--bs-btn-color)}.btn-link:hover{color:var(--bs-btn-hover-color)}.btn-group-lg>.btn,.btn-lg{--bs-btn-padding-y:0.5rem;--bs-btn-padding-x:1rem;--bs-btn-font-size:1.25rem;--bs-btn-border-radius:var(--bs-border-radius-lg)}.btn-group-sm>.btn,.btn-sm{--bs-btn-padding-y:0.25rem;--bs-btn-padding-x:0.5rem;--bs-btn-font-size:0.875rem;--bs-btn-border-radius:var(--bs-border-radius-sm)}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropdown-center,.dropend,.dropstart,.dropup,.dropup-center{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{--bs-dropdown-zindex:1000;--bs-dropdown-min-width:10rem;--bs-dropdown-padding-x:0;--bs-dropdown-padding-y:0.5rem;--bs-dropdown-spacer:0.125rem;--bs-dropdown-font-size:1rem;--bs-dropdown-color:var(--bs-body-color);--bs-dropdown-bg:var(--bs-body-bg);--bs-dropdown-border-color:var(--bs-border-color-translucent);--bs-dropdown-border-radius:var(--bs-border-radius);--bs-dropdown-border-width:var(--bs-border-width);--bs-dropdown-inner-border-radius:calc(var(--bs-border-radius) - var(--bs-border-width));--bs-dropdown-divider-bg:var(--bs-border-color-translucent);--bs-dropdown-divider-margin-y:0.5rem;--bs-dropdown-box-shadow:var(--bs-box-shadow);--bs-dropdown-link-color:var(--bs-body-color);--bs-dropdown-link-hover-color:var(--bs-body-color);--bs-dropdown-link-hover-bg:var(--bs-tertiary-bg);--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#0d6efd;--bs-dropdown-link-disabled-color:var(--bs-tertiary-color);--bs-dropdown-item-padding-x:1rem;--bs-dropdown-item-padding-y:0.25rem;--bs-dropdown-header-color:#6c757d;--bs-dropdown-header-padding-x:1rem;--bs-dropdown-header-padding-y:0.5rem;position:absolute;z-index:var(--bs-dropdown-zindex);display:none;min-width:var(--bs-dropdown-min-width);padding:var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x);margin:0;font-size:var(--bs-dropdown-font-size);color:var(--bs-dropdown-color);text-align:left;list-style:none;background-color:var(--bs-dropdown-bg);background-clip:padding-box;border:var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color);border-radius:var(--bs-dropdown-border-radius)}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:var(--bs-dropdown-spacer)}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:var(--bs-dropdown-spacer)}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:var(--bs-dropdown-spacer)}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:var(--bs-dropdown-spacer)}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:var(--bs-dropdown-divider-margin-y) 0;overflow:hidden;border-top:1px solid var(--bs-dropdown-divider-bg);opacity:1}.dropdown-item{display:block;width:100%;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);clear:both;font-weight:400;color:var(--bs-dropdown-link-color);text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0;border-radius:var(--bs-dropdown-item-border-radius,0)}.dropdown-item:focus,.dropdown-item:hover{color:var(--bs-dropdown-link-hover-color);background-color:var(--bs-dropdown-link-hover-bg)}.dropdown-item.active,.dropdown-item:active{color:var(--bs-dropdown-link-active-color);text-decoration:none;background-color:var(--bs-dropdown-link-active-bg)}.dropdown-item.disabled,.dropdown-item:disabled{color:var(--bs-dropdown-link-disabled-color);pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x);margin-bottom:0;font-size:.875rem;color:var(--bs-dropdown-header-color);white-space:nowrap}.dropdown-item-text{display:block;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);color:var(--bs-dropdown-link-color)}.dropdown-menu-dark{--bs-dropdown-color:#dee2e6;--bs-dropdown-bg:#343a40;--bs-dropdown-border-color:var(--bs-border-color-translucent);--bs-dropdown-box-shadow: ;--bs-dropdown-link-color:#dee2e6;--bs-dropdown-link-hover-color:#fff;--bs-dropdown-divider-bg:var(--bs-border-color-translucent);--bs-dropdown-link-hover-bg:rgba(255, 255, 255, 0.15);--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#0d6efd;--bs-dropdown-link-disabled-color:#adb5bd;--bs-dropdown-header-color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group{border-radius:var(--bs-border-radius)}.btn-group>.btn-group:not(:first-child),.btn-group>:not(.btn-check:first-child)+.btn{margin-left:calc(var(--bs-border-width) * -1)}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn.dropdown-toggle-split:first-child,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:calc(var(--bs-border-width) * -1)}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{--bs-nav-link-padding-x:1rem;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-link-color);--bs-nav-link-hover-color:var(--bs-link-hover-color);--bs-nav-link-disabled-color:var(--bs-secondary-color);display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);font-size:var(--bs-nav-link-font-size);font-weight:var(--bs-nav-link-font-weight);color:var(--bs-nav-link-color);text-decoration:none;background:0 0;border:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:var(--bs-nav-link-hover-color)}.nav-link:focus-visible{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.nav-link.disabled,.nav-link:disabled{color:var(--bs-nav-link-disabled-color);pointer-events:none;cursor:default}.nav-tabs{--bs-nav-tabs-border-width:var(--bs-border-width);--bs-nav-tabs-border-color:var(--bs-border-color);--bs-nav-tabs-border-radius:var(--bs-border-radius);--bs-nav-tabs-link-hover-border-color:var(--bs-secondary-bg) var(--bs-secondary-bg) var(--bs-border-color);--bs-nav-tabs-link-active-color:var(--bs-emphasis-color);--bs-nav-tabs-link-active-bg:var(--bs-body-bg);--bs-nav-tabs-link-active-border-color:var(--bs-border-color) var(--bs-border-color) var(--bs-body-bg);border-bottom:var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color)}.nav-tabs .nav-link{margin-bottom:calc(-1 * var(--bs-nav-tabs-border-width));border:var(--bs-nav-tabs-border-width) solid transparent;border-top-left-radius:var(--bs-nav-tabs-border-radius);border-top-right-radius:var(--bs-nav-tabs-border-radius)}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{isolation:isolate;border-color:var(--bs-nav-tabs-link-hover-border-color)}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:var(--bs-nav-tabs-link-active-color);background-color:var(--bs-nav-tabs-link-active-bg);border-color:var(--bs-nav-tabs-link-active-border-color)}.nav-tabs .dropdown-menu{margin-top:calc(-1 * var(--bs-nav-tabs-border-width));border-top-left-radius:0;border-top-right-radius:0}.nav-pills{--bs-nav-pills-border-radius:var(--bs-border-radius);--bs-nav-pills-link-active-color:#fff;--bs-nav-pills-link-active-bg:#0d6efd}.nav-pills .nav-link{border-radius:var(--bs-nav-pills-border-radius)}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:var(--bs-nav-pills-link-active-color);background-color:var(--bs-nav-pills-link-active-bg)}.nav-underline{--bs-nav-underline-gap:1rem;--bs-nav-underline-border-width:0.125rem;--bs-nav-underline-link-active-color:var(--bs-emphasis-color);gap:var(--bs-nav-underline-gap)}.nav-underline .nav-link{padding-right:0;padding-left:0;border-bottom:var(--bs-nav-underline-border-width) solid transparent}.nav-underline .nav-link:focus,.nav-underline .nav-link:hover{border-bottom-color:currentcolor}.nav-underline .nav-link.active,.nav-underline .show>.nav-link{font-weight:700;color:var(--bs-nav-underline-link-active-color);border-bottom-color:currentcolor}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{--bs-navbar-padding-x:0;--bs-navbar-padding-y:0.5rem;--bs-navbar-color:rgba(var(--bs-emphasis-color-rgb), 0.65);--bs-navbar-hover-color:rgba(var(--bs-emphasis-color-rgb), 0.8);--bs-navbar-disabled-color:rgba(var(--bs-emphasis-color-rgb), 0.3);--bs-navbar-active-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-padding-y:0.3125rem;--bs-navbar-brand-margin-end:1rem;--bs-navbar-brand-font-size:1.25rem;--bs-navbar-brand-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-hover-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-nav-link-padding-x:0.5rem;--bs-navbar-toggler-padding-y:0.25rem;--bs-navbar-toggler-padding-x:0.75rem;--bs-navbar-toggler-font-size:1.25rem;--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2833, 37, 41, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");--bs-navbar-toggler-border-color:rgba(var(--bs-emphasis-color-rgb), 0.15);--bs-navbar-toggler-border-radius:var(--bs-border-radius);--bs-navbar-toggler-focus-width:0.25rem;--bs-navbar-toggler-transition:box-shadow 0.15s ease-in-out;position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:var(--bs-navbar-padding-y) var(--bs-navbar-padding-x)}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:var(--bs-navbar-brand-padding-y);padding-bottom:var(--bs-navbar-brand-padding-y);margin-right:var(--bs-navbar-brand-margin-end);font-size:var(--bs-navbar-brand-font-size);color:var(--bs-navbar-brand-color);text-decoration:none;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{color:var(--bs-navbar-brand-hover-color)}.navbar-nav{--bs-nav-link-padding-x:0;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-navbar-color);--bs-nav-link-hover-color:var(--bs-navbar-hover-color);--bs-nav-link-disabled-color:var(--bs-navbar-disabled-color);display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link.active,.navbar-nav .nav-link.show{color:var(--bs-navbar-active-color)}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-navbar-color)}.navbar-text a,.navbar-text a:focus,.navbar-text a:hover{color:var(--bs-navbar-active-color)}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x);font-size:var(--bs-navbar-toggler-font-size);line-height:1;color:var(--bs-navbar-color);background-color:transparent;border:var(--bs-border-width) solid var(--bs-navbar-toggler-border-color);border-radius:var(--bs-navbar-toggler-border-radius);transition:var(--bs-navbar-toggler-transition)}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 var(--bs-navbar-toggler-focus-width)}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-image:var(--bs-navbar-toggler-icon-bg);background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-sm .offcanvas .offcanvas-header{display:none}.navbar-expand-sm .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-md .offcanvas .offcanvas-header{display:none}.navbar-expand-md .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-lg .offcanvas .offcanvas-header{display:none}.navbar-expand-lg .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xl .offcanvas .offcanvas-header{display:none}.navbar-expand-xl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xxl .offcanvas .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand .offcanvas .offcanvas-header{display:none}.navbar-expand .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-dark,.navbar[data-bs-theme=dark]{--bs-navbar-color:rgba(255, 255, 255, 0.55);--bs-navbar-hover-color:rgba(255, 255, 255, 0.75);--bs-navbar-disabled-color:rgba(255, 255, 255, 0.25);--bs-navbar-active-color:#fff;--bs-navbar-brand-color:#fff;--bs-navbar-brand-hover-color:#fff;--bs-navbar-toggler-border-color:rgba(255, 255, 255, 0.1);--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}[data-bs-theme=dark] .navbar-toggler-icon{--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.card{--bs-card-spacer-y:1rem;--bs-card-spacer-x:1rem;--bs-card-title-spacer-y:0.5rem;--bs-card-title-color: ;--bs-card-subtitle-color: ;--bs-card-border-width:var(--bs-border-width);--bs-card-border-color:var(--bs-border-color-translucent);--bs-card-border-radius:var(--bs-border-radius);--bs-card-box-shadow: ;--bs-card-inner-border-radius:calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-card-cap-padding-y:0.5rem;--bs-card-cap-padding-x:1rem;--bs-card-cap-bg:rgba(var(--bs-body-color-rgb), 0.03);--bs-card-cap-color: ;--bs-card-height: ;--bs-card-color: ;--bs-card-bg:var(--bs-body-bg);--bs-card-img-overlay-padding:1rem;--bs-card-group-margin:0.75rem;position:relative;display:flex;flex-direction:column;min-width:0;height:var(--bs-card-height);color:var(--bs-body-color);word-wrap:break-word;background-color:var(--bs-card-bg);background-clip:border-box;border:var(--bs-card-border-width) solid var(--bs-card-border-color);border-radius:var(--bs-card-border-radius)}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:var(--bs-card-spacer-y) var(--bs-card-spacer-x);color:var(--bs-card-color)}.card-title{margin-bottom:var(--bs-card-title-spacer-y);color:var(--bs-card-title-color)}.card-subtitle{margin-top:calc(-.5 * var(--bs-card-title-spacer-y));margin-bottom:0;color:var(--bs-card-subtitle-color)}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:var(--bs-card-spacer-x)}.card-header{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);margin-bottom:0;color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-bottom:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-header:first-child{border-radius:var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius) 0 0}.card-footer{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-top:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-footer:last-child{border-radius:0 0 var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius)}.card-header-tabs{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-bottom:calc(-1 * var(--bs-card-cap-padding-y));margin-left:calc(-.5 * var(--bs-card-cap-padding-x));border-bottom:0}.card-header-tabs .nav-link.active{background-color:var(--bs-card-bg);border-bottom-color:var(--bs-card-bg)}.card-header-pills{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-left:calc(-.5 * var(--bs-card-cap-padding-x))}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:var(--bs-card-img-overlay-padding);border-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom{border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card-group>.card{margin-bottom:var(--bs-card-group-margin)}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion{--bs-accordion-color:var(--bs-body-color);--bs-accordion-bg:var(--bs-body-bg);--bs-accordion-transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out,border-radius 0.15s ease;--bs-accordion-border-color:var(--bs-border-color);--bs-accordion-border-width:var(--bs-border-width);--bs-accordion-border-radius:var(--bs-border-radius);--bs-accordion-inner-border-radius:calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-accordion-btn-padding-x:1.25rem;--bs-accordion-btn-padding-y:1rem;--bs-accordion-btn-color:var(--bs-body-color);--bs-accordion-btn-bg:var(--bs-accordion-bg);--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23212529' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5L8 11L14 5'/%3e%3c/svg%3e");--bs-accordion-btn-icon-width:1.25rem;--bs-accordion-btn-icon-transform:rotate(-180deg);--bs-accordion-btn-icon-transition:transform 0.2s ease-in-out;--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23052c65' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5L8 11L14 5'/%3e%3c/svg%3e");--bs-accordion-btn-focus-box-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-accordion-body-padding-x:1.25rem;--bs-accordion-body-padding-y:1rem;--bs-accordion-active-color:var(--bs-primary-text-emphasis);--bs-accordion-active-bg:var(--bs-primary-bg-subtle)}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x);font-size:1rem;color:var(--bs-accordion-btn-color);text-align:left;background-color:var(--bs-accordion-btn-bg);border:0;border-radius:0;overflow-anchor:none;transition:var(--bs-accordion-transition)}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:var(--bs-accordion-active-color);background-color:var(--bs-accordion-active-bg);box-shadow:inset 0 calc(-1 * var(--bs-accordion-border-width)) 0 var(--bs-accordion-border-color)}.accordion-button:not(.collapsed)::after{background-image:var(--bs-accordion-btn-active-icon);transform:var(--bs-accordion-btn-icon-transform)}.accordion-button::after{flex-shrink:0;width:var(--bs-accordion-btn-icon-width);height:var(--bs-accordion-btn-icon-width);margin-left:auto;content:"";background-image:var(--bs-accordion-btn-icon);background-repeat:no-repeat;background-size:var(--bs-accordion-btn-icon-width);transition:var(--bs-accordion-btn-icon-transition)}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;outline:0;box-shadow:var(--bs-accordion-btn-focus-box-shadow)}.accordion-header{margin-bottom:0}.accordion-item{color:var(--bs-accordion-color);background-color:var(--bs-accordion-bg);border:var(--bs-accordion-border-width) solid var(--bs-accordion-border-color)}.accordion-item:first-of-type{border-top-left-radius:var(--bs-accordion-border-radius);border-top-right-radius:var(--bs-accordion-border-radius)}.accordion-item:first-of-type>.accordion-header .accordion-button{border-top-left-radius:var(--bs-accordion-inner-border-radius);border-top-right-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-item:last-of-type>.accordion-header .accordion-button.collapsed{border-bottom-right-radius:var(--bs-accordion-inner-border-radius);border-bottom-left-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:last-of-type>.accordion-collapse{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-body{padding:var(--bs-accordion-body-padding-y) var(--bs-accordion-body-padding-x)}.accordion-flush>.accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush>.accordion-item:first-child{border-top:0}.accordion-flush>.accordion-item:last-child{border-bottom:0}.accordion-flush>.accordion-item>.accordion-header .accordion-button,.accordion-flush>.accordion-item>.accordion-header .accordion-button.collapsed{border-radius:0}.accordion-flush>.accordion-item>.accordion-collapse{border-radius:0}[data-bs-theme=dark] .accordion-button::after{--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.breadcrumb{--bs-breadcrumb-padding-x:0;--bs-breadcrumb-padding-y:0;--bs-breadcrumb-margin-bottom:1rem;--bs-breadcrumb-bg: ;--bs-breadcrumb-border-radius: ;--bs-breadcrumb-divider-color:var(--bs-secondary-color);--bs-breadcrumb-item-padding-x:0.5rem;--bs-breadcrumb-item-active-color:var(--bs-secondary-color);display:flex;flex-wrap:wrap;padding:var(--bs-breadcrumb-padding-y) var(--bs-breadcrumb-padding-x);margin-bottom:var(--bs-breadcrumb-margin-bottom);font-size:var(--bs-breadcrumb-font-size);list-style:none;background-color:var(--bs-breadcrumb-bg);border-radius:var(--bs-breadcrumb-border-radius)}.breadcrumb-item+.breadcrumb-item{padding-left:var(--bs-breadcrumb-item-padding-x)}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:var(--bs-breadcrumb-item-padding-x);color:var(--bs-breadcrumb-divider-color);content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:var(--bs-breadcrumb-item-active-color)}.pagination{--bs-pagination-padding-x:0.75rem;--bs-pagination-padding-y:0.375rem;--bs-pagination-font-size:1rem;--bs-pagination-color:var(--bs-link-color);--bs-pagination-bg:var(--bs-body-bg);--bs-pagination-border-width:var(--bs-border-width);--bs-pagination-border-color:var(--bs-border-color);--bs-pagination-border-radius:var(--bs-border-radius);--bs-pagination-hover-color:var(--bs-link-hover-color);--bs-pagination-hover-bg:var(--bs-tertiary-bg);--bs-pagination-hover-border-color:var(--bs-border-color);--bs-pagination-focus-color:var(--bs-link-hover-color);--bs-pagination-focus-bg:var(--bs-secondary-bg);--bs-pagination-focus-box-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-pagination-active-color:#fff;--bs-pagination-active-bg:#0d6efd;--bs-pagination-active-border-color:#0d6efd;--bs-pagination-disabled-color:var(--bs-secondary-color);--bs-pagination-disabled-bg:var(--bs-secondary-bg);--bs-pagination-disabled-border-color:var(--bs-border-color);display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;padding:var(--bs-pagination-padding-y) var(--bs-pagination-padding-x);font-size:var(--bs-pagination-font-size);color:var(--bs-pagination-color);text-decoration:none;background-color:var(--bs-pagination-bg);border:var(--bs-pagination-border-width) solid var(--bs-pagination-border-color);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:var(--bs-pagination-hover-color);background-color:var(--bs-pagination-hover-bg);border-color:var(--bs-pagination-hover-border-color)}.page-link:focus{z-index:3;color:var(--bs-pagination-focus-color);background-color:var(--bs-pagination-focus-bg);outline:0;box-shadow:var(--bs-pagination-focus-box-shadow)}.active>.page-link,.page-link.active{z-index:3;color:var(--bs-pagination-active-color);background-color:var(--bs-pagination-active-bg);border-color:var(--bs-pagination-active-border-color)}.disabled>.page-link,.page-link.disabled{color:var(--bs-pagination-disabled-color);pointer-events:none;background-color:var(--bs-pagination-disabled-bg);border-color:var(--bs-pagination-disabled-border-color)}.page-item:not(:first-child) .page-link{margin-left:calc(var(--bs-border-width) * -1)}.page-item:first-child .page-link{border-top-left-radius:var(--bs-pagination-border-radius);border-bottom-left-radius:var(--bs-pagination-border-radius)}.page-item:last-child .page-link{border-top-right-radius:var(--bs-pagination-border-radius);border-bottom-right-radius:var(--bs-pagination-border-radius)}.pagination-lg{--bs-pagination-padding-x:1.5rem;--bs-pagination-padding-y:0.75rem;--bs-pagination-font-size:1.25rem;--bs-pagination-border-radius:var(--bs-border-radius-lg)}.pagination-sm{--bs-pagination-padding-x:0.5rem;--bs-pagination-padding-y:0.25rem;--bs-pagination-font-size:0.875rem;--bs-pagination-border-radius:var(--bs-border-radius-sm)}.badge{--bs-badge-padding-x:0.65em;--bs-badge-padding-y:0.35em;--bs-badge-font-size:0.75em;--bs-badge-font-weight:700;--bs-badge-color:#fff;--bs-badge-border-radius:var(--bs-border-radius);display:inline-block;padding:var(--bs-badge-padding-y) var(--bs-badge-padding-x);font-size:var(--bs-badge-font-size);font-weight:var(--bs-badge-font-weight);line-height:1;color:var(--bs-badge-color);text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:var(--bs-badge-border-radius)}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{--bs-alert-bg:transparent;--bs-alert-padding-x:1rem;--bs-alert-padding-y:1rem;--bs-alert-margin-bottom:1rem;--bs-alert-color:inherit;--bs-alert-border-color:transparent;--bs-alert-border:var(--bs-border-width) solid var(--bs-alert-border-color);--bs-alert-border-radius:var(--bs-border-radius);--bs-alert-link-color:inherit;position:relative;padding:var(--bs-alert-padding-y) var(--bs-alert-padding-x);margin-bottom:var(--bs-alert-margin-bottom);color:var(--bs-alert-color);background-color:var(--bs-alert-bg);border:var(--bs-alert-border);border-radius:var(--bs-alert-border-radius)}.alert-heading{color:inherit}.alert-link{font-weight:700;color:var(--bs-alert-link-color)}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{--bs-alert-color:var(--bs-primary-text-emphasis);--bs-alert-bg:var(--bs-primary-bg-subtle);--bs-alert-border-color:var(--bs-primary-border-subtle);--bs-alert-link-color:var(--bs-primary-text-emphasis)}.alert-secondary{--bs-alert-color:var(--bs-secondary-text-emphasis);--bs-alert-bg:var(--bs-secondary-bg-subtle);--bs-alert-border-color:var(--bs-secondary-border-subtle);--bs-alert-link-color:var(--bs-secondary-text-emphasis)}.alert-success{--bs-alert-color:var(--bs-success-text-emphasis);--bs-alert-bg:var(--bs-success-bg-subtle);--bs-alert-border-color:var(--bs-success-border-subtle);--bs-alert-link-color:var(--bs-success-text-emphasis)}.alert-info{--bs-alert-color:var(--bs-info-text-emphasis);--bs-alert-bg:var(--bs-info-bg-subtle);--bs-alert-border-color:var(--bs-info-border-subtle);--bs-alert-link-color:var(--bs-info-text-emphasis)}.alert-warning{--bs-alert-color:var(--bs-warning-text-emphasis);--bs-alert-bg:var(--bs-warning-bg-subtle);--bs-alert-border-color:var(--bs-warning-border-subtle);--bs-alert-link-color:var(--bs-warning-text-emphasis)}.alert-danger{--bs-alert-color:var(--bs-danger-text-emphasis);--bs-alert-bg:var(--bs-danger-bg-subtle);--bs-alert-border-color:var(--bs-danger-border-subtle);--bs-alert-link-color:var(--bs-danger-text-emphasis)}.alert-light{--bs-alert-color:var(--bs-light-text-emphasis);--bs-alert-bg:var(--bs-light-bg-subtle);--bs-alert-border-color:var(--bs-light-border-subtle);--bs-alert-link-color:var(--bs-light-text-emphasis)}.alert-dark{--bs-alert-color:var(--bs-dark-text-emphasis);--bs-alert-bg:var(--bs-dark-bg-subtle);--bs-alert-border-color:var(--bs-dark-border-subtle);--bs-alert-link-color:var(--bs-dark-text-emphasis)}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress,.progress-stacked{--bs-progress-height:1rem;--bs-progress-font-size:0.75rem;--bs-progress-bg:var(--bs-secondary-bg);--bs-progress-border-radius:var(--bs-border-radius);--bs-progress-box-shadow:var(--bs-box-shadow-inset);--bs-progress-bar-color:#fff;--bs-progress-bar-bg:#0d6efd;--bs-progress-bar-transition:width 0.6s ease;display:flex;height:var(--bs-progress-height);overflow:hidden;font-size:var(--bs-progress-font-size);background-color:var(--bs-progress-bg);border-radius:var(--bs-progress-border-radius)}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:var(--bs-progress-bar-color);text-align:center;white-space:nowrap;background-color:var(--bs-progress-bar-bg);transition:var(--bs-progress-bar-transition)}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:var(--bs-progress-height) var(--bs-progress-height)}.progress-stacked>.progress{overflow:visible}.progress-stacked>.progress>.progress-bar{width:100%}.progress-bar-animated{animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{animation:none}}.list-group{--bs-list-group-color:var(--bs-body-color);--bs-list-group-bg:var(--bs-body-bg);--bs-list-group-border-color:var(--bs-border-color);--bs-list-group-border-width:var(--bs-border-width);--bs-list-group-border-radius:var(--bs-border-radius);--bs-list-group-item-padding-x:1rem;--bs-list-group-item-padding-y:0.5rem;--bs-list-group-action-color:var(--bs-secondary-color);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-tertiary-bg);--bs-list-group-action-active-color:var(--bs-body-color);--bs-list-group-action-active-bg:var(--bs-secondary-bg);--bs-list-group-disabled-color:var(--bs-secondary-color);--bs-list-group-disabled-bg:var(--bs-body-bg);--bs-list-group-active-color:#fff;--bs-list-group-active-bg:#0d6efd;--bs-list-group-active-border-color:#0d6efd;display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:var(--bs-list-group-border-radius)}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>.list-group-item::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:var(--bs-list-group-action-color);text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:var(--bs-list-group-action-hover-color);text-decoration:none;background-color:var(--bs-list-group-action-hover-bg)}.list-group-item-action:active{color:var(--bs-list-group-action-active-color);background-color:var(--bs-list-group-action-active-bg)}.list-group-item{position:relative;display:block;padding:var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x);color:var(--bs-list-group-color);text-decoration:none;background-color:var(--bs-list-group-bg);border:var(--bs-list-group-border-width) solid var(--bs-list-group-border-color)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:var(--bs-list-group-disabled-color);pointer-events:none;background-color:var(--bs-list-group-disabled-bg)}.list-group-item.active{z-index:2;color:var(--bs-list-group-active-color);background-color:var(--bs-list-group-active-bg);border-color:var(--bs-list-group-active-border-color)}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:calc(-1 * var(--bs-list-group-border-width));border-top-width:var(--bs-list-group-border-width)}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 var(--bs-list-group-border-width)}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{--bs-list-group-color:var(--bs-primary-text-emphasis);--bs-list-group-bg:var(--bs-primary-bg-subtle);--bs-list-group-border-color:var(--bs-primary-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-primary-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-primary-border-subtle);--bs-list-group-active-color:var(--bs-primary-bg-subtle);--bs-list-group-active-bg:var(--bs-primary-text-emphasis);--bs-list-group-active-border-color:var(--bs-primary-text-emphasis)}.list-group-item-secondary{--bs-list-group-color:var(--bs-secondary-text-emphasis);--bs-list-group-bg:var(--bs-secondary-bg-subtle);--bs-list-group-border-color:var(--bs-secondary-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-secondary-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-secondary-border-subtle);--bs-list-group-active-color:var(--bs-secondary-bg-subtle);--bs-list-group-active-bg:var(--bs-secondary-text-emphasis);--bs-list-group-active-border-color:var(--bs-secondary-text-emphasis)}.list-group-item-success{--bs-list-group-color:var(--bs-success-text-emphasis);--bs-list-group-bg:var(--bs-success-bg-subtle);--bs-list-group-border-color:var(--bs-success-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-success-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-success-border-subtle);--bs-list-group-active-color:var(--bs-success-bg-subtle);--bs-list-group-active-bg:var(--bs-success-text-emphasis);--bs-list-group-active-border-color:var(--bs-success-text-emphasis)}.list-group-item-info{--bs-list-group-color:var(--bs-info-text-emphasis);--bs-list-group-bg:var(--bs-info-bg-subtle);--bs-list-group-border-color:var(--bs-info-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-info-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-info-border-subtle);--bs-list-group-active-color:var(--bs-info-bg-subtle);--bs-list-group-active-bg:var(--bs-info-text-emphasis);--bs-list-group-active-border-color:var(--bs-info-text-emphasis)}.list-group-item-warning{--bs-list-group-color:var(--bs-warning-text-emphasis);--bs-list-group-bg:var(--bs-warning-bg-subtle);--bs-list-group-border-color:var(--bs-warning-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-warning-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-warning-border-subtle);--bs-list-group-active-color:var(--bs-warning-bg-subtle);--bs-list-group-active-bg:var(--bs-warning-text-emphasis);--bs-list-group-active-border-color:var(--bs-warning-text-emphasis)}.list-group-item-danger{--bs-list-group-color:var(--bs-danger-text-emphasis);--bs-list-group-bg:var(--bs-danger-bg-subtle);--bs-list-group-border-color:var(--bs-danger-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-danger-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-danger-border-subtle);--bs-list-group-active-color:var(--bs-danger-bg-subtle);--bs-list-group-active-bg:var(--bs-danger-text-emphasis);--bs-list-group-active-border-color:var(--bs-danger-text-emphasis)}.list-group-item-light{--bs-list-group-color:var(--bs-light-text-emphasis);--bs-list-group-bg:var(--bs-light-bg-subtle);--bs-list-group-border-color:var(--bs-light-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-light-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-light-border-subtle);--bs-list-group-active-color:var(--bs-light-bg-subtle);--bs-list-group-active-bg:var(--bs-light-text-emphasis);--bs-list-group-active-border-color:var(--bs-light-text-emphasis)}.list-group-item-dark{--bs-list-group-color:var(--bs-dark-text-emphasis);--bs-list-group-bg:var(--bs-dark-bg-subtle);--bs-list-group-border-color:var(--bs-dark-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-dark-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-dark-border-subtle);--bs-list-group-active-color:var(--bs-dark-bg-subtle);--bs-list-group-active-bg:var(--bs-dark-text-emphasis);--bs-list-group-active-border-color:var(--bs-dark-text-emphasis)}.btn-close{--bs-btn-close-color:#000;--bs-btn-close-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e");--bs-btn-close-opacity:0.5;--bs-btn-close-hover-opacity:0.75;--bs-btn-close-focus-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-btn-close-focus-opacity:1;--bs-btn-close-disabled-opacity:0.25;--bs-btn-close-white-filter:invert(1) grayscale(100%) brightness(200%);box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:var(--bs-btn-close-color);background:transparent var(--bs-btn-close-bg) center/1em auto no-repeat;border:0;border-radius:.375rem;opacity:var(--bs-btn-close-opacity)}.btn-close:hover{color:var(--bs-btn-close-color);text-decoration:none;opacity:var(--bs-btn-close-hover-opacity)}.btn-close:focus{outline:0;box-shadow:var(--bs-btn-close-focus-shadow);opacity:var(--bs-btn-close-focus-opacity)}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:var(--bs-btn-close-disabled-opacity)}.btn-close-white{filter:var(--bs-btn-close-white-filter)}[data-bs-theme=dark] .btn-close{filter:var(--bs-btn-close-white-filter)}.toast{--bs-toast-zindex:1090;--bs-toast-padding-x:0.75rem;--bs-toast-padding-y:0.5rem;--bs-toast-spacing:1.5rem;--bs-toast-max-width:350px;--bs-toast-font-size:0.875rem;--bs-toast-color: ;--bs-toast-bg:rgba(var(--bs-body-bg-rgb), 0.85);--bs-toast-border-width:var(--bs-border-width);--bs-toast-border-color:var(--bs-border-color-translucent);--bs-toast-border-radius:var(--bs-border-radius);--bs-toast-box-shadow:var(--bs-box-shadow);--bs-toast-header-color:var(--bs-secondary-color);--bs-toast-header-bg:rgba(var(--bs-body-bg-rgb), 0.85);--bs-toast-header-border-color:var(--bs-border-color-translucent);width:var(--bs-toast-max-width);max-width:100%;font-size:var(--bs-toast-font-size);color:var(--bs-toast-color);pointer-events:auto;background-color:var(--bs-toast-bg);background-clip:padding-box;border:var(--bs-toast-border-width) solid var(--bs-toast-border-color);box-shadow:var(--bs-toast-box-shadow);border-radius:var(--bs-toast-border-radius)}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{--bs-toast-zindex:1090;position:absolute;z-index:var(--bs-toast-zindex);width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:var(--bs-toast-spacing)}.toast-header{display:flex;align-items:center;padding:var(--bs-toast-padding-y) var(--bs-toast-padding-x);color:var(--bs-toast-header-color);background-color:var(--bs-toast-header-bg);background-clip:padding-box;border-bottom:var(--bs-toast-border-width) solid var(--bs-toast-header-border-color);border-top-left-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width));border-top-right-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width))}.toast-header .btn-close{margin-right:calc(-.5 * var(--bs-toast-padding-x));margin-left:var(--bs-toast-padding-x)}.toast-body{padding:var(--bs-toast-padding-x);word-wrap:break-word}.modal{--bs-modal-zindex:1055;--bs-modal-width:500px;--bs-modal-padding:1rem;--bs-modal-margin:0.5rem;--bs-modal-color: ;--bs-modal-bg:var(--bs-body-bg);--bs-modal-border-color:var(--bs-border-color-translucent);--bs-modal-border-width:var(--bs-border-width);--bs-modal-border-radius:var(--bs-border-radius-lg);--bs-modal-box-shadow:var(--bs-box-shadow-sm);--bs-modal-inner-border-radius:calc(var(--bs-border-radius-lg) - (var(--bs-border-width)));--bs-modal-header-padding-x:1rem;--bs-modal-header-padding-y:1rem;--bs-modal-header-padding:1rem 1rem;--bs-modal-header-border-color:var(--bs-border-color);--bs-modal-header-border-width:var(--bs-border-width);--bs-modal-title-line-height:1.5;--bs-modal-footer-gap:0.5rem;--bs-modal-footer-bg: ;--bs-modal-footer-border-color:var(--bs-border-color);--bs-modal-footer-border-width:var(--bs-border-width);position:fixed;top:0;left:0;z-index:var(--bs-modal-zindex);display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:var(--bs-modal-margin);pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - var(--bs-modal-margin) * 2)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - var(--bs-modal-margin) * 2)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;color:var(--bs-modal-color);pointer-events:auto;background-color:var(--bs-modal-bg);background-clip:padding-box;border:var(--bs-modal-border-width) solid var(--bs-modal-border-color);border-radius:var(--bs-modal-border-radius);outline:0}.modal-backdrop{--bs-backdrop-zindex:1050;--bs-backdrop-bg:#000;--bs-backdrop-opacity:0.5;position:fixed;top:0;left:0;z-index:var(--bs-backdrop-zindex);width:100vw;height:100vh;background-color:var(--bs-backdrop-bg)}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:var(--bs-backdrop-opacity)}.modal-header{display:flex;flex-shrink:0;align-items:center;padding:var(--bs-modal-header-padding);border-bottom:var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color);border-top-left-radius:var(--bs-modal-inner-border-radius);border-top-right-radius:var(--bs-modal-inner-border-radius)}.modal-header .btn-close{padding:calc(var(--bs-modal-header-padding-y) * .5) calc(var(--bs-modal-header-padding-x) * .5);margin:calc(-.5 * var(--bs-modal-header-padding-y)) calc(-.5 * var(--bs-modal-header-padding-x)) calc(-.5 * var(--bs-modal-header-padding-y)) auto}.modal-title{margin-bottom:0;line-height:var(--bs-modal-title-line-height)}.modal-body{position:relative;flex:1 1 auto;padding:var(--bs-modal-padding)}.modal-footer{display:flex;flex-shrink:0;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap) * .5);background-color:var(--bs-modal-footer-bg);border-top:var(--bs-modal-footer-border-width) solid var(--bs-modal-footer-border-color);border-bottom-right-radius:var(--bs-modal-inner-border-radius);border-bottom-left-radius:var(--bs-modal-inner-border-radius)}.modal-footer>*{margin:calc(var(--bs-modal-footer-gap) * .5)}@media (min-width:576px){.modal{--bs-modal-margin:1.75rem;--bs-modal-box-shadow:var(--bs-box-shadow)}.modal-dialog{max-width:var(--bs-modal-width);margin-right:auto;margin-left:auto}.modal-sm{--bs-modal-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{--bs-modal-width:800px}}@media (min-width:1200px){.modal-xl{--bs-modal-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-footer,.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-footer,.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-footer,.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-footer,.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-footer,.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-footer,.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}}.tooltip{--bs-tooltip-zindex:1080;--bs-tooltip-max-width:200px;--bs-tooltip-padding-x:0.5rem;--bs-tooltip-padding-y:0.25rem;--bs-tooltip-margin: ;--bs-tooltip-font-size:0.875rem;--bs-tooltip-color:var(--bs-body-bg);--bs-tooltip-bg:var(--bs-emphasis-color);--bs-tooltip-border-radius:var(--bs-border-radius);--bs-tooltip-opacity:0.9;--bs-tooltip-arrow-width:0.8rem;--bs-tooltip-arrow-height:0.4rem;z-index:var(--bs-tooltip-zindex);display:block;margin:var(--bs-tooltip-margin);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-tooltip-font-size);word-wrap:break-word;opacity:0}.tooltip.show{opacity:var(--bs-tooltip-opacity)}.tooltip .tooltip-arrow{display:block;width:var(--bs-tooltip-arrow-width);height:var(--bs-tooltip-arrow-height)}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:calc(-1 * var(--bs-tooltip-arrow-height))}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-top-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:calc(-1 * var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-right-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:calc(-1 * var(--bs-tooltip-arrow-height))}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-bottom-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:calc(-1 * var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) 0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-left-color:var(--bs-tooltip-bg)}.tooltip-inner{max-width:var(--bs-tooltip-max-width);padding:var(--bs-tooltip-padding-y) var(--bs-tooltip-padding-x);color:var(--bs-tooltip-color);text-align:center;background-color:var(--bs-tooltip-bg);border-radius:var(--bs-tooltip-border-radius)}.popover{--bs-popover-zindex:1070;--bs-popover-max-width:276px;--bs-popover-font-size:0.875rem;--bs-popover-bg:var(--bs-body-bg);--bs-popover-border-width:var(--bs-border-width);--bs-popover-border-color:var(--bs-border-color-translucent);--bs-popover-border-radius:var(--bs-border-radius-lg);--bs-popover-inner-border-radius:calc(var(--bs-border-radius-lg) - var(--bs-border-width));--bs-popover-box-shadow:var(--bs-box-shadow);--bs-popover-header-padding-x:1rem;--bs-popover-header-padding-y:0.5rem;--bs-popover-header-font-size:1rem;--bs-popover-header-color:inherit;--bs-popover-header-bg:var(--bs-secondary-bg);--bs-popover-body-padding-x:1rem;--bs-popover-body-padding-y:1rem;--bs-popover-body-color:var(--bs-body-color);--bs-popover-arrow-width:1rem;--bs-popover-arrow-height:0.5rem;--bs-popover-arrow-border:var(--bs-popover-border-color);z-index:var(--bs-popover-zindex);display:block;max-width:var(--bs-popover-max-width);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-popover-font-size);word-wrap:break-word;background-color:var(--bs-popover-bg);background-clip:padding-box;border:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-radius:var(--bs-popover-border-radius)}.popover .popover-arrow{display:block;width:var(--bs-popover-arrow-width);height:var(--bs-popover-arrow-height)}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid;border-width:0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::after,.bs-popover-top>.popover-arrow::before{border-width:var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-top-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:var(--bs-popover-border-width);border-top-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::after,.bs-popover-end>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-right-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:var(--bs-popover-border-width);border-right-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::before{border-width:0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-bottom-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:var(--bs-popover-border-width);border-bottom-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:var(--bs-popover-arrow-width);margin-left:calc(-.5 * var(--bs-popover-arrow-width));content:"";border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-header-bg)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::after,.bs-popover-start>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) 0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-left-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:var(--bs-popover-border-width);border-left-color:var(--bs-popover-bg)}.popover-header{padding:var(--bs-popover-header-padding-y) var(--bs-popover-header-padding-x);margin-bottom:0;font-size:var(--bs-popover-header-font-size);color:var(--bs-popover-header-color);background-color:var(--bs-popover-header-bg);border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-top-left-radius:var(--bs-popover-inner-border-radius);border-top-right-radius:var(--bs-popover-inner-border-radius)}.popover-header:empty{display:none}.popover-body{padding:var(--bs-popover-body-padding-y) var(--bs-popover-body-padding-x);color:var(--bs-popover-body-color)}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}[data-bs-theme=dark] .carousel .carousel-control-next-icon,[data-bs-theme=dark] .carousel .carousel-control-prev-icon,[data-bs-theme=dark].carousel .carousel-control-next-icon,[data-bs-theme=dark].carousel .carousel-control-prev-icon{filter:invert(1) grayscale(100)}[data-bs-theme=dark] .carousel .carousel-indicators [data-bs-target],[data-bs-theme=dark].carousel .carousel-indicators [data-bs-target]{background-color:#000}[data-bs-theme=dark] .carousel .carousel-caption,[data-bs-theme=dark].carousel .carousel-caption{color:#000}.spinner-border,.spinner-grow{display:inline-block;width:var(--bs-spinner-width);height:var(--bs-spinner-height);vertical-align:var(--bs-spinner-vertical-align);border-radius:50%;animation:var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name)}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-border-width:0.25em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-border;border:var(--bs-spinner-border-width) solid currentcolor;border-right-color:transparent}.spinner-border-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem;--bs-spinner-border-width:0.2em}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-grow;background-color:currentcolor;opacity:0}.spinner-grow-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{--bs-spinner-animation-speed:1.5s}}.offcanvas,.offcanvas-lg,.offcanvas-md,.offcanvas-sm,.offcanvas-xl,.offcanvas-xxl{--bs-offcanvas-zindex:1045;--bs-offcanvas-width:400px;--bs-offcanvas-height:30vh;--bs-offcanvas-padding-x:1rem;--bs-offcanvas-padding-y:1rem;--bs-offcanvas-color:var(--bs-body-color);--bs-offcanvas-bg:var(--bs-body-bg);--bs-offcanvas-border-width:var(--bs-border-width);--bs-offcanvas-border-color:var(--bs-border-color-translucent);--bs-offcanvas-box-shadow:var(--bs-box-shadow-sm);--bs-offcanvas-transition:transform 0.3s ease-in-out;--bs-offcanvas-title-line-height:1.5}@media (max-width:575.98px){.offcanvas-sm{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:575.98px) and (prefers-reduced-motion:reduce){.offcanvas-sm{transition:none}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-sm.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-sm.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-sm.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-sm.show:not(.hiding),.offcanvas-sm.showing{transform:none}.offcanvas-sm.hiding,.offcanvas-sm.show,.offcanvas-sm.showing{visibility:visible}}@media (min-width:576px){.offcanvas-sm{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-sm .offcanvas-header{display:none}.offcanvas-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:767.98px){.offcanvas-md{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:767.98px) and (prefers-reduced-motion:reduce){.offcanvas-md{transition:none}}@media (max-width:767.98px){.offcanvas-md.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-md.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-md.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-md.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-md.show:not(.hiding),.offcanvas-md.showing{transform:none}.offcanvas-md.hiding,.offcanvas-md.show,.offcanvas-md.showing{visibility:visible}}@media (min-width:768px){.offcanvas-md{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-md .offcanvas-header{display:none}.offcanvas-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:991.98px){.offcanvas-lg{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:991.98px) and (prefers-reduced-motion:reduce){.offcanvas-lg{transition:none}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-lg.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-lg.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-lg.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-lg.show:not(.hiding),.offcanvas-lg.showing{transform:none}.offcanvas-lg.hiding,.offcanvas-lg.show,.offcanvas-lg.showing{visibility:visible}}@media (min-width:992px){.offcanvas-lg{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-lg .offcanvas-header{display:none}.offcanvas-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1199.98px){.offcanvas-xl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:1199.98px) and (prefers-reduced-motion:reduce){.offcanvas-xl{transition:none}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xl.show:not(.hiding),.offcanvas-xl.showing{transform:none}.offcanvas-xl.hiding,.offcanvas-xl.show,.offcanvas-xl.showing{visibility:visible}}@media (min-width:1200px){.offcanvas-xl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xl .offcanvas-header{display:none}.offcanvas-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1399.98px){.offcanvas-xxl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:1399.98px) and (prefers-reduced-motion:reduce){.offcanvas-xxl{transition:none}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xxl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xxl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xxl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xxl.show:not(.hiding),.offcanvas-xxl.showing{transform:none}.offcanvas-xxl.hiding,.offcanvas-xxl.show,.offcanvas-xxl.showing{visibility:visible}}@media (min-width:1400px){.offcanvas-xxl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xxl .offcanvas-header{display:none}.offcanvas-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}.offcanvas{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas.show:not(.hiding),.offcanvas.showing{transform:none}.offcanvas.hiding,.offcanvas.show,.offcanvas.showing{visibility:visible}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x)}.offcanvas-header .btn-close{padding:calc(var(--bs-offcanvas-padding-y) * .5) calc(var(--bs-offcanvas-padding-x) * .5);margin:calc(-.5 * var(--bs-offcanvas-padding-y)) calc(-.5 * var(--bs-offcanvas-padding-x)) calc(-.5 * var(--bs-offcanvas-padding-y)) auto}.offcanvas-title{margin-bottom:0;line-height:var(--bs-offcanvas-title-line-height)}.offcanvas-body{flex-grow:1;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x);overflow-y:auto}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentcolor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{animation:placeholder-glow 2s ease-in-out infinite}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;animation:placeholder-wave 2s linear infinite}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.text-bg-primary{color:#fff!important;background-color:RGBA(var(--bs-primary-rgb),var(--bs-bg-opacity,1))!important}.text-bg-secondary{color:#fff!important;background-color:RGBA(var(--bs-secondary-rgb),var(--bs-bg-opacity,1))!important}.text-bg-success{color:#fff!important;background-color:RGBA(var(--bs-success-rgb),var(--bs-bg-opacity,1))!important}.text-bg-info{color:#000!important;background-color:RGBA(var(--bs-info-rgb),var(--bs-bg-opacity,1))!important}.text-bg-warning{color:#000!important;background-color:RGBA(var(--bs-warning-rgb),var(--bs-bg-opacity,1))!important}.text-bg-danger{color:#fff!important;background-color:RGBA(var(--bs-danger-rgb),var(--bs-bg-opacity,1))!important}.text-bg-light{color:#000!important;background-color:RGBA(var(--bs-light-rgb),var(--bs-bg-opacity,1))!important}.text-bg-dark{color:#fff!important;background-color:RGBA(var(--bs-dark-rgb),var(--bs-bg-opacity,1))!important}.link-primary{color:RGBA(var(--bs-primary-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-primary-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-primary-rgb),var(--bs-link-underline-opacity,1))!important}.link-primary:focus,.link-primary:hover{color:RGBA(10,88,202,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(10,88,202,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(10,88,202,var(--bs-link-underline-opacity,1))!important}.link-secondary{color:RGBA(var(--bs-secondary-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-secondary-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-secondary-rgb),var(--bs-link-underline-opacity,1))!important}.link-secondary:focus,.link-secondary:hover{color:RGBA(86,94,100,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(86,94,100,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(86,94,100,var(--bs-link-underline-opacity,1))!important}.link-success{color:RGBA(var(--bs-success-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-success-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-success-rgb),var(--bs-link-underline-opacity,1))!important}.link-success:focus,.link-success:hover{color:RGBA(20,108,67,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(20,108,67,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(20,108,67,var(--bs-link-underline-opacity,1))!important}.link-info{color:RGBA(var(--bs-info-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-info-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-info-rgb),var(--bs-link-underline-opacity,1))!important}.link-info:focus,.link-info:hover{color:RGBA(61,213,243,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(61,213,243,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(61,213,243,var(--bs-link-underline-opacity,1))!important}.link-warning{color:RGBA(var(--bs-warning-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-warning-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-warning-rgb),var(--bs-link-underline-opacity,1))!important}.link-warning:focus,.link-warning:hover{color:RGBA(255,205,57,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(255,205,57,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(255,205,57,var(--bs-link-underline-opacity,1))!important}.link-danger{color:RGBA(var(--bs-danger-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-danger-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-danger-rgb),var(--bs-link-underline-opacity,1))!important}.link-danger:focus,.link-danger:hover{color:RGBA(176,42,55,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(176,42,55,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(176,42,55,var(--bs-link-underline-opacity,1))!important}.link-light{color:RGBA(var(--bs-light-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-light-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-light-rgb),var(--bs-link-underline-opacity,1))!important}.link-light:focus,.link-light:hover{color:RGBA(249,250,251,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(249,250,251,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(249,250,251,var(--bs-link-underline-opacity,1))!important}.link-dark{color:RGBA(var(--bs-dark-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-dark-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-dark-rgb),var(--bs-link-underline-opacity,1))!important}.link-dark:focus,.link-dark:hover{color:RGBA(26,30,33,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(26,30,33,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(26,30,33,var(--bs-link-underline-opacity,1))!important}.link-body-emphasis{color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,1))!important}.link-body-emphasis:focus,.link-body-emphasis:hover{color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-opacity,.75))!important;-webkit-text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,0.75))!important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,0.75))!important}.focus-ring:focus{outline:0;box-shadow:var(--bs-focus-ring-x,0) var(--bs-focus-ring-y,0) var(--bs-focus-ring-blur,0) var(--bs-focus-ring-width) var(--bs-focus-ring-color)}.icon-link{display:inline-flex;gap:.375rem;align-items:center;-webkit-text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,0.5));text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,0.5));text-underline-offset:0.25em;-webkit-backface-visibility:hidden;backface-visibility:hidden}.icon-link>.bi{flex-shrink:0;width:1em;height:1em;fill:currentcolor;transition:.2s ease-in-out transform}@media (prefers-reduced-motion:reduce){.icon-link>.bi{transition:none}}.icon-link-hover:focus-visible>.bi,.icon-link-hover:hover>.bi{transform:var(--bs-icon-link-transform,translate3d(.25em,0,0))}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:75%}.ratio-16x9{--bs-aspect-ratio:56.25%}.ratio-21x9{--bs-aspect-ratio:42.8571428571%}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-sm-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-md-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-lg-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xxl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.visually-hidden-focusable:not(:focus):not(:focus-within):not(caption),.visually-hidden:not(caption){position:absolute!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:var(--bs-border-width);min-height:1em;background-color:currentcolor;opacity:.25}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.object-fit-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-none{-o-object-fit:none!important;object-fit:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.overflow-x-auto{overflow-x:auto!important}.overflow-x-hidden{overflow-x:hidden!important}.overflow-x-visible{overflow-x:visible!important}.overflow-x-scroll{overflow-x:scroll!important}.overflow-y-auto{overflow-y:auto!important}.overflow-y-hidden{overflow-y:hidden!important}.overflow-y-visible{overflow-y:visible!important}.overflow-y-scroll{overflow-y:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-inline-grid{display:inline-grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:var(--bs-box-shadow)!important}.shadow-sm{box-shadow:var(--bs-box-shadow-sm)!important}.shadow-lg{box-shadow:var(--bs-box-shadow-lg)!important}.shadow-none{box-shadow:none!important}.focus-ring-primary{--bs-focus-ring-color:rgba(var(--bs-primary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-secondary{--bs-focus-ring-color:rgba(var(--bs-secondary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-success{--bs-focus-ring-color:rgba(var(--bs-success-rgb), var(--bs-focus-ring-opacity))}.focus-ring-info{--bs-focus-ring-color:rgba(var(--bs-info-rgb), var(--bs-focus-ring-opacity))}.focus-ring-warning{--bs-focus-ring-color:rgba(var(--bs-warning-rgb), var(--bs-focus-ring-opacity))}.focus-ring-danger{--bs-focus-ring-color:rgba(var(--bs-danger-rgb), var(--bs-focus-ring-opacity))}.focus-ring-light{--bs-focus-ring-color:rgba(var(--bs-light-rgb), var(--bs-focus-ring-opacity))}.focus-ring-dark{--bs-focus-ring-color:rgba(var(--bs-dark-rgb), var(--bs-focus-ring-opacity))}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-0{border:0!important}.border-top{border-top:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-top-0{border-top:0!important}.border-end{border-right:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-start-0{border-left:0!important}.border-primary{--bs-border-opacity:1;border-color:rgba(var(--bs-primary-rgb),var(--bs-border-opacity))!important}.border-secondary{--bs-border-opacity:1;border-color:rgba(var(--bs-secondary-rgb),var(--bs-border-opacity))!important}.border-success{--bs-border-opacity:1;border-color:rgba(var(--bs-success-rgb),var(--bs-border-opacity))!important}.border-info{--bs-border-opacity:1;border-color:rgba(var(--bs-info-rgb),var(--bs-border-opacity))!important}.border-warning{--bs-border-opacity:1;border-color:rgba(var(--bs-warning-rgb),var(--bs-border-opacity))!important}.border-danger{--bs-border-opacity:1;border-color:rgba(var(--bs-danger-rgb),var(--bs-border-opacity))!important}.border-light{--bs-border-opacity:1;border-color:rgba(var(--bs-light-rgb),var(--bs-border-opacity))!important}.border-dark{--bs-border-opacity:1;border-color:rgba(var(--bs-dark-rgb),var(--bs-border-opacity))!important}.border-black{--bs-border-opacity:1;border-color:rgba(var(--bs-black-rgb),var(--bs-border-opacity))!important}.border-white{--bs-border-opacity:1;border-color:rgba(var(--bs-white-rgb),var(--bs-border-opacity))!important}.border-primary-subtle{border-color:var(--bs-primary-border-subtle)!important}.border-secondary-subtle{border-color:var(--bs-secondary-border-subtle)!important}.border-success-subtle{border-color:var(--bs-success-border-subtle)!important}.border-info-subtle{border-color:var(--bs-info-border-subtle)!important}.border-warning-subtle{border-color:var(--bs-warning-border-subtle)!important}.border-danger-subtle{border-color:var(--bs-danger-border-subtle)!important}.border-light-subtle{border-color:var(--bs-light-border-subtle)!important}.border-dark-subtle{border-color:var(--bs-dark-border-subtle)!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.border-opacity-10{--bs-border-opacity:0.1}.border-opacity-25{--bs-border-opacity:0.25}.border-opacity-50{--bs-border-opacity:0.5}.border-opacity-75{--bs-border-opacity:0.75}.border-opacity-100{--bs-border-opacity:1}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.row-gap-0{row-gap:0!important}.row-gap-1{row-gap:.25rem!important}.row-gap-2{row-gap:.5rem!important}.row-gap-3{row-gap:1rem!important}.row-gap-4{row-gap:1.5rem!important}.row-gap-5{row-gap:3rem!important}.column-gap-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-lighter{font-weight:lighter!important}.fw-light{font-weight:300!important}.fw-normal{font-weight:400!important}.fw-medium{font-weight:500!important}.fw-semibold{font-weight:600!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-color-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:var(--bs-secondary-color)!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-body-secondary{--bs-text-opacity:1;color:var(--bs-secondary-color)!important}.text-body-tertiary{--bs-text-opacity:1;color:var(--bs-tertiary-color)!important}.text-body-emphasis{--bs-text-opacity:1;color:var(--bs-emphasis-color)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.text-primary-emphasis{color:var(--bs-primary-text-emphasis)!important}.text-secondary-emphasis{color:var(--bs-secondary-text-emphasis)!important}.text-success-emphasis{color:var(--bs-success-text-emphasis)!important}.text-info-emphasis{color:var(--bs-info-text-emphasis)!important}.text-warning-emphasis{color:var(--bs-warning-text-emphasis)!important}.text-danger-emphasis{color:var(--bs-danger-text-emphasis)!important}.text-light-emphasis{color:var(--bs-light-text-emphasis)!important}.text-dark-emphasis{color:var(--bs-dark-text-emphasis)!important}.link-opacity-10{--bs-link-opacity:0.1}.link-opacity-10-hover:hover{--bs-link-opacity:0.1}.link-opacity-25{--bs-link-opacity:0.25}.link-opacity-25-hover:hover{--bs-link-opacity:0.25}.link-opacity-50{--bs-link-opacity:0.5}.link-opacity-50-hover:hover{--bs-link-opacity:0.5}.link-opacity-75{--bs-link-opacity:0.75}.link-opacity-75-hover:hover{--bs-link-opacity:0.75}.link-opacity-100{--bs-link-opacity:1}.link-opacity-100-hover:hover{--bs-link-opacity:1}.link-offset-1{text-underline-offset:0.125em!important}.link-offset-1-hover:hover{text-underline-offset:0.125em!important}.link-offset-2{text-underline-offset:0.25em!important}.link-offset-2-hover:hover{text-underline-offset:0.25em!important}.link-offset-3{text-underline-offset:0.375em!important}.link-offset-3-hover:hover{text-underline-offset:0.375em!important}.link-underline-primary{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-primary-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-primary-rgb),var(--bs-link-underline-opacity))!important}.link-underline-secondary{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-secondary-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-secondary-rgb),var(--bs-link-underline-opacity))!important}.link-underline-success{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-success-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-success-rgb),var(--bs-link-underline-opacity))!important}.link-underline-info{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-info-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-info-rgb),var(--bs-link-underline-opacity))!important}.link-underline-warning{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-warning-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-warning-rgb),var(--bs-link-underline-opacity))!important}.link-underline-danger{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-danger-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-danger-rgb),var(--bs-link-underline-opacity))!important}.link-underline-light{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-light-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-light-rgb),var(--bs-link-underline-opacity))!important}.link-underline-dark{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-dark-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-dark-rgb),var(--bs-link-underline-opacity))!important}.link-underline{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-underline-opacity,1))!important}.link-underline-opacity-0{--bs-link-underline-opacity:0}.link-underline-opacity-0-hover:hover{--bs-link-underline-opacity:0}.link-underline-opacity-10{--bs-link-underline-opacity:0.1}.link-underline-opacity-10-hover:hover{--bs-link-underline-opacity:0.1}.link-underline-opacity-25{--bs-link-underline-opacity:0.25}.link-underline-opacity-25-hover:hover{--bs-link-underline-opacity:0.25}.link-underline-opacity-50{--bs-link-underline-opacity:0.5}.link-underline-opacity-50-hover:hover{--bs-link-underline-opacity:0.5}.link-underline-opacity-75{--bs-link-underline-opacity:0.75}.link-underline-opacity-75-hover:hover{--bs-link-underline-opacity:0.75}.link-underline-opacity-100{--bs-link-underline-opacity:1}.link-underline-opacity-100-hover:hover{--bs-link-underline-opacity:1}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-bg-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important}.bg-body-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-bg-rgb),var(--bs-bg-opacity))!important}.bg-body-tertiary{--bs-bg-opacity:1;background-color:rgba(var(--bs-tertiary-bg-rgb),var(--bs-bg-opacity))!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-primary-subtle{background-color:var(--bs-primary-bg-subtle)!important}.bg-secondary-subtle{background-color:var(--bs-secondary-bg-subtle)!important}.bg-success-subtle{background-color:var(--bs-success-bg-subtle)!important}.bg-info-subtle{background-color:var(--bs-info-bg-subtle)!important}.bg-warning-subtle{background-color:var(--bs-warning-bg-subtle)!important}.bg-danger-subtle{background-color:var(--bs-danger-bg-subtle)!important}.bg-light-subtle{background-color:var(--bs-light-bg-subtle)!important}.bg-dark-subtle{background-color:var(--bs-dark-bg-subtle)!important}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:var(--bs-border-radius)!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:var(--bs-border-radius-sm)!important}.rounded-2{border-radius:var(--bs-border-radius)!important}.rounded-3{border-radius:var(--bs-border-radius-lg)!important}.rounded-4{border-radius:var(--bs-border-radius-xl)!important}.rounded-5{border-radius:var(--bs-border-radius-xxl)!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:var(--bs-border-radius-pill)!important}.rounded-top{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-top-0{border-top-left-radius:0!important;border-top-right-radius:0!important}.rounded-top-1{border-top-left-radius:var(--bs-border-radius-sm)!important;border-top-right-radius:var(--bs-border-radius-sm)!important}.rounded-top-2{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-top-3{border-top-left-radius:var(--bs-border-radius-lg)!important;border-top-right-radius:var(--bs-border-radius-lg)!important}.rounded-top-4{border-top-left-radius:var(--bs-border-radius-xl)!important;border-top-right-radius:var(--bs-border-radius-xl)!important}.rounded-top-5{border-top-left-radius:var(--bs-border-radius-xxl)!important;border-top-right-radius:var(--bs-border-radius-xxl)!important}.rounded-top-circle{border-top-left-radius:50%!important;border-top-right-radius:50%!important}.rounded-top-pill{border-top-left-radius:var(--bs-border-radius-pill)!important;border-top-right-radius:var(--bs-border-radius-pill)!important}.rounded-end{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-end-0{border-top-right-radius:0!important;border-bottom-right-radius:0!important}.rounded-end-1{border-top-right-radius:var(--bs-border-radius-sm)!important;border-bottom-right-radius:var(--bs-border-radius-sm)!important}.rounded-end-2{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-end-3{border-top-right-radius:var(--bs-border-radius-lg)!important;border-bottom-right-radius:var(--bs-border-radius-lg)!important}.rounded-end-4{border-top-right-radius:var(--bs-border-radius-xl)!important;border-bottom-right-radius:var(--bs-border-radius-xl)!important}.rounded-end-5{border-top-right-radius:var(--bs-border-radius-xxl)!important;border-bottom-right-radius:var(--bs-border-radius-xxl)!important}.rounded-end-circle{border-top-right-radius:50%!important;border-bottom-right-radius:50%!important}.rounded-end-pill{border-top-right-radius:var(--bs-border-radius-pill)!important;border-bottom-right-radius:var(--bs-border-radius-pill)!important}.rounded-bottom{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-bottom-0{border-bottom-right-radius:0!important;border-bottom-left-radius:0!important}.rounded-bottom-1{border-bottom-right-radius:var(--bs-border-radius-sm)!important;border-bottom-left-radius:var(--bs-border-radius-sm)!important}.rounded-bottom-2{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-bottom-3{border-bottom-right-radius:var(--bs-border-radius-lg)!important;border-bottom-left-radius:var(--bs-border-radius-lg)!important}.rounded-bottom-4{border-bottom-right-radius:var(--bs-border-radius-xl)!important;border-bottom-left-radius:var(--bs-border-radius-xl)!important}.rounded-bottom-5{border-bottom-right-radius:var(--bs-border-radius-xxl)!important;border-bottom-left-radius:var(--bs-border-radius-xxl)!important}.rounded-bottom-circle{border-bottom-right-radius:50%!important;border-bottom-left-radius:50%!important}.rounded-bottom-pill{border-bottom-right-radius:var(--bs-border-radius-pill)!important;border-bottom-left-radius:var(--bs-border-radius-pill)!important}.rounded-start{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.rounded-start-0{border-bottom-left-radius:0!important;border-top-left-radius:0!important}.rounded-start-1{border-bottom-left-radius:var(--bs-border-radius-sm)!important;border-top-left-radius:var(--bs-border-radius-sm)!important}.rounded-start-2{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.rounded-start-3{border-bottom-left-radius:var(--bs-border-radius-lg)!important;border-top-left-radius:var(--bs-border-radius-lg)!important}.rounded-start-4{border-bottom-left-radius:var(--bs-border-radius-xl)!important;border-top-left-radius:var(--bs-border-radius-xl)!important}.rounded-start-5{border-bottom-left-radius:var(--bs-border-radius-xxl)!important;border-top-left-radius:var(--bs-border-radius-xxl)!important}.rounded-start-circle{border-bottom-left-radius:50%!important;border-top-left-radius:50%!important}.rounded-start-pill{border-bottom-left-radius:var(--bs-border-radius-pill)!important;border-top-left-radius:var(--bs-border-radius-pill)!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}.z-n1{z-index:-1!important}.z-0{z-index:0!important}.z-1{z-index:1!important}.z-2{z-index:2!important}.z-3{z-index:3!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.object-fit-sm-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-sm-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-sm-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-sm-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-sm-none{-o-object-fit:none!important;object-fit:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-inline-grid{display:inline-grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.row-gap-sm-0{row-gap:0!important}.row-gap-sm-1{row-gap:.25rem!important}.row-gap-sm-2{row-gap:.5rem!important}.row-gap-sm-3{row-gap:1rem!important}.row-gap-sm-4{row-gap:1.5rem!important}.row-gap-sm-5{row-gap:3rem!important}.column-gap-sm-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-sm-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-sm-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-sm-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-sm-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-sm-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.object-fit-md-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-md-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-md-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-md-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-md-none{-o-object-fit:none!important;object-fit:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-inline-grid{display:inline-grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.row-gap-md-0{row-gap:0!important}.row-gap-md-1{row-gap:.25rem!important}.row-gap-md-2{row-gap:.5rem!important}.row-gap-md-3{row-gap:1rem!important}.row-gap-md-4{row-gap:1.5rem!important}.row-gap-md-5{row-gap:3rem!important}.column-gap-md-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-md-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-md-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-md-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-md-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-md-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.object-fit-lg-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-lg-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-lg-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-lg-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-lg-none{-o-object-fit:none!important;object-fit:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-inline-grid{display:inline-grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.row-gap-lg-0{row-gap:0!important}.row-gap-lg-1{row-gap:.25rem!important}.row-gap-lg-2{row-gap:.5rem!important}.row-gap-lg-3{row-gap:1rem!important}.row-gap-lg-4{row-gap:1.5rem!important}.row-gap-lg-5{row-gap:3rem!important}.column-gap-lg-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-lg-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-lg-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-lg-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-lg-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-lg-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.object-fit-xl-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-xl-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-xl-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-xl-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-xl-none{-o-object-fit:none!important;object-fit:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-inline-grid{display:inline-grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.row-gap-xl-0{row-gap:0!important}.row-gap-xl-1{row-gap:.25rem!important}.row-gap-xl-2{row-gap:.5rem!important}.row-gap-xl-3{row-gap:1rem!important}.row-gap-xl-4{row-gap:1.5rem!important}.row-gap-xl-5{row-gap:3rem!important}.column-gap-xl-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-xl-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-xl-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-xl-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-xl-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-xl-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.object-fit-xxl-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-xxl-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-xxl-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-xxl-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-xxl-none{-o-object-fit:none!important;object-fit:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-inline-grid{display:inline-grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.row-gap-xxl-0{row-gap:0!important}.row-gap-xxl-1{row-gap:.25rem!important}.row-gap-xxl-2{row-gap:.5rem!important}.row-gap-xxl-3{row-gap:1rem!important}.row-gap-xxl-4{row-gap:1.5rem!important}.row-gap-xxl-5{row-gap:3rem!important}.column-gap-xxl-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-xxl-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-xxl-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-xxl-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-xxl-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-xxl-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-inline-grid{display:inline-grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/public/assets/css/style.css b/public/assets/css/style.css new file mode 100644 index 0000000..cf82aba --- /dev/null +++ b/public/assets/css/style.css @@ -0,0 +1,1809 @@ +/* varlog — style.css */ + +/* Inter — auto-hébergée */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 100 900; + font-display: swap; + src: url('/assets/fonts/inter-normal-latin-ext.woff2') format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, + U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, + U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 100 900; + font-display: swap; + src: url('/assets/fonts/inter-normal-latin.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, + U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, + U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: 'Inter'; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url('/assets/fonts/inter-italic-latin-ext.woff2') format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, + U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, + U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: 'Inter'; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url('/assets/fonts/inter-italic-latin.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, + U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, + U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +/* ─── Variables ─────────────────────────── */ +:root { + /* Override Bootstrap link defaults */ + --bs-link-color: #4f46e5; + --bs-link-color-rgb: 79, 70, 229; + --bs-link-hover-color: #4338ca; + --bs-link-decoration: none; + --vl-accent: #4f46e5; + --vl-accent-dark: #4338ca; + --vl-accent-soft: #e0e7ff; + --vl-bg: #f8fafc; + --vl-surface: #ffffff; + --vl-border: #e2e8f0; + --vl-text: #1e293b; + --vl-muted: #64748b; + --vl-radius: 0.75rem; + --vl-shadow-sm: 0 1px 3px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.04); + --vl-shadow-md: 0 4px 16px rgba(0,0,0,.08), 0 2px 4px rgba(0,0,0,.04); +} + +/* ─── Base ──────────────────────────────── */ +body { + background-color: var(--vl-bg) !important; + color: var(--vl-text); + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + font-size: 1rem; + line-height: 1.7; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* ─── Navbar ────────────────────────────── */ +.navbar { + background: linear-gradient(135deg, #312e81 0%, #4f46e5 55%, #7c3aed 100%) !important; + border-bottom: none; + box-shadow: 0 2px 24px rgba(79,70,229,.35); + padding: 0.65rem 1.5rem; +} + +.navbar-brand { + font-weight: 800; + font-size: 1.25rem; + color: #fff !important; + letter-spacing: -0.5px; + transition: opacity 0.15s; + text-decoration: none !important; +} + +.navbar-brand:hover { + opacity: 0.85; +} + +/* Caret du dropdown sur fond coloré */ +.navbar-brand.dropdown-toggle::after { + border-top-color: rgba(255,255,255,.7); + vertical-align: .2em; +} + +.navbar .nav-link { + color: rgba(255,255,255,.85) !important; + font-weight: 500; + font-size: 0.9rem; + padding: 0.4rem 0.75rem !important; + border-radius: 0.5rem; + transition: color 0.15s, background 0.15s; +} + +.navbar .nav-link:hover, +.navbar .nav-link:focus { + color: #fff !important; + background-color: rgba(255,255,255,.15); +} + +.navbar-toggler { + border-color: rgba(255,255,255,.35); +} + +.navbar-toggler-icon { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255%2C255%2C255%2C0.85%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); +} + +/* Dropdown catégories */ +.navbar .dropdown-menu { + border: none; + border-radius: 0.75rem; + box-shadow: 0 8px 32px rgba(0,0,0,.18); + padding: 0.5rem; + min-width: 180px; +} + +.navbar .dropdown-item { + border-radius: 0.5rem; + font-size: 0.9rem; + font-weight: 500; + color: var(--vl-text); + padding: 0.4rem 0.75rem; + transition: background 0.12s, color 0.12s; +} + +.navbar .dropdown-item:hover { + background-color: var(--vl-accent-soft); + color: var(--vl-accent); +} + +.navbar .dropdown-item.active { + background-color: var(--vl-accent); + color: #fff; +} + +/* ─── Category nav in navbar ─────────────── */ + +/* ─── Main ───────────────────────────────── */ +main.container, +main.container-xl, +main.container-fluid { + padding-top: 2rem; + padding-bottom: 3rem; +} + +main.container { + max-width: 980px; +} + +/* ─── Headings ───────────────────────────── */ +h1 { + font-weight: 700; + font-size: 1.75rem; + letter-spacing: -0.5px; + color: var(--vl-text); +} + +h2 { + font-weight: 600; + letter-spacing: -0.3px; +} + +/* ─── Liens globaux ─────────────────────── */ +a { + color: var(--vl-accent); + text-decoration: none !important; +} + +a:hover { + color: var(--vl-accent-dark); +} + +/* ─── Cards ─────────────────────────────── */ + +.card-cover { + height: 120px; + border-radius: var(--vl-radius) var(--vl-radius) 0 0; + flex-shrink: 0; + background-size: cover; + background-position: center; +} + +/* Couverture pleine largeur dans la vue article */ +.article-cover { + margin: 0; + overflow: hidden; + border-radius: var(--vl-radius) var(--vl-radius) 0 0; + flex-shrink: 0; +} + +.article-cover img { + width: 100%; + height: 340px; + object-fit: cover; + display: block; +} + +.article-cover--gradient { + height: 160px; + position: relative; +} + +/* Hero : titre sur l'image */ +.article-cover--hero { + position: relative; +} + +.article-cover--hero img { + height: clamp(260px, 45vw, 480px); +} + +.article-cover--hero.article-cover--gradient { + height: clamp(200px, 35vw, 340px); +} + +.article-hero-text { + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 0.9rem 1.4rem 1.25rem; + background: linear-gradient( + to top, + rgba(0,0,0,.80) 0%, + rgba(0,0,0,.32) 50%, + transparent 100% + ); +} + +.article-hero-top { + display: flex; + justify-content: flex-end; + gap: 0.4rem; + align-items: flex-start; + min-height: 2rem; +} + +.article-hero-bottom { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 1rem; +} + +.article-hero-left { flex: 1; min-width: 0; } + +.article-hero-right { + flex-shrink: 0; + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 0.5rem; +} + +.article-hero-text .cover-category { + position: static; + align-self: flex-start; + margin-bottom: 0.55rem; +} + +.article-hero-text .article-title { + color: #fff; + text-shadow: 0 1px 8px rgba(0,0,0,.5); + margin-bottom: 0.3rem; +} + +.article-hero-meta { + font-size: 0.82rem; + color: rgba(255,255,255,.72); + margin: 0; +} + +/* Boutons glass sur le hero */ +.hero-btn { + display: inline-flex; + align-items: center; + gap: 0.25rem; + padding: 0.28rem 0.75rem; + font-size: 0.73rem; + font-weight: 600; + color: #fff !important; + background: rgba(0,0,0,.35); + border: 1px solid rgba(255,255,255,.22); + border-radius: 999px; + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + text-decoration: none !important; + cursor: pointer; + transition: background 0.15s; + white-space: nowrap; + line-height: 1.4; +} +.hero-btn:hover { background: rgba(0,0,0,.58); color: #fff !important; } +.hero-btn--danger { border-color: rgba(239,68,68,.5); } +.hero-btn--danger:hover { background: rgba(180,30,30,.65); } + +/* Score de notation dans le hero */ +.hero-reactions { + display: flex; + gap: .4rem; + flex-wrap: wrap; + justify-content: flex-end; +} + +.hero-reaction-btn { + display: inline-flex; + align-items: center; + gap: .3rem; + background: rgba(255,255,255,.15); + border: 1px solid rgba(255,255,255,.35); + border-radius: 100px; + padding: .25rem .7rem; + color: #fff; + font-size: .875rem; + cursor: pointer; + transition: background .15s, border-color .15s; +} + +.hero-reaction-btn:hover, +.hero-reaction-btn--active { + background: rgba(255,255,255,.32); + border-color: rgba(255,255,255,.7); +} + +.card-cover { + position: relative; +} + +.cover-category { + position: absolute; + bottom: 0.6rem; + left: 0.75rem; + font-size: 0.65rem; + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; + color: #fff; + background: rgba(0, 0, 0, 0.28); + padding: 0.2rem 0.55rem; + border-radius: 999px; + backdrop-filter: blur(6px); + -webkit-backdrop-filter: blur(6px); +} + +.card { + border: 1px solid var(--vl-border) !important; + border-radius: var(--vl-radius) !important; + box-shadow: var(--vl-shadow-sm); + background-color: var(--vl-surface); + overflow: hidden; + transition: box-shadow 0.2s ease, transform 0.2s ease; +} + +.card:hover { + box-shadow: var(--vl-shadow-md); + transform: translateY(-2px); +} + +.card-body { + padding: 1.35rem 1.5rem; +} + +.card-title { + font-weight: 600; + font-size: 1rem; + margin-bottom: 0.5rem; + color: var(--vl-text); +} + +.article-title { + font-weight: 800; + font-size: clamp(1.6rem, 4vw, 2.4rem); + line-height: 1.2; + letter-spacing: -0.5px; + color: var(--vl-text); + margin-bottom: 0.5rem; +} + +/* Override la couleur text-primary Bootstrap sur les card-title */ +.card-title.text-primary { + color: var(--vl-text) !important; +} + +.card-title a, +.card-title a:visited { + color: var(--vl-text) !important; + transition: color 0.15s; +} + +.card-title a:hover { + color: var(--vl-accent) !important; +} + +.card-text { + color: var(--vl-muted); + font-size: 0.9rem; + line-height: 1.65; +} + +/* Neutralise les borders colorées Bootstrap */ +.card[class*="border-primary"], +.card[class*="border-warning"], +.card[class*="border-success"], +.card[class*="border-danger"] { + border-color: var(--vl-border) !important; +} + +/* ─── Ribbons (brouillon / avant-première) ── */ +.draft-ribbon, +.premiere-ribbon, +.private-ribbon { + position: absolute; + top: 26px; + right: -34px; + width: 130px; + text-align: center; + color: #fff; + font-size: 0.6rem; + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; + padding: 5px 0; + transform: rotate(45deg); + pointer-events: none; + z-index: 2; + box-shadow: 0 1px 3px rgba(0,0,0,.18); +} + +.draft-ribbon { background: #f59e0b; } +.premiere-ribbon { background: #6366f1; } +.private-ribbon { background: #64748b; } + +/* ─── Buttons ─────────────────────────────── */ +.btn { + font-weight: 500; + font-size: 0.875rem; + border-radius: 0.5rem; + transition: all 0.15s ease; +} + +.btn-outline-primary { + --bs-btn-color: var(--vl-accent); + --bs-btn-border-color: var(--vl-accent); + --bs-btn-hover-bg: var(--vl-accent); + --bs-btn-hover-border-color: var(--vl-accent); + --bs-btn-active-bg: var(--vl-accent-dark); +} + +.btn-outline-secondary { + --bs-btn-color: var(--vl-muted); + --bs-btn-border-color: var(--vl-border); + --bs-btn-hover-bg: var(--vl-bg); + --bs-btn-hover-border-color: var(--vl-border); + --bs-btn-hover-color: var(--vl-text); +} + +.btn-success { + background-color: var(--vl-accent) !important; + border-color: var(--vl-accent) !important; +} + +.btn-success:hover, +.btn-success:focus { + background-color: var(--vl-accent-dark) !important; + border-color: var(--vl-accent-dark) !important; +} + +.btn-secondary { + background-color: #f1f5f9 !important; + border-color: var(--vl-border) !important; + color: var(--vl-text) !important; +} + +.btn-secondary:hover { + background-color: #e2e8f0 !important; + border-color: var(--vl-border) !important; +} + +.btn-danger { + --bs-btn-bg: #ef4444; + --bs-btn-border-color: #ef4444; + --bs-btn-hover-bg: #dc2626; + --bs-btn-hover-border-color: #dc2626; +} + +/* ─── Forms ──────────────────────────────── */ +.form-control, +.form-select { + border-color: var(--vl-border); + border-radius: 0.5rem; + font-size: 0.9375rem; + padding: 0.5rem 0.75rem; + transition: border-color 0.15s, box-shadow 0.15s; + background-color: var(--vl-surface); +} + +.form-control:focus, +.form-select:focus { + border-color: var(--vl-accent); + box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.12); +} + +.form-label { + font-weight: 500; + font-size: 0.875rem; + color: var(--vl-text); + margin-bottom: 0.375rem; +} + +.form-check-input:checked { + background-color: var(--vl-accent); + border-color: var(--vl-accent); +} + +textarea.form-control { + font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; + font-size: 0.875rem; + line-height: 1.6; + min-height: 320px; + resize: vertical; +} + +/* ─── Alerts ─────────────────────────────── */ +.alert { + border-radius: var(--vl-radius); + border: none; + font-size: 0.9rem; +} + +.alert-danger { + background-color: #fef2f2; + color: #991b1b; +} + +/* ─── Left category sidebar ──────────────── */ +.left-sidebar { + position: sticky; + top: 1.5rem; +} + +.left-sidebar-section { + margin-bottom: 1.5rem; +} + +.left-sidebar-cat { + display: block; + font-size: 0.7rem; + font-weight: 700; + letter-spacing: .08em; + text-transform: uppercase; + color: var(--vl-muted); + text-decoration: none; + margin-bottom: 0.4rem; + padding: 0.2rem 0.5rem; + border-radius: 0.4rem; + transition: background 0.15s, color 0.15s; +} + +.left-sidebar-cat:hover { + background: var(--vl-accent-soft); + color: var(--vl-accent); +} + +.left-sidebar-list { + list-style: none; + padding: 0; + margin: 0; +} + +.left-sidebar-list li { + border-left: 2px solid var(--vl-border); + padding-left: 0.6rem; + margin-bottom: 0.3rem; +} + +.left-sidebar-list li a { + font-size: 0.8rem; + line-height: 1.35; + color: var(--vl-text); + text-decoration: none; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + transition: color 0.15s; +} + +.left-sidebar-list li a:hover { + color: var(--vl-accent); +} + +/* ─── Related articles sidebar ───────────── */ +.related-sidebar { + position: sticky; + top: 1.5rem; + max-height: calc(100vh - 3rem); + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: var(--vl-border) transparent; +} + +/* ─── Post layout: colonnes sidebar fixe + article flexible ── */ +@media (min-width: 992px) { + .post-sidebar-col { + flex: 0 0 260px; + width: 260px; + max-width: 260px; + align-self: stretch; + } +} + +.related-sidebar-title { + font-size: 0.7rem; + font-weight: 700; + letter-spacing: .08em; + text-transform: uppercase; + color: var(--vl-muted); + margin-bottom: 1rem; +} + +.toc-list { + list-style: none; + padding: 0; + margin: 0 0 1.5rem; + display: flex; + flex-direction: column; + gap: .1rem; +} + +.toc-list a { + display: block; + font-size: .8rem; + line-height: 1.45; + color: var(--vl-muted); + padding: .25rem .5rem; + border-left: 2px solid transparent; + transition: color .15s, border-color .15s; +} + +.toc-list a:hover, +.toc-list a.toc-active { + color: var(--vl-accent); + border-left-color: var(--vl-accent); +} + +.toc-list a.toc-active { + font-weight: 600; +} + +.toc-h3 a { + padding-left: 1.25rem; + font-size: .775rem; +} + +.toc-nav { + display: flex; + gap: .5rem; + padding-top: .75rem; + border-top: 1px solid var(--vl-border); + margin-top: .25rem; +} + +.toc-nav-btn { + flex: 1; + background: none; + border: 1px solid var(--vl-border); + border-radius: .5rem; + padding: .3rem .4rem; + font-size: .75rem; + color: var(--vl-muted); + cursor: pointer; + transition: color .15s, border-color .15s; +} + +.toc-nav-btn:hover { + color: var(--vl-accent); + border-color: var(--vl-accent); +} + +.also-read-title { + font-size: .7rem; + font-weight: 700; + letter-spacing: .08em; + text-transform: uppercase; + color: var(--vl-muted); + margin-bottom: .75rem; +} + +.also-read-grid { + display: flex; + flex-wrap: wrap; + gap: .75rem; +} + +.also-read-grid .related-card { + flex: 1 1 200px; + max-width: 280px; + background: var(--vl-surface); + border-radius: var(--vl-radius); + padding: .65rem; + box-shadow: var(--vl-shadow-sm); + transition: box-shadow .15s, transform .15s; +} + +.also-read-grid .related-card:hover { + box-shadow: var(--vl-shadow-md); + transform: translateY(-1px); +} + +.related-card { + display: flex; + gap: 0.75rem; + align-items: flex-start; + text-decoration: none; + color: var(--vl-text); + padding: 0.65rem; + border-radius: var(--vl-radius); + transition: background 0.15s; + margin-bottom: 0.25rem; +} + +.related-card:hover { + background: var(--vl-accent-soft); + color: var(--vl-accent); +} + +.related-card-thumb { + width: 64px; + height: 52px; + border-radius: 0.5rem; + flex-shrink: 0; + background-size: cover; + background-position: center; +} + +.related-card-body { + flex: 1; + min-width: 0; +} + +.related-card-title { + font-size: 0.875rem; + font-weight: 600; + line-height: 1.35; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.related-card-date { + font-size: 0.75rem; + color: var(--vl-muted); + margin-top: 0.2rem; +} + +/* ─── Source / attachment cards (sidebar) ── */ +.source-card { + display: flex; + gap: 0.65rem; + align-items: flex-start; + text-decoration: none; + color: var(--vl-text); + padding: 0.5rem 0.65rem; + border-radius: var(--vl-radius); + border: 1px solid var(--vl-border); + background: var(--vl-surface); + transition: border-color 0.15s, box-shadow 0.15s; +} + +.source-card:hover { + border-color: var(--vl-accent); + box-shadow: 0 0 0 3px rgba(79,70,229,.08); + color: var(--vl-accent); +} + +.source-card-thumb { + width: 52px; + height: 42px; + border-radius: 0.4rem; + flex-shrink: 0; + background-color: var(--vl-accent-soft); +} + +.source-card-thumb--pdf, +.source-card-thumb--link { + display: flex; + align-items: center; + justify-content: center; + font-size: 1.2rem; + color: var(--vl-accent); +} + +.source-card-body { + flex: 1; + min-width: 0; +} + +.source-card-title { + font-size: 0.82rem; + font-weight: 600; + line-height: 1.3; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.source-card-meta { + font-size: 0.72rem; + color: var(--vl-muted); + margin-top: 0.2rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* ─── Post content (Markdown rendu) ──────── */ +.post-content { + font-size: 1rem; + line-height: 1.8; + color: var(--vl-text); +} + +.post-content h1, +.post-content h2, +.post-content h3, +.post-content h4, +.post-content h5, +.post-content h6 { + font-weight: 600; + margin-top: 2rem; + margin-bottom: 0.75rem; + letter-spacing: -0.3px; + line-height: 1.3; +} + +.post-content p { + margin-bottom: 1.25rem; +} + +.post-content a { + color: var(--vl-accent); + text-decoration: underline; + text-decoration-color: var(--vl-accent-soft); + text-underline-offset: 3px; + transition: text-decoration-color 0.15s; +} + +.post-content a:hover { + text-decoration-color: var(--vl-accent); +} + +.post-content code { + background: #f1f5f9; + padding: 0.2em 0.45em; + border-radius: 4px; + font-size: 0.85em; + color: #be185d; + font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; +} + +.post-content pre { + background: #1e293b; + color: #e2e8f0; + padding: 1.25rem 1.5rem; + border-radius: var(--vl-radius); + overflow-x: auto; + font-size: 0.875rem; + line-height: 1.6; + margin: 1.5rem 0; +} + +.post-content pre code { + background: none; + color: inherit; + padding: 0; + font-size: inherit; +} + +.post-content blockquote { + border-left: 3px solid var(--vl-accent); + padding: 0.5rem 0 0.5rem 1.25rem; + margin: 1.5rem 0; + color: var(--vl-muted); + font-style: italic; +} + +.post-content ul, +.post-content ol { + padding-left: 1.5rem; + margin-bottom: 1.25rem; +} + +.post-content li { + margin-bottom: 0.375rem; +} + +.post-content img { + max-width: 100%; + border-radius: var(--vl-radius); + margin: 1rem 0; +} + +.post-content table { + width: 100%; + border-collapse: collapse; + margin: 1.5rem 0; + font-size: 0.9rem; +} + +.post-content th, +.post-content td { + padding: 0.625rem 1rem; + border: 1px solid var(--vl-border); + text-align: left; +} + +.post-content th { + background: var(--vl-bg); + font-weight: 600; +} + +.post-content hr { + border: none; + border-top: 1px solid var(--vl-border); + margin: 2rem 0; +} + +/* ─── Post grid — auto-fill columns ─────────── */ +.post-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 1.5rem; +} + +/* ─── Post list — date / meta ─────────────── */ +.post-meta { + font-size: 0.8rem; + color: var(--vl-muted); +} + +/* ─── Footer ─────────────────────────────── */ +footer { + border-top: 1px solid var(--vl-border); + color: var(--vl-muted) !important; + font-size: 0.85rem; +} + +/* ─── Navbar tagline ────────────────────── */ +.navbar-tagline { + font-size: 0.68rem; + font-weight: 400; + color: rgba(255,255,255,.6); + letter-spacing: 0; + margin-top: 2px; +} + +/* ─── Liste éditoriale ───────────────────── */ +.posts-list { + max-width: 720px; + margin: 0 auto; +} + +.post-entry { + padding: 2rem 0; + border-bottom: 1px solid var(--vl-border); +} + +.post-entry:last-child { + border-bottom: none; +} + +.post-entry-title { + font-size: 1.3rem; + font-weight: 600; + margin-bottom: 0.6rem; + line-height: 1.35; + letter-spacing: -0.3px; +} + +.post-entry-title a { + color: var(--vl-text); + text-decoration: none; + transition: color 0.15s; +} + +.post-entry-title a:hover { + color: var(--vl-accent); +} + +.post-entry-excerpt { + color: var(--vl-muted); + font-size: 0.95rem; + margin-bottom: 0.85rem; + line-height: 1.65; +} + +.post-entry-meta { + display: flex; + align-items: center; + gap: 1.25rem; + font-size: 0.82rem; + color: var(--vl-muted); +} + +.post-entry-edit { + color: var(--vl-muted); + text-decoration: none; + transition: color 0.15s; +} + +.post-entry-edit:hover { + color: var(--vl-text); +} + +.post-entry-read { + color: var(--vl-accent); + text-decoration: none; + font-weight: 600; + margin-left: auto; + transition: opacity 0.15s; +} + +.post-entry-read:hover { + opacity: 0.75; +} + +/* ─── Pagination ─────────────────────────── */ +.pagination-nav { + display: flex; + align-items: center; + justify-content: center; + gap: 0.3rem; + flex-wrap: wrap; +} + +.pagination-btn { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 2.2rem; + height: 2.2rem; + padding: 0 0.5rem; + border-radius: 0.5rem; + border: 1px solid var(--vl-border); + background: var(--vl-surface); + color: var(--vl-text); + font-size: 0.875rem; + font-weight: 500; + text-decoration: none; + transition: border-color 0.15s, background 0.15s, color 0.15s; +} + +.pagination-btn:hover:not(.disabled):not(.active) { + border-color: var(--vl-accent); + color: var(--vl-accent); + background: var(--vl-accent-soft); +} + +.pagination-btn.active { + background: var(--vl-accent); + border-color: var(--vl-accent); + color: #fff; +} + +.pagination-btn.disabled { + opacity: 0.35; + cursor: default; + pointer-events: none; +} + +.pagination-ellipsis { + padding: 0 0.25rem; + color: var(--vl-muted); + font-size: 0.875rem; +} + +/* ─── Footer ──────────────────────────────── */ +footer { + background: linear-gradient(135deg, #312e81 0%, #4f46e5 55%, #7c3aed 100%); + border-top: none; + box-shadow: 0 -2px 24px rgba(79,70,229,.25); + color: rgba(255,255,255,.85); +} + +footer.py-5 { padding-top: 2.5rem !important; padding-bottom: 2.5rem !important; } +footer.mt-5 { margin-top: 0 !important; } + +.footer-inner { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 2rem; +} + +.footer-about strong { + font-size: 1rem; + font-weight: 800; + color: #fff; + display: block; + margin-bottom: 0.4rem; + letter-spacing: -0.2px; +} + +.footer-about p { + font-size: 0.82rem; + color: rgba(255,255,255,.75); + margin: 0 0 0.5rem; + line-height: 1.55; +} + +.footer-about small { + font-size: 0.78rem; + color: rgba(255,255,255,.6); +} + +.footer-about small a { + color: rgba(255,255,255,.7); + text-decoration: underline; + text-decoration-color: rgba(255,255,255,.3); + transition: color 0.15s; +} + +.footer-about small a:hover { + color: #fff; +} + +.footer-nav { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 0.5rem; + flex-shrink: 0; +} + +.footer-nav a { + font-size: 0.85rem; + color: rgba(255,255,255,.75); + text-decoration: none; + transition: color 0.15s; +} + +.footer-nav a:hover { + color: #fff; +} + +@media (max-width: 576px) { + .footer-inner { + flex-direction: column; + gap: 1.5rem; + } + + .footer-nav { + align-items: flex-start; + flex-direction: row; + gap: 1rem; + } +} + +/* ─── Utilitaires ─────────────────────────── */ +.text-muted { + color: var(--vl-muted) !important; +} + +.text-primary { + color: var(--vl-accent) !important; +} + +/* ─── Widget étoiles ──────────────────────── */ + +/* ─── Barre de recherche (navbar) ────────── */ +.search-form { + margin: 0 .5rem; +} + +.search-input { + width: 160px; + transition: width .2s, background .15s; + background: rgba(255,255,255,.15); + border-color: rgba(255,255,255,.25); + border-radius: 0.5rem; + color: #fff; +} + +.search-input::placeholder { + color: rgba(255,255,255,.5); +} + +.search-input:focus { + width: 220px; + background: rgba(255,255,255,.22); + border-color: rgba(255,255,255,.5); + box-shadow: 0 0 0 2px rgba(255,255,255,.15); + color: #fff; +} + +@media (max-width: 991px) { + .search-form { margin: .5rem 0; } + .search-input, .search-input:focus { width: 100%; } +} + +/* ─── Barre de recherche hero (accueil) ────── */ +.hero-search { + text-align: center; + padding: 2.25rem 1rem 1.5rem; +} +.hero-search-form { + display: flex; + max-width: 600px; + margin: 0 auto; + gap: .5rem; +} +.hero-search-input { + flex: 1; + padding: .7rem 1.1rem; + font-size: 1.05rem; + border: 1.5px solid var(--vl-border); + border-radius: .6rem; + background: var(--vl-surface); + color: var(--vl-text); + transition: border-color .2s, box-shadow .2s; +} +.hero-search-input::placeholder { color: var(--vl-muted); } +.hero-search-input:focus { + outline: none; + border-color: var(--vl-accent); + box-shadow: 0 0 0 3px rgba(79,70,229,.12); +} +.hero-search-btn { + padding: .7rem 1.3rem; + background: var(--vl-accent); + color: #fff; + border: none; + border-radius: .6rem; + font-size: .95rem; + font-weight: 500; + cursor: pointer; + white-space: nowrap; + transition: background .2s; +} +.hero-search-btn:hover { background: var(--vl-accent-dark); } +.hero-search-stats { + margin-top: .8rem; + font-size: .83rem; + color: var(--vl-muted); + letter-spacing: .01em; +} + +/* ─── Page de recherche ───────────────────── */ +.search-page { max-width: 780px; margin: 0 auto; } + +.search-bar .input-group { max-width: 600px; } + +.search-results { display: flex; flex-direction: column; gap: 2rem; } + +.search-result { + border-bottom: 1px solid var(--vl-border); + padding-bottom: 1.5rem; +} + +.search-result-meta { + display: flex; + align-items: center; + gap: .75rem; + margin-bottom: .25rem; + font-size: .8rem; +} + +.search-result-cat { + background: var(--vl-surface); + border: 1px solid var(--vl-border); + border-radius: 20px; + padding: 1px 10px; + color: var(--vl-muted); + text-decoration: none; +} + +.search-result-cat:hover { color: var(--vl-accent); } + +.search-result-date { color: var(--vl-muted); } + +.search-result-title { + font-size: 1.15rem; + margin: 0 0 .4rem; +} + +.search-result-title a { + color: var(--vl-text); + text-decoration: none; +} + +.search-result-title a:hover { color: var(--vl-accent); } + +.search-result-snippet { + font-size: .9rem; + color: var(--vl-muted); + margin: 0; + line-height: 1.6; +} + +.search-result-snippet mark { + background: rgba(250, 204, 21, .35); + color: inherit; + border-radius: 2px; + padding: 0 2px; +} + +/* ── Tag cloud ────────────────────────────────────────────────── */ +.tag-cloud { + display: flex; + flex-wrap: wrap; + gap: .5rem; + align-items: center; + padding: 1.25rem 0; + border-top: 1px solid var(--vl-border, #e5e7eb); +} + +.tag-cloud-item { + display: inline-flex; + align-items: center; + gap: .3em; + padding: .25em .75em; + border-radius: 999px; + border: 1px solid var(--vl-border, #e5e7eb); + background: transparent; + color: var(--vl-muted); + text-decoration: none; + white-space: nowrap; + transition: background .15s, color .15s, border-color .15s; + font-size: .85rem !important; +} + +.tag-cloud-item:hover { + background: var(--vl-accent); + border-color: var(--vl-accent); + color: #fff; +} + +.tag-cloud-item.active { + background: var(--vl-accent); + border-color: var(--vl-accent); + color: #fff; + font-weight: 600; +} + +.tag-count { + font-size: .8em; + opacity: .7; +} + +.tag-cloud-reset { + margin-left: auto; + font-size: .85rem; + color: var(--vl-muted); + text-decoration: none; +} + +.tag-cloud-reset:hover { color: var(--vl-accent); } + +/* ─── Profil public auteur ───────────────── */ + +.author-profile-hero { + display: flex; + align-items: flex-start; + gap: 1.5rem; +} + +.author-avatar { + flex-shrink: 0; + width: 4rem; + height: 4rem; + border-radius: 50%; + background: var(--vl-accent); + color: #fff; + font-size: 1.75rem; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; +} + +.author-profile-name { + font-size: 1.5rem; + font-weight: 700; + margin: 0 0 .25rem; +} + +.author-profile-link { + font-size: .875rem; + color: var(--vl-muted); +} + +.author-profile-link:hover { color: var(--vl-accent); } + +.liens-cta { + display: inline-block; + margin-top: .625rem; + padding: .5rem 1.25rem; + border-radius: 100px; + background: var(--vl-accent); + color: #fff; + font-size: .875rem; + font-weight: 700; + text-decoration: none; + transition: background .2s, transform .15s; +} +.liens-cta:hover { + background: var(--vl-accent-dark); + color: #fff; + transform: translateY(-1px); +} + +.author-bio-wrap { + flex: 1; +} + +.author-profile-bio { + margin: 0 0 .25rem; + color: var(--vl-muted); + line-height: 1.7; + font-size: .9375rem; +} + +.author-profile-bio.bio-clamped { + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.bio-toggle { + background: none; + border: none; + padding: 0; + color: var(--vl-accent); + font-size: .875rem; + cursor: pointer; + text-decoration: underline; +} + +/* ─── Page "Mes liens" ───────────────────── */ + +.liens-page { + max-width: 520px; + margin: 0 auto; + padding: 3rem 1.25rem 5rem; + display: flex; + flex-direction: column; + align-items: center; + gap: 2.5rem; +} + +.liens-header { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + text-align: center; +} + +.liens-avatar { + width: 6rem; + height: 6rem; + border-radius: 50%; + background: var(--vl-accent); + color: #fff; + font-size: 2.5rem; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; +} + +.liens-name { + font-size: 1.5rem; + font-weight: 700; + margin: 0; +} + +.liens-bio { + font-size: .9375rem; + color: var(--vl-muted); + line-height: 1.6; + margin: 0; +} + +.liens-sep { + width: 100%; + border: none; + border-top: 1.5px solid var(--vl-border); + margin: 0; +} + +.liens-list { + width: 100%; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.liens-item { + display: flex; + flex-direction: column; + align-items: center; + gap: .3rem; + width: 100%; + padding: 1.125rem 1.75rem; + border-radius: 100px; + border: none; + background: var(--btn-bg, var(--vl-accent)); + text-align: center; + text-decoration: none; + color: #fff; + font-weight: 700; + transition: filter .2s, transform .15s, box-shadow .2s; + box-shadow: 0 2px 8px rgba(0,0,0,.12); +} + +.liens-item:hover { + color: #fff; + filter: brightness(1.12); + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(0,0,0,.18); +} + +.liens-item-title { font-size: 1.125rem; } + +.liens-item-desc { + font-size: .85rem; + font-weight: 400; + color: rgba(255,255,255,.8); +} + +.liens-footer { + font-size: .8rem; + color: var(--vl-muted); +} +.liens-footer a { color: inherit; } +.liens-footer a:hover { color: var(--vl-accent); } + +.liens-bg { background: #f1f5f9; } + +/* ─── Agrégateur de flux ─────────────────── */ + +.flux-list { + display: flex; + flex-direction: column; + gap: 1.5rem; + max-width: 52rem; +} + +.flux-item { + border-left: 3px solid var(--vl-border); + padding-left: 1rem; +} + +.flux-item-meta { + display: flex; + align-items: center; + gap: .5rem; + flex-wrap: wrap; + font-size: .8rem; + color: var(--vl-muted); + margin-bottom: .25rem; +} + +.flux-author { + font-weight: 600; + color: var(--vl-accent); + text-decoration: none; +} +.flux-author:hover { text-decoration: underline; } + +.flux-feed-name::before { content: '·'; margin-right: .5rem; } + +.flux-date::before { content: '·'; margin-right: .5rem; } + +.flux-item-title { + font-size: 1rem; + font-weight: 600; + margin: 0 0 .25rem; +} + +.flux-item-title a { + color: var(--vl-text); + text-decoration: none; +} +.flux-item-title a:hover { color: var(--vl-accent); } + +.flux-item-summary { + font-size: .875rem; + color: var(--vl-muted); + margin: 0; + line-height: 1.6; +} + +/* Bouton flottant "+" — nouvel article */ +.fab-new { + position: fixed; + bottom: 2rem; + right: 2rem; + z-index: 1050; + width: 3.25rem; + height: 3.25rem; + border-radius: 50%; + background: var(--vl-accent); + color: #fff; + font-size: 1.75rem; + line-height: 3.25rem; + text-align: center; + text-decoration: none; + box-shadow: 0 4px 16px rgba(79,70,229,.45); + transition: background 0.15s, box-shadow 0.15s, transform 0.15s; + user-select: none; +} +.fab-new:hover, +.fab-new:focus { + background: var(--vl-accent-dark); + color: #fff; + box-shadow: 0 6px 20px rgba(79,70,229,.55); + transform: scale(1.08); +} + +/* ─── Homepage sections ─────────────────────────────────────────── */ +.home-section { margin-top: 3rem; } +.home-section--first { margin-top: 0; } + +.home-section-title { + font-size: 0.7rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.12em; + color: var(--vl-muted); + display: flex; + align-items: center; + gap: 0.6rem; + margin-bottom: 1.25rem; +} +.home-section-title::after { + content: ''; + flex: 1; + height: 1px; + background: var(--vl-border); +} +.home-section-title-sub { + font-weight: 400; + text-transform: none; + letter-spacing: 0; +} +.home-section-badge { + font-size: 0.65rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + background: var(--vl-accent); + color: #fff; + padding: 0.15rem 0.55rem; + border-radius: 999px; +} + +/* ─── Hero card homepage ────────────────────────────────────────── */ +.home-hero-card { + display: block; + text-decoration: none; + color: inherit; + border-radius: var(--vl-radius); + overflow: hidden; + margin-bottom: 1.5rem; + box-shadow: var(--vl-shadow-md); + transition: box-shadow 0.2s, transform 0.2s; +} +.home-hero-card:hover { box-shadow: 0 8px 32px rgba(0,0,0,.14); transform: translateY(-2px); } + +.home-hero-card-cover { + position: relative; + height: 340px; + background-size: cover; + background-position: center; +} +.home-hero-card-cover::after { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(to top, rgba(10,15,30,.75) 0%, rgba(10,15,30,.15) 55%, transparent 100%); +} +.home-hero-badge { + position: absolute; + top: 1rem; + left: 1rem; + z-index: 2; + background: var(--vl-accent); + color: #fff; + font-size: 0.65rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.1em; + padding: 0.2rem 0.65rem; + border-radius: 999px; +} +.home-hero-card-meta { + position: absolute; + bottom: 0; + left: 0; + right: 0; + padding: 1.25rem 1.5rem; + z-index: 1; + color: #fff; +} +.home-hero-card-cat { + font-size: 0.65rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.12em; + opacity: 0.8; + margin-bottom: 0.35rem; +} +.home-hero-card-title { + font-size: clamp(1.3rem, 3vw, 1.9rem); + font-weight: 700; + line-height: 1.2; + margin: 0 0 0.45rem; +} +.home-hero-card-excerpt { + font-size: 0.9rem; + opacity: 0.78; + margin: 0 0 0.6rem; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} +.home-hero-card-date { + font-size: 0.8rem; + opacity: 0.65; +} + +@media (max-width: 640px) { + .home-hero-card-cover { height: 240px; } +} + +/* ─── More link ─────────────────────────────────────────────────── */ +.home-more-link { margin-top: 1.25rem; text-align: center; } + +/* ─── Compact list (récemment mis à jour, redécouvertes) ────────── */ +.home-compact-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + gap: 0.6rem; +} +.home-compact-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.6rem 0.875rem; + background: var(--vl-surface); + border: 1px solid var(--vl-border); + border-radius: var(--vl-radius); + text-decoration: none; + color: var(--vl-text); + transition: border-color 0.15s, box-shadow 0.15s; +} +.home-compact-item:hover { + border-color: var(--vl-accent); + box-shadow: var(--vl-shadow-sm); + color: var(--vl-text); +} +.home-compact-thumb { + width: 44px; + height: 44px; + border-radius: 6px; + flex-shrink: 0; + background-size: cover; + background-position: center; +} +.home-compact-meta { flex: 1; min-width: 0; } +.home-compact-title { + font-size: 0.875rem; + font-weight: 600; + line-height: 1.35; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} +.home-compact-date { + font-size: 0.75rem; + color: var(--vl-muted); + margin-top: 0.15rem; +} diff --git a/public/assets/favicon.svg b/public/assets/favicon.svg new file mode 100644 index 0000000..80913eb --- /dev/null +++ b/public/assets/favicon.svg @@ -0,0 +1,4 @@ + + + v + diff --git a/public/assets/fonts/LICENSE-Inter.txt b/public/assets/fonts/LICENSE-Inter.txt new file mode 100644 index 0000000..9b2ca37 --- /dev/null +++ b/public/assets/fonts/LICENSE-Inter.txt @@ -0,0 +1,92 @@ +Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION AND CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/public/assets/fonts/inter-italic-latin-ext.woff2 b/public/assets/fonts/inter-italic-latin-ext.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..9de5e3e3584b2263b8f6599d449ee31463627763 GIT binary patch literal 37592 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 literal 0 HcmV?d00001 diff --git a/public/assets/fonts/inter-italic-latin.woff2 b/public/assets/fonts/inter-italic-latin.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..9e98286ff4db8fd160712170c288138d8ada1ca4 GIT binary patch literal 25040 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
4m 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 literal 0 HcmV?d00001 diff --git a/public/assets/fonts/inter-normal-latin.woff2 b/public/assets/fonts/inter-normal-latin.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..d15208de03cd1ad7c5199f0a0ce915fe841e4722 GIT binary patch literal 48256 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∾Na#*TBTU(!jF?9zmR-!C*+RdvGrd0&{`| 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' + f.name.split('.').pop().toUpperCase() + ''; + wrap.appendChild(icon); + } + + var overlay = document.createElement('div'); + overlay.style.cssText = 'position:absolute;inset:0;border-radius:6px;display:none;align-items:center;justify-content:center;background:rgba(25,135,84,.8);color:#fff;font-size:1.4rem'; + overlay.textContent = '✓'; + wrap.appendChild(overlay); + + wrap.addEventListener('mouseenter', function () { + if (!wrap._copied) wrap.firstChild.style.borderColor = '#0d6efd'; + }); + wrap.addEventListener('mouseleave', function () { + if (!wrap._copied) wrap.firstChild.style.borderColor = 'transparent'; + }); + + wrap.addEventListener('click', async function () { + if (wrap._copying || wrap._copied) return; + wrap._copying = true; + wrap.firstChild.style.opacity = '.5'; + try { + var fd = new FormData(); + fd.append('from_uuid', group.article.uuid); + fd.append('name', f.name); + fd.append('to_uuid', toUuid); + var r = await fetch('/?action=copy_file&uuid=' + encodeURIComponent(toUuid), {method: 'POST', body: fd}); + var d = await r.json(); + if (d.ok) { + wrap._copied = true; + wrap.firstChild.style.opacity = '1'; + wrap.firstChild.style.borderColor = '#198754'; + overlay.style.display = 'flex'; + } else { + wrap.firstChild.style.opacity = '1'; + wrap.firstChild.style.borderColor = '#dc3545'; + wrap.title = d.error || 'Erreur'; + } + } catch (e) { + wrap.firstChild.style.opacity = '1'; + } finally { + wrap._copying = false; + } + }); + + grid.appendChild(wrap); + }); + + section.appendChild(grid); + box.appendChild(section); + }); + } catch (e) { + box.innerHTML = '

Erreur de recherche.

'; + } finally { + btn.disabled = false; + } + } + + btn.addEventListener('click', doSearch); + input.addEventListener('keydown', function (e) { + if (e.key === 'Enter') { e.preventDefault(); doSearch(); } + }); + doSearch(); +}); diff --git a/public/assets/js/admin.js b/public/assets/js/admin.js new file mode 100644 index 0000000..12d6f90 --- /dev/null +++ b/public/assets/js/admin.js @@ -0,0 +1,48 @@ +document.addEventListener('DOMContentLoaded', function () { + // Confirmation data-confirm sur les formulaires (evite confirm() inline bloqué par CSP) + document.querySelectorAll('form[data-confirm]').forEach(function (form) { + form.addEventListener('submit', function (e) { + var msg = form.getAttribute('data-confirm') || 'Confirmer ?'; + if (!window.confirm(msg)) { + e.preventDefault(); + } + }); + }); + + // Sélection globale articles + var checkAll = document.getElementById('check-all'); + if (checkAll) { + checkAll.addEventListener('change', function () { + document.querySelectorAll('.bulk-check').forEach(function (cb) { + cb.checked = checkAll.checked; + }); + }); + } + + // Indicateurs de traitement formulaire SMTP (config + tester connexion) + var smtpForm = document.getElementById('smtp-config-form'); + if (smtpForm) { + smtpForm.addEventListener('submit', function (e) { + var clicked = e.submitter; + if (!clicked) return; + smtpForm.querySelectorAll('button[type="submit"]').forEach(function (btn) { + btn.disabled = true; + }); + var isSave = clicked.id === 'smtp-save-btn'; + clicked.innerHTML = '' + + (isSave ? 'Enregistrement…' : 'En cours…'); + }); + } + + // Indicateur de traitement envoi email de test + var smtpTestForm = document.getElementById('smtp-test-form'); + if (smtpTestForm) { + smtpTestForm.addEventListener('submit', function () { + var btn = document.getElementById('smtp-send-btn'); + if (btn) { + btn.disabled = true; + btn.innerHTML = 'En cours…'; + } + }); + } +}); diff --git a/public/assets/js/app.js b/public/assets/js/app.js new file mode 100644 index 0000000..9f85bfa --- /dev/null +++ b/public/assets/js/app.js @@ -0,0 +1,462 @@ +// varlog — app.js + +document.addEventListener('DOMContentLoaded', function () { + + // ─── Auto-resize textareas ─────────────────────────────────────────────── + document.querySelectorAll('textarea.form-control').forEach(function (ta) { + function resize() { + ta.style.height = 'auto'; + ta.style.height = ta.scrollHeight + 'px'; + } + ta.addEventListener('input', resize); + resize(); + }); + + // ─── Ctrl+Enter : soumettre le formulaire ──────────────────────────────── + var form = document.querySelector('form[method="POST"]'); + if (form) { + form.addEventListener('keydown', function (e) { + if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { + form.submit(); + } + }); + } + + // ─── Slug auto-génération ──────────────────────────────────────────────── + const titleInput = document.getElementById('title'); + const slugField = document.getElementById('slug'); + const slugPreview = document.getElementById('slug-preview'); + + if (titleInput && slugField) { + if (slugField.value !== '') slugField._auto = false; + + titleInput.addEventListener('input', function () { + if (slugField._auto !== false) { + const generated = slugify(this.value); + slugField.value = generated; + if (slugPreview) slugPreview.textContent = generated; + } + }); + + slugField.addEventListener('input', function () { + this._auto = (this.value === ''); + if (slugPreview) slugPreview.textContent = this.value; + }); + } + + function slugify(s) { + const map = {'à':'a','â':'a','ä':'a','é':'e','è':'e','ê':'e','ë':'e','î':'i','ï':'i','ô':'o','ö':'o','ù':'u','û':'u','ü':'u','ç':'c','æ':'ae','œ':'oe'}; + return s.toLowerCase().replace(/[àâäéèêëîïôöùûüçæœ]/g, c => map[c] || c) + .replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); + } + + // ─── Rôle : nom technique auto depuis le label ─────────────────────────── + var roleLabelInput = document.getElementById('role-label'); + var roleNameInput = document.getElementById('role-name'); + if (roleLabelInput && roleNameInput) { + roleLabelInput.addEventListener('input', function () { + if (roleNameInput._manual) return; + roleNameInput.value = slugify(this.value); + }); + roleNameInput.addEventListener('input', function () { + this._manual = (this.value !== ''); + }); + roleNameInput.addEventListener('blur', function () { + if (this.value === '') { + this._manual = false; + this.value = slugify(roleLabelInput.value); + } + }); + } + + // ─── Aperçu couleur catégorie ──────────────────────────────────────────── + const KNOWN_CATS = { + 'actualité': 10, 'travaux': 35, 'scolaire': 55, + 'linux': 120, 'domotique': 160, 'télécom': 190, + 'blog': 220, 'informatique': 255, 'réflexion': 285, + 'loisirs': 320, 'perso': 345, + }; + const FREE_HUES = [87, 140, 205, 237, 302]; + + function gradient(hue) { + return `linear-gradient(135deg,hsl(${hue},70%,88%) 0%,hsl(${hue},60%,28%) 100%)`; + } + + function hashHue(str) { + let h = 5381; + for (let i = 0; i < str.length; i++) h = (((h << 5) + h) + str.charCodeAt(i)) | 0; + return ((Math.abs(h) * 0.6180339887) * 360 | 0) % 360; + } + + function nearestKnown(hue) { + let best = null, bestDist = Infinity; + for (const [name, h] of Object.entries(KNOWN_CATS)) { + const d = Math.min(Math.abs(hue - h), 360 - Math.abs(hue - h)); + if (d < bestDist) { bestDist = d; best = name; } + } + return { name: best, dist: bestDist }; + } + + function updateCatPreview(val) { + const key = val.trim().toLowerCase(); + const swatch = document.getElementById('cat-swatch'); + const hint = document.getElementById('cat-hint'); + const freeEl = document.getElementById('cat-free-swatches'); + if (!swatch) return; + freeEl.innerHTML = ''; + + if (!key) { + swatch.style.background = '#e5e7eb'; + swatch.title = ''; + hint.textContent = ''; + return; + } + + if (KNOWN_CATS[key] !== undefined) { + const hue = KNOWN_CATS[key]; + swatch.style.background = gradient(hue); + swatch.title = `${hue}°`; + hint.textContent = `Catégorie existante · teinte fixe (${hue}°)`; + hint.className = 'text-muted d-block mt-1'; + return; + } + + const hue = hashHue(key); + const { name, dist } = nearestKnown(hue); + + swatch.style.background = gradient(hue); + swatch.title = `${hue}°`; + + if (dist < 20) { + hint.innerHTML = `⚠ Teinte proche de ${name} (${dist}° d'écart) · couleurs disponibles :`; + hint.className = 'text-warning d-block mt-1'; + FREE_HUES.forEach(h => { + const el = document.createElement('span'); + el.title = `${h}°`; + el.style.cssText = `display:inline-block;width:28px;height:20px;border-radius:4px;cursor:help;background:${gradient(h)}`; + freeEl.appendChild(el); + }); + } else { + hint.textContent = `Nouvelle catégorie · teinte libre (${hue}°)`; + hint.className = 'text-muted d-block mt-1'; + } + } + + const catInput = document.getElementById('category'); + if (catInput) { + catInput.addEventListener('input', function () { updateCatPreview(this.value); }); + updateCatPreview(catInput.value); + } + + // ─── Copier la référence Markdown ──────────────────────────────────────── + document.querySelectorAll('[data-copy-md-name]').forEach(function (btn) { + btn.addEventListener('click', function () { + const name = this.dataset.copyMdName; + const isImage = this.dataset.copyMdIsImage === '1'; + const ref = isImage ? `![](${name})` : `[${name}](${name})`; + navigator.clipboard.writeText(ref).then(() => { + const orig = this.textContent; + this.textContent = 'Copié !'; + setTimeout(() => { this.textContent = orig; }, 1500); + }); + }); + }); + + // ─── Boîtes de confirmation (suppression) ─────────────────────────────── + document.querySelectorAll('button[data-confirm], a[data-confirm]').forEach(function (el) { + el.addEventListener('click', function (e) { + if (!confirm(this.dataset.confirm)) e.preventDefault(); + }); + }); + document.querySelectorAll('form[data-confirm]').forEach(function (form) { + form.addEventListener('submit', function (e) { + if (!confirm(this.dataset.confirm)) e.preventDefault(); + }); + }); + + // ─── Insérer une référence Markdown au curseur ─────────────────────────── + const ta = document.getElementById('content'); + if (ta) { + ta._savedStart = null; + ta._savedEnd = null; + + function saveCursor() { + if (document.activeElement === ta) { + ta._savedStart = ta.selectionStart; + ta._savedEnd = ta.selectionEnd; + } + } + document.addEventListener('mousedown', saveCursor); + ta.addEventListener('keyup', saveCursor); + ta.addEventListener('mouseup', saveCursor); + + document.querySelectorAll('[data-insert-ref]').forEach(function (el) { + el.addEventListener('click', function () { + insertRef(this.dataset.insertRef); + }); + if (el.tagName === 'IMG') { + el.addEventListener('mouseenter', function () { this.style.borderColor = '#0d6efd'; }); + el.addEventListener('mouseleave', function () { this.style.borderColor = 'transparent'; }); + } + }); + } + + function insertRef(url) { + if (!ta) return; + const isImage = /\.(jpe?g|png|gif|webp|svg|avif)(\?.*)?$/i.test(url); + const label = url.startsWith('http') + ? (decodeURIComponent(url.split('/').pop().split('?')[0]) || url) + : url; + const ref = isImage ? `![](${url})` : `[${label}](${url})`; + const len = ta.value.length; + const start = ta._savedStart !== null ? ta._savedStart : len; + const end = ta._savedEnd !== null ? ta._savedEnd : len; + ta.focus(); + ta.setRangeText(ref, start, end, 'end'); + ta._savedStart = ta._savedEnd = start + ref.length; + ta.dispatchEvent(new Event('input')); + } + + // ─── Compteurs SEO ─────────────────────────────────────────────────────── + function initCounter(inputId, counterId, max) { + const input = document.getElementById(inputId); + const counter = document.getElementById(counterId); + if (!input || !counter) return; + function update() { + const len = input.value.length; + counter.textContent = `${len} / ${max}`; + counter.className = len > max ? 'text-danger' : 'text-muted'; + } + input.addEventListener('input', update); + update(); + } + initCounter('seo_title', 'seo_title_counter', 60); + initCounter('seo_description', 'seo_desc_counter', 155); + + // ─── Page catégories ───────────────────────────────────────────────────── + function catComputeGradient(val) { + const key = val.trim().toLowerCase(); + if (!key) return null; + if (KNOWN_CATS[key] !== undefined) return { hue: KNOWN_CATS[key], known: true }; + const hue = hashHue(key); + const { name, dist } = nearestKnown(hue); + return { hue, known: false, conflict: dist < 20 ? name : null }; + } + + document.querySelectorAll('form[action="/?action=rename_category"] input[name="new"]').forEach(function (input) { + input.addEventListener('input', function () { + const swatch = input.closest('form').querySelector('.rename-swatch'); + const result = catComputeGradient(input.value); + if (swatch) swatch.style.background = result ? gradient(result.hue) : '#e5e7eb'; + }); + }); + + const newCatInput = document.getElementById('new-cat-input'); + if (newCatInput) { + newCatInput.addEventListener('input', function () { + const swatch = document.getElementById('new-cat-swatch'); + const hint = document.getElementById('new-cat-hint'); + const result = catComputeGradient(this.value); + + if (!result) { + swatch.style.background = '#e5e7eb'; + hint.textContent = ''; + return; + } + + swatch.style.background = gradient(result.hue); + + if (result.known) { + hint.textContent = `Catégorie existante · teinte fixe (${result.hue}°)`; + hint.className = 'text-muted d-block mb-3'; + } else if (result.conflict) { + hint.textContent = `⚠ Teinte proche de « ${result.conflict} » — choisissez un autre nom ou une couleur disponible ci-dessous`; + hint.className = 'text-warning d-block mb-3'; + } else { + hint.textContent = `Couleur libre · teinte ${result.hue}°`; + hint.className = 'text-success d-block mb-3'; + } + }); + } + + // ─── Import image : récupérer les métadonnées ──────────────────────────── + const fetchMetaBtn = document.getElementById('fetch-meta-btn'); + if (fetchMetaBtn) { + fetchMetaBtn.addEventListener('click', async function () { + const urlInput = document.getElementById('import-url'); + const resultDiv = document.getElementById('meta-result'); + const url = urlInput ? urlInput.value.trim() : ''; + + if (!url) { + resultDiv.innerHTML = 'Saisissez une URL d\'abord.'; + return; + } + + fetchMetaBtn.disabled = true; + fetchMetaBtn.textContent = 'Chargement…'; + resultDiv.innerHTML = ''; + + try { + const res = await fetch(`/?action=fetch_file_meta&url=${encodeURIComponent(url)}`); + const data = await res.json(); + + if (!data.ok) { + resultDiv.innerHTML = `${data.error || 'Erreur lors de la récupération.'}`; + return; + } + + // Auto-remplissage dynamique des champs (si vides) + const AUTOFILL = { + img_author: { keys: ['author', 'credit'], label: 'Auteur / crédit' }, + img_source: { keys: ['canonical', 'source'], label: 'URL source' }, + }; + const autofillKeys = new Set(); + const autofillNotice = []; + for (const [fieldName, cfg] of Object.entries(AUTOFILL)) { + const f = document.querySelector(`input[name="${fieldName}"]`); + if (!f || f.value) continue; + for (const key of cfg.keys) { + if (data[key]) { + f.value = data[key]; + autofillKeys.add(key); + autofillNotice.push(`${cfg.label} : ${data[key]}`); + break; + } + } + } + + // Affichage dynamique de tous les champs retournés + const isPdf = (data.mime === 'application/pdf'); + const isHtml = (data.mime || '').startsWith('text/html'); + + const META_ORDER = ['mime','size','pages','page_size','pdf_version', + 'width','site_name','og_type','language', + 'title','description','author','subject','keywords', + 'credit','source','creator','producer','date','camera','copyright', + 'canonical','og_image']; + const META_LABELS = { + mime: 'Type', size: 'Taille', width: 'Dimensions', + pages: 'Pages', page_size: 'Format', pdf_version: 'Version PDF', + site_name: 'Site', og_type: 'Type OG', language: 'Langue', + title: isPdf || isHtml ? 'Titre' : 'Titre EXIF/IPTC', + author: isPdf || isHtml ? 'Auteur' : 'Auteur EXIF/IPTC', + date: isPdf ? 'Créé le' : isHtml ? 'Publié le' : 'Prise de vue', + description: 'Description', subject: 'Sujet', keywords: 'Mots-clés', + credit: 'Crédit', source: 'Source IPTC', + creator: 'Créé avec', producer: 'Produit par', + camera: 'Appareil', copyright: 'Copyright', + canonical: 'URL canonique', og_image: 'Image OG', + }; + + function fmtVal(key, val) { + if (key === 'size') return (val/1024).toFixed(0) + ' Ko' + (val >= 1048576 ? ` (${(val/1048576).toFixed(1)} Mo)` : ''); + if (key === 'width') return `${data.width} × ${data.height} px`; + if (key === 'og_image') return ``; + if (key === 'canonical') return `${val}`; + return String(val); + } + + const SKIP = new Set(['ok', 'height']); + const seen = new Set(); + const rows = []; + + for (const key of META_ORDER) { + const val = data[key]; + if (val == null || val === '' || key === 'height') continue; + seen.add(key); + const badge = autofillKeys.has(key) + ? ' ↓ pré-rempli' + : ''; + rows.push([META_LABELS[key] ?? key, fmtVal(key, val) + badge]); + } + for (const [key, val] of Object.entries(data)) { + if (seen.has(key) || SKIP.has(key) || val == null || val === '') continue; + rows.push([key, fmtVal(key, val)]); + } + + let html = ''; + if (rows.length > 0) { + const trs = rows.map(([k, v]) => + `${k}${v}` + ).join(''); + html = `${trs}
`; + } else { + html = 'Aucune métadonnée disponible pour ce fichier.'; + } + if (autofillNotice.length > 0) { + html += `
✓ Pré-rempli — ${autofillNotice.join(' · ')}
`; + } + resultDiv.innerHTML = html; + } catch { + resultDiv.innerHTML = 'Erreur de connexion.'; + } finally { + fetchMetaBtn.disabled = false; + fetchMetaBtn.textContent = 'Métadonnées'; + } + }); + } + + // ─── Import image : toggle mode download ──────────────────────────────── + document.querySelectorAll('input[name="mode"]').forEach(function (r) { + r.addEventListener('change', function () { + const dl = this.value === 'download'; + const ss = this.value === 'screenshot'; + const warn = document.getElementById('copyright-warning'); + const fields = document.getElementById('download-fields'); + if (warn) warn.style.display = dl ? 'block' : 'none'; + if (fields) fields.style.display = (dl || ss) ? 'block' : 'none'; + }); + }); + + // ─── Données page (mode édition uniquement) ────────────────────────────── + const pageEl = document.getElementById('vl-page'); + if (!pageEl) return; + + const uuid = pageEl.dataset.uuid; + const insertUrl = pageEl.dataset.insertUrl; + + // Auto-insertion après import d'image + if (insertUrl && ta) { + const isImage = /\.(jpe?g|png|gif|webp|svg|avif)(\?.*)?$/i.test(insertUrl); + const name = decodeURIComponent(insertUrl.split('/').pop().split('?')[0]) || 'fichier'; + const ref = isImage ? `![](${insertUrl})` : `[${name}](${insertUrl})`; + const sep = ta.value.length > 0 && !ta.value.endsWith('\n') ? '\n' : ''; + ta.value += sep + ref; + ta.focus(); + ta.selectionStart = ta.selectionEnd = ta.value.length; + ta.dispatchEvent(new Event('input')); + } + + // ─── Autosave ──────────────────────────────────────────────────────────── + const indicator = document.getElementById('autosave-indicator'); + if (!indicator || !uuid) return; + + let timer = null; + + function scheduleAutosave() { + clearTimeout(timer); + timer = setTimeout(doAutosave, 3000); + } + + async function doAutosave() { + const title = document.getElementById('title').value; + const slug = document.getElementById('slug').value; + const content = document.getElementById('content').value; + indicator.textContent = 'Sauvegarde…'; + try { + const res = await fetch(`/?action=autosave&uuid=${encodeURIComponent(uuid)}`, { + method: 'POST', + headers: {'Content-Type': 'application/x-www-form-urlencoded'}, + body: new URLSearchParams({title, slug, content}), + }); + const data = await res.json(); + indicator.textContent = data.ok ? `Brouillon sauvegardé à ${data.time}` : 'Erreur de sauvegarde'; + } catch { + indicator.textContent = 'Erreur de sauvegarde'; + } + } + + ['title', 'slug', 'content'].forEach(id => { + document.getElementById(id)?.addEventListener('input', scheduleAutosave); + }); +}); diff --git a/public/assets/js/bio-toggle.js b/public/assets/js/bio-toggle.js new file mode 100644 index 0000000..549bcad --- /dev/null +++ b/public/assets/js/bio-toggle.js @@ -0,0 +1,14 @@ +(function(){ + var bio = document.getElementById('author-bio'); + var btn = document.getElementById('bio-toggle'); + if (!bio || !btn) return; + requestAnimationFrame(function() { + if (bio.scrollHeight > bio.clientHeight + 2) { btn.hidden = false; } + }); + btn.addEventListener('click', function() { + var exp = btn.getAttribute('aria-expanded') === 'true'; + bio.classList.toggle('bio-clamped', exp); + btn.textContent = exp ? 'plus' : 'moins'; + btn.setAttribute('aria-expanded', exp ? 'false' : 'true'); + }); +})(); diff --git a/public/assets/js/bootstrap.bundle.min.js b/public/assets/js/bootstrap.bundle.min.js new file mode 100644 index 0000000..bbd9b5e --- /dev/null +++ b/public/assets/js/bootstrap.bundle.min.js @@ -0,0 +1,6 @@ +/*! + * Bootstrap v5.3.3 (https://getbootstrap.com/) + * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e()}(this,(function(){"use strict";const t=new Map,e={set(e,i,n){t.has(e)||t.set(e,new Map);const s=t.get(e);s.has(i)||0===s.size?s.set(i,n):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(s.keys())[0]}.`)},get:(e,i)=>t.has(e)&&t.get(e).get(i)||null,remove(e,i){if(!t.has(e))return;const n=t.get(e);n.delete(i),0===n.size&&t.delete(e)}},i="transitionend",n=t=>(t&&window.CSS&&window.CSS.escape&&(t=t.replace(/#([^\s"#']+)/g,((t,e)=>`#${CSS.escape(e)}`))),t),s=t=>{t.dispatchEvent(new Event(i))},o=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),r=t=>o(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(n(t)):null,a=t=>{if(!o(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},l=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),c=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?c(t.parentNode):null},h=()=>{},d=t=>{t.offsetHeight},u=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,f=[],p=()=>"rtl"===document.documentElement.dir,m=t=>{var e;e=()=>{const e=u();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(f.length||document.addEventListener("DOMContentLoaded",(()=>{for(const t of f)t()})),f.push(e)):e()},g=(t,e=[],i=t)=>"function"==typeof t?t(...e):i,_=(t,e,n=!0)=>{if(!n)return void g(t);const o=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let r=!1;const a=({target:n})=>{n===e&&(r=!0,e.removeEventListener(i,a),g(t))};e.addEventListener(i,a),setTimeout((()=>{r||s(e)}),o)},b=(t,e,i,n)=>{const s=t.length;let o=t.indexOf(e);return-1===o?!i&&n?t[s-1]:t[0]:(o+=i?1:-1,n&&(o=(o+s)%s),t[Math.max(0,Math.min(o,s-1))])},v=/[^.]*(?=\..*)\.|.*/,y=/\..*/,w=/::\d+$/,A={};let E=1;const T={mouseenter:"mouseover",mouseleave:"mouseout"},C=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function O(t,e){return e&&`${e}::${E++}`||t.uidEvent||E++}function x(t){const e=O(t);return t.uidEvent=e,A[e]=A[e]||{},A[e]}function k(t,e,i=null){return Object.values(t).find((t=>t.callable===e&&t.delegationSelector===i))}function L(t,e,i){const n="string"==typeof e,s=n?i:e||i;let o=I(t);return C.has(o)||(o=t),[n,s,o]}function S(t,e,i,n,s){if("string"!=typeof e||!t)return;let[o,r,a]=L(e,i,n);if(e in T){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=x(t),c=l[a]||(l[a]={}),h=k(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=O(r,e.replace(v,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return P(s,{delegateTarget:r}),n.oneOff&&N.off(t,s.type,e,i),i.apply(r,[s])}}(t,i,r):function(t,e){return function i(n){return P(n,{delegateTarget:t}),i.oneOff&&N.off(t,n.type,e),e.apply(t,[n])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function D(t,e,i,n,s){const o=k(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function $(t,e,i,n){const s=e[i]||{};for(const[o,r]of Object.entries(s))o.includes(n)&&D(t,e,i,r.callable,r.delegationSelector)}function I(t){return t=t.replace(y,""),T[t]||t}const N={on(t,e,i,n){S(t,e,i,n,!1)},one(t,e,i,n){S(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=L(e,i,n),a=r!==e,l=x(t),c=l[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))$(t,l,i,e.slice(1));for(const[i,n]of Object.entries(c)){const s=i.replace(w,"");a&&!e.includes(s)||D(t,l,r,n.callable,n.delegationSelector)}}else{if(!Object.keys(c).length)return;D(t,l,r,o,s?i:null)}},trigger(t,e,i){if("string"!=typeof e||!t)return null;const n=u();let s=null,o=!0,r=!0,a=!1;e!==I(e)&&n&&(s=n.Event(e,i),n(t).trigger(s),o=!s.isPropagationStopped(),r=!s.isImmediatePropagationStopped(),a=s.isDefaultPrevented());const l=P(new Event(e,{bubbles:o,cancelable:!0}),i);return a&&l.preventDefault(),r&&t.dispatchEvent(l),l.defaultPrevented&&s&&s.preventDefault(),l}};function P(t,e={}){for(const[i,n]of Object.entries(e))try{t[i]=n}catch(e){Object.defineProperty(t,i,{configurable:!0,get:()=>n})}return t}function j(t){if("true"===t)return!0;if("false"===t)return!1;if(t===Number(t).toString())return Number(t);if(""===t||"null"===t)return null;if("string"!=typeof t)return t;try{return JSON.parse(decodeURIComponent(t))}catch(e){return t}}function M(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}const F={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${M(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${M(e)}`)},getDataAttributes(t){if(!t)return{};const e={},i=Object.keys(t.dataset).filter((t=>t.startsWith("bs")&&!t.startsWith("bsConfig")));for(const n of i){let i=n.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1,i.length),e[i]=j(t.dataset[n])}return e},getDataAttribute:(t,e)=>j(t.getAttribute(`data-bs-${M(e)}`))};class H{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(t){return t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t}_mergeConfigObj(t,e){const i=o(e)?F.getDataAttribute(e,"config"):{};return{...this.constructor.Default,..."object"==typeof i?i:{},...o(e)?F.getDataAttributes(e):{},..."object"==typeof t?t:{}}}_typeCheckConfig(t,e=this.constructor.DefaultType){for(const[n,s]of Object.entries(e)){const e=t[n],r=o(e)?"element":null==(i=e)?`${i}`:Object.prototype.toString.call(i).match(/\s([a-z]+)/i)[1].toLowerCase();if(!new RegExp(s).test(r))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${n}" provided type "${r}" but expected type "${s}".`)}var i}}class W extends H{constructor(t,i){super(),(t=r(t))&&(this._element=t,this._config=this._getConfig(i),e.set(this._element,this.constructor.DATA_KEY,this))}dispose(){e.remove(this._element,this.constructor.DATA_KEY),N.off(this._element,this.constructor.EVENT_KEY);for(const t of Object.getOwnPropertyNames(this))this[t]=null}_queueCallback(t,e,i=!0){_(t,e,i)}_getConfig(t){return t=this._mergeConfigObj(t,this._element),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}static getInstance(t){return e.get(r(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.3.3"}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(t){return`${t}${this.EVENT_KEY}`}}const B=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return e?e.split(",").map((t=>n(t))).join(","):null},z={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter((t=>t.matches(e))),parents(t,e){const i=[];let n=t.parentNode.closest(e);for(;n;)i.push(n),n=n.parentNode.closest(e);return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((t=>`${t}:not([tabindex^="-"])`)).join(",");return this.find(e,t).filter((t=>!l(t)&&a(t)))},getSelectorFromElement(t){const e=B(t);return e&&z.findOne(e)?e:null},getElementFromSelector(t){const e=B(t);return e?z.findOne(e):null},getMultipleElementsFromSelector(t){const e=B(t);return e?z.find(e):[]}},R=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,n=t.NAME;N.on(document,i,`[data-bs-dismiss="${n}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),l(this))return;const s=z.getElementFromSelector(this)||this.closest(`.${n}`);t.getOrCreateInstance(s)[e]()}))},q=".bs.alert",V=`close${q}`,K=`closed${q}`;class Q extends W{static get NAME(){return"alert"}close(){if(N.trigger(this._element,V).defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),N.trigger(this._element,K),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=Q.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}R(Q,"close"),m(Q);const X='[data-bs-toggle="button"]';class Y extends W{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=Y.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}N.on(document,"click.bs.button.data-api",X,(t=>{t.preventDefault();const e=t.target.closest(X);Y.getOrCreateInstance(e).toggle()})),m(Y);const U=".bs.swipe",G=`touchstart${U}`,J=`touchmove${U}`,Z=`touchend${U}`,tt=`pointerdown${U}`,et=`pointerup${U}`,it={endCallback:null,leftCallback:null,rightCallback:null},nt={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class st extends H{constructor(t,e){super(),this._element=t,t&&st.isSupported()&&(this._config=this._getConfig(e),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return it}static get DefaultType(){return nt}static get NAME(){return"swipe"}dispose(){N.off(this._element,U)}_start(t){this._supportPointerEvents?this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX):this._deltaX=t.touches[0].clientX}_end(t){this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX-this._deltaX),this._handleSwipe(),g(this._config.endCallback)}_move(t){this._deltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this._deltaX}_handleSwipe(){const t=Math.abs(this._deltaX);if(t<=40)return;const e=t/this._deltaX;this._deltaX=0,e&&g(e>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(N.on(this._element,tt,(t=>this._start(t))),N.on(this._element,et,(t=>this._end(t))),this._element.classList.add("pointer-event")):(N.on(this._element,G,(t=>this._start(t))),N.on(this._element,J,(t=>this._move(t))),N.on(this._element,Z,(t=>this._end(t))))}_eventIsPointerPenTouch(t){return this._supportPointerEvents&&("pen"===t.pointerType||"touch"===t.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const ot=".bs.carousel",rt=".data-api",at="next",lt="prev",ct="left",ht="right",dt=`slide${ot}`,ut=`slid${ot}`,ft=`keydown${ot}`,pt=`mouseenter${ot}`,mt=`mouseleave${ot}`,gt=`dragstart${ot}`,_t=`load${ot}${rt}`,bt=`click${ot}${rt}`,vt="carousel",yt="active",wt=".active",At=".carousel-item",Et=wt+At,Tt={ArrowLeft:ht,ArrowRight:ct},Ct={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},Ot={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class xt extends W{constructor(t,e){super(t,e),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=z.findOne(".carousel-indicators",this._element),this._addEventListeners(),this._config.ride===vt&&this.cycle()}static get Default(){return Ct}static get DefaultType(){return Ot}static get NAME(){return"carousel"}next(){this._slide(at)}nextWhenVisible(){!document.hidden&&a(this._element)&&this.next()}prev(){this._slide(lt)}pause(){this._isSliding&&s(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval((()=>this.nextWhenVisible()),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?N.one(this._element,ut,(()=>this.cycle())):this.cycle())}to(t){const e=this._getItems();if(t>e.length-1||t<0)return;if(this._isSliding)return void N.one(this._element,ut,(()=>this.to(t)));const i=this._getItemIndex(this._getActive());if(i===t)return;const n=t>i?at:lt;this._slide(n,e[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(t){return t.defaultInterval=t.interval,t}_addEventListeners(){this._config.keyboard&&N.on(this._element,ft,(t=>this._keydown(t))),"hover"===this._config.pause&&(N.on(this._element,pt,(()=>this.pause())),N.on(this._element,mt,(()=>this._maybeEnableCycle()))),this._config.touch&&st.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const t of z.find(".carousel-item img",this._element))N.on(t,gt,(t=>t.preventDefault()));const t={leftCallback:()=>this._slide(this._directionToOrder(ct)),rightCallback:()=>this._slide(this._directionToOrder(ht)),endCallback:()=>{"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((()=>this._maybeEnableCycle()),500+this._config.interval))}};this._swipeHelper=new st(this._element,t)}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=Tt[t.key];e&&(t.preventDefault(),this._slide(this._directionToOrder(e)))}_getItemIndex(t){return this._getItems().indexOf(t)}_setActiveIndicatorElement(t){if(!this._indicatorsElement)return;const e=z.findOne(wt,this._indicatorsElement);e.classList.remove(yt),e.removeAttribute("aria-current");const i=z.findOne(`[data-bs-slide-to="${t}"]`,this._indicatorsElement);i&&(i.classList.add(yt),i.setAttribute("aria-current","true"))}_updateInterval(){const t=this._activeElement||this._getActive();if(!t)return;const e=Number.parseInt(t.getAttribute("data-bs-interval"),10);this._config.interval=e||this._config.defaultInterval}_slide(t,e=null){if(this._isSliding)return;const i=this._getActive(),n=t===at,s=e||b(this._getItems(),i,n,this._config.wrap);if(s===i)return;const o=this._getItemIndex(s),r=e=>N.trigger(this._element,e,{relatedTarget:s,direction:this._orderToDirection(t),from:this._getItemIndex(i),to:o});if(r(dt).defaultPrevented)return;if(!i||!s)return;const a=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(o),this._activeElement=s;const l=n?"carousel-item-start":"carousel-item-end",c=n?"carousel-item-next":"carousel-item-prev";s.classList.add(c),d(s),i.classList.add(l),s.classList.add(l),this._queueCallback((()=>{s.classList.remove(l,c),s.classList.add(yt),i.classList.remove(yt,c,l),this._isSliding=!1,r(ut)}),i,this._isAnimated()),a&&this.cycle()}_isAnimated(){return this._element.classList.contains("slide")}_getActive(){return z.findOne(Et,this._element)}_getItems(){return z.find(At,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(t){return p()?t===ct?lt:at:t===ct?at:lt}_orderToDirection(t){return p()?t===lt?ct:ht:t===lt?ht:ct}static jQueryInterface(t){return this.each((function(){const e=xt.getOrCreateInstance(this,t);if("number"!=typeof t){if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}else e.to(t)}))}}N.on(document,bt,"[data-bs-slide], [data-bs-slide-to]",(function(t){const e=z.getElementFromSelector(this);if(!e||!e.classList.contains(vt))return;t.preventDefault();const i=xt.getOrCreateInstance(e),n=this.getAttribute("data-bs-slide-to");return n?(i.to(n),void i._maybeEnableCycle()):"next"===F.getDataAttribute(this,"slide")?(i.next(),void i._maybeEnableCycle()):(i.prev(),void i._maybeEnableCycle())})),N.on(window,_t,(()=>{const t=z.find('[data-bs-ride="carousel"]');for(const e of t)xt.getOrCreateInstance(e)})),m(xt);const kt=".bs.collapse",Lt=`show${kt}`,St=`shown${kt}`,Dt=`hide${kt}`,$t=`hidden${kt}`,It=`click${kt}.data-api`,Nt="show",Pt="collapse",jt="collapsing",Mt=`:scope .${Pt} .${Pt}`,Ft='[data-bs-toggle="collapse"]',Ht={parent:null,toggle:!0},Wt={parent:"(null|element)",toggle:"boolean"};class Bt extends W{constructor(t,e){super(t,e),this._isTransitioning=!1,this._triggerArray=[];const i=z.find(Ft);for(const t of i){const e=z.getSelectorFromElement(t),i=z.find(e).filter((t=>t===this._element));null!==e&&i.length&&this._triggerArray.push(t)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return Ht}static get DefaultType(){return Wt}static get NAME(){return"collapse"}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t=[];if(this._config.parent&&(t=this._getFirstLevelChildren(".collapse.show, .collapse.collapsing").filter((t=>t!==this._element)).map((t=>Bt.getOrCreateInstance(t,{toggle:!1})))),t.length&&t[0]._isTransitioning)return;if(N.trigger(this._element,Lt).defaultPrevented)return;for(const e of t)e.hide();const e=this._getDimension();this._element.classList.remove(Pt),this._element.classList.add(jt),this._element.style[e]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const i=`scroll${e[0].toUpperCase()+e.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(jt),this._element.classList.add(Pt,Nt),this._element.style[e]="",N.trigger(this._element,St)}),this._element,!0),this._element.style[e]=`${this._element[i]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(N.trigger(this._element,Dt).defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,d(this._element),this._element.classList.add(jt),this._element.classList.remove(Pt,Nt);for(const t of this._triggerArray){const e=z.getElementFromSelector(t);e&&!this._isShown(e)&&this._addAriaAndCollapsedClass([t],!1)}this._isTransitioning=!0,this._element.style[t]="",this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(jt),this._element.classList.add(Pt),N.trigger(this._element,$t)}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(Nt)}_configAfterMerge(t){return t.toggle=Boolean(t.toggle),t.parent=r(t.parent),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=this._getFirstLevelChildren(Ft);for(const e of t){const t=z.getElementFromSelector(e);t&&this._addAriaAndCollapsedClass([e],this._isShown(t))}}_getFirstLevelChildren(t){const e=z.find(Mt,this._config.parent);return z.find(t,this._config.parent).filter((t=>!e.includes(t)))}_addAriaAndCollapsedClass(t,e){if(t.length)for(const i of t)i.classList.toggle("collapsed",!e),i.setAttribute("aria-expanded",e)}static jQueryInterface(t){const e={};return"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1),this.each((function(){const i=Bt.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}N.on(document,It,Ft,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();for(const t of z.getMultipleElementsFromSelector(this))Bt.getOrCreateInstance(t,{toggle:!1}).toggle()})),m(Bt);var zt="top",Rt="bottom",qt="right",Vt="left",Kt="auto",Qt=[zt,Rt,qt,Vt],Xt="start",Yt="end",Ut="clippingParents",Gt="viewport",Jt="popper",Zt="reference",te=Qt.reduce((function(t,e){return t.concat([e+"-"+Xt,e+"-"+Yt])}),[]),ee=[].concat(Qt,[Kt]).reduce((function(t,e){return t.concat([e,e+"-"+Xt,e+"-"+Yt])}),[]),ie="beforeRead",ne="read",se="afterRead",oe="beforeMain",re="main",ae="afterMain",le="beforeWrite",ce="write",he="afterWrite",de=[ie,ne,se,oe,re,ae,le,ce,he];function ue(t){return t?(t.nodeName||"").toLowerCase():null}function fe(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function pe(t){return t instanceof fe(t).Element||t instanceof Element}function me(t){return t instanceof fe(t).HTMLElement||t instanceof HTMLElement}function ge(t){return"undefined"!=typeof ShadowRoot&&(t instanceof fe(t).ShadowRoot||t instanceof ShadowRoot)}const _e={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];me(s)&&ue(s)&&(Object.assign(s.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});me(n)&&ue(n)&&(Object.assign(n.style,o),Object.keys(s).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function be(t){return t.split("-")[0]}var ve=Math.max,ye=Math.min,we=Math.round;function Ae(){var t=navigator.userAgentData;return null!=t&&t.brands&&Array.isArray(t.brands)?t.brands.map((function(t){return t.brand+"/"+t.version})).join(" "):navigator.userAgent}function Ee(){return!/^((?!chrome|android).)*safari/i.test(Ae())}function Te(t,e,i){void 0===e&&(e=!1),void 0===i&&(i=!1);var n=t.getBoundingClientRect(),s=1,o=1;e&&me(t)&&(s=t.offsetWidth>0&&we(n.width)/t.offsetWidth||1,o=t.offsetHeight>0&&we(n.height)/t.offsetHeight||1);var r=(pe(t)?fe(t):window).visualViewport,a=!Ee()&&i,l=(n.left+(a&&r?r.offsetLeft:0))/s,c=(n.top+(a&&r?r.offsetTop:0))/o,h=n.width/s,d=n.height/o;return{width:h,height:d,top:c,right:l+h,bottom:c+d,left:l,x:l,y:c}}function Ce(t){var e=Te(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function Oe(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&ge(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function xe(t){return fe(t).getComputedStyle(t)}function ke(t){return["table","td","th"].indexOf(ue(t))>=0}function Le(t){return((pe(t)?t.ownerDocument:t.document)||window.document).documentElement}function Se(t){return"html"===ue(t)?t:t.assignedSlot||t.parentNode||(ge(t)?t.host:null)||Le(t)}function De(t){return me(t)&&"fixed"!==xe(t).position?t.offsetParent:null}function $e(t){for(var e=fe(t),i=De(t);i&&ke(i)&&"static"===xe(i).position;)i=De(i);return i&&("html"===ue(i)||"body"===ue(i)&&"static"===xe(i).position)?e:i||function(t){var e=/firefox/i.test(Ae());if(/Trident/i.test(Ae())&&me(t)&&"fixed"===xe(t).position)return null;var i=Se(t);for(ge(i)&&(i=i.host);me(i)&&["html","body"].indexOf(ue(i))<0;){var n=xe(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function Ie(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}function Ne(t,e,i){return ve(t,ye(e,i))}function Pe(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function je(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}const Me={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,n=t.name,s=t.options,o=i.elements.arrow,r=i.modifiersData.popperOffsets,a=be(i.placement),l=Ie(a),c=[Vt,qt].indexOf(a)>=0?"height":"width";if(o&&r){var h=function(t,e){return Pe("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:je(t,Qt))}(s.padding,i),d=Ce(o),u="y"===l?zt:Vt,f="y"===l?Rt:qt,p=i.rects.reference[c]+i.rects.reference[l]-r[l]-i.rects.popper[c],m=r[l]-i.rects.reference[l],g=$e(o),_=g?"y"===l?g.clientHeight||0:g.clientWidth||0:0,b=p/2-m/2,v=h[u],y=_-d[c]-h[f],w=_/2-d[c]/2+b,A=Ne(v,w,y),E=l;i.modifiersData[n]=((e={})[E]=A,e.centerOffset=A-w,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&Oe(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function Fe(t){return t.split("-")[1]}var He={top:"auto",right:"auto",bottom:"auto",left:"auto"};function We(t){var e,i=t.popper,n=t.popperRect,s=t.placement,o=t.variation,r=t.offsets,a=t.position,l=t.gpuAcceleration,c=t.adaptive,h=t.roundOffsets,d=t.isFixed,u=r.x,f=void 0===u?0:u,p=r.y,m=void 0===p?0:p,g="function"==typeof h?h({x:f,y:m}):{x:f,y:m};f=g.x,m=g.y;var _=r.hasOwnProperty("x"),b=r.hasOwnProperty("y"),v=Vt,y=zt,w=window;if(c){var A=$e(i),E="clientHeight",T="clientWidth";A===fe(i)&&"static"!==xe(A=Le(i)).position&&"absolute"===a&&(E="scrollHeight",T="scrollWidth"),(s===zt||(s===Vt||s===qt)&&o===Yt)&&(y=Rt,m-=(d&&A===w&&w.visualViewport?w.visualViewport.height:A[E])-n.height,m*=l?1:-1),s!==Vt&&(s!==zt&&s!==Rt||o!==Yt)||(v=qt,f-=(d&&A===w&&w.visualViewport?w.visualViewport.width:A[T])-n.width,f*=l?1:-1)}var C,O=Object.assign({position:a},c&&He),x=!0===h?function(t,e){var i=t.x,n=t.y,s=e.devicePixelRatio||1;return{x:we(i*s)/s||0,y:we(n*s)/s||0}}({x:f,y:m},fe(i)):{x:f,y:m};return f=x.x,m=x.y,l?Object.assign({},O,((C={})[y]=b?"0":"",C[v]=_?"0":"",C.transform=(w.devicePixelRatio||1)<=1?"translate("+f+"px, "+m+"px)":"translate3d("+f+"px, "+m+"px, 0)",C)):Object.assign({},O,((e={})[y]=b?m+"px":"",e[v]=_?f+"px":"",e.transform="",e))}const Be={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:be(e.placement),variation:Fe(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s,isFixed:"fixed"===e.options.strategy};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,We(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,We(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}};var ze={passive:!0};const Re={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=fe(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,ze)})),a&&l.addEventListener("resize",i.update,ze),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,ze)})),a&&l.removeEventListener("resize",i.update,ze)}},data:{}};var qe={left:"right",right:"left",bottom:"top",top:"bottom"};function Ve(t){return t.replace(/left|right|bottom|top/g,(function(t){return qe[t]}))}var Ke={start:"end",end:"start"};function Qe(t){return t.replace(/start|end/g,(function(t){return Ke[t]}))}function Xe(t){var e=fe(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function Ye(t){return Te(Le(t)).left+Xe(t).scrollLeft}function Ue(t){var e=xe(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function Ge(t){return["html","body","#document"].indexOf(ue(t))>=0?t.ownerDocument.body:me(t)&&Ue(t)?t:Ge(Se(t))}function Je(t,e){var i;void 0===e&&(e=[]);var n=Ge(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=fe(n),r=s?[o].concat(o.visualViewport||[],Ue(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(Je(Se(r)))}function Ze(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function ti(t,e,i){return e===Gt?Ze(function(t,e){var i=fe(t),n=Le(t),s=i.visualViewport,o=n.clientWidth,r=n.clientHeight,a=0,l=0;if(s){o=s.width,r=s.height;var c=Ee();(c||!c&&"fixed"===e)&&(a=s.offsetLeft,l=s.offsetTop)}return{width:o,height:r,x:a+Ye(t),y:l}}(t,i)):pe(e)?function(t,e){var i=Te(t,!1,"fixed"===e);return i.top=i.top+t.clientTop,i.left=i.left+t.clientLeft,i.bottom=i.top+t.clientHeight,i.right=i.left+t.clientWidth,i.width=t.clientWidth,i.height=t.clientHeight,i.x=i.left,i.y=i.top,i}(e,i):Ze(function(t){var e,i=Le(t),n=Xe(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=ve(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=ve(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+Ye(t),l=-n.scrollTop;return"rtl"===xe(s||i).direction&&(a+=ve(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(Le(t)))}function ei(t){var e,i=t.reference,n=t.element,s=t.placement,o=s?be(s):null,r=s?Fe(s):null,a=i.x+i.width/2-n.width/2,l=i.y+i.height/2-n.height/2;switch(o){case zt:e={x:a,y:i.y-n.height};break;case Rt:e={x:a,y:i.y+i.height};break;case qt:e={x:i.x+i.width,y:l};break;case Vt:e={x:i.x-n.width,y:l};break;default:e={x:i.x,y:i.y}}var c=o?Ie(o):null;if(null!=c){var h="y"===c?"height":"width";switch(r){case Xt:e[c]=e[c]-(i[h]/2-n[h]/2);break;case Yt:e[c]=e[c]+(i[h]/2-n[h]/2)}}return e}function ii(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=void 0===n?t.placement:n,o=i.strategy,r=void 0===o?t.strategy:o,a=i.boundary,l=void 0===a?Ut:a,c=i.rootBoundary,h=void 0===c?Gt:c,d=i.elementContext,u=void 0===d?Jt:d,f=i.altBoundary,p=void 0!==f&&f,m=i.padding,g=void 0===m?0:m,_=Pe("number"!=typeof g?g:je(g,Qt)),b=u===Jt?Zt:Jt,v=t.rects.popper,y=t.elements[p?b:u],w=function(t,e,i,n){var s="clippingParents"===e?function(t){var e=Je(Se(t)),i=["absolute","fixed"].indexOf(xe(t).position)>=0&&me(t)?$e(t):t;return pe(i)?e.filter((function(t){return pe(t)&&Oe(t,i)&&"body"!==ue(t)})):[]}(t):[].concat(e),o=[].concat(s,[i]),r=o[0],a=o.reduce((function(e,i){var s=ti(t,i,n);return e.top=ve(s.top,e.top),e.right=ye(s.right,e.right),e.bottom=ye(s.bottom,e.bottom),e.left=ve(s.left,e.left),e}),ti(t,r,n));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}(pe(y)?y:y.contextElement||Le(t.elements.popper),l,h,r),A=Te(t.elements.reference),E=ei({reference:A,element:v,strategy:"absolute",placement:s}),T=Ze(Object.assign({},v,E)),C=u===Jt?T:A,O={top:w.top-C.top+_.top,bottom:C.bottom-w.bottom+_.bottom,left:w.left-C.left+_.left,right:C.right-w.right+_.right},x=t.modifiersData.offset;if(u===Jt&&x){var k=x[s];Object.keys(O).forEach((function(t){var e=[qt,Rt].indexOf(t)>=0?1:-1,i=[zt,Rt].indexOf(t)>=0?"y":"x";O[t]+=k[i]*e}))}return O}function ni(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,l=i.allowedAutoPlacements,c=void 0===l?ee:l,h=Fe(n),d=h?a?te:te.filter((function(t){return Fe(t)===h})):Qt,u=d.filter((function(t){return c.indexOf(t)>=0}));0===u.length&&(u=d);var f=u.reduce((function(e,i){return e[i]=ii(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[be(i)],e}),{});return Object.keys(f).sort((function(t,e){return f[t]-f[e]}))}const si={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name;if(!e.modifiersData[n]._skip){for(var s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0===r||r,l=i.fallbackPlacements,c=i.padding,h=i.boundary,d=i.rootBoundary,u=i.altBoundary,f=i.flipVariations,p=void 0===f||f,m=i.allowedAutoPlacements,g=e.options.placement,_=be(g),b=l||(_!==g&&p?function(t){if(be(t)===Kt)return[];var e=Ve(t);return[Qe(t),e,Qe(e)]}(g):[Ve(g)]),v=[g].concat(b).reduce((function(t,i){return t.concat(be(i)===Kt?ni(e,{placement:i,boundary:h,rootBoundary:d,padding:c,flipVariations:p,allowedAutoPlacements:m}):i)}),[]),y=e.rects.reference,w=e.rects.popper,A=new Map,E=!0,T=v[0],C=0;C=0,S=L?"width":"height",D=ii(e,{placement:O,boundary:h,rootBoundary:d,altBoundary:u,padding:c}),$=L?k?qt:Vt:k?Rt:zt;y[S]>w[S]&&($=Ve($));var I=Ve($),N=[];if(o&&N.push(D[x]<=0),a&&N.push(D[$]<=0,D[I]<=0),N.every((function(t){return t}))){T=O,E=!1;break}A.set(O,N)}if(E)for(var P=function(t){var e=v.find((function(e){var i=A.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return T=e,"break"},j=p?3:1;j>0&&"break"!==P(j);j--);e.placement!==T&&(e.modifiersData[n]._skip=!0,e.placement=T,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function oi(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function ri(t){return[zt,qt,Rt,Vt].some((function(e){return t[e]>=0}))}const ai={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=ii(e,{elementContext:"reference"}),a=ii(e,{altBoundary:!0}),l=oi(r,n),c=oi(a,s,o),h=ri(l),d=ri(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":h,"data-popper-escaped":d})}},li={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.offset,o=void 0===s?[0,0]:s,r=ee.reduce((function(t,i){return t[i]=function(t,e,i){var n=be(t),s=[Vt,zt].indexOf(n)>=0?-1:1,o="function"==typeof i?i(Object.assign({},e,{placement:t})):i,r=o[0],a=o[1];return r=r||0,a=(a||0)*s,[Vt,qt].indexOf(n)>=0?{x:a,y:r}:{x:r,y:a}}(i,e.rects,o),t}),{}),a=r[e.placement],l=a.x,c=a.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=l,e.modifiersData.popperOffsets.y+=c),e.modifiersData[n]=r}},ci={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=ei({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}},hi={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0!==r&&r,l=i.boundary,c=i.rootBoundary,h=i.altBoundary,d=i.padding,u=i.tether,f=void 0===u||u,p=i.tetherOffset,m=void 0===p?0:p,g=ii(e,{boundary:l,rootBoundary:c,padding:d,altBoundary:h}),_=be(e.placement),b=Fe(e.placement),v=!b,y=Ie(_),w="x"===y?"y":"x",A=e.modifiersData.popperOffsets,E=e.rects.reference,T=e.rects.popper,C="function"==typeof m?m(Object.assign({},e.rects,{placement:e.placement})):m,O="number"==typeof C?{mainAxis:C,altAxis:C}:Object.assign({mainAxis:0,altAxis:0},C),x=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,k={x:0,y:0};if(A){if(o){var L,S="y"===y?zt:Vt,D="y"===y?Rt:qt,$="y"===y?"height":"width",I=A[y],N=I+g[S],P=I-g[D],j=f?-T[$]/2:0,M=b===Xt?E[$]:T[$],F=b===Xt?-T[$]:-E[$],H=e.elements.arrow,W=f&&H?Ce(H):{width:0,height:0},B=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},z=B[S],R=B[D],q=Ne(0,E[$],W[$]),V=v?E[$]/2-j-q-z-O.mainAxis:M-q-z-O.mainAxis,K=v?-E[$]/2+j+q+R+O.mainAxis:F+q+R+O.mainAxis,Q=e.elements.arrow&&$e(e.elements.arrow),X=Q?"y"===y?Q.clientTop||0:Q.clientLeft||0:0,Y=null!=(L=null==x?void 0:x[y])?L:0,U=I+K-Y,G=Ne(f?ye(N,I+V-Y-X):N,I,f?ve(P,U):P);A[y]=G,k[y]=G-I}if(a){var J,Z="x"===y?zt:Vt,tt="x"===y?Rt:qt,et=A[w],it="y"===w?"height":"width",nt=et+g[Z],st=et-g[tt],ot=-1!==[zt,Vt].indexOf(_),rt=null!=(J=null==x?void 0:x[w])?J:0,at=ot?nt:et-E[it]-T[it]-rt+O.altAxis,lt=ot?et+E[it]+T[it]-rt-O.altAxis:st,ct=f&&ot?function(t,e,i){var n=Ne(t,e,i);return n>i?i:n}(at,et,lt):Ne(f?at:nt,et,f?lt:st);A[w]=ct,k[w]=ct-et}e.modifiersData[n]=k}},requiresIfExists:["offset"]};function di(t,e,i){void 0===i&&(i=!1);var n,s,o=me(e),r=me(e)&&function(t){var e=t.getBoundingClientRect(),i=we(e.width)/t.offsetWidth||1,n=we(e.height)/t.offsetHeight||1;return 1!==i||1!==n}(e),a=Le(e),l=Te(t,r,i),c={scrollLeft:0,scrollTop:0},h={x:0,y:0};return(o||!o&&!i)&&(("body"!==ue(e)||Ue(a))&&(c=(n=e)!==fe(n)&&me(n)?{scrollLeft:(s=n).scrollLeft,scrollTop:s.scrollTop}:Xe(n)),me(e)?((h=Te(e,!0)).x+=e.clientLeft,h.y+=e.clientTop):a&&(h.x=Ye(a))),{x:l.left+c.scrollLeft-h.x,y:l.top+c.scrollTop-h.y,width:l.width,height:l.height}}function ui(t){var e=new Map,i=new Set,n=[];function s(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach((function(t){if(!i.has(t)){var n=e.get(t);n&&s(n)}})),n.push(t)}return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||s(t)})),n}var fi={placement:"bottom",modifiers:[],strategy:"absolute"};function pi(){for(var t=arguments.length,e=new Array(t),i=0;iNumber.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||"static"===this._config.display)&&(F.setDataAttribute(this._menu,"popper","static"),t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,...g(this._config.popperConfig,[t])}}_selectMenuItem({key:t,target:e}){const i=z.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter((t=>a(t)));i.length&&b(i,e,t===Ti,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=qi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(2===t.button||"keyup"===t.type&&"Tab"!==t.key)return;const e=z.find(Ni);for(const i of e){const e=qi.getInstance(i);if(!e||!1===e._config.autoClose)continue;const n=t.composedPath(),s=n.includes(e._menu);if(n.includes(e._element)||"inside"===e._config.autoClose&&!s||"outside"===e._config.autoClose&&s)continue;if(e._menu.contains(t.target)&&("keyup"===t.type&&"Tab"===t.key||/input|select|option|textarea|form/i.test(t.target.tagName)))continue;const o={relatedTarget:e._element};"click"===t.type&&(o.clickEvent=t),e._completeHide(o)}}static dataApiKeydownHandler(t){const e=/input|textarea/i.test(t.target.tagName),i="Escape"===t.key,n=[Ei,Ti].includes(t.key);if(!n&&!i)return;if(e&&!i)return;t.preventDefault();const s=this.matches(Ii)?this:z.prev(this,Ii)[0]||z.next(this,Ii)[0]||z.findOne(Ii,t.delegateTarget.parentNode),o=qi.getOrCreateInstance(s);if(n)return t.stopPropagation(),o.show(),void o._selectMenuItem(t);o._isShown()&&(t.stopPropagation(),o.hide(),s.focus())}}N.on(document,Si,Ii,qi.dataApiKeydownHandler),N.on(document,Si,Pi,qi.dataApiKeydownHandler),N.on(document,Li,qi.clearMenus),N.on(document,Di,qi.clearMenus),N.on(document,Li,Ii,(function(t){t.preventDefault(),qi.getOrCreateInstance(this).toggle()})),m(qi);const Vi="backdrop",Ki="show",Qi=`mousedown.bs.${Vi}`,Xi={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},Yi={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class Ui extends H{constructor(t){super(),this._config=this._getConfig(t),this._isAppended=!1,this._element=null}static get Default(){return Xi}static get DefaultType(){return Yi}static get NAME(){return Vi}show(t){if(!this._config.isVisible)return void g(t);this._append();const e=this._getElement();this._config.isAnimated&&d(e),e.classList.add(Ki),this._emulateAnimation((()=>{g(t)}))}hide(t){this._config.isVisible?(this._getElement().classList.remove(Ki),this._emulateAnimation((()=>{this.dispose(),g(t)}))):g(t)}dispose(){this._isAppended&&(N.off(this._element,Qi),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_configAfterMerge(t){return t.rootElement=r(t.rootElement),t}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),N.on(t,Qi,(()=>{g(this._config.clickCallback)})),this._isAppended=!0}_emulateAnimation(t){_(t,this._getElement(),this._config.isAnimated)}}const Gi=".bs.focustrap",Ji=`focusin${Gi}`,Zi=`keydown.tab${Gi}`,tn="backward",en={autofocus:!0,trapElement:null},nn={autofocus:"boolean",trapElement:"element"};class sn extends H{constructor(t){super(),this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return en}static get DefaultType(){return nn}static get NAME(){return"focustrap"}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),N.off(document,Gi),N.on(document,Ji,(t=>this._handleFocusin(t))),N.on(document,Zi,(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,N.off(document,Gi))}_handleFocusin(t){const{trapElement:e}=this._config;if(t.target===document||t.target===e||e.contains(t.target))return;const i=z.focusableChildren(e);0===i.length?e.focus():this._lastTabNavDirection===tn?i[i.length-1].focus():i[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?tn:"forward")}}const on=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",rn=".sticky-top",an="padding-right",ln="margin-right";class cn{constructor(){this._element=document.body}getWidth(){const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)}hide(){const t=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,an,(e=>e+t)),this._setElementAttributes(on,an,(e=>e+t)),this._setElementAttributes(rn,ln,(e=>e-t))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,an),this._resetElementAttributes(on,an),this._resetElementAttributes(rn,ln)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t).getPropertyValue(e);t.style.setProperty(e,`${i(Number.parseFloat(s))}px`)}))}_saveInitialAttribute(t,e){const i=t.style.getPropertyValue(e);i&&F.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=F.getDataAttribute(t,e);null!==i?(F.removeDataAttribute(t,e),t.style.setProperty(e,i)):t.style.removeProperty(e)}))}_applyManipulationCallback(t,e){if(o(t))e(t);else for(const i of z.find(t,this._element))e(i)}}const hn=".bs.modal",dn=`hide${hn}`,un=`hidePrevented${hn}`,fn=`hidden${hn}`,pn=`show${hn}`,mn=`shown${hn}`,gn=`resize${hn}`,_n=`click.dismiss${hn}`,bn=`mousedown.dismiss${hn}`,vn=`keydown.dismiss${hn}`,yn=`click${hn}.data-api`,wn="modal-open",An="show",En="modal-static",Tn={backdrop:!0,focus:!0,keyboard:!0},Cn={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class On extends W{constructor(t,e){super(t,e),this._dialog=z.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new cn,this._addEventListeners()}static get Default(){return Tn}static get DefaultType(){return Cn}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||N.trigger(this._element,pn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(wn),this._adjustDialog(),this._backdrop.show((()=>this._showElement(t))))}hide(){this._isShown&&!this._isTransitioning&&(N.trigger(this._element,dn).defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(An),this._queueCallback((()=>this._hideModal()),this._element,this._isAnimated())))}dispose(){N.off(window,hn),N.off(this._dialog,hn),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new Ui({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new sn({trapElement:this._element})}_showElement(t){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0;const e=z.findOne(".modal-body",this._dialog);e&&(e.scrollTop=0),d(this._element),this._element.classList.add(An),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,N.trigger(this._element,mn,{relatedTarget:t})}),this._dialog,this._isAnimated())}_addEventListeners(){N.on(this._element,vn,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():this._triggerBackdropTransition())})),N.on(window,gn,(()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()})),N.on(this._element,bn,(t=>{N.one(this._element,_n,(e=>{this._element===t.target&&this._element===e.target&&("static"!==this._config.backdrop?this._config.backdrop&&this.hide():this._triggerBackdropTransition())}))}))}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide((()=>{document.body.classList.remove(wn),this._resetAdjustments(),this._scrollBar.reset(),N.trigger(this._element,fn)}))}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(N.trigger(this._element,un).defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._element.style.overflowY;"hidden"===e||this._element.classList.contains(En)||(t||(this._element.style.overflowY="hidden"),this._element.classList.add(En),this._queueCallback((()=>{this._element.classList.remove(En),this._queueCallback((()=>{this._element.style.overflowY=e}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;if(i&&!t){const t=p()?"paddingLeft":"paddingRight";this._element.style[t]=`${e}px`}if(!i&&t){const t=p()?"paddingRight":"paddingLeft";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=On.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}N.on(document,yn,'[data-bs-toggle="modal"]',(function(t){const e=z.getElementFromSelector(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),N.one(e,pn,(t=>{t.defaultPrevented||N.one(e,fn,(()=>{a(this)&&this.focus()}))}));const i=z.findOne(".modal.show");i&&On.getInstance(i).hide(),On.getOrCreateInstance(e).toggle(this)})),R(On),m(On);const xn=".bs.offcanvas",kn=".data-api",Ln=`load${xn}${kn}`,Sn="show",Dn="showing",$n="hiding",In=".offcanvas.show",Nn=`show${xn}`,Pn=`shown${xn}`,jn=`hide${xn}`,Mn=`hidePrevented${xn}`,Fn=`hidden${xn}`,Hn=`resize${xn}`,Wn=`click${xn}${kn}`,Bn=`keydown.dismiss${xn}`,zn={backdrop:!0,keyboard:!0,scroll:!1},Rn={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class qn extends W{constructor(t,e){super(t,e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return zn}static get DefaultType(){return Rn}static get NAME(){return"offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||N.trigger(this._element,Nn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new cn).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(Dn),this._queueCallback((()=>{this._config.scroll&&!this._config.backdrop||this._focustrap.activate(),this._element.classList.add(Sn),this._element.classList.remove(Dn),N.trigger(this._element,Pn,{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&(N.trigger(this._element,jn).defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add($n),this._backdrop.hide(),this._queueCallback((()=>{this._element.classList.remove(Sn,$n),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new cn).reset(),N.trigger(this._element,Fn)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const t=Boolean(this._config.backdrop);return new Ui({className:"offcanvas-backdrop",isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?()=>{"static"!==this._config.backdrop?this.hide():N.trigger(this._element,Mn)}:null})}_initializeFocusTrap(){return new sn({trapElement:this._element})}_addEventListeners(){N.on(this._element,Bn,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():N.trigger(this._element,Mn))}))}static jQueryInterface(t){return this.each((function(){const e=qn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}N.on(document,Wn,'[data-bs-toggle="offcanvas"]',(function(t){const e=z.getElementFromSelector(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),l(this))return;N.one(e,Fn,(()=>{a(this)&&this.focus()}));const i=z.findOne(In);i&&i!==e&&qn.getInstance(i).hide(),qn.getOrCreateInstance(e).toggle(this)})),N.on(window,Ln,(()=>{for(const t of z.find(In))qn.getOrCreateInstance(t).show()})),N.on(window,Hn,(()=>{for(const t of z.find("[aria-modal][class*=show][class*=offcanvas-]"))"fixed"!==getComputedStyle(t).position&&qn.getOrCreateInstance(t).hide()})),R(qn),m(qn);const Vn={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],dd:[],div:[],dl:[],dt:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},Kn=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Qn=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,Xn=(t,e)=>{const i=t.nodeName.toLowerCase();return e.includes(i)?!Kn.has(i)||Boolean(Qn.test(t.nodeValue)):e.filter((t=>t instanceof RegExp)).some((t=>t.test(i)))},Yn={allowList:Vn,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:"
"},Un={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},Gn={entry:"(string|element|function|null)",selector:"(string|element)"};class Jn extends H{constructor(t){super(),this._config=this._getConfig(t)}static get Default(){return Yn}static get DefaultType(){return Un}static get NAME(){return"TemplateFactory"}getContent(){return Object.values(this._config.content).map((t=>this._resolvePossibleFunction(t))).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(t){return this._checkContent(t),this._config.content={...this._config.content,...t},this}toHtml(){const t=document.createElement("div");t.innerHTML=this._maybeSanitize(this._config.template);for(const[e,i]of Object.entries(this._config.content))this._setContent(t,i,e);const e=t.children[0],i=this._resolvePossibleFunction(this._config.extraClass);return i&&e.classList.add(...i.split(" ")),e}_typeCheckConfig(t){super._typeCheckConfig(t),this._checkContent(t.content)}_checkContent(t){for(const[e,i]of Object.entries(t))super._typeCheckConfig({selector:e,entry:i},Gn)}_setContent(t,e,i){const n=z.findOne(i,t);n&&((e=this._resolvePossibleFunction(e))?o(e)?this._putElementInTemplate(r(e),n):this._config.html?n.innerHTML=this._maybeSanitize(e):n.textContent=e:n.remove())}_maybeSanitize(t){return this._config.sanitize?function(t,e,i){if(!t.length)return t;if(i&&"function"==typeof i)return i(t);const n=(new window.DOMParser).parseFromString(t,"text/html"),s=[].concat(...n.body.querySelectorAll("*"));for(const t of s){const i=t.nodeName.toLowerCase();if(!Object.keys(e).includes(i)){t.remove();continue}const n=[].concat(...t.attributes),s=[].concat(e["*"]||[],e[i]||[]);for(const e of n)Xn(e,s)||t.removeAttribute(e.nodeName)}return n.body.innerHTML}(t,this._config.allowList,this._config.sanitizeFn):t}_resolvePossibleFunction(t){return g(t,[this])}_putElementInTemplate(t,e){if(this._config.html)return e.innerHTML="",void e.append(t);e.textContent=t.textContent}}const Zn=new Set(["sanitize","allowList","sanitizeFn"]),ts="fade",es="show",is=".modal",ns="hide.bs.modal",ss="hover",os="focus",rs={AUTO:"auto",TOP:"top",RIGHT:p()?"left":"right",BOTTOM:"bottom",LEFT:p()?"right":"left"},as={allowList:Vn,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,6],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},ls={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class cs extends W{constructor(t,e){if(void 0===vi)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t,e),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return as}static get DefaultType(){return ls}static get NAME(){return"tooltip"}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._activeTrigger.click=!this._activeTrigger.click,this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),N.off(this._element.closest(is),ns,this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const t=N.trigger(this._element,this.constructor.eventName("show")),e=(c(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(t.defaultPrevented||!e)return;this._disposePopper();const i=this._getTipElement();this._element.setAttribute("aria-describedby",i.getAttribute("id"));const{container:n}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(n.append(i),N.trigger(this._element,this.constructor.eventName("inserted"))),this._popper=this._createPopper(i),i.classList.add(es),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))N.on(t,"mouseover",h);this._queueCallback((()=>{N.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1}),this.tip,this._isAnimated())}hide(){if(this._isShown()&&!N.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented){if(this._getTipElement().classList.remove(es),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))N.off(t,"mouseover",h);this._activeTrigger.click=!1,this._activeTrigger[os]=!1,this._activeTrigger[ss]=!1,this._isHovered=null,this._queueCallback((()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),N.trigger(this._element,this.constructor.eventName("hidden")))}),this.tip,this._isAnimated())}}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(t){const e=this._getTemplateFactory(t).toHtml();if(!e)return null;e.classList.remove(ts,es),e.classList.add(`bs-${this.constructor.NAME}-auto`);const i=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME).toString();return e.setAttribute("id",i),this._isAnimated()&&e.classList.add(ts),e}setContent(t){this._newContent=t,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(t){return this._templateFactory?this._templateFactory.changeContent(t):this._templateFactory=new Jn({...this._config,content:t,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{".tooltip-inner":this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(t){return this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(ts)}_isShown(){return this.tip&&this.tip.classList.contains(es)}_createPopper(t){const e=g(this._config.placement,[this,t,this._element]),i=rs[e.toUpperCase()];return bi(this._element,t,this._getPopperConfig(i))}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return g(t,[this._element])}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:t=>{this._getTipElement().setAttribute("data-popper-placement",t.state.placement)}}]};return{...e,...g(this._config.popperConfig,[e])}}_setListeners(){const t=this._config.trigger.split(" ");for(const e of t)if("click"===e)N.on(this._element,this.constructor.eventName("click"),this._config.selector,(t=>{this._initializeOnDelegatedTarget(t).toggle()}));else if("manual"!==e){const t=e===ss?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),i=e===ss?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");N.on(this._element,t,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusin"===t.type?os:ss]=!0,e._enter()})),N.on(this._element,i,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusout"===t.type?os:ss]=e._element.contains(t.relatedTarget),e._leave()}))}this._hideModalHandler=()=>{this._element&&this.hide()},N.on(this._element.closest(is),ns,this._hideModalHandler)}_fixTitle(){const t=this._element.getAttribute("title");t&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",t),this._element.setAttribute("data-bs-original-title",t),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout((()=>{this._isHovered&&this.show()}),this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout((()=>{this._isHovered||this.hide()}),this._config.delay.hide))}_setTimeout(t,e){clearTimeout(this._timeout),this._timeout=setTimeout(t,e)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(t){const e=F.getDataAttributes(this._element);for(const t of Object.keys(e))Zn.has(t)&&delete e[t];return t={...e,..."object"==typeof t&&t?t:{}},t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t.container=!1===t.container?document.body:r(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),t}_getDelegateConfig(){const t={};for(const[e,i]of Object.entries(this._config))this.constructor.Default[e]!==i&&(t[e]=i);return t.selector=!1,t.trigger="manual",t}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(t){return this.each((function(){const e=cs.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}m(cs);const hs={...cs.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},ds={...cs.DefaultType,content:"(null|string|element|function)"};class us extends cs{static get Default(){return hs}static get DefaultType(){return ds}static get NAME(){return"popover"}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{".popover-header":this._getTitle(),".popover-body":this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(t){return this.each((function(){const e=us.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}m(us);const fs=".bs.scrollspy",ps=`activate${fs}`,ms=`click${fs}`,gs=`load${fs}.data-api`,_s="active",bs="[href]",vs=".nav-link",ys=`${vs}, .nav-item > ${vs}, .list-group-item`,ws={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},As={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"};class Es extends W{constructor(t,e){super(t,e),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return ws}static get DefaultType(){return As}static get NAME(){return"scrollspy"}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const t of this._observableSections.values())this._observer.observe(t)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(t){return t.target=r(t.target)||document.body,t.rootMargin=t.offset?`${t.offset}px 0px -30%`:t.rootMargin,"string"==typeof t.threshold&&(t.threshold=t.threshold.split(",").map((t=>Number.parseFloat(t)))),t}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(N.off(this._config.target,ms),N.on(this._config.target,ms,bs,(t=>{const e=this._observableSections.get(t.target.hash);if(e){t.preventDefault();const i=this._rootElement||window,n=e.offsetTop-this._element.offsetTop;if(i.scrollTo)return void i.scrollTo({top:n,behavior:"smooth"});i.scrollTop=n}})))}_getNewObserver(){const t={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver((t=>this._observerCallback(t)),t)}_observerCallback(t){const e=t=>this._targetLinks.get(`#${t.target.id}`),i=t=>{this._previousScrollData.visibleEntryTop=t.target.offsetTop,this._process(e(t))},n=(this._rootElement||document.documentElement).scrollTop,s=n>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=n;for(const o of t){if(!o.isIntersecting){this._activeTarget=null,this._clearActiveClass(e(o));continue}const t=o.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(s&&t){if(i(o),!n)return}else s||t||i(o)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const t=z.find(bs,this._config.target);for(const e of t){if(!e.hash||l(e))continue;const t=z.findOne(decodeURI(e.hash),this._element);a(t)&&(this._targetLinks.set(decodeURI(e.hash),e),this._observableSections.set(e.hash,t))}}_process(t){this._activeTarget!==t&&(this._clearActiveClass(this._config.target),this._activeTarget=t,t.classList.add(_s),this._activateParents(t),N.trigger(this._element,ps,{relatedTarget:t}))}_activateParents(t){if(t.classList.contains("dropdown-item"))z.findOne(".dropdown-toggle",t.closest(".dropdown")).classList.add(_s);else for(const e of z.parents(t,".nav, .list-group"))for(const t of z.prev(e,ys))t.classList.add(_s)}_clearActiveClass(t){t.classList.remove(_s);const e=z.find(`${bs}.${_s}`,t);for(const t of e)t.classList.remove(_s)}static jQueryInterface(t){return this.each((function(){const e=Es.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}N.on(window,gs,(()=>{for(const t of z.find('[data-bs-spy="scroll"]'))Es.getOrCreateInstance(t)})),m(Es);const Ts=".bs.tab",Cs=`hide${Ts}`,Os=`hidden${Ts}`,xs=`show${Ts}`,ks=`shown${Ts}`,Ls=`click${Ts}`,Ss=`keydown${Ts}`,Ds=`load${Ts}`,$s="ArrowLeft",Is="ArrowRight",Ns="ArrowUp",Ps="ArrowDown",js="Home",Ms="End",Fs="active",Hs="fade",Ws="show",Bs=".dropdown-toggle",zs=`:not(${Bs})`,Rs='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',qs=`.nav-link${zs}, .list-group-item${zs}, [role="tab"]${zs}, ${Rs}`,Vs=`.${Fs}[data-bs-toggle="tab"], .${Fs}[data-bs-toggle="pill"], .${Fs}[data-bs-toggle="list"]`;class Ks extends W{constructor(t){super(t),this._parent=this._element.closest('.list-group, .nav, [role="tablist"]'),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),N.on(this._element,Ss,(t=>this._keydown(t))))}static get NAME(){return"tab"}show(){const t=this._element;if(this._elemIsActive(t))return;const e=this._getActiveElem(),i=e?N.trigger(e,Cs,{relatedTarget:t}):null;N.trigger(t,xs,{relatedTarget:e}).defaultPrevented||i&&i.defaultPrevented||(this._deactivate(e,t),this._activate(t,e))}_activate(t,e){t&&(t.classList.add(Fs),this._activate(z.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.removeAttribute("tabindex"),t.setAttribute("aria-selected",!0),this._toggleDropDown(t,!0),N.trigger(t,ks,{relatedTarget:e})):t.classList.add(Ws)}),t,t.classList.contains(Hs)))}_deactivate(t,e){t&&(t.classList.remove(Fs),t.blur(),this._deactivate(z.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.setAttribute("aria-selected",!1),t.setAttribute("tabindex","-1"),this._toggleDropDown(t,!1),N.trigger(t,Os,{relatedTarget:e})):t.classList.remove(Ws)}),t,t.classList.contains(Hs)))}_keydown(t){if(![$s,Is,Ns,Ps,js,Ms].includes(t.key))return;t.stopPropagation(),t.preventDefault();const e=this._getChildren().filter((t=>!l(t)));let i;if([js,Ms].includes(t.key))i=e[t.key===js?0:e.length-1];else{const n=[Is,Ps].includes(t.key);i=b(e,t.target,n,!0)}i&&(i.focus({preventScroll:!0}),Ks.getOrCreateInstance(i).show())}_getChildren(){return z.find(qs,this._parent)}_getActiveElem(){return this._getChildren().find((t=>this._elemIsActive(t)))||null}_setInitialAttributes(t,e){this._setAttributeIfNotExists(t,"role","tablist");for(const t of e)this._setInitialAttributesOnChild(t)}_setInitialAttributesOnChild(t){t=this._getInnerElement(t);const e=this._elemIsActive(t),i=this._getOuterElement(t);t.setAttribute("aria-selected",e),i!==t&&this._setAttributeIfNotExists(i,"role","presentation"),e||t.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(t,"role","tab"),this._setInitialAttributesOnTargetPanel(t)}_setInitialAttributesOnTargetPanel(t){const e=z.getElementFromSelector(t);e&&(this._setAttributeIfNotExists(e,"role","tabpanel"),t.id&&this._setAttributeIfNotExists(e,"aria-labelledby",`${t.id}`))}_toggleDropDown(t,e){const i=this._getOuterElement(t);if(!i.classList.contains("dropdown"))return;const n=(t,n)=>{const s=z.findOne(t,i);s&&s.classList.toggle(n,e)};n(Bs,Fs),n(".dropdown-menu",Ws),i.setAttribute("aria-expanded",e)}_setAttributeIfNotExists(t,e,i){t.hasAttribute(e)||t.setAttribute(e,i)}_elemIsActive(t){return t.classList.contains(Fs)}_getInnerElement(t){return t.matches(qs)?t:z.findOne(qs,t)}_getOuterElement(t){return t.closest(".nav-item, .list-group-item")||t}static jQueryInterface(t){return this.each((function(){const e=Ks.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}N.on(document,Ls,Rs,(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),l(this)||Ks.getOrCreateInstance(this).show()})),N.on(window,Ds,(()=>{for(const t of z.find(Vs))Ks.getOrCreateInstance(t)})),m(Ks);const Qs=".bs.toast",Xs=`mouseover${Qs}`,Ys=`mouseout${Qs}`,Us=`focusin${Qs}`,Gs=`focusout${Qs}`,Js=`hide${Qs}`,Zs=`hidden${Qs}`,to=`show${Qs}`,eo=`shown${Qs}`,io="hide",no="show",so="showing",oo={animation:"boolean",autohide:"boolean",delay:"number"},ro={animation:!0,autohide:!0,delay:5e3};class ao extends W{constructor(t,e){super(t,e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return ro}static get DefaultType(){return oo}static get NAME(){return"toast"}show(){N.trigger(this._element,to).defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(io),d(this._element),this._element.classList.add(no,so),this._queueCallback((()=>{this._element.classList.remove(so),N.trigger(this._element,eo),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this.isShown()&&(N.trigger(this._element,Js).defaultPrevented||(this._element.classList.add(so),this._queueCallback((()=>{this._element.classList.add(io),this._element.classList.remove(so,no),N.trigger(this._element,Zs)}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(no),super.dispose()}isShown(){return this._element.classList.contains(no)}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout((()=>{this.hide()}),this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){N.on(this._element,Xs,(t=>this._onInteraction(t,!0))),N.on(this._element,Ys,(t=>this._onInteraction(t,!1))),N.on(this._element,Us,(t=>this._onInteraction(t,!0))),N.on(this._element,Gs,(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=ao.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return R(ao),m(ao),{Alert:Q,Button:Y,Carousel:xt,Collapse:Bt,Dropdown:qi,Modal:On,Offcanvas:qn,Popover:us,ScrollSpy:Es,Tab:Ks,Toast:ao,Tooltip:cs}})); diff --git a/public/assets/js/links-sortable.js b/public/assets/js/links-sortable.js new file mode 100644 index 0000000..7a02393 --- /dev/null +++ b/public/assets/js/links-sortable.js @@ -0,0 +1,29 @@ +(function() { + const list = document.getElementById('links-sortable'); + if (!list) return; + let dragged = null; + list.querySelectorAll('li').forEach(li => { + li.setAttribute('draggable', true); + li.addEventListener('dragstart', () => { dragged = li; li.style.opacity = '.4'; }); + li.addEventListener('dragend', () => { dragged = null; li.style.opacity = ''; saveOrder(); }); + li.addEventListener('dragover', e => { e.preventDefault(); const after = getDragAfter(list, e.clientY); after ? list.insertBefore(dragged, after) : list.appendChild(dragged); }); + }); + function getDragAfter(container, y) { + return [...container.querySelectorAll('li:not([style*="opacity"])')].reduce((closest, el) => { + const box = el.getBoundingClientRect(); + const offset = y - box.top - box.height / 2; + return offset < 0 && offset > closest.offset ? { offset, element: el } : closest; + }, { offset: Number.NEGATIVE_INFINITY }).element; + } + function saveOrder() { + const form = document.getElementById('reorder-form'); + if (!form) return; + form.querySelectorAll('input').forEach(i => i.remove()); + list.querySelectorAll('li[data-id]').forEach(li => { + const inp = document.createElement('input'); + inp.type = 'hidden'; inp.name = 'order[]'; inp.value = li.dataset.id; + form.appendChild(inp); + }); + form.submit(); + } +})(); diff --git a/public/assets/js/post_confirm.js b/public/assets/js/post_confirm.js new file mode 100644 index 0000000..2c0834f --- /dev/null +++ b/public/assets/js/post_confirm.js @@ -0,0 +1,62 @@ +document.addEventListener('DOMContentLoaded', function () { + var data = document.getElementById('pc-data'); + if (!data) return; + + var defaultTitle = data.dataset.defaultTitle; + var defaultDesc = data.dataset.defaultDesc; + var baseUrl = data.dataset.baseUrl; + + function initCounter(inputId, counterId, max) { + var el = document.getElementById(inputId); + var ct = document.getElementById(counterId); + if (!el || !ct) return; + function upd() { + var n = el.value.length; + ct.textContent = n + ' / ' + max; + ct.className = n > max ? 'text-danger' : 'text-muted'; + } + el.addEventListener('input', upd); + upd(); + } + initCounter('seo_title', 'seo_title_counter', 60); + initCounter('seo_description', 'seo_desc_counter', 155); + + function updatePreview() { + var seoTitle = document.getElementById('seo_title').value.trim(); + var seoDesc = document.getElementById('seo_description').value.trim(); + var slug = document.getElementById('confirm-slug').value.trim(); + document.getElementById('preview-title').textContent = seoTitle || defaultTitle; + document.getElementById('preview-desc').textContent = seoDesc || defaultDesc; + document.getElementById('preview-url').textContent = baseUrl + slug; + } + + ['seo_title', 'seo_description', 'confirm-slug'].forEach(function (id) { + var el = document.getElementById(id); + if (el) el.addEventListener('input', updatePreview); + }); + + var slugInput = document.getElementById('confirm-slug'); + var slugDisplay = document.getElementById('slug-display'); + + var btnSuggest = document.getElementById('slug-btn-suggest'); + if (btnSuggest) { + btnSuggest.addEventListener('click', function () { + var val = btnSuggest.dataset.slugSuggest; + slugInput.value = val; + slugDisplay.textContent = val; + updatePreview(); + }); + } + + var btnKeep = document.getElementById('slug-btn-keep'); + if (btnKeep) { + btnKeep.addEventListener('click', function () { + var val = btnKeep.dataset.slugKeep; + slugInput.value = val; + slugDisplay.textContent = val; + updatePreview(); + }); + } + + updatePreview(); +}); diff --git a/public/assets/js/reactions.js b/public/assets/js/reactions.js new file mode 100644 index 0000000..d5d4fa1 --- /dev/null +++ b/public/assets/js/reactions.js @@ -0,0 +1,45 @@ +// reactions.js — toggle réactions via fetch, fallback formulaire natif + +document.addEventListener('DOMContentLoaded', function () { + document.querySelectorAll('.reaction-form').forEach(function (form) { + form.addEventListener('submit', function (e) { + e.preventDefault(); + + var btn = form.querySelector('.reaction-btn'); + var type = form.querySelector('[name="type"]').value; + var uuid = form.querySelector('[name="uuid"]').value; + var badge = form.querySelector('.reaction-count'); + var active = btn.classList.contains('btn-primary'); + + var data = new URLSearchParams(); + data.append('uuid', uuid); + data.append('type', type); + data.append('_ajax', '1'); + + fetch('/react', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: data.toString(), + }) + .then(function (r) { return r.json(); }) + .then(function (json) { + if (!json.ok) { form.submit(); return; } + + var nowActive = json.active; + if (btn.classList.contains('hero-reaction-btn')) { + btn.classList.toggle('hero-reaction-btn--active', nowActive); + } else { + btn.classList.toggle('btn-primary', nowActive); + btn.classList.toggle('btn-outline-secondary', !nowActive); + if (badge) { + badge.classList.toggle('bg-light', nowActive); + badge.classList.toggle('text-primary', nowActive); + badge.classList.toggle('bg-secondary', !nowActive); + } + } + if (badge) { badge.textContent = json.count; } + }) + .catch(function () { form.submit(); }); + }); + }); +}); diff --git a/public/assets/js/toc.js b/public/assets/js/toc.js new file mode 100644 index 0000000..ea52b50 --- /dev/null +++ b/public/assets/js/toc.js @@ -0,0 +1,37 @@ +(function () { + var headings = document.querySelectorAll('.post-content h2, .post-content h3'); + var links = document.querySelectorAll('.toc-list a'); + + if (headings.length && links.length) { + var map = {}; + links.forEach(function (a) { + map[decodeURIComponent(a.getAttribute('href').slice(1))] = a; + }); + + var active = null; + var observer = new IntersectionObserver(function (entries) { + entries.forEach(function (entry) { + if (entry.isIntersecting) { + if (active) active.classList.remove('toc-active'); + active = map[entry.target.id] || null; + if (active) active.classList.add('toc-active'); + } + }); + }, { rootMargin: '-8% 0px -82% 0px', threshold: 0 }); + + headings.forEach(function (h) { observer.observe(h); }); + } + + var btnTop = document.getElementById('toc-go-top'); + var btnBot = document.getElementById('toc-go-bottom'); + if (btnTop) { + btnTop.addEventListener('click', function () { + window.scrollTo({ top: 0, behavior: 'smooth' }); + }); + } + if (btnBot) { + btnBot.addEventListener('click', function () { + window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); + }); + } +})(); diff --git a/public/feed.php b/public/feed.php new file mode 100644 index 0000000..302820e --- /dev/null +++ b/public/feed.php @@ -0,0 +1,110 @@ +getPrivateCategories(); +$Parsedown = new Parsedown(); + +$now = time(); +$base = rtrim(APP_URL, '/'); + +$all = array_values(array_filter( + $articles->getAll(publishedOnly: true), + static function (array $a) use ($now, $privateCats): bool { + if (strtotime((string)($a['published_at'] ?? '')) > $now) { + return false; + } + $cat = trim($a['category'] ?? ''); + return $cat === '' || !in_array($cat, $privateCats, true); + } +)); + +// ─── Pagination curseur ────────────────────────────────────────────────────── +$after = trim($_GET['after'] ?? ''); +$offset = 0; +if ($after !== '') { + foreach ($all as $i => $a) { + if ($a['uuid'] === $after) { + $offset = $i + 1; + break; + } + } +} + +$items = array_slice($all, $offset, FEED_PAGE_SIZE); +$nextCursor = (count($all) > $offset + FEED_PAGE_SIZE) + ? ($all[$offset + FEED_PAGE_SIZE - 1]['uuid'] ?? null) + : null; + +$feedUrl = $base . '/feed'; +$feedNextUrl = $nextCursor !== null ? $base . '/feed/' . $nextCursor : null; + +// ─── lastBuildDate ─────────────────────────────────────────────────────────── +$lastBuild = ''; +foreach ($all as $a) { + $ts = (int)strtotime((string)($a['updated_at'] ?? $a['published_at'] ?? '')); + if ($ts > (int)strtotime($lastBuild ?: '1970-01-01')) { + $lastBuild = date(DATE_RSS, $ts); + } +} +if ($lastBuild === '') { + $lastBuild = date(DATE_RSS); +} + +header('Content-Type: application/rss+xml; charset=UTF-8'); +header('X-Content-Type-Options: nosniff'); + +echo '' . "\n"; +?> + + + <?= htmlspecialchars(siteTitle()) ?> + + + + + + + + 0): ?> + + + + + diff --git a/public/file.php b/public/file.php new file mode 100644 index 0000000..7dbb4ff --- /dev/null +++ b/public/file.php @@ -0,0 +1,35 @@ + 0, 'path' => '/', 'secure' => $isHttps, 'httponly' => true, 'samesite' => 'Lax']); + session_start(); +} + +require_once BASE_PATH . '/src/helpers.php'; +require_once BASE_PATH . '/src/auth.php'; +require_once BASE_PATH . '/src/SiteSettings.php'; +require_once BASE_PATH . '/config/config.php'; +require_once BASE_PATH . '/src/ArticleManager.php'; + +$articles = new ArticleManager(BASE_PATH . '/data'); + +$action = $_GET['action'] ?? 'list'; +$uuid = $_GET['uuid'] ?? ''; +$slug = $_GET['slug'] ?? ''; + +$_noindexActions = ['create', 'edit', 'admin', 'categories', 'diff', 'add_files', 'import_image', 'import_image_step2', 'sources', 'profile', 'delete_file', 'delete_external_link', 'rename_category', 'delete_category', 'toggle_private_category', 'admin_save_site', 'not_found', 'add_feed', 'delete_feed', 'add_link', 'delete_link', 'reorder_links', 'react', 'comment', 'verify_comment', 'comment_moderate', 'comment_delete', 'comment_resend', 'create_tag_type', 'delete_tag_type', 'edit_tags']; +$metaRobots = in_array($action, $_noindexActions, true) ? 'noindex, nofollow' : null; +unset($_noindexActions); + +// ─── Recherche de l'article le plus proche et redirection 301 ──────────────── +function searchAndRedirect(string $rawPath, ArticleManager $articles): void +{ + require_once BASE_PATH . '/src/SearchEngine.php'; + $query = (string)preg_replace('/\s{2,}/', ' ', trim( + (string)preg_replace('/[^a-zA-ZÀ-ÿ0-9\s]/u', ' ', str_replace(['-', '_', '/'], ' ', $rawPath)) + )); + if ($query === '') { + return; + } + $privateCats = $articles->getPrivateCategories(); + $pool = array_values(array_filter( + $articles->getAll(true), + static function (array $a) use ($privateCats): bool { + if (strtotime((string)($a['published_at'] ?? '')) > time()) { + return false; + } + $cat = trim($a['category'] ?? ''); + return $cat === '' || !in_array($cat, $privateCats, true); + } + )); + $results = (new SearchEngine())->search($query, $pool); + if (!empty($results)) { + header('Location: /post/' . rawurlencode($results[0]['article']['slug'] ?? ''), true, 301); + exit; + } +} + +// ─── Pages statiques depuis data/site/ ────────────────────────────────────── +function loadSitePageData(string $slug): array +{ + $base = BASE_PATH . '/data/site'; + $meta = []; + $raw = @file_get_contents($base . '/' . $slug . '.json'); + if ($raw !== false) { + $meta = json_decode($raw, true) ?? []; + } + $html = ''; + $mdRaw = @file_get_contents($base . '/' . $slug . '.md'); + if ($mdRaw !== false) { + require_once BASE_PATH . '/src/Parsedown.php'; + $html = (new Parsedown())->text($mdRaw); + } + return ['meta' => $meta, 'html' => $html]; +} + +// ─── Extraction de métadonnées depuis une URL ──────────────────────────────── +function fetchUrlMeta(string $url): array +{ + if (!filter_var($url, FILTER_VALIDATE_URL) || !preg_match('#^https?://#i', $url)) { + return ['ok' => false, 'error' => 'URL invalide']; + } + $tmpFile = tempnam(sys_get_temp_dir(), 'vl_meta_'); + $fp = fopen($tmpFile, 'wb'); + $downloaded = 0; + $limit = 4 * 1024 * 1024; + $contentLength = null; + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_MAXREDIRS => 3, + CURLOPT_CONNECTTIMEOUT => 8, + CURLOPT_TIMEOUT => 20, + CURLOPT_USERAGENT => 'Mozilla/5.0 varlog-meta/1.0', + CURLOPT_SSL_VERIFYPEER => true, + CURLOPT_ENCODING => '', // accepte gzip/deflate/br, décompresse automatiquement + CURLOPT_HEADERFUNCTION => static function ($curl, $header) use (&$contentLength): int { + if (preg_match('/^content-length:\s*(\d+)/i', $header, $m)) { + $contentLength = (int) $m[1]; + } + return strlen($header); + }, + CURLOPT_WRITEFUNCTION => static function ($curl, $chunk) use ($fp, &$downloaded, $limit): int { + $downloaded += strlen($chunk); + if ($downloaded > $limit) { + return -1; + } + fwrite($fp, $chunk); + return strlen($chunk); + }, + ]); + curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $mimeRaw = (string) curl_getinfo($ch, CURLINFO_CONTENT_TYPE); + $errno = curl_errno($ch); + curl_close($ch); + fclose($fp); + // 403 = site accessible mais protégé (Cloudflare bot-check, etc.) : on laisse passer + // avec métadonnées vides pour que l'utilisateur saisisse manuellement le titre. + if ($httpCode === 403) { + @unlink($tmpFile); + return ['ok' => true, 'mime' => 'text/html', 'blocked' => true]; + } + if ($httpCode < 200 || $httpCode >= 400 || ($errno !== 0 && $errno !== 23)) { + @unlink($tmpFile); + return ['ok' => false, 'error' => "Téléchargement impossible (HTTP $httpCode)"]; + } + $mime = strtok($mimeRaw ?: 'application/octet-stream', '; '); + $result = ['ok' => true, 'mime' => $mime, 'size' => $contentLength ?? $downloaded]; + + if (!str_starts_with($mime, 'text/html')) { + $etJson = @shell_exec('exiftool -json -charset utf8 -struct ' . escapeshellarg($tmpFile) . ' 2>/dev/null'); + if ($etJson) { + $et = json_decode($etJson, true)[0] ?? []; + $etVal = static function (string ...$keys) use ($et): ?string { + foreach ($keys as $key) { + $v = $et[$key] ?? null; + if (is_array($v)) { + $v = implode(', ', array_filter(array_map('trim', $v))); + } + if (is_string($v) && trim($v) !== '') { + return trim($v); + } + } + return null; + }; + if ($v = $etVal('Title', 'Headline', 'ObjectName')) { + $result['title'] = $v; + } + if ($v = $etVal('Description', 'Caption-Abstract', 'ImageDescription')) { + $result['description'] = $v; + } + if ($v = $etVal('Keywords')) { + $result['keywords'] = $v; + } + if ($v = $etVal('Copyright', 'CopyrightNotice', 'Rights')) { + $result['copyright'] = $v; + } + if ($v = $etVal('DateTimeOriginal', 'CreateDate', 'ModifyDate')) { + $result['date'] = preg_replace('/^(\d{4}):(\d{2}):(\d{2})/', '$1-$2-$3', $v); + } + if (str_starts_with($mime, 'image/')) { + if ($v = $etVal('Artist', 'Creator', 'By-line')) { + $result['author'] = $v; + } + $w = $et['ImageWidth'] ?? $et['ExifImageWidth'] ?? null; + $h = $et['ImageHeight'] ?? $et['ExifImageHeight'] ?? null; + if ($w !== null && $h !== null) { + $result['width'] = (int)$w; + $result['height'] = (int)$h; + } + $camera = trim(($et['Make'] ?? '') . ' ' . ($et['Model'] ?? '')); + if ($camera !== '') { + $result['camera'] = $camera; + } + if ($v = $etVal('Credit')) { + $result['credit'] = $v; + } + if ($v = $etVal('Source')) { + $result['source'] = $v; + } + } + if ($mime === 'application/pdf') { + if ($v = $etVal('Author')) { + $result['author'] = $v; + } + if ($v = $etVal('Subject')) { + $result['subject'] = $v; + } + if ($v = $etVal('Creator', 'CreatorTool')) { + $result['creator'] = $v; + } + if ($v = $etVal('Producer')) { + $result['producer'] = $v; + } + if (isset($et['PageCount'])) { + $result['pages'] = (int) $et['PageCount']; + } + if (isset($et['PDFVersion'])) { + $result['pdf_version'] = 'PDF ' . $et['PDFVersion']; + } + $fhPdf = fopen($tmpFile, 'rb'); + $pdfHead = fread($fhPdf, min(filesize($tmpFile), 65536)); + fclose($fhPdf); + if (preg_match('/\/MediaBox\s*\[\s*([\d.]+)\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)\s*\]/', $pdfHead, $mb)) { + $wPt = (float)$mb[3] - (float)$mb[1]; + $hPt = (float)$mb[4] - (float)$mb[2]; + $wMm = (int) round($wPt * 25.4 / 72); + $hMm = (int) round($hPt * 25.4 / 72); + $landscape = $wMm > $hMm; + if ($landscape) { + [$wMm, $hMm] = [$hMm, $wMm]; + } + $paperSizes = ['A0' => [841,1189],'A1' => [594,841],'A2' => [420,594], + 'A3' => [297,420],'A4' => [210,297],'A5' => [148,210], + 'Letter' => [216,279],'Legal' => [216,356]]; + $paperName = null; + foreach ($paperSizes as $pname => [$pw, $ph]) { + if (abs($wMm - $pw) <= 2 && abs($hMm - $ph) <= 2) { + $paperName = $pname; + break; + } + } + $label = $paperName ? $paperName . ($landscape ? ' paysage' : '') : ($landscape ? 'Paysage' : 'Portrait'); + $result['page_size'] = "$label ({$wMm}×{$hMm} mm)"; + } + } + } + } + + if (str_starts_with($mime, 'text/html')) { + try { + $fhHtml = fopen($tmpFile, 'rb'); + $html = fread($fhHtml, min(filesize($tmpFile), 65536)); + fclose($fhHtml); + + // Détection du charset : 1) en-tête HTTP, 2) , 3) + $charset = null; + if (preg_match('/charset=([^\s;]+)/i', $mimeRaw, $cm)) { + $charset = trim($cm[1], '"\''); + } + if (!$charset && preg_match('/]+charset=["\']?\s*([^"\'\s;>]+)/i', $html, $cm)) { + $charset = trim($cm[1]); + } + if (!$charset && preg_match('/]+content=["\'][^"\']*charset=([^"\'\s;]+)/i', $html, $cm)) { + $charset = trim($cm[1]); + } + if ($charset && strtolower(str_replace(['-','_'], '', $charset)) !== 'utf8') { + $converted = @mb_convert_encoding($html, 'UTF-8', $charset); + if ($converted !== false && $converted !== '') { + $html = $converted; + } + } + + preg_match('/]*>(.*?)<\/head>/si', $html, $headMatch); + $headHtml = $headMatch[1] ?? $html; + if (preg_match('/]*>\s*([^<]+)\s*<\/title>/i', $headHtml, $m)) { + $result['title'] = html_entity_decode(trim($m[1]), ENT_QUOTES | ENT_HTML5, 'UTF-8'); + } + $metaMap = []; + preg_match_all('/]+)>/i', $headHtml, $metaTags); + foreach ($metaTags[1] as $attrs) { + $key = $val = null; + if (preg_match('/(?:name|property)\s*=\s*["\']([^"\']+)["\']/', $attrs, $m)) { + $key = strtolower($m[1]); + } + if (preg_match('/content\s*=\s*["\']([^"\']*)["\']/', $attrs, $m)) { + $val = html_entity_decode($m[1], ENT_QUOTES | ENT_HTML5, 'UTF-8'); + } + if ($key !== null && $val !== null && $val !== '') { + $metaMap[$key] = $val; + } + } + $result['title'] ??= $metaMap['og:title'] ?? $metaMap['twitter:title'] ?? $metaMap['title'] ?? null; + $result['description'] ??= $metaMap['og:description'] ?? $metaMap['twitter:description'] ?? $metaMap['description'] ?? null; + $result['author'] ??= $metaMap['author'] ?? $metaMap['article:author'] ?? $metaMap['dc.creator'] ?? null; + $result['keywords'] ??= $metaMap['keywords'] ?? $metaMap['news_keywords'] ?? null; + $result['og_image'] ??= $metaMap['og:image'] ?? $metaMap['twitter:image'] ?? null; + $result['site_name'] ??= $metaMap['og:site_name'] ?? null; + $result['og_type'] ??= $metaMap['og:type'] ?? null; + $result['language'] ??= $metaMap['og:locale'] ?? $metaMap['dc.language'] ?? null; + $result['date'] ??= $metaMap['article:published_time'] ?? $metaMap['dc.date'] ?? null; + if (preg_match('/]+rel=["\']canonical["\'][^>]+href=["\']([^"\']+)["\'][^>]*>/i', $headHtml, $m) + || preg_match('/]+href=["\']([^"\']+)["\'][^>]+rel=["\']canonical["\'][^>]*>/i', $headHtml, $m)) { + $result['canonical'] = $m[1]; + } + preg_match_all('/]+type=["\']application\/ld\+json["\'][^>]*>(.*?)<\/script>/si', $headHtml, $ldTags); + foreach ($ldTags[1] as $jsonStr) { + $ld = @json_decode(trim($jsonStr), true); + if (!is_array($ld)) { + continue; + } + foreach (isset($ld[0]) ? $ld : [$ld] as $item) { + if (!is_array($item)) { + continue; + } + if (empty($result['title']) && !empty($item['headline'])) { + $result['title'] = $item['headline']; + } + if (empty($result['description']) && !empty($item['description'])) { + $result['description'] = $item['description']; + } + if (empty($result['date'])) { + $d = $item['datePublished'] ?? $item['dateCreated'] ?? null; + if ($d) { + $result['date'] = $d; + } + } + if (empty($result['author'])) { + $au = $item['author'] ?? null; + if (is_array($au)) { + $au = $au['name'] ?? ($au[0]['name'] ?? null); + } + if (is_string($au) && $au !== '') { + $result['author'] = $au; + } + } + } + break; + } + $result = array_filter($result, static fn ($v) => $v !== null && $v !== ''); + $result['ok'] = true; + } catch (\Throwable) { + } + } + + @unlink($tmpFile); + return $result; +} + +// ─── Télécharge une image distante → _thumb_ local, retourne le nom du fichier ─ +function downloadImageToThumb(string $imageUrl, string $filesDir): ?string +{ + if (!filter_var($imageUrl, FILTER_VALIDATE_URL) || !preg_match('#^https?://#i', $imageUrl)) { + return null; + } + $ch = curl_init($imageUrl); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_MAXREDIRS => 3, + CURLOPT_TIMEOUT => 10, + CURLOPT_CONNECTTIMEOUT => 5, + CURLOPT_USERAGENT => 'Mozilla/5.0 varlog/1.0', + ]); + $body = curl_exec($ch); + $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + if ($body === false || $code !== 200 || strlen($body) < 512) { + return null; + } + $urlPath = parse_url($imageUrl, PHP_URL_PATH) ?? ''; + $ext = strtolower(pathinfo($urlPath, PATHINFO_EXTENSION)); + if (!in_array($ext, ['jpg', 'jpeg', 'png', 'webp', 'gif', 'avif'], true)) { + $ext = 'jpg'; + } + if ($ext === 'jpeg') { + $ext = 'jpg'; + } + if (!is_dir($filesDir)) { + mkdir($filesDir, 0755, true); + } + $hash = substr(hash('sha256', $body), 0, 16); + $size = strlen($body); + $name = '_thumb_' . $hash . '-' . $size . '.' . $ext; + file_put_contents($filesDir . '/' . $name, $body); + return $name; +} + +// ─── Trouve l'URL de la plus grande image d'une page HTML ──────────────────── +function findLargestPageImage(string $pageUrl): ?string +{ + if (!filter_var($pageUrl, FILTER_VALIDATE_URL)) { + return null; + } + $ch = curl_init($pageUrl); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_MAXREDIRS => 3, + CURLOPT_TIMEOUT => 10, + CURLOPT_CONNECTTIMEOUT => 5, + CURLOPT_USERAGENT => 'Mozilla/5.0 varlog/1.0', + CURLOPT_ENCODING => '', + CURLOPT_WRITEFUNCTION => static function ($curl, $chunk) use (&$htmlBuf, &$htmlLen): int { + $htmlLen = ($htmlLen ?? 0) + strlen($chunk); + if ($htmlLen <= 131072) { + $htmlBuf = ($htmlBuf ?? '') . $chunk; + } + return strlen($chunk); + }, + ]); + $htmlBuf = ''; + $htmlLen = 0; + curl_exec($ch); + curl_close($ch); + if (!$htmlBuf) { + return null; + } + + $scheme = parse_url($pageUrl, PHP_URL_SCHEME) ?? 'https'; + $host = $scheme . '://' . (parse_url($pageUrl, PHP_URL_HOST) ?? ''); + + preg_match_all('/]+src=["\']([^"\']+)["\'][^>]*>/i', $htmlBuf, $m); + $candidates = []; + foreach ($m[1] as $src) { + if (preg_match('#^https?://#i', $src)) { + $candidates[] = $src; + } elseif (str_starts_with($src, '//')) { + $candidates[] = $scheme . ':' . $src; + } elseif (str_starts_with($src, '/')) { + $candidates[] = $host . $src; + } + } + // Filtre les icônes/avatars courants par leur chemin + $candidates = array_filter( + $candidates, + fn ($u) => + !preg_match('#/(icon|logo|avatar|favicon|sprite|pixel|spacer|blank|1x1|tracking)#i', $u) + && preg_match('#\.(jpe?g|png|webp|gif|avif)(\?.*)?$#i', $u) + ); + $candidates = array_slice(array_values($candidates), 0, 10); + if (empty($candidates)) { + return null; + } + + // HEAD requests pour comparer Content-Length + $best = null; + $bestSize = 0; + foreach ($candidates as $imgUrl) { + $ch = curl_init($imgUrl); + curl_setopt_array($ch, [ + CURLOPT_NOBODY => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_TIMEOUT => 5, + CURLOPT_CONNECTTIMEOUT => 3, + CURLOPT_USERAGENT => 'Mozilla/5.0 varlog/1.0', + ]); + curl_exec($ch); + $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + $len = (int) curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD); + curl_close($ch); + if ($code === 200 && $len > $bestSize) { + $bestSize = $len; + $best = $imgUrl; + } + } + // Si aucun Content-Length retourné, prend le premier candidat + return $best ?? $candidates[0]; +} + +// ─── Capture d'écran via Chromium headless ────────────────────────────────── +function takeScreenshot(string $url, string $outputPath): bool +{ + $bin = ''; + foreach (['chromium-headless-shell', 'chromium', 'chromium-browser', 'google-chrome'] as $name) { + $found = trim((string) shell_exec('which ' . escapeshellarg($name) . ' 2>/dev/null')); + if ($found !== '') { + $bin = $found; + break; + } + } + if ($bin === '') { + return false; + } + + $cmd = 'timeout 20 ' . escapeshellarg($bin) + . ' --headless=new' + . ' --disable-gpu' + . ' --no-sandbox' + . ' --disable-setuid-sandbox' + . ' --hide-scrollbars' + . ' --window-size=1200,630' + . ' --screenshot=' . escapeshellarg($outputPath) + . ' --virtual-time-budget=6000' + . ' ' . escapeshellarg($url) + . ' 2>/dev/null'; + shell_exec($cmd); + return file_exists($outputPath) && filesize($outputPath) > 0; +} + +switch ($action) { + + case 'create': + requireAuth(); + + $title = $_POST['title'] ?? ''; + $content = $_POST['content'] ?? ''; + $postSlug = $_POST['slug'] ?? ''; + $published = isset($_POST['published']); + $published_at = str_replace('T', ' ', $_POST['published_at'] ?? date('Y-m-d H:i:s')); + $seoTitle = $_POST['seo_title'] ?? ''; + $seoDescription = $_POST['seo_description'] ?? ''; + $ogImage = $_POST['og_image'] ?? ''; + $category = $_POST['category'] ?? ''; + $errors = []; + + $postTags = []; + foreach (($_POST['tags'] ?? []) as $_tk => $_tv) { + $_vals = array_values(array_filter(array_map('trim', explode(',', (string)$_tv)), fn ($v) => $v !== '')); + if ($_vals !== []) { + $postTags[trim((string)$_tk)] = $_vals; + } + } + + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + if (trim($title) === '') { + $errors[] = 'Le titre est obligatoire.'; + } + if (empty($errors)) { + $newUuid = $articles->create($title, $content, $published, $postSlug, $published_at, currentUserEmail() ?? '', $seoTitle, $seoDescription, $ogImage, $category, $postTags); + + foreach ($_FILES['files']['tmp_name'] ?? [] as $i => $tmpName) { + if ($_FILES['files']['error'][$i] === UPLOAD_ERR_OK) { + $articles->addFile($newUuid, [ + 'name' => $_FILES['files']['name'][$i], + 'tmp_name' => $tmpName, + 'error' => $_FILES['files']['error'][$i], + ]); + } + } + + header('Location: /'); + exit; + } + } + + $formAction = '/new'; + $action = 'create'; + $tagTypes = $articles->getTagTypes(); + $articleTags = $postTags; + $allTagValues = []; + foreach ($tagTypes as $_tk => $_) { + $allTagValues[$_tk] = $articles->getAllTagValues($_tk); + } + include BASE_PATH . '/templates/post_form.php'; + break; + + case 'view': + $article = $slug !== '' ? $articles->getBySlug($slug) : null; + if (!$article) { + searchAndRedirect($slug, $articles); + http_response_code(404); + echo 'Article introuvable.'; + exit; + } + + if (!$article['published']) { + if (!canDoOnArticle('view_drafts', $article)) { + http_response_code(404); + echo 'Article introuvable.'; + exit; + } + } + + // Avant-première : publié mais date future → réservé aux utilisateurs avec view_previews + if ($article['published'] && strtotime((string)($article['published_at'] ?? '')) > time()) { + if (!hasCapability('view_previews')) { + http_response_code(404); + echo 'Article introuvable.'; + exit; + } + } + + // Catégorie privée → réservé aux connectés + $allCats = $articles->getCategories(); + $privateCats = $articles->getPrivateCategories(); + $articleCat = trim($article['category'] ?? ''); + $isPrivateCat = $articleCat !== '' && in_array($articleCat, $privateCats, true); + if ($isPrivateCat && !isLoggedIn()) { + http_response_code(404); + echo 'Article introuvable.'; + exit; + } + + $files = $articles->getFiles($article['uuid']); + + // Résout les chemins de fichiers relatifs dans le contenu + $rawContent = $articles->resolveFileUrls($article['uuid'], $article['content']); + + // Ratings + $ratingStats = ['avg' => null, 'count' => 0]; + $userRating = null; + $pdo = dbPdo(); + if ($pdo) { + require_once BASE_PATH . '/src/RatingManager.php'; + $ratingMgr = new RatingManager($pdo); + $ratingStats = $ratingMgr->statsForArticle($article['uuid']); + if (isLoggedIn()) { + $userRating = $ratingMgr->userRating($article['uuid'], currentUserEmail() ?? ''); + } + } + + // Tous les articles publiés — depuis le search_index (1 fichier) si dispo, sinon getAll() + $_si = $articles->getSearchIndex(); + $_allPublished = $_si !== null + ? array_values(array_filter($_si, fn ($a) => (bool)($a['published'] ?? false))) + : $articles->getAll(true); + unset($_si); + + // Articles liés (même catégorie) + $relatedArticles = []; + if ($articleCat !== '') { + foreach ($_allPublished as $a) { + if ($a['uuid'] === $article['uuid']) { + continue; + } + if (trim($a['category'] ?? '') !== $articleCat) { + continue; + } + if (strtotime((string)($a['published_at'] ?? '')) > time() && !hasCapability('view_previews')) { + continue; + } + $relatedArticles[] = $a; + if (count($relatedArticles) >= 5) { + break; + } + } + } + + // Articles proches par titre → OR implicite, cumul des scores + require_once BASE_PATH . '/src/SearchEngine.php'; + $_simEngine = new SearchEngine(); + $_stopWords = ['avec', 'dans', 'pour', 'une', 'les', 'des', 'sur', 'par', 'qui', 'que', + 'tout', 'mais', 'donc', 'comment', 'quand', 'plus', 'cette', 'cet', 'ces', + 'mon', 'ton', 'son', 'notre', 'votre', 'leur', 'tres', 'bien', 'fait', + 'aussi', 'comme', 'sans', 'sous', 'entre', 'vers', 'chez']; + $_simPool = array_values(array_filter( + $_allPublished, + static function (array $a) use ($article, $privateCats): bool { + if ($a['uuid'] === $article['uuid']) { + return false; + } + if (strtotime((string)($a['published_at'] ?? '')) > time() && !hasCapability('view_previews')) { + return false; + } + $cat = trim($a['category'] ?? ''); + return $cat === '' || !in_array($cat, $privateCats, true) || isLoggedIn(); + } + )); + $_titleWords = array_unique(array_values(array_filter( + preg_split('/\W+/u', mb_strtolower($article['title']), -1, PREG_SPLIT_NO_EMPTY) ?: [], + fn ($w) => mb_strlen($w) >= 4 && !in_array($w, $_stopWords, true) + ))); + [$_scoreMap, $_articleMap] = $_simEngine->scorePool($_titleWords, $_simPool); + arsort($_scoreMap); + $_similarArticles = array_map( + fn ($uuid) => $_articleMap[$uuid], + array_slice(array_keys($_scoreMap), 0, 5) + ); + unset($_simEngine, $_simPool, $_titleWords, $_stopWords, $_scoreMap, $_articleMap); + + // "À lire aussi" : similaires en premier, même catégorie pour compléter jusqu'à 5 + $alsoReadArticles = $_similarArticles; + if (count($alsoReadArticles) < 5) { + $_used = array_column($alsoReadArticles, 'uuid'); + foreach ($relatedArticles as $_ra) { + if (count($alsoReadArticles) >= 5) { + break; + } + if (!in_array($_ra['uuid'], $_used, true)) { + $alsoReadArticles[] = $_ra; + } + } + unset($_used); + } + unset($_similarArticles); + + unset($_allPublished); + + $backlinks = $articles->getBacklinks($article['slug'] ?? '', $article['uuid']); + + // Réactions et commentaires + require_once BASE_PATH . '/src/ReactionManager.php'; + require_once BASE_PATH . '/src/CommentManager.php'; + $reactionStats = array_fill_keys(ReactionManager::TYPES, 0); + $visitorReactions = []; + $comments = []; + $commentFlash = isset($_GET['commented']); + $commentVerified = isset($_GET['verified']); + $commentError = null; + if ($pdo) { + $reactionMgr = new ReactionManager($pdo); + $commentMgr = new CommentManager($pdo); + + // Cookie visiteur (fingerprint anti-doublon) + if (empty($_COOKIE['vl_vid'])) { + $vid = bin2hex(random_bytes(16)); + setcookie('vl_vid', $vid, [ + 'expires' => time() + 365 * 86400, + 'path' => '/', + 'secure' => !empty($_SERVER['HTTPS']), + 'httponly' => true, + 'samesite' => 'Lax', + ]); + } else { + $vid = $_COOKIE['vl_vid']; + } + + $reactionStats = $reactionMgr->statsForArticle($article['uuid']); + $visitorReactions = $reactionMgr->visitorReactions($article['uuid'], $vid); + $comments = $commentMgr->forArticle($article['uuid']); + } + + include BASE_PATH . '/templates/post_view.php'; + break; + + case 'edit': + requireAuth(); + + $article = $articles->getByUuid($uuid); + if (!$article) { + http_response_code(404); + echo 'Article introuvable.'; + exit; + } + + if (!canDoOnArticle('edit_articles', $article)) { + http_response_code(403); + echo 'Accès refusé.'; + exit; + } + + if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['_toggle_featured'])) { + if (!isAdmin()) { + http_response_code(403); + exit; + } + $articles->setFeatured($uuid, !((bool)($article['featured'] ?? false))); + header('Location: /edit/' . rawurlencode($uuid)); + exit; + } + + $title = $_POST['title'] ?? $article['title']; + $content = $_POST['content'] ?? $article['content']; + $postSlug = $_POST['slug'] ?? $article['slug']; + $published = isset($_POST['published']) ? true : $article['published']; + $published_at = $_POST['published_at'] + ?? date('Y-m-d\TH:i', strtotime((string)($article['published_at'] ?? 'now'))); + $seoTitle = $_POST['seo_title'] ?? ($article['seo_title'] ?? ''); + $seoDescription = $_POST['seo_description'] ?? ($article['seo_description'] ?? ''); + $ogImage = $_POST['og_image'] ?? ($article['og_image'] ?? ''); + $category = $_POST['category'] ?? ($article['category'] ?? ''); + $errors = []; + + // Tags : lire depuis POST si soumis, sinon depuis l'article + $pendingTags = []; + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + foreach (($_POST['tags'] ?? []) as $_tk => $_tv) { + $_vals = array_values(array_filter(array_map('trim', explode(',', (string)$_tv)), fn ($v) => $v !== '')); + if ($_vals !== []) { + $pendingTags[trim((string)$_tk)] = $_vals; + } + } + } else { + $pendingTags = $article['tags'] ?? []; + } + + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + if (trim($title) === '') { + $errors[] = 'Le titre est obligatoire.'; + } + if (empty($errors)) { + if (!empty($_POST['_confirm'])) { + $coverFile = trim($_POST['cover_file'] ?? '') ?: ($article['cover'] ?? ''); + $ogImageFromCover = $coverFile !== '' + ? rtrim(APP_URL, '/') . '/file?uuid=' . rawurlencode($uuid) . '&name=' . rawurlencode($coverFile) + : ''; + + $articles->update( + $uuid, + $title, + $content, + $published, + $_POST['slug'] ?? '', + str_replace('T', ' ', $_POST['published_at'] ?? ''), + $_POST['revision_comment'] ?? '', + $_POST['seo_title'] ?? '', + $_POST['seo_description'] ?? '', + $ogImageFromCover, + $_POST['category'] ?? '', + $pendingTags + ); + + $fmetaNames = $_POST['fmeta_name'] ?? []; + $fmetaAuthors = $_POST['fmeta_author'] ?? []; + $fmetaSources = $_POST['fmeta_source'] ?? []; + foreach ($fmetaNames as $fi => $fname) { + $articles->addFileMeta($uuid, $fname, trim($fmetaAuthors[$fi] ?? ''), trim($fmetaSources[$fi] ?? '')); + } + + $coverFile = trim($_POST['cover_file'] ?? ''); + if ($coverFile !== '') { + $articles->setCover($uuid, $coverFile); + } + + $updated = $articles->getByUuid($uuid); + header('Location: /post/' . rawurlencode($updated['slug'] ?? $uuid)); + exit; + } + + // ─── Page de confirmation ──────────────────────────────────── + $diffLines = lineDiff((string)($article['content'] ?? ''), $content); + $titleChanged = ($title !== ($article['title'] ?? '')); + $autoSlug = slugify($title); + + $changes = []; + if ($titleChanged) { + $changes[] = 'titre modifié'; + } + if (($category ?? '') !== ($article['category'] ?? '')) { + $changes[] = 'catégorie modifiée'; + } + if ($pendingTags !== ($article['tags'] ?? [])) { + $changes[] = 'tags modifiés'; + } + if ($content !== ($article['content'] ?? '')) { + $changes[] = 'contenu modifié'; + } + $oldPublished = (bool)($article['published'] ?? false); + if ($published !== $oldPublished) { + $changes[] = $published ? 'article publié' : 'article dépublié'; + } + $newCover = trim($_POST['cover_file'] ?? ''); + if ($newCover !== '' && $newCover !== ($article['cover'] ?? '')) { + $changes[] = 'couverture modifiée'; + } + $fmetaNames = $_POST['fmeta_name'] ?? []; + $fmetaAuthors = $_POST['fmeta_author'] ?? []; + $fmetaSources = $_POST['fmeta_source'] ?? []; + foreach ($fmetaNames as $fi => $fname) { + $savedMeta = ($article['files_meta'][$fname] ?? []); + if (trim($fmetaAuthors[$fi] ?? '') !== ($savedMeta['author'] ?? '') + || trim($fmetaSources[$fi] ?? '') !== ($savedMeta['source_url'] ?? '')) { + $changes[] = 'métadonnées fichiers modifiées'; + break; + } + } + $autoRevisionComment = !empty($changes) ? ucfirst(implode(', ', $changes)) : ''; + + require_once BASE_PATH . '/src/Parsedown.php'; + $_pd = new Parsedown(); + $autoSeoDesc = mb_strimwidth( + trim((string)preg_replace('/\s+/', ' ', strip_tags($_pd->text($content)))), + 0, + 155, + '…' + ); + unset($_pd); + + // Tags sous forme de chaînes CSV pour les champs hidden du formulaire de confirmation + $confirmTags = []; + foreach ($pendingTags as $_tk => $_vals) { + $confirmTags[$_tk] = implode(', ', $_vals); + } + + include BASE_PATH . '/templates/post_confirm.php'; + exit; + } + } + + $formAction = '/edit/' . rawurlencode($uuid); + $action = 'edit'; + $existingFiles = $articles->getFiles($uuid); + $insertUrl = ''; + if (isset($_GET['insert_url']) && filter_var($_GET['insert_url'], FILTER_VALIDATE_URL)) { + $insertUrl = $_GET['insert_url']; + } + $tagTypes = $articles->getTagTypes(); + $articleTags = $pendingTags; + $allTagValues = []; + foreach ($tagTypes as $_tk => $_) { + $allTagValues[$_tk] = $articles->getAllTagValues($_tk); + } + include BASE_PATH . '/templates/post_form.php'; + break; + + case 'edit_tags': + requireAuth(); + $article = $uuid !== '' ? $articles->getByUuid($uuid) : null; + if (!$article) { + http_response_code(404); + echo 'Article introuvable.'; + exit; + } + if (!canDoOnArticle('edit_articles', $article)) { + http_response_code(403); + echo 'Accès refusé.'; + exit; + } + + $tagType = urldecode(trim($_GET['tag_type'] ?? '')); + $isCatField = ($tagType === 'categorie' || $tagType === 'catégorie'); + $tagTypes = $articles->getTagTypes(); + + require_once BASE_PATH . '/src/TagSuggester.php'; + $suggester = new TagSuggester(); + + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + if ($isCatField) { + $newCat = trim($_POST['category'] ?? ''); + // Met à jour uniquement la catégorie via update() en préservant tout le reste + $articles->update( + $uuid, + $article['title'], + $article['content'], + $article['published'], + $article['slug'], + $article['published_at'] ?? '', + '', + $article['seo_title'] ?? '', + $article['seo_description'] ?? '', + $article['og_image'] ?? '', + $newCat, + $article['tags'] ?? [] + ); + } else { + // Tags : les valeurs cochées remplacent les tags du type concerné + $selected = array_values(array_filter(array_map('trim', $_POST['selected'] ?? []), fn ($v) => $v !== '')); + $allTags = $article['tags'] ?? []; + if (empty($selected)) { + unset($allTags[$tagType]); + } else { + $allTags[$tagType] = $selected; + } + $articles->setTags($uuid, $allTags); + } + header('Location: /edit/' . rawurlencode($uuid)); + exit; + } + + // GET — calculer les suggestions + if ($isCatField) { + $allCats = $articles->getCategories(); + $currentCat = $article['category'] ?? ''; + $suggestions = $suggester->suggestCategory($article['content'], $allCats, $currentCat); + $currentTags = []; + } else { + $existingValues = $articles->getAllTagValues($tagType); + $currentTags = $article['tags'][$tagType] ?? []; + $suggestions = $suggester->suggest($article['content'], $existingValues, $currentTags); + $allCats = null; + $currentCat = null; + } + + include BASE_PATH . '/templates/edit_tags.php'; + break; + + case 'delete_file': + requireAuth(); + $fileName = basename($_POST['name'] ?? ''); + if ($uuid !== '' && $fileName !== '' && $fileName[0] !== '.') { + $articles->deleteFile($uuid, $fileName); + } + header('Location: /edit/' . rawurlencode($uuid)); + exit; + + case 'delete': + requireAuth(); + if ($uuid !== '') { + $articles->delete($uuid); + } + header('Location: /'); + exit; + + case 'delete_revision': + requireAuth(); + if (!isAdmin()) { + http_response_code(403); + exit; + } + if ($uuid !== '' && isset($_POST['rev_n'])) { + $articles->deleteRevision($uuid, (int)$_POST['rev_n']); + } + header('Location: /edit/' . rawurlencode($uuid) . '#historyPanel'); + exit; + + case 'delete_all_revisions': + requireAuth(); + if (!isAdmin()) { + http_response_code(403); + exit; + } + if ($uuid !== '') { + $articles->deleteAllRevisions($uuid); + } + header('Location: /edit/' . rawurlencode($uuid)); + exit; + + case 'categories': + header('Location: /admin/categories'); + exit; + + case 'create_tag_type': + requireAuth(); + if (!isAdmin()) { + http_response_code(403); + exit; + } + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $typeKey = strtolower((string)preg_replace('/[^a-z0-9_]/i', '', trim($_POST['type_key'] ?? ''))); + $typeLabel = trim($_POST['type_label'] ?? ''); + if ($typeKey !== '' && $typeLabel !== '') { + $types = $articles->getTagTypes(); + if (!isset($types[$typeKey])) { + $types[$typeKey] = $typeLabel; + $articles->saveTagTypes($types); + } + } + } + header('Location: /admin/categories'); + exit; + + case 'delete_tag_type': + requireAuth(); + if (!isAdmin()) { + http_response_code(403); + exit; + } + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $typeKey = trim($_POST['type_key'] ?? ''); + if ($typeKey !== '') { + $types = $articles->getTagTypes(); + unset($types[$typeKey]); + $articles->saveTagTypes($types); + } + } + header('Location: /admin/categories'); + exit; + + case 'rename_category': + requireAuth(); + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $old = trim($_POST['old'] ?? ''); + $new = trim($_POST['new'] ?? ''); + if ($old !== '' && $new !== '' && $old !== $new) { + $articles->renameCategory($old, $new); + } + } + header('Location: /admin/categories'); + exit; + + case 'delete_category': + requireAuth(); + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $cat = trim($_POST['category'] ?? ''); + if ($cat !== '') { + $articles->deleteCategory($cat); + } + } + header('Location: /admin/categories'); + exit; + + case 'toggle_private_category': + requireAuth(); + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $cat = trim($_POST['category'] ?? ''); + if ($cat !== '') { + $articles->togglePrivateCategory($cat); + } + } + header('Location: /admin/categories'); + exit; + + case 'author': + $authorSlug = trim($_GET['slug'] ?? ''); + $authorRow = profileBySlug($authorSlug); + if (!$authorRow) { + http_response_code(404); + $content = '

Profil introuvable.

'; + $title = 'Profil introuvable'; + include BASE_PATH . '/templates/layout.php'; + break; + } + $privateCats = $articles->getPrivateCategories(); + $authorArticles = array_values(array_filter( + $articles->getAll(publishedOnly: true), + static function (array $a) use ($authorRow, $privateCats): bool { + if (($a['author'] ?? '') !== $authorRow['email']) { + return false; + } + $cat = trim($a['category'] ?? ''); + if ($cat !== '' && in_array($cat, $privateCats, true) && !isLoggedIn()) { + return false; + } + if (strtotime((string)($a['published_at'] ?? '')) > time() && !hasCapability('view_previews')) { + return false; + } + return true; + } + )); + include BASE_PATH . '/templates/author_profile.php'; + break; + + case 'author_articles': + $authorSlug = trim($_GET['slug'] ?? ''); + $authorRow = profileBySlug($authorSlug); + if (!$authorRow) { + http_response_code(404); + $content = '

Profil introuvable.

'; + $title = 'Profil introuvable'; + include BASE_PATH . '/templates/layout.php'; + break; + } + $privateCats = $articles->getPrivateCategories(); + $allCats = $articles->getCategories(); + $authorArticles = array_values(array_filter( + $articles->getAll(publishedOnly: true), + static function (array $a) use ($authorRow, $privateCats): bool { + if (($a['author'] ?? '') !== $authorRow['email']) { + return false; + } + $cat = trim($a['category'] ?? ''); + if ($cat !== '' && in_array($cat, $privateCats, true) && !isLoggedIn()) { + return false; + } + if (strtotime((string)($a['published_at'] ?? '')) > time() && !hasCapability('view_previews')) { + return false; + } + return true; + } + )); + $perPage = postsPerPage(); + $cursor = trim($_GET['cursor'] ?? ''); + $offset = 0; + if ($cursor !== '') { + foreach ($authorArticles as $i => $a) { + if ($a['uuid'] === $cursor) { + $offset = $i + 1; + break; + } + } + } + $posts = array_slice($authorArticles, $offset, $perPage); + $nextCursor = count($posts) === $perPage ? end($posts)['uuid'] : null; + $prevCursor = null; + if ($offset > 0) { + $prevOffset = max(0, $offset - $perPage); + $prevCursor = $prevOffset > 0 ? $authorArticles[$prevOffset - 1]['uuid'] : ''; + } + include BASE_PATH . '/templates/author_articles.php'; + break; + + case 'liens': + $liensSlug = trim($_GET['slug'] ?? ''); + $liensRow = profileBySlug($liensSlug); + if (!$liensRow) { + http_response_code(404); + $content = '

Page introuvable.

'; + $title = 'Page introuvable'; + include BASE_PATH . '/templates/layout.php'; + break; + } + $_lName = $liensRow['display_name'] ?? ''; + $_lBio = $liensRow['bio'] ?? ''; + $_lSlug = $liensRow['profile_slug'] ?? ''; + $_lInitials = mb_strtoupper(mb_substr($_lName, 0, 1, 'UTF-8'), 'UTF-8'); + $profileLinks = []; + $pdo = dbPdo(); + if ($pdo) { + try { + $st = $pdo->prepare( + 'SELECT id, url, title, description FROM profile_links + WHERE user_email = :e ORDER BY position, id' + ); + $st->execute([':e' => $liensRow['email']]); + $profileLinks = $st->fetchAll(PDO::FETCH_ASSOC); + } catch (\Throwable) { + } + } + include BASE_PATH . '/templates/liens.php'; + break; + + case 'add_link': + requireAuth(); + $linkUrl = filter_var(trim($_POST['link_url'] ?? ''), FILTER_VALIDATE_URL) ?: ''; + $linkTitle = trim($_POST['link_title'] ?? ''); + $linkDesc = trim($_POST['link_desc'] ?? ''); + if ($linkUrl !== '') { + $pdo = dbPdo(); + if ($pdo) { + try { + $st = $pdo->prepare( + 'INSERT INTO profile_links (user_email, url, title, description, position) + VALUES (:e, :u, :t, :d, + COALESCE((SELECT MAX(position)+1 FROM profile_links WHERE user_email = :e), 0))' + ); + $st->execute([':e' => currentUserEmail(), ':u' => $linkUrl, ':t' => $linkTitle, ':d' => $linkDesc]); + } catch (\Throwable) { + } + } + } + header('Location: /profile#links'); + exit; + + case 'delete_link': + requireAuth(); + $linkId = (int)($_POST['link_id'] ?? 0); + if ($linkId > 0) { + $pdo = dbPdo(); + if ($pdo) { + try { + $st = $pdo->prepare('DELETE FROM profile_links WHERE id = :id AND user_email = :e'); + $st->execute([':id' => $linkId, ':e' => currentUserEmail()]); + } catch (\Throwable) { + } + } + } + header('Location: /profile#links'); + exit; + + case 'reorder_links': + requireAuth(); + $order = $_POST['order'] ?? []; + if (is_array($order)) { + $pdo = dbPdo(); + if ($pdo) { + try { + $st = $pdo->prepare('UPDATE profile_links SET position = :p WHERE id = :id AND user_email = :e'); + foreach (array_values($order) as $pos => $id) { + $st->execute([':p' => $pos, ':id' => (int)$id, ':e' => currentUserEmail()]); + } + } catch (\Throwable) { + } + } + } + header('Location: /profile#links'); + exit; + + case 'flux': + require_once BASE_PATH . '/src/FeedFetcher.php'; + $fetcher = new FeedFetcher(BASE_PATH . '/data/_cache/feeds'); + $fluxItems = []; + $pdo = dbPdo(); + if ($pdo) { + try { + $st = $pdo->query( + 'SELECT f.user_email, f.feed_url, f.label, + p.display_name, p.profile_slug + FROM rss_feeds f + LEFT JOIN user_profiles p ON p.email = f.user_email + ORDER BY f.created_at' + ); + foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $_row) { + $data = $fetcher->get($_row['feed_url']); + if (!$data) { + continue; + } + $feedTitle = $_row['label'] !== '' ? $_row['label'] : $data['feed_title']; + $authorName = $_row['display_name'] ?? ''; + $authorSlug = $_row['profile_slug'] ?? ''; + foreach ($data['items'] as $_item) { + $fluxItems[] = array_merge($_item, [ + 'feed_title' => $feedTitle, + 'feed_url' => $_row['feed_url'], + 'author_name' => $authorName, + 'author_slug' => $authorSlug, + ]); + } + } + } catch (\Throwable) { + } + } + usort($fluxItems, static fn ($a, $b) => $b['date'] <=> $a['date']); + include BASE_PATH . '/templates/flux.php'; + break; + + case 'about': + ['meta' => $siteMeta, 'html' => $siteContent] = loadSitePageData('about'); + include BASE_PATH . '/templates/about.php'; + break; + + case 'legal': + ['meta' => $siteMeta, 'html' => $siteContent] = loadSitePageData('legal'); + include BASE_PATH . '/templates/legal.php'; + break; + + case 'contact': + include BASE_PATH . '/templates/contact.php'; + break; + + case 'licenses': + ['meta' => $siteMeta, 'html' => $siteContent] = loadSitePageData('licenses'); + include BASE_PATH . '/templates/licenses.php'; + break; + + case 'diff': + requireAuth(); + $article = $articles->getByUuid($uuid); + if (!$article) { + http_response_code(404); + echo 'Article introuvable.'; + exit; + } + $revisions = $article['revisions'] ?? []; + $revN = (int)($_GET['rev'] ?? 0); + // Trouver l'index dans le tableau par numéro de révision + $revIndex = null; + foreach ($revisions as $ri => $r) { + if ((int)($r['n'] ?? 0) === $revN) { + $revIndex = $ri; + break; + } + } + if ($revIndex === null || $revN < 1) { + header('Location: /edit/' . rawurlencode($uuid)); + exit; + } + $oldContent = $articles->getRevisionContent($uuid, $revN); + if ($oldContent === null) { + http_response_code(404); + echo 'Révision introuvable.'; + exit; + } + $diffLines = lineDiff($oldContent, $article['content']); + include BASE_PATH . '/templates/diff.php'; + break; + + case 'autosave': + requireAuth(); + header('Content-Type: application/json'); + if ($_SERVER['REQUEST_METHOD'] !== 'POST' || $uuid === '') { + echo json_encode(['ok' => false]); + exit; + } + $asTitle = trim($_POST['title'] ?? ''); + $asContent = $_POST['content'] ?? ''; + $asSlug = trim($_POST['slug'] ?? ''); + if ($asTitle === '') { + echo json_encode(['ok' => false]); + exit; + } + $ok = $articles->autosave($uuid, $asTitle, $asContent, $asSlug); + echo json_encode(['ok' => $ok, 'time' => date('H:i:s')]); + exit; + + case 'copy_file': + requireAuth(); + header('Content-Type: application/json'); + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + echo json_encode(['ok' => false]); + exit; + } + $cfFrom = trim($_POST['from_uuid'] ?? ''); + $cfTo = $uuid !== '' ? $uuid : trim($_POST['to_uuid'] ?? ''); + $cfName = basename($_POST['name'] ?? ''); + if (!preg_match('/^[0-9a-f-]{36}$/', $cfFrom) + || !preg_match('/^[0-9a-f-]{36}$/', $cfTo) + || $cfName === '' + || str_starts_with($cfName, '.')) { + echo json_encode(['ok' => false, 'error' => 'Paramètres invalides']); + exit; + } + $cfSrc = BASE_PATH . '/data/' . $cfFrom . '/files/' . $cfName; + $cfDstDir = BASE_PATH . '/data/' . $cfTo . '/files'; + $cfDst = $cfDstDir . '/' . $cfName; + if (!file_exists($cfSrc)) { + echo json_encode(['ok' => false, 'error' => 'Fichier source introuvable']); + exit; + } + if (!is_dir($cfDstDir)) { + mkdir($cfDstDir, 0775, true); + } + echo json_encode(['ok' => copy($cfSrc, $cfDst)]); + exit; + + case 'add_files': + requireAuth(); + $addFilesArticle = $articles->getByUuid($uuid); + if (!$addFilesArticle) { + http_response_code(404); + echo 'Article introuvable.'; + exit; + } + $addFilesError = null; + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + // Quand post_max_size est dépassé, PHP vide $_FILES et $_POST silencieusement + if ((int)($_SERVER['CONTENT_LENGTH'] ?? 0) > 0 && empty($_FILES)) { + $addFilesError = sprintf('Fichier trop lourd — limite serveur : %s.', ini_get('post_max_size')); + } else { + foreach ($_FILES['files']['tmp_name'] ?? [] as $i => $tmpName) { + if ($_FILES['files']['error'][$i] === UPLOAD_ERR_OK) { + $articles->addFile($uuid, [ + 'name' => $_FILES['files']['name'][$i], + 'tmp_name' => $tmpName, + 'error' => $_FILES['files']['error'][$i], + ]); + } + } + header('Location: /edit/' . rawurlencode($uuid)); + exit; + } + } + include BASE_PATH . '/templates/add_files.php'; + break; + + case 'import_image': + requireAuth(); + $importArticle = $articles->getByUuid($uuid); + if (!$importArticle) { + http_response_code(404); + echo 'Article introuvable.'; + exit; + } + $importError = $_GET['error'] ?? ''; + include BASE_PATH . '/templates/import_image.php'; + break; + + case 'fetch_file_meta': + requireAuth(); + header('Content-Type: application/json'); + echo json_encode(fetchUrlMeta(trim($_GET['url'] ?? ''))); + exit; + + case 'import_image_step2': + requireAuth(); + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + header('Location: /import/' . rawurlencode($uuid)); + exit; + } + $step2Article = $articles->getByUuid($uuid); + if (!$step2Article) { + http_response_code(404); + echo 'Article introuvable.'; + exit; + } + $step2Url = trim($_POST['image_url'] ?? ''); + if (!filter_var($step2Url, FILTER_VALIDATE_URL) || !preg_match('#^https?://#i', $step2Url)) { + header('Location: /import/' . rawurlencode($uuid) . '?error=1'); + exit; + } + // Détection URL interne (même hostname que APP_URL → lecture directe sans cURL) + $step2Meta = null; + $step2IsInternal = false; + $_iHost = parse_url(APP_URL, PHP_URL_HOST) ?? ''; + $_uHost = parse_url($step2Url, PHP_URL_HOST) ?? ''; + $_uPath = parse_url($step2Url, PHP_URL_PATH) ?? ''; + if ($_iHost !== '' && $_uHost === $_iHost && preg_match('#^/post/([a-z0-9][a-z0-9-]*)/?$#', $_uPath, $_sm)) { + $_ia = $articles->getBySlug($_sm[1]); + if ($_ia) { + $step2IsInternal = true; + $step2Meta = ['ok' => true, 'title' => $_ia['title'] ?? '', 'mime' => 'text/html']; + if (!empty($_ia['seo_description'])) { + $step2Meta['description'] = $_ia['seo_description']; + } elseif (!empty($_ia['content'])) { + require_once BASE_PATH . '/src/Parsedown.php'; + $_plain = strip_tags((new Parsedown())->text($_ia['content'])); + $step2Meta['description'] = mb_strimwidth(trim(preg_replace('/\s+/', ' ', $_plain)), 0, 155, '…'); + unset($_plain); + } + if (!empty($_ia['cover'])) { + $step2Meta['og_image'] = '/file?uuid=' . rawurlencode($_ia['uuid']) . '&name=' . rawurlencode($_ia['cover']); + } + } + unset($_ia); + } + unset($_iHost, $_uHost, $_uPath, $_sm); + if ($step2Meta === null) { + $step2Meta = fetchUrlMeta($step2Url); + } + if (!($step2Meta['ok'] ?? false)) { + header('Location: /import/' . rawurlencode($uuid) . '?error=1'); + exit; + } + // Capture d'écran pour prévisualisation (pages HTML uniquement, URL externes uniquement) + $step2Screenshot = null; + if (!$step2IsInternal && str_starts_with($step2Meta['mime'] ?? '', 'text/html')) { + $filesDir = BASE_PATH . '/data/' . $uuid . '/files'; + if (!is_dir($filesDir)) { + mkdir($filesDir, 0755, true); + } + $previewPath = $filesDir . '/_preview.png'; + @unlink($previewPath); // supprime le résidu d'une analyse précédente + if (takeScreenshot($step2Url, $previewPath)) { + $step2Screenshot = '_preview.png'; + } + } + include BASE_PATH . '/templates/import_image_step2.php'; + break; + + case 'copyright_ack': + requireAuth(); + $ackArticle = $articles->getByUuid($uuid); + if (!$ackArticle) { + http_response_code(404); + echo 'Article introuvable.'; + exit; + } + $ackUrl = filter_var($_GET['image_url'] ?? '', FILTER_VALIDATE_URL) + ? $_GET['image_url'] : ''; + if ($ackUrl === '') { + header('Location: /import/' . rawurlencode($uuid)); + exit; + } + $ackTitle = $_GET['img_title'] ?? ''; + $ackAuthor = $_GET['img_author'] ?? ''; + $ackSource = $_GET['img_source'] ?? ''; + $ackIsCover = !empty($_GET['is_cover']); + $ackMetaJson = $_GET['meta_json'] ?? ''; + include BASE_PATH . '/templates/copyright_ack.php'; + break; + + case 'add_file_from_url': + requireAuth(); + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + header('Location: /'); + exit; + } + $urlUuid = $_GET['uuid'] ?? $_POST['uuid'] ?? ''; + $imageUrl = trim($_POST['image_url'] ?? ''); + $mode = $_POST['mode'] ?? 'link'; + $isCover = isset($_POST['is_cover']); + $imgTitle = trim($_POST['img_title'] ?? ''); + $imgAuthor = trim($_POST['img_author'] ?? ''); + $imgSource = trim($_POST['img_source'] ?? '') ?: $imageUrl; + + // Métadonnées supplémentaires (passées depuis l'étape 2) + $importedMeta = []; + $rawMetaJson = $_POST['meta_json'] ?? ''; + if ($rawMetaJson !== '') { + $dec = @json_decode($rawMetaJson, true); + if (is_array($dec)) { + foreach ($dec as $k => $v) { + if (is_string($k) && strlen($k) <= 60 && is_scalar($v)) { + $importedMeta[$k] = $v; + } + } + } + } + + $urlArticle = $articles->getByUuid($urlUuid); + if (!$urlArticle || $imageUrl === '' || !filter_var($imageUrl, FILTER_VALIDATE_URL)) { + header('Location: /import/' . rawurlencode($urlUuid)); + exit; + } + + $screenshotFile = basename(trim($_POST['screenshot_file'] ?? '')); + + if ($mode === 'screenshot') { + if ($screenshotFile === '' || $screenshotFile !== '_preview.png') { + header('Location: /import/' . rawurlencode($urlUuid) . '?error=1'); + exit; + } + $filesDir = BASE_PATH . '/data/' . $urlUuid . '/files'; + $previewPath = $filesDir . '/' . $screenshotFile; + if (!file_exists($previewPath)) { + header('Location: /import/' . rawurlencode($urlUuid) . '?error=1'); + exit; + } + $hash = substr(hash_file('sha256', $previewPath), 0, 16); + $size = filesize($previewPath); + $destName = $hash . '-' . $size . '.png'; + rename($previewPath, $filesDir . '/' . $destName); + $articles->addFileMeta($urlUuid, $destName, $imgAuthor, $imgSource, $imgTitle, $importedMeta); + if ($isCover) { + $articles->setCover($urlUuid, $destName); + } + header('Location: /edit/' . rawurlencode($urlUuid)); + exit; + } + + if ($mode === 'link') { + $filesDir = BASE_PATH . '/data/' . $urlUuid . '/files'; + if (!is_dir($filesDir)) { + mkdir($filesDir, 0755, true); + } + $linkMime = $importedMeta['mime'] ?? ''; + $isHtmlLink = str_starts_with($linkMime, 'text/html') || $linkMime === ''; + if ($isHtmlLink) { + $thumbSet = false; + // 1. Télécharge l'og:image distante + $extOg = $importedMeta['og_image'] ?? ''; + if (!$thumbSet && $extOg !== '' && !str_starts_with($extOg, '/')) { + $thumbName = downloadImageToThumb($extOg, $filesDir); + if ($thumbName !== null) { + $importedMeta['og_image'] = '/file?uuid=' . rawurlencode($urlUuid) . '&name=' . rawurlencode($thumbName); + $thumbSet = true; + } + } + // 2. Plus grande image trouvée sur la page + if (!$thumbSet) { + $bigImg = findLargestPageImage($imageUrl); + if ($bigImg !== null) { + $thumbName = downloadImageToThumb($bigImg, $filesDir); + if ($thumbName !== null) { + $importedMeta['og_image'] = '/file?uuid=' . rawurlencode($urlUuid) . '&name=' . rawurlencode($thumbName); + $thumbSet = true; + } + } + } + // 3. Screenshot pré-généré depuis step2 + if (!$thumbSet && $screenshotFile !== '' && file_exists($filesDir . '/' . $screenshotFile)) { + $previewPath = $filesDir . '/' . $screenshotFile; + $hash = substr(hash_file('sha256', $previewPath), 0, 16); + $size = filesize($previewPath); + $thumbName = '_thumb_' . $hash . '-' . $size . '.png'; + rename($previewPath, $filesDir . '/' . $thumbName); + $importedMeta['og_image'] = '/file?uuid=' . rawurlencode($urlUuid) . '&name=' . rawurlencode($thumbName); + $thumbSet = true; + } + // 4. Screenshot à la volée en dernier recours + if (!$thumbSet) { + $screenshotTmp = tempnam(sys_get_temp_dir(), 'vl_ss_') . '.png'; + if (takeScreenshot($imageUrl, $screenshotTmp)) { + $hash = substr(hash_file('sha256', $screenshotTmp), 0, 16); + $size = filesize($screenshotTmp); + $thumbName = '_thumb_' . $hash . '-' . $size . '.png'; + rename($screenshotTmp, $filesDir . '/' . $thumbName); + $importedMeta['og_image'] = '/file?uuid=' . rawurlencode($urlUuid) . '&name=' . rawurlencode($thumbName); + } else { + @unlink($screenshotTmp); + } + } + // Supprime le preview inutilisé si toujours présent + if ($screenshotFile !== '' && file_exists($filesDir . '/' . $screenshotFile)) { + @unlink($filesDir . '/' . $screenshotFile); + } + } elseif ($screenshotFile !== '') { + // Non-HTML : supprime le preview inutilisé + @unlink($filesDir . '/' . $screenshotFile); + } + $articles->addExternalLink($urlUuid, $imageUrl, $imgTitle, $imgAuthor, $importedMeta); + header('Location: /edit/' . rawurlencode($urlUuid)); + exit; + } + + // Mode téléchargement : accusé de réception obligatoire + if (empty($_POST['copyright_acked'])) { + header('Location: /?action=copyright_ack&' . http_build_query([ + 'uuid' => $urlUuid, + 'image_url' => $imageUrl, + 'img_title' => $imgTitle, + 'img_author' => $imgAuthor, + 'img_source' => trim($_POST['img_source'] ?? ''), + 'is_cover' => $isCover ? '1' : '', + 'meta_json' => $rawMetaJson, + ])); + exit; + } + + $imported = $articles->addFileFromUrl($urlUuid, $imageUrl, $isCover, $imgAuthor, $imgSource, $imgTitle, $importedMeta); + if ($imported) { + header('Location: /edit/' . rawurlencode($urlUuid)); + } else { + header('Location: /import/' . rawurlencode($urlUuid) . '?error=1&mode=download'); + } + exit; + + case 'sources': + $article = $articles->getByUuid($uuid); + if (!$article) { + http_response_code(404); + echo 'Article introuvable.'; + exit; + } + requireAuth(); + if (!canDoOnArticle('view_sources', $article)) { + http_response_code(403); + echo 'Accès refusé.'; + exit; + } + $sourcesFiles = $articles->getFiles($uuid); + include BASE_PATH . '/templates/sources.php'; + break; + + case 'regen_thumbs': + requireAuth(); + + // Page de confirmation si pas encore lancé + if (!isset($_GET['run'])) { + ob_start(); + ?> +

Génération des aperçus de liens

+
+ +
+
+ + +
+ Supprime et régénère les miniatures locales déjà enregistrées. +
+
+ +
+
+ ← Retour + + Génération des aperçus + + '; + $heading = $force ? 'Régénération de tous les aperçus' : 'Génération des aperçus manquants'; + echo '

' . $heading . '

    '; + @ob_flush(); + flush(); + + $done = $fail = $skip = 0; + foreach ($articles->getAll() as $article) { + $artUuid = $article['uuid']; + $filesDir = BASE_PATH . '/data/' . $artUuid . '/files'; + foreach ($article['external_links'] ?? [] as $link) { + $lMeta = $link['meta'] ?? []; + $lMime = $lMeta['mime'] ?? 'text/html'; + $lUrl = $link['url'] ?? ''; + + // Ignore si ce n'est pas du HTML + if ($lMime !== '' && !str_starts_with($lMime, 'text/html')) { + $skip++; + continue; + } + + $hasLocal = !empty($lMeta['og_image']) && str_starts_with($lMeta['og_image'], '/'); + if ($hasLocal && !$force) { + $skip++; + continue; + } + + echo '
  • '; + echo '' . htmlspecialchars($article['title']) . ''; + echo htmlspecialchars($lUrl) . ' … '; + @ob_flush(); + flush(); + + // Supprime l'ancienne miniature locale si on force + if ($force && $hasLocal) { + $oldMeta = $lMeta['og_image']; + $oldName = rawurldecode(parse_url($oldMeta, PHP_URL_QUERY) ? (explode('name=', $oldMeta)[1] ?? '') : ''); + // Extrait le paramètre name= de l'URL /file?uuid=...&name=... + parse_str(parse_url($oldMeta, PHP_URL_QUERY) ?? '', $oldQs); + $oldFile = $oldQs['name'] ?? ''; + if ($oldFile !== '' && file_exists($filesDir . '/' . $oldFile)) { + @unlink($filesDir . '/' . $oldFile); + } + } + + $thumbName = null; + $method = ''; + + // 1. og_image → téléchargement direct + // Non-force : utilise l'og_image stockée si externe + // Force : refetch la page pour récupérer l'URL d'origine + $ogToDownload = ''; + if (!$force) { + $stored = $lMeta['og_image'] ?? ''; + if ($stored !== '' && filter_var($stored, FILTER_VALIDATE_URL)) { + $ogToDownload = $stored; + } + } else { + $freshMeta = fetchUrlMeta($lUrl); + $ogToDownload = $freshMeta['og_image'] ?? ''; + if (!filter_var($ogToDownload, FILTER_VALIDATE_URL)) { + $ogToDownload = ''; + } + } + if ($ogToDownload !== '') { + $thumbName = downloadImageToThumb($ogToDownload, $filesDir); + if ($thumbName) { + $method = '✓ og:image'; + } + } + + // 2. Plus grande image de la page + if ($thumbName === null) { + $largestUrl = findLargestPageImage($lUrl); + if ($largestUrl) { + $thumbName = downloadImageToThumb($largestUrl, $filesDir); + if ($thumbName) { + $method = '✓ plus grande image'; + } + } + } + + // 3. Screenshot Chromium en dernier recours + if ($thumbName === null) { + $screenshotTmp = tempnam(sys_get_temp_dir(), 'vl_ss_') . '.png'; + if (takeScreenshot($lUrl, $screenshotTmp)) { + if (!is_dir($filesDir)) { + mkdir($filesDir, 0755, true); + } + $hash = substr(hash_file('sha256', $screenshotTmp), 0, 16); + $size = filesize($screenshotTmp); + $thumbName = '_thumb_' . $hash . '-' . $size . '.png'; + rename($screenshotTmp, $filesDir . '/' . $thumbName); + $method = '✓ screenshot'; + } else { + @unlink($screenshotTmp); + } + } + + if ($thumbName !== null) { + $ogUrl = '/file?uuid=' . rawurlencode($artUuid) . '&name=' . rawurlencode($thumbName); + $articles->updateExternalLinkMeta($artUuid, $lUrl, ['og_image' => $ogUrl]); + echo '' . $method . ''; + $done++; + } else { + echo '✗ échec'; + $fail++; + } + echo '
  • '; + @ob_flush(); + flush(); + } + } + + echo '
'; + echo '

Terminé — '; + echo $done . ' capturé' . ($done > 1 ? 's' : '') . ', '; + echo $fail . ' échec' . ($fail > 1 ? 's' : '') . ', '; + echo $skip . ' ignoré' . ($skip > 1 ? 's' : '') . '.

'; + echo '← Retour'; + echo ''; + exit; + + case 'delete_external_link': + requireAuth(); + if ($_SERVER['REQUEST_METHOD'] === 'POST' && $uuid !== '') { + $linkUrl = $_POST['url'] ?? ''; + if ($linkUrl !== '' && filter_var($linkUrl, FILTER_VALIDATE_URL)) { + $articles->removeExternalLink($uuid, $linkUrl); + } + } + header('Location: /edit/' . rawurlencode($uuid)); + exit; + + case 'react': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + header('Location: /'); + exit; + } + $reactUuid = trim($_POST['uuid'] ?? ''); + $reactType = trim($_POST['type'] ?? ''); + $isAjax = ($_POST['_ajax'] ?? '') === '1'; + + // Cookie visiteur + if (empty($_COOKIE['vl_vid'])) { + $vid = bin2hex(random_bytes(16)); + setcookie('vl_vid', $vid, [ + 'expires' => time() + 365 * 86400, + 'path' => '/', + 'secure' => !empty($_SERVER['HTTPS']), + 'httponly' => true, + 'samesite' => 'Lax', + ]); + } else { + $vid = $_COOKIE['vl_vid']; + } + + $pdo = dbPdo(); + if ($pdo && $reactUuid !== '') { + require_once BASE_PATH . '/src/ReactionManager.php'; + $rm = new ReactionManager($pdo); + $added = $rm->toggle($reactUuid, $reactType, $vid); + $count = $rm->statsForArticle($reactUuid)[$reactType] ?? 0; + + if ($isAjax) { + header('Content-Type: application/json'); + echo json_encode(['ok' => true, 'active' => $added, 'count' => $count]); + exit; + } + } + + $reactBack = $_POST['_back'] ?? '/'; + header('Location: ' . $reactBack); + exit; + + case 'comment': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + header('Location: /'); + exit; + } + + // Honeypot + if (($_POST['website'] ?? '') !== '') { + header('Location: /'); + exit; + } + + // CSRF + $csrfOk = isset($_POST['_token'], $_SESSION['comment_csrf']) + && hash_equals($_SESSION['comment_csrf'], $_POST['_token']); + unset($_SESSION['comment_csrf']); + if (!$csrfOk) { + header('Location: /'); + exit; + } + + $cmtUuid = trim($_POST['uuid'] ?? ''); + $cmtName = trim($_POST['author_name'] ?? ''); + $cmtEmail = trim($_POST['author_email'] ?? ''); + $cmtContent = trim($_POST['content'] ?? ''); + + $cmtArticle = $cmtUuid !== '' ? $articles->getByUuid($cmtUuid) : null; + $cmtBack = $cmtArticle ? '/post/' . rawurlencode($cmtArticle['slug'] ?? $cmtUuid) : '/'; + + $pdo = dbPdo(); + if (!$pdo || !$cmtArticle || $cmtName === '' || !filter_var($cmtEmail, FILTER_VALIDATE_EMAIL) || $cmtContent === '') { + header('Location: ' . $cmtBack . '#comment-form-card'); + exit; + } + + if (mb_strlen($cmtContent) > 2000) { + header('Location: ' . $cmtBack . '#comment-form-card'); + exit; + } + + require_once BASE_PATH . '/src/CommentManager.php'; + require_once BASE_PATH . '/src/mailer.php'; + + $cm = new CommentManager($pdo); + $ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? ''; + $ua = $_SERVER['HTTP_USER_AGENT'] ?? ''; + ['token' => $cmtToken, 'code' => $cmtCode] = $cm->submit($cmtUuid, $cmtName, $cmtEmail, $cmtContent, $ip, $ua); + + $verifyUrl = rtrim(APP_URL, '/') . '/verify-comment/' . $cmtToken; + $subject = '[' . siteTitle() . '] Confirmez votre commentaire'; + $html = '' + . '

Bonjour ' . htmlspecialchars($cmtName) . ',

' + . '

Votre commentaire sur ' . htmlspecialchars($cmtArticle['title']) . ' a bien été reçu.

' + . '

Cliquez sur le lien ci-dessous, puis saisissez le code à 6 chiffres :

' + . '

Confirmer mon commentaire

' + . '

Votre code : ' . htmlspecialchars($cmtCode) . '

' + . '

Ce lien et ce code expirent dans 24 heures. Si vous n\'êtes pas à l\'origine de ce message, ignorez-le.

' + . ''; + + try { + envoyer_mail_smtp($cmtEmail, $subject, $html); + } catch (\RuntimeException) { + // Taux limité ou erreur SMTP : on continue sans planter le visiteur + } + + header('Location: ' . $cmtBack . '?commented=1#comments'); + exit; + + case 'verify_comment': + $vcToken = trim($_GET['token'] ?? ''); + $vcCode = trim($_POST['code'] ?? ''); + $vcError = false; + $vcDeleted = false; + $vcAttemptsLeft = null; + + if (!preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/', $vcToken)) { + header('Location: /'); + exit; + } + + if ($vcCode !== '' && $_SERVER['REQUEST_METHOD'] === 'POST') { + $pdo = dbPdo(); + if ($pdo && preg_match('/^[0-9]{6}$/', $vcCode)) { + require_once BASE_PATH . '/src/CommentManager.php'; + $cm = new CommentManager($pdo); + $result = $cm->verify($vcToken, $vcCode); + if (is_string($result)) { + $vcArticle = $articles->getByUuid($result); + $vcSlug = $vcArticle ? ($vcArticle['slug'] ?? $result) : $result; + header('Location: /post/' . rawurlencode($vcSlug) . '?verified=1#comments'); + exit; + } + if ($result === 0) { + $vcDeleted = true; + } else { + $vcError = true; + $vcAttemptsLeft = (int)$result; + } + } else { + $vcError = true; + $vcAttemptsLeft = null; + } + } + + ob_start(); + ?> +
+

Confirmer mon commentaire

+ +
+ Trop de tentatives incorrectes. Votre commentaire a été annulé. +
+ ← Retour à l'accueil + + +
+ Code incorrect. + + Il vous reste essai 1 ? 's' : '' ?>. + +
+ +

Saisissez le code à 6 chiffres reçu par email.

+ +
+
+ + +
+ +
+ +
+ 0) { + require_once BASE_PATH . '/src/CommentManager.php'; + (new CommentManager($pdo))->setPublished($modId, $modPub === 1); + } + header('Location: /admin/comments'); + exit; + + case 'comment_delete': + requireAuth(); + if (!isAdmin() || $_SERVER['REQUEST_METHOD'] !== 'POST') { + http_response_code(403); + exit; + } + $delId = (int)($_POST['id'] ?? 0); + $pdo = dbPdo(); + if ($pdo && $delId > 0) { + require_once BASE_PATH . '/src/CommentManager.php'; + (new CommentManager($pdo))->delete($delId); + } + header('Location: /admin/comments'); + exit; + + case 'comment_resend': + requireAuth(); + if (!isAdmin() || $_SERVER['REQUEST_METHOD'] !== 'POST') { + http_response_code(403); + exit; + } + $resendId = (int)($_POST['id'] ?? 0); + $pdo = dbPdo(); + if ($pdo && $resendId > 0) { + require_once BASE_PATH . '/src/CommentManager.php'; + require_once BASE_PATH . '/src/mailer.php'; + $cm = new CommentManager($pdo); + $resendRow = $cm->getById($resendId); + if ($resendRow && !$resendRow['verified'] && !empty($resendRow['verification_code']) && !empty($resendRow['verify_token'])) { + $resendArticle = $articles->getByUuid((string)$resendRow['article_uuid']); + if ($resendArticle) { + $resendCode = (string)$resendRow['verification_code']; + $resendToken = (string)$resendRow['verify_token']; + $resendName = (string)$resendRow['author_name']; + $resendEmail = (string)$resendRow['author_email']; + $resendBack = '/post/' . rawurlencode($resendArticle['slug'] ?? $resendRow['article_uuid']); + $resendVerifyUrl = rtrim(APP_URL, '/') . '/verify-comment/' . $resendToken; + $resendSubject = '[' . siteTitle() . '] Confirmez votre commentaire'; + $resendHtml = '' + . '

Bonjour ' . htmlspecialchars($resendName) . ',

' + . '

Votre commentaire sur ' . htmlspecialchars($resendArticle['title']) . ' a bien été reçu.

' + . '

Cliquez sur le lien ci-dessous, puis saisissez le code à 6 chiffres :

' + . '

Confirmer mon commentaire

' + . '

Votre code : ' . htmlspecialchars($resendCode) . '

' + . '

Ce lien et ce code expirent dans 24 heures. Si vous n\'êtes pas à l\'origine de ce message, ignorez-le.

' + . ''; + try { + envoyer_mail_smtp($resendEmail, $resendSubject, $resendHtml, null, ['bypass_rate_limit' => true]); + } catch (\RuntimeException) { + // Erreur SMTP : on continue sans planter + } + } + } + } + header('Location: /admin/comments'); + exit; + + case 'rate': + requireAuth(); + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + header('Location: /'); + exit; + } + $rateUuid = $_POST['uuid'] ?? ''; + $rateValue = (int)($_POST['rating'] ?? 0); + $rateArticle = $articles->getByUuid($rateUuid); + if ($rateArticle && $rateValue >= 1 && $rateValue <= 5) { + $pdo = dbPdo(); + if ($pdo) { + require_once BASE_PATH . '/src/RatingManager.php'; + (new RatingManager($pdo))->rate($rateUuid, currentUserEmail() ?? '', $rateValue); + } + header('Location: /post/' . rawurlencode($rateArticle['slug'] ?? $rateUuid)); + } else { + header('Location: /'); + } + exit; + + case 'admin': + requireAuth(); + $tab = $_GET['tab'] ?? (isAdmin() ? 'dashboard' : 'articles'); + $adminData = []; + $siteSettingsSaved = isset($_GET['saved']); + + if ($tab === 'dashboard') { + if (!isAdmin()) { + http_response_code(403); + exit; + } + $allArticles = $articles->getAll(); + $now = time(); + $adminData['total'] = count($allArticles); + $adminData['published'] = count(array_filter($allArticles, fn ($a) => $a['published'] && strtotime((string)($a['published_at'] ?? '')) <= $now)); + $adminData['drafts'] = count(array_filter($allArticles, fn ($a) => !$a['published'])); + $adminData['previews'] = count(array_filter($allArticles, fn ($a) => $a['published'] && strtotime((string)($a['published_at'] ?? '')) > $now)); + $adminData['recent'] = array_slice( + usort($allArticles, fn ($a, $b) => strcmp($b['updated_at'] ?? '', $a['updated_at'] ?? '')) ? $allArticles : $allArticles, + 0, + 5 + ); + // Trier par updated_at desc + usort($allArticles, fn ($a, $b) => strcmp($b['updated_at'] ?? '', $a['updated_at'] ?? '')); + $adminData['recent'] = array_slice($allArticles, 0, 5); + } + + if ($tab === 'articles') { + $allArticles = $articles->getAll(); + if (!isAdmin()) { + $me = currentUserEmail() ?? ''; + $allArticles = array_values(array_filter($allArticles, fn ($a) => ($a['author'] ?? '') === $me)); + } + usort($allArticles, fn ($a, $b) => strcmp($b['updated_at'] ?? '', $a['updated_at'] ?? '')); + + $adminData['filter_authors'] = array_values(array_unique(array_filter(array_column($allArticles, 'author')))); + $adminData['filter_categories'] = array_values(array_unique(array_filter(array_column($allArticles, 'category')))); + sort($adminData['filter_authors']); + sort($adminData['filter_categories']); + + $filterAuthor = trim($_GET['filter_author'] ?? ''); + $filterCategory = trim($_GET['filter_category'] ?? ''); + $filterStatus = trim($_GET['filter_status'] ?? ''); + $adminData['filter_author'] = $filterAuthor; + $adminData['filter_category'] = $filterCategory; + $adminData['filter_status'] = $filterStatus; + + $nowTs = time(); + if ($filterAuthor !== '') { + $allArticles = array_values(array_filter($allArticles, fn ($a) => ($a['author'] ?? '') === $filterAuthor)); + } + if ($filterCategory !== '') { + $allArticles = array_values(array_filter($allArticles, fn ($a) => trim($a['category'] ?? '') === $filterCategory)); + } + if ($filterStatus === 'published') { + $allArticles = array_values(array_filter($allArticles, fn ($a) => $a['published'] && strtotime((string)($a['published_at'] ?? '')) <= $nowTs)); + } elseif ($filterStatus === 'draft') { + $allArticles = array_values(array_filter($allArticles, fn ($a) => !$a['published'])); + } elseif ($filterStatus === 'preview') { + $allArticles = array_values(array_filter($allArticles, fn ($a) => $a['published'] && strtotime((string)($a['published_at'] ?? '')) > $nowTs)); + } + + $adminData['articles'] = $allArticles; + } + + if ($tab === 'roles') { + if (!isAdmin()) { + http_response_code(403); + exit; + } + $pdo = dbPdo(); + if ($pdo) { + $st = $pdo->query( + 'SELECT r.id, r.name, r.label, COUNT(ur.user_email) AS user_count + FROM roles r + LEFT JOIN user_roles ur ON ur.role_id = r.id + GROUP BY r.id, r.name, r.label + ORDER BY r.name' + ); + $roles = $st->fetchAll(PDO::FETCH_ASSOC); + try { + $capRows = $pdo->query('SELECT role_id, capability FROM role_capabilities')->fetchAll(PDO::FETCH_ASSOC); + $capsMap = []; + foreach ($capRows as $cr) { + $capsMap[(int)$cr['role_id']][] = $cr['capability']; + } + } catch (\Throwable) { + $capsMap = []; + } + foreach ($roles as &$r) { + $r['capabilities'] = $capsMap[(int)$r['id']] ?? []; + } + unset($r); + $adminData['roles'] = $roles; + } else { + $adminData['roles'] = []; + } + } + + if ($tab === 'users') { + if (!isAdmin()) { + http_response_code(403); + exit; + } + $pdo = dbPdo(); + if ($pdo) { + // users table may not exist yet — degrade gracefully + $usersFromDb = []; + try { + $st = $pdo->query('SELECT email, is_active FROM users ORDER BY email'); + foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $row) { + $v = $row['is_active']; + $usersFromDb[$row['email']] = is_bool($v) ? $v : in_array(strtolower((string)$v), ['t', '1', 'true', 'yes'], true); + } + } catch (\PDOException) { + // table absente, on continue avec la liste user_roles seulement + } + + $st = $pdo->query('SELECT ur.user_email, r.name, r.label FROM user_roles ur JOIN roles r ON r.id = ur.role_id ORDER BY ur.user_email'); + $rolesMap = []; + foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $row) { + $rolesMap[$row['user_email']][] = ['name' => $row['name'], 'label' => $row['label']]; + } + + $merged = []; + foreach (array_unique(array_merge(array_keys($usersFromDb), array_keys($rolesMap))) as $email) { + $merged[$email] = [ + 'email' => $email, + 'is_active' => $usersFromDb[$email] ?? null, + 'roles' => $rolesMap[$email] ?? [], // [['name'=>..., 'label'=>...], ...] + ]; + } + ksort($merged); + $adminData['users'] = array_values($merged); + + $st = $pdo->query('SELECT id, name, label FROM roles ORDER BY name'); + $adminData['roles'] = $st->fetchAll(PDO::FETCH_ASSOC); + } else { + $adminData['users'] = []; + $adminData['roles'] = []; + } + } + + if ($tab === 'comments') { + if (!isAdmin()) { + http_response_code(403); + exit; + } + $pdo = dbPdo(); + $cmtFilterStatus = trim($_GET['filter_status'] ?? ''); + $adminData['cmt_filter_status'] = $cmtFilterStatus; + if ($pdo) { + require_once BASE_PATH . '/src/CommentManager.php'; + $cm = new CommentManager($pdo); + $adminData['comments'] = $cm->allForAdmin($cmtFilterStatus); + $adminData['cmt_counts'] = $cm->countsByStatus(); + // Enrichit avec le slug de chaque article pour les liens + $adminData['articleSlugs'] = []; + foreach ($adminData['comments'] as $cmtRow) { + $uuid = $cmtRow['article_uuid']; + if (!isset($adminData['articleSlugs'][$uuid])) { + $a = $articles->getByUuid($uuid); + $adminData['articleSlugs'][$uuid] = $a ? ($a['slug'] ?? null) : null; + } + } + } else { + $adminData['comments'] = []; + $adminData['articleSlugs'] = []; + $adminData['cmt_counts'] = ['all' => 0, 'pending' => 0, 'verified' => 0, 'hidden' => 0]; + } + } + + if ($tab === 'emails') { + if (!isAdmin()) { + http_response_code(403); + exit; + } + $pdo = dbPdo(); + if ($pdo) { + $emlFilter = in_array($_GET['filter'] ?? '', ['sent', 'error', 'queued'], true) ? $_GET['filter'] : ''; + $emlPage = max(0, (int)($_GET['page'] ?? 0)); + $emlLimit = 50; + $emlOffset = $emlPage * $emlLimit; + + $whereEml = $emlFilter !== '' ? 'WHERE status = ' . $pdo->quote($emlFilter) : ''; + + $row = $pdo->query("SELECT + COUNT(*) AS all, + COUNT(*) FILTER (WHERE status = 'sent') AS sent, + COUNT(*) FILTER (WHERE status = 'error') AS error, + COUNT(*) FILTER (WHERE status = 'queued') AS queued + FROM journal_smtp")->fetch(PDO::FETCH_ASSOC); + $adminData['eml_counts'] = [ + 'all' => (int)($row['all'] ?? 0), + 'sent' => (int)($row['sent'] ?? 0), + 'error' => (int)($row['error'] ?? 0), + 'queued' => (int)($row['queued'] ?? 0), + ]; + $adminData['emails'] = $pdo->query( + "SELECT id, created_at, to_email, subject, status, error_message, content_text, sent_at + FROM journal_smtp $whereEml + ORDER BY created_at DESC + LIMIT $emlLimit OFFSET $emlOffset" + )->fetchAll(PDO::FETCH_ASSOC); + $adminData['eml_filter'] = $emlFilter; + $adminData['eml_page'] = $emlPage; + } else { + $adminData['emails'] = []; + $adminData['eml_counts'] = ['all' => 0, 'sent' => 0, 'error' => 0, 'queued' => 0]; + $adminData['eml_filter'] = ''; + $adminData['eml_page'] = 0; + } + } + + if ($tab === 'smtp') { + if (!isAdmin()) { + http_response_code(403); + exit; + } + require_once BASE_PATH . '/src/SmtpSettings.php'; + $adminData['smtp_config'] = [ + 'host' => smtpCfg('host', 'SMTP_HOST'), + 'port' => smtpCfg('port', 'SMTP_PORT'), + 'secure' => smtpCfg('secure', 'SMTP_SECURE'), + 'user' => smtpCfg('user', 'SMTP_USER'), + 'has_pass' => smtpCfg('pass', 'SMTP_PASS') !== '', + 'from' => smtpCfg('from', 'SMTP_FROM'), + 'from_name' => smtpCfg('from_name', 'SMTP_FROM_NAME'), + ]; + $adminData['smtp_test'] = $_SESSION['smtp_test_result'] ?? null; + unset($_SESSION['smtp_test_result']); + } + + if ($tab === 'searches') { + if (!isAdmin()) { + http_response_code(403); + exit; + } + require_once BASE_PATH . '/src/SearchLogParser.php'; + $parser = new SearchLogParser(); + $adminData['search_terms'] = $parser->topTerms(100); + $adminData['search_log_readable'] = $parser->isReadable(); + } + + if ($tab === 'categories') { + $adminData['cats'] = $articles->getCategories(); + $adminData['privateCats'] = $articles->getPrivateCategories(); + $adminData['tagTypes'] = $articles->getTagTypes(); + } + + include BASE_PATH . '/templates/admin.php'; + break; + + case 'admin_smtp_save': + requireAuth(); + if (!isAdmin()) { + http_response_code(403); + exit; + } + require_once BASE_PATH . '/src/SmtpSettings.php'; + + saveSmtpSettings([ + 'host' => $_POST['smtp_host'] ?? '', + 'port' => $_POST['smtp_port'] ?? '', + 'secure' => $_POST['smtp_secure'] ?? '', + 'user' => $_POST['smtp_user'] ?? '', + 'pass' => $_POST['smtp_pass'] ?? '', + 'from' => $_POST['smtp_from'] ?? '', + 'from_name' => $_POST['smtp_from_name'] ?? '', + ]); + header('Location: /admin/smtp?saved=1'); + exit; + + case 'admin_smtp_test': + requireAuth(); + if (!isAdmin()) { + http_response_code(403); + exit; + } + require_once BASE_PATH . '/src/SmtpSettings.php'; + require_once BASE_PATH . '/src/mailer.php'; + + $mode = in_array($_POST['mode'] ?? '', ['connect', 'send'], true) ? $_POST['mode'] : 'connect'; + $testEmail = trim($_POST['test_email'] ?? ''); + if ($testEmail !== '' && !filter_var($testEmail, FILTER_VALIDATE_EMAIL)) { + $testEmail = ''; + } + + $smtpLogs = []; + $smtpOk = false; + $smtpErrMsg = ''; + + try { + $mail = new \PHPMailer\PHPMailer\PHPMailer(true); + $mail->isSMTP(); + $mail->Host = smtpCfg('host', 'SMTP_HOST', 'localhost'); + $mail->Port = (int)smtpCfg('port', 'SMTP_PORT', '587'); + $_stUser = smtpCfg('user', 'SMTP_USER'); + $_stPass = smtpCfg('pass', 'SMTP_PASS'); + $mail->SMTPAuth = ($_stUser !== '' || $_stPass !== ''); + $mail->Username = $_stUser; + $mail->Password = $_stPass; + $smtpSecure = strtolower(smtpCfg('secure', 'SMTP_SECURE', 'tls')); + if ($smtpSecure === 'ssl') { + $mail->SMTPSecure = \PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_SMTPS; + } elseif ($smtpSecure === 'tls') { + $mail->SMTPSecure = \PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_STARTTLS; + } + $mail->Timeout = 15; + $mail->SMTPOptions = ['ssl' => ['verify_peer' => true, 'verify_peer_name' => true, 'allow_self_signed' => false]]; + $mail->SMTPDebug = \PHPMailer\PHPMailer\SMTP::DEBUG_SERVER; + $mail->Debugoutput = static function (string $str) use (&$smtpLogs): void { + $smtpLogs[] = rtrim($str); + }; + + if ($mode === 'send' && $testEmail !== '') { + $mail->CharSet = 'UTF-8'; + $mail->isHTML(true); + $_smtpFrom = smtpCfg('from', 'SMTP_FROM', 'no-reply@varlog.a5l.fr'); + $_smtpFromName = smtpCfg('from_name', 'SMTP_FROM_NAME', 'varlog'); + $mail->setFrom($_smtpFrom, $_smtpFromName); + $mail->addAddress($testEmail); + $_siteName = siteTitle(); + $_siteUrl = rtrim(APP_URL, '/'); + $_sentAt = date('d/m/Y à H\hi', time()); + $mail->Subject = 'Vérification de la configuration email — ' . $_siteName; + $mail->Body = '' + . '' + . '' + . '

Bonjour,

' + . '

Cet email confirme que la configuration SMTP de ' . htmlspecialchars($_siteName) . ' fonctionne correctement.

' + . '

Envoyé le ' . $_sentAt . ' depuis ' . htmlspecialchars($_siteUrl) . '.

' + . '
' + . '

Vous recevez cet email car un administrateur a effectué un test de configuration depuis l\'interface d\'administration de ' . htmlspecialchars($_siteName) . '.' + . ' Si vous n\'attendiez pas cet email, vous pouvez l\'ignorer.

' + . ''; + $mail->AltBody = "Bonjour,\r\n\r\n" + . "Cet email confirme que la configuration SMTP de {$_siteName} fonctionne correctement.\r\n\r\n" + . "Envoyé le {$_sentAt} depuis {$_siteUrl}.\r\n\r\n" + . "--\r\n" + . "Vous recevez cet email car un administrateur a effectué un test de configuration depuis l'interface d'administration de {$_siteName}." + . " Si vous n'attendiez pas cet email, vous pouvez l'ignorer."; + $mail->send(); + } else { + $mail->smtpConnect(); + $mail->smtpClose(); + } + $smtpOk = true; + } catch (\Exception $e) { + $smtpErrMsg = $e->getMessage(); + } + + $_SESSION['smtp_test_result'] = [ + 'success' => $smtpOk, + 'error' => $smtpErrMsg, + 'logs' => $smtpLogs, + 'mode' => $mode, + 'email' => $testEmail, + 'ts' => date('d/m/Y H:i:s'), + ]; + header('Location: /admin/smtp'); + exit; + + case 'admin_bulk_delete': + requireAuth(); + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $uuids = $_POST['uuids'] ?? []; + if (is_array($uuids)) { + $me = currentUserEmail() ?? ''; + foreach ($uuids as $uid) { + $uid = trim((string)$uid); + if ($uid === '') { + continue; + } + $art = $articles->getByUuid($uid); + if (!$art) { + continue; + } + if (isAdmin() || ($art['author'] ?? '') === $me) { + $articles->delete($uid); + } + } + } + } + header('Location: /admin/articles'); + exit; + + case 'admin_grant_role': + requireAuth(); + if (!isAdmin() || $_SERVER['REQUEST_METHOD'] !== 'POST') { + http_response_code(403); + exit; + } + $targetEmail = strtolower(trim($_POST['email'] ?? '')); + $roleName = trim($_POST['role'] ?? ''); + if ($targetEmail && $roleName && filter_var($targetEmail, FILTER_VALIDATE_EMAIL)) { + $pdo = dbPdo(); + if ($pdo) { + $st = $pdo->prepare( + 'INSERT INTO user_roles (user_email, role_id, granted_by) + SELECT :email, id, :by FROM roles WHERE name = :role + ON CONFLICT DO NOTHING' + ); + $st->execute([':email' => $targetEmail, ':role' => $roleName, ':by' => currentUserEmail()]); + } + } + header('Location: /admin/users'); + exit; + + case 'admin_revoke_role': + requireAuth(); + if (!isAdmin() || $_SERVER['REQUEST_METHOD'] !== 'POST') { + http_response_code(403); + exit; + } + $targetEmail = strtolower(trim($_POST['email'] ?? '')); + $roleName = trim($_POST['role'] ?? ''); + if ($targetEmail && $roleName) { + $pdo = dbPdo(); + if ($pdo) { + // Bloquer si c'est le dernier admin (en DB — hors ADMIN_EMAIL env) + if ($roleName === 'admin') { + $st = $pdo->prepare( + 'SELECT COUNT(*) FROM user_roles ur + JOIN roles r ON r.id = ur.role_id + WHERE r.name = :role AND ur.user_email != :email' + ); + $st->execute([':role' => 'admin', ':email' => $targetEmail]); + if ((int)$st->fetchColumn() === 0) { + header('Location: /admin/users?error=last_admin'); + exit; + } + } + $st = $pdo->prepare( + 'DELETE FROM user_roles + WHERE user_email = :email + AND role_id = (SELECT id FROM roles WHERE name = :role)' + ); + $st->execute([':email' => $targetEmail, ':role' => $roleName]); + } + } + header('Location: /admin/users'); + exit; + + case 'admin_save_site': + requireAuth(); + if (!isAdmin() || $_SERVER['REQUEST_METHOD'] !== 'POST') { + http_response_code(403); + exit; + } + saveSiteSettings([ + 'site_title' => $_POST['site_title'] ?? '', + 'site_claim' => $_POST['site_claim'] ?? '', + 'site_lang' => $_POST['site_lang'] ?? '', + 'posts_per_page' => $_POST['posts_per_page'] ?? '', + 'site_license_label' => $_POST['site_license_label'] ?? '', + 'site_license_url' => $_POST['site_license_url'] ?? '', + ]); + header('Location: /admin/site?saved=1'); + exit; + + case 'admin_create_role': + requireAuth(); + if (!isAdmin() || $_SERVER['REQUEST_METHOD'] !== 'POST') { + http_response_code(403); + exit; + } + $roleLabel = trim($_POST['label'] ?? ''); + $roleName = preg_replace('/[^a-z0-9_-]/', '', strtolower(trim($_POST['name'] ?? ''))); + if ($roleName === '' && $roleLabel !== '') { + $roleName = slugify($roleLabel); + } + if ($roleName && $roleLabel) { + $pdo = dbPdo(); + if ($pdo) { + try { + $st = $pdo->prepare('INSERT INTO roles (name, label) VALUES (:n, :l) ON CONFLICT (name) DO NOTHING'); + $st->execute([':n' => $roleName, ':l' => $roleLabel]); + } catch (\PDOException) { + } + } + } + header('Location: /admin/roles'); + exit; + + case 'admin_update_role': + requireAuth(); + if (!isAdmin() || $_SERVER['REQUEST_METHOD'] !== 'POST') { + http_response_code(403); + exit; + } + $roleId = (int)($_POST['id'] ?? 0); + $roleLabel = trim($_POST['label'] ?? ''); + if ($roleId > 0 && $roleLabel) { + $pdo = dbPdo(); + if ($pdo) { + $st = $pdo->prepare('UPDATE roles SET label = :l WHERE id = :id'); + $st->execute([':l' => $roleLabel, ':id' => $roleId]); + } + } + header('Location: /admin/roles'); + exit; + + case 'admin_delete_role': + requireAuth(); + if (!isAdmin() || $_SERVER['REQUEST_METHOD'] !== 'POST') { + http_response_code(403); + exit; + } + $roleId = (int)($_POST['id'] ?? 0); + if ($roleId > 0) { + $pdo = dbPdo(); + if ($pdo) { + $st = $pdo->prepare('DELETE FROM roles WHERE id = :id'); + $st->execute([':id' => $roleId]); + } + } + header('Location: /admin/roles'); + exit; + + case 'admin_update_role_caps': + requireAuth(); + if (!isAdmin() || $_SERVER['REQUEST_METHOD'] !== 'POST') { + http_response_code(403); + exit; + } + $roleId = (int)($_POST['role_id'] ?? 0); + $newCaps = array_filter((array)($_POST['caps'] ?? []), fn ($c) => array_key_exists($c, KNOWN_CAPABILITIES)); + if ($roleId > 0) { + $pdo = dbPdo(); + if ($pdo) { + $pdo->prepare('DELETE FROM role_capabilities WHERE role_id = :id')->execute([':id' => $roleId]); + $ins = $pdo->prepare('INSERT INTO role_capabilities (role_id, capability) VALUES (:id, :cap)'); + foreach ($newCaps as $cap) { + $ins->execute([':id' => $roleId, ':cap' => $cap]); + } + // Invalide le cache de capacités en session (affecte l'utilisateur courant) + unset($_SESSION['user_capabilities']); + } + } + header('Location: /admin/roles'); + exit; + + case 'admin_role_edit': + requireAuth(); + if (!isAdmin()) { + http_response_code(403); + exit; + } + $editRoleName = preg_replace('/[^a-z0-9_-]/', '', strtolower(trim($_GET['role_name'] ?? ''))); + if (!$editRoleName) { + header('Location: /admin/roles'); + exit; + } + $pdo = dbPdo(); + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + if ($pdo) { + $newLabel = trim($_POST['label'] ?? ''); + $newCaps = array_filter( + (array)($_POST['caps'] ?? []), + fn ($c) => array_key_exists($c, KNOWN_CAPABILITIES) + ); + if ($newLabel) { + $pdo->prepare('UPDATE roles SET label = :l WHERE name = :n') + ->execute([':l' => $newLabel, ':n' => $editRoleName]); + } + $st = $pdo->prepare('SELECT id FROM roles WHERE name = :n'); + $st->execute([':n' => $editRoleName]); + $editRoleId = $st->fetchColumn(); + if ($editRoleId) { + $pdo->prepare('DELETE FROM role_capabilities WHERE role_id = :id') + ->execute([':id' => $editRoleId]); + $ins = $pdo->prepare('INSERT INTO role_capabilities (role_id, capability) VALUES (:id, :cap)'); + foreach ($newCaps as $cap) { + $ins->execute([':id' => $editRoleId, ':cap' => $cap]); + } + } + unset($_SESSION['user_capabilities']); + } + header('Location: /admin/roles'); + exit; + } + // GET — charge le rôle et ses capacités + $editRole = null; + $editRoleCaps = []; + if ($pdo) { + try { + $st = $pdo->prepare('SELECT id, name, label FROM roles WHERE name = :n'); + $st->execute([':n' => $editRoleName]); + $editRole = $st->fetch(PDO::FETCH_ASSOC) ?: null; + } catch (\Throwable) { + } + if ($editRole) { + try { + $st = $pdo->prepare('SELECT capability FROM role_capabilities WHERE role_id = :id'); + $st->execute([':id' => $editRole['id']]); + $editRoleCaps = $st->fetchAll(PDO::FETCH_COLUMN) ?: []; + } catch (\Throwable) { + } + } + } + if (!$editRole) { + header('Location: /admin/roles'); + exit; + } + include BASE_PATH . '/templates/admin_role_edit.php'; + exit; + + case 'profile': + requireAuth(); + $profileError = ''; + $profileSuccess = false; + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $newName = trim($_POST['display_name'] ?? ''); + $newUrl = trim($_POST['profile_url'] ?? ''); + if ($newUrl !== '' && !filter_var($newUrl, FILTER_VALIDATE_URL)) { + $newUrl = ''; + } + $newBio = trim($_POST['bio'] ?? ''); + if ($newName === '') { + $profileError = 'Le nom ne peut pas être vide.'; + } else { + $pdo = dbPdo(); + if ($pdo) { + try { + $newSlug = slugify($newName); + $st = $pdo->prepare( + 'INSERT INTO user_profiles (email, display_name, profile_url, profile_slug, bio, updated_at) + VALUES (:e, :n, :u, :s, :b, now()) + ON CONFLICT (email) DO UPDATE SET display_name = :n, profile_url = :u, profile_slug = :s, bio = :b, updated_at = now()' + ); + $st->execute([':e' => currentUserEmail(), ':n' => $newName, ':u' => $newUrl, ':s' => $newSlug, ':b' => $newBio]); + $_SESSION['user_display_name'] = $newName; + $profileSuccess = true; + } catch (\Throwable $ex) { + $profileError = 'Erreur lors de la sauvegarde.'; + } + } + } + } + $profileCurrentName = currentUserName(); + $_profileData = authorProfile(currentUserEmail() ?? ''); + $profileCurrentUrl = $_profileData['url']; + $profileCurrentBio = $_profileData['bio']; + $profileCurrentSlug = $_profileData['slug']; + // Pré-remplir l'URL avec l'URL de profil public si vide + if ($profileCurrentUrl === '' && $profileCurrentSlug !== '') { + $profileCurrentUrl = rtrim(APP_URL, '/') . '/profil/' . rawurlencode($profileCurrentSlug); + } + // Liens de la page "Mes liens" + $profileLinks = []; + $pdo = dbPdo(); + if ($pdo) { + try { + $st = $pdo->prepare('SELECT id, url, title, description FROM profile_links WHERE user_email = :e ORDER BY position, id'); + $st->execute([':e' => currentUserEmail()]); + $profileLinks = $st->fetchAll(PDO::FETCH_ASSOC); + } catch (\Throwable) { + } + } + // Feeds RSS de l'utilisateur + $profileFeeds = []; + $pdo = dbPdo(); + if ($pdo) { + try { + $st = $pdo->prepare('SELECT id, feed_url, label FROM rss_feeds WHERE user_email = :e ORDER BY created_at'); + $st->execute([':e' => currentUserEmail()]); + $profileFeeds = $st->fetchAll(PDO::FETCH_ASSOC); + } catch (\Throwable) { + } + } + include BASE_PATH . '/templates/profile.php'; + break; + + case 'add_feed': + requireAuth(); + $feedUrl = filter_var(trim($_POST['feed_url'] ?? ''), FILTER_VALIDATE_URL) ?: ''; + $feedLabel = trim($_POST['feed_label'] ?? ''); + if ($feedUrl !== '') { + $pdo = dbPdo(); + if ($pdo) { + try { + $st = $pdo->prepare( + 'INSERT INTO rss_feeds (user_email, feed_url, label) VALUES (:e, :u, :l) + ON CONFLICT (user_email, feed_url) DO UPDATE SET label = :l' + ); + $st->execute([':e' => currentUserEmail(), ':u' => $feedUrl, ':l' => $feedLabel]); + } catch (\Throwable) { + } + } + } + header('Location: /profile#feeds'); + exit; + + case 'delete_feed': + requireAuth(); + $feedId = (int)($_POST['feed_id'] ?? 0); + if ($feedId > 0) { + $pdo = dbPdo(); + if ($pdo) { + try { + $st = $pdo->prepare('DELETE FROM rss_feeds WHERE id = :id AND user_email = :e'); + $st->execute([':id' => $feedId, ':e' => currentUserEmail()]); + } catch (\Throwable) { + } + } + } + header('Location: /profile#feeds'); + exit; + + case 'search_files': + requireAuth(); + header('Content-Type: application/json'); + $q = trim($_GET['q'] ?? ''); + $sfExclude = trim($_GET['exclude'] ?? ''); + if ($q === '') { + echo json_encode([]); + exit; + } + require_once BASE_PATH . '/src/SearchEngine.php'; + $sfPool = $articles->getSearchIndex() ?? $articles->getAll(); + $sfResults = (new SearchEngine())->search($q, $sfPool); + $sfOut = []; + foreach ($sfResults as $r) { + $a = $r['article']; + $aId = $a['uuid'] ?? ''; + if ($aId === '' || $aId === $sfExclude) { + continue; + } + $aFiles = $articles->getFiles($aId); + if (empty($aFiles)) { + continue; + } + $sfFiles = []; + foreach ($aFiles as $f) { + if (str_starts_with($f['name'], '_thumb_')) { + continue; + } + $sfFiles[] = [ + 'url' => '/file?uuid=' . rawurlencode($aId) . '&name=' . rawurlencode($f['name']), + 'name' => $f['name'], + 'mime' => $f['mime'], + 'is_image' => $f['is_image'], + 'size' => $f['size'], + ]; + } + if (empty($sfFiles)) { + continue; + } + $sfOut[] = [ + 'article' => ['uuid' => $aId, 'title' => $a['title'] ?? '', 'slug' => $a['slug'] ?? ''], + 'files' => $sfFiles, + ]; + if (count($sfOut) >= 20) { + break; + } + } + echo json_encode($sfOut); + exit; + + case 'search': + require_once BASE_PATH . '/src/SearchEngine.php'; + $searchQuery = trim($_GET['q'] ?? ''); + $searchResults = []; + if ($searchQuery !== '') { + $isAnonSearch = !isLoggedIn(); + // Lecture du cache pour les visiteurs anonymes + if ($isAnonSearch) { + $searchResults = $articles->getSearchCache($searchQuery) ?? []; + } + if (empty($searchResults)) { + $privateCats = $articles->getPrivateCategories(); + $rawPool = $articles->getSearchIndex() ?? $articles->getAll(true); + $searchPool = array_values(array_filter($rawPool, static function (array $a) use ($privateCats): bool { + if (!($a['published'] ?? false)) { + return false; + } + $cat = trim($a['category'] ?? ''); + if ($cat !== '' && in_array($cat, $privateCats, true) && !isLoggedIn()) { + return false; + } + if (strtotime((string)($a['published_at'] ?? '')) > time() && !hasCapability('view_previews')) { + return false; + } + return true; + })); + $searchResults = (new SearchEngine())->search($searchQuery, $searchPool); + if ($isAnonSearch && !empty($searchResults)) { + $articles->setSearchCache($searchQuery, $searchResults); + } + } + } + include BASE_PATH . '/templates/search.php'; + break; + + case 'not_found': + $notFoundPath = trim( + (string)(parse_url($_SERVER['REDIRECT_URL'] ?? $_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH) ?? ''), + '/' + ); + if ($notFoundPath !== '') { + searchAndRedirect(basename($notFoundPath), $articles); + } + http_response_code(404); + ob_start(); + ?> +
+

Page introuvable

+

Cette adresse ne correspond à aucun article.
Vous avez peut-être suivi un ancien lien.

+ ← Retour à l'accueil +
+ getPrivateCategories(); + $allCats = $articles->getCategories(); + + $filterCat = array_key_exists('cat', $_GET) ? trim($_GET['cat']) : ''; + $allPosts = array_values(array_filter($articles->getAll(), static function (array $a) use ($privateCats, $filterCat): bool { + if (!$a['published']) { + return canDoOnArticle('view_drafts', $a); + } + $cat = trim($a['category'] ?? ''); + if ($cat !== '' && in_array($cat, $privateCats, true) && !isLoggedIn()) { + return false; + } + if (strtotime((string)($a['published_at'] ?? '')) > time() && !hasCapability('view_previews')) { + return false; + } + if ($filterCat !== '' && $cat !== $filterCat) { + return false; + } + return true; + })); + $perPage = postsPerPage(); + $cursor = trim($_GET['cursor'] ?? ''); + + // Trouve la position du curseur dans la liste triée + $offset = 0; + if ($cursor !== '') { + foreach ($allPosts as $i => $a) { + if ($a['uuid'] === $cursor) { + $offset = $i + 1; + break; + } + } + } + + $posts = array_slice($allPosts, $offset, $perPage); + $nextCursor = count($posts) === $perPage ? end($posts)['uuid'] : null; + $prevCursor = null; + if ($offset > 0) { + $prevOffset = max(0, $offset - $perPage); + $prevCursor = $prevOffset > 0 ? $allPosts[$prevOffset - 1]['uuid'] : ''; + } + + // Compteurs pour le hero de la page d'accueil + $totalPublished = 0; + $totalUpcoming = 0; + $_now = time(); + foreach ($articles->getSearchIndex() ?? [] as $_a) { + if (!($_a['published'] ?? false)) { + continue; + } + $_cat = trim($_a['category'] ?? ''); + if ($_cat !== '' && in_array($_cat, $privateCats, true) && !isLoggedIn()) { + continue; + } + if (strtotime((string)($_a['published_at'] ?? '')) > $_now) { + $totalUpcoming++; + } else { + $totalPublished++; + } + } + unset($_now, $_a, $_cat); + + // ─── Sections spécifiques à la page d'accueil ───────────────────── + $isHomepage = ($cursor === '' && $filterCat === ''); + $heroPost = null; + $latestPosts = []; + $popularPosts = []; + $recentlyUpdated = []; + $redecouvertes = []; + $featuredArticle = null; + + if ($isHomepage) { + // Article mis en avant + foreach ($allPosts as $_hp) { + if (!empty($_hp['featured'])) { + $featuredArticle = $_hp; + break; + } + } + $heroPost = $featuredArticle ?? ($allPosts[0] ?? null); + + // 5 articles les plus récents (hors hero) + $_heroUuid = $heroPost['uuid'] ?? ''; + $_count = 0; + foreach ($allPosts as $_hp) { + if ($_hp['uuid'] === $_heroUuid) { + continue; + } + $latestPosts[] = $_hp; + if (++$_count >= 5) { + break; + } + } + unset($_heroUuid, $_count, $_hp); + + $allPostsMap = array_column($allPosts, null, 'uuid'); + + // Articles populaires (10 derniers jours) — score pondéré + $_pdo = dbPdo(); + if ($_pdo) { + try { + $_stmt = $_pdo->query(" + SELECT article_uuid, SUM(score) AS total + FROM ( + SELECT article_uuid, 1 AS score FROM article_reactions + WHERE created_at >= NOW() - INTERVAL '10 days' + UNION ALL + SELECT article_uuid, 2 AS score FROM article_ratings + WHERE rated_at >= NOW() - INTERVAL '10 days' + UNION ALL + SELECT article_uuid, 3 AS score FROM comments + WHERE created_at >= NOW() - INTERVAL '10 days' AND published = TRUE + ) ev + GROUP BY article_uuid + ORDER BY total DESC + LIMIT 20 + "); + foreach ($_stmt->fetchAll(PDO::FETCH_ASSOC) as $_row) { + if (count($popularPosts) >= 6) { + break; + } + $_uuid = $_row['article_uuid']; + if (!isset($allPostsMap[$_uuid])) { + continue; + } + $popularPosts[] = $allPostsMap[$_uuid]; + } + } catch (Throwable) { + } + + // Redécouvertes : anciens articles (> 30 j) avec activité récente + try { + $_stmt = $_pdo->query(" + SELECT DISTINCT article_uuid FROM ( + SELECT article_uuid FROM article_reactions + WHERE created_at >= NOW() - INTERVAL '10 days' + UNION + SELECT article_uuid FROM comments + WHERE created_at >= NOW() - INTERVAL '10 days' AND published = TRUE + ) ev + "); + $_thirtyDaysAgo = date('Y-m-d H:i:s', time() - 30 * 86400); + $_latestUuids = array_column($latestPosts, 'uuid'); + $_popularUuids = array_column($popularPosts, 'uuid'); + $_heroUuid = $heroPost['uuid'] ?? ''; + foreach ($_stmt->fetchAll(PDO::FETCH_COLUMN, 0) as $_uuid) { + if (count($redecouvertes) >= 4) { + break; + } + if (!isset($allPostsMap[$_uuid])) { + continue; + } + $_a = $allPostsMap[$_uuid]; + if (($_a['published_at'] ?? '') >= $_thirtyDaysAgo) { + continue; + } + if ($_a['uuid'] === $_heroUuid) { + continue; + } + if (in_array($_uuid, $_latestUuids, true)) { + continue; + } + if (in_array($_uuid, $_popularUuids, true)) { + continue; + } + $redecouvertes[] = $_a; + } + unset($_thirtyDaysAgo, $_latestUuids, $_popularUuids, $_heroUuid, $_uuid, $_a); + } catch (Throwable) { + } + } + unset($_pdo, $_stmt, $_row); + + // Récemment mis à jour (7 derniers jours, pas dans les autres sections) + $_sevenDaysAgo = date('Y-m-d H:i:s', time() - 7 * 86400); + $_latestUuids = array_column($latestPosts, 'uuid'); + $_popularUuids = array_column($popularPosts, 'uuid'); + $_heroUuid = $heroPost['uuid'] ?? ''; + foreach ($allPosts as $_a) { + if (count($recentlyUpdated) >= 4) { + break; + } + if ($_a['uuid'] === $_heroUuid) { + continue; + } + if (in_array($_a['uuid'], $_latestUuids, true)) { + continue; + } + if (in_array($_a['uuid'], $_popularUuids, true)) { + continue; + } + if (($_a['updated_at'] ?? '') < $_sevenDaysAgo) { + continue; + } + if (($_a['published_at'] ?? '') >= $_sevenDaysAgo) { + continue; + } // déjà récent dans latest + $recentlyUpdated[] = $_a; + } + unset($_sevenDaysAgo, $_latestUuids, $_popularUuids, $_heroUuid, $_a, $allPostsMap); + } + // ────────────────────────────────────────────────────────────────── + + include BASE_PATH . '/templates/post_list.php'; + break; +} diff --git a/public/login/config.php b/public/login/config.php new file mode 100644 index 0000000..923af8c --- /dev/null +++ b/public/login/config.php @@ -0,0 +1,136 @@ + trim((string)($_POST['oidc_issuer'] ?? '')), + 'oidc_name' => trim((string)($_POST['oidc_name'] ?? '')), + 'oidc_client_id' => trim((string)($_POST['oidc_client_id'] ?? '')), + 'oidc_client_secret' => trim((string)($_POST['oidc_client_secret'] ?? '')), + 'oidc_redirect_uri' => trim((string)($_POST['oidc_redirect_uri'] ?? '')), + ]; + + // validations simples + if ($in['allow_oidc']) { + if ($in['oidc_issuer'] === '' || $in['oidc_client_id'] === '' || $in['oidc_client_secret'] === '' || $in['oidc_redirect_uri'] === '') { + $err = 'OIDC activé mais champs incomplets.'; + } + } + + if (!$err) { + config_repo_save($in); + + // Mise à jour du .env + $envPairs = [ + 'OIDC_ISSUER' => $in['oidc_issuer'] !== '' ? $in['oidc_issuer'] : null, + 'OIDC_NAME' => $in['oidc_name'] !== '' ? $in['oidc_name'] : null, + 'OIDC_CLIENT_ID' => $in['oidc_client_id'] !== '' ? $in['oidc_client_id'] : null, + 'OIDC_CLIENT_SECRET' => $in['oidc_client_secret'] !== '' ? $in['oidc_client_secret'] : null, + 'OIDC_REDIRECT_URI' => $in['oidc_redirect_uri'] !== '' ? $in['oidc_redirect_uri'] : null, + ]; + env_set_pairs(BASE_PATH.'/.env', $envPairs); + + $cfg = config_repo_get(); + $msg = 'Configuration enregistrée.'; + } +} +?> + + + + + Configuration authentification + + + + +
+

Configuration authentification

+ +
+
+ +
+ +
+ Modes de connexion +
+ > + +
+
+ > + +
+
+ +
+ Inscriptions +
+ > + +
+
+ > + +
+
+ +
+ Paramètres OIDC +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+

Ces champs alimentent le fichier .env.

+
+ +
+ +
+
+
+ + + diff --git a/public/login/index.php b/public/login/index.php new file mode 100644 index 0000000..3c84d1f --- /dev/null +++ b/public/login/index.php @@ -0,0 +1,216 @@ +beginTransaction(); + try { + // purge expirés / consommés + $pdo->prepare('DELETE FROM auth_magic_links WHERE email = :e AND (expires_at < NOW() OR consumed_at IS NOT NULL)') + ->execute([':e' => $email]); + + // 1) cooldown: refuser si un envoi récent < coolMin + $sql = sprintf( + "SELECT 1 FROM auth_magic_links + WHERE email = :e AND created_at >= NOW() - INTERVAL '%d minutes' + LIMIT 1", + max(0, $coolMin) + ); + $stmt = $pdo->prepare($sql); + $stmt->execute([':e' => $email]); + if ($stmt->fetchColumn()) { + throw new RuntimeException(sprintf('Un lien vient d’être envoyé. Réessayez dans %d min. + Si vous ne recevez toujours rien, envisagez d\'utiliser un fournisseur de messagerie respectueux de la vie privée, + comme Proton Mail, Tuta, Posteo, Mailfence ou Infomaniak, qui garantissent un hébergement européen + et ne revendent pas vos données. -- Cédrix, le 11/10/2025', $coolMin)); + } + + // 2) plafond: maxPerWin liens sur winHours + $sql = sprintf( + "SELECT COUNT(*) FROM auth_magic_links + WHERE email = :e AND created_at >= NOW() - INTERVAL '%d hours'", + max(0, $winHours) + ); + $stmt = $pdo->prepare($sql); + $stmt->execute([':e' => $email]); + if ((int)$stmt->fetchColumn() >= $maxPerWin) { + throw new RuntimeException('Quota atteint. Réessayez plus tard.'); + } + + // Génère et enregistre le lien avec TTL ttlMin + $raw = random_bytes(32); + $token = rtrim(strtr(base64_encode($raw), '+/', '-_'), '='); + + $sql = sprintf( + "INSERT INTO auth_magic_links (id,email,token,created_at,expires_at,ip,user_agent,return_to) + VALUES (gen_random_uuid(), :email, :token, NOW(), NOW() + INTERVAL '%d minutes', :ip, :ua, :rt) + RETURNING token", + max(1, $ttlMin) + ); + $stmt = $pdo->prepare($sql); + $stmt->execute([ + ':email' => $email, + ':token' => $token, + ':ip' => $ip, + ':ua' => substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 512), + ':rt' => ($returnTo !== '/' ? $returnTo : null), + ]); + $pdo->commit(); + + // construit l'URL et envoie le mail + $magicUrl = url('/login/magic.php') . '?token=' . urlencode($token); + $siteName = htmlspecialchars(env('SMTP_FROM_NAME', 'varlog'), ENT_QUOTES); + $html = <<Bonjour,

+

Cliquez sur le lien ci-dessous pour vous connecter à {$siteName} :

+

{$magicUrl}

+

Ce lien est valable {$ttlMin} minutes et ne peut être utilisé qu'une seule fois.

+

Si vous n'avez pas demandé ce lien, ignorez cet email.

+ HTML; + envoyer_mail_smtp( + $email, + "Votre lien de connexion — {$siteName}", + $html, + null, + ['bypass_rate_limit' => true] + ); + + // message utilisateur + $okMsg = 'Un lien vient d’être envoyé. Vérifiez votre boîte de réception et le dossier spam/indésirables.'; + + } catch (\Throwable $ex) { + if ($pdo->inTransaction()) { + $pdo->rollBack(); + } + $errors[] = $ex->getMessage(); + } + } + } +} + +$csrf = Csrf::token(); + +ob_start(); +?> + + +
+
+

Connexion

+

Vous n'êtes pas connecté.

+ + +
+ + +
+ + + + +
ou
+ +
A5L indisponible : configurez OIDC_ISSUER et OIDC_CLIENT_ID dans .env.
+
ou
+ + +
+ +
+ + +
+ +
+
+
+ +beginTransaction(); +try { + // récupère lien non consommé et non expiré + $sql = 'SELECT id, email, token, created_at, expires_at, consumed_at, return_to + FROM auth_magic_links + WHERE token = :t + FOR UPDATE'; + $stmt = $pdo->prepare($sql); + $stmt->execute([':t' => $token]); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + + if (!$row) { + throw new RuntimeException('Lien inconnu.'); + } + if ($row['consumed_at'] !== null) { + throw new RuntimeException('Lien déjà utilisé.'); + } + if (strtotime((string)$row['expires_at']) < time()) { + throw new RuntimeException('Lien expiré.'); + } + + // consomme le lien + $pdo->prepare('UPDATE auth_magic_links SET consumed_at = NOW() WHERE id = :id')->execute([':id' => $row['id']]); + $pdo->commit(); + + if (session_status() !== PHP_SESSION_ACTIVE) { + session_start(); + } + session_regenerate_id(true); + $_SESSION['user_email'] = strtolower(trim((string)$row['email'])); + + $dest = $row['return_to'] ?? '/'; + // sécurité: ne renvoyer que des chemins relatifs + if (!is_string($dest) || !str_starts_with($dest, '/')) { + $dest = '/'; + } + header('Location: ' . $dest, true, 303); + exit; +} catch (\Throwable $e) { + if ($pdo->inTransaction()) { + $pdo->rollBack(); + } + http_response_code(400); + echo htmlspecialchars($e->getMessage(), ENT_QUOTES); +} diff --git a/public/login/oidc.php b/public/login/oidc.php new file mode 100644 index 0000000..61b7d1d --- /dev/null +++ b/public/login/oidc.php @@ -0,0 +1,6 @@ + 'authorization_code', + 'code' => $code, + 'redirect_uri' => $OIDC_REDIRECT_URI, + 'client_id' => $OIDC_CLIENT_ID, + 'code_verifier' => $codeVerifier, +]; +if ($OIDC_CLIENT_SECRET !== '') { + $post['client_secret'] = $OIDC_CLIENT_SECRET; +} + +$ch = curl_init($tokenEndpoint); +curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => http_build_query($post, '', '&', PHP_QUERY_RFC3986), + CURLOPT_TIMEOUT => 15, + CURLOPT_SSL_VERIFYPEER => true, + CURLOPT_SSL_VERIFYHOST => 2, +]); +$tokenResponse = curl_exec($ch); +$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); +$curlErr = curl_error($ch); +curl_close($ch); + +if ($tokenResponse === false || $httpCode !== 200) { + http_response_code(500); + echo $debug ? 'Échec échange token : ' . htmlspecialchars($curlErr ?: (string)$tokenResponse) : 'Erreur d\'authentification.'; + exit; +} + +$tokens = json_decode((string)$tokenResponse, true) ?: []; +$accessToken = $tokens['access_token'] ?? null; +$idToken = $tokens['id_token'] ?? null; + +if (!$accessToken) { + http_response_code(500); + echo $debug ? 'Access token manquant.' : 'Erreur d\'authentification.'; + exit; +} + +// UserInfo +$ch = curl_init($userInfoEndpoint); +curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $accessToken], + CURLOPT_TIMEOUT => 10, + CURLOPT_SSL_VERIFYPEER => true, + CURLOPT_SSL_VERIFYHOST => 2, +]); +$userInfoResponse = curl_exec($ch); +$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); +curl_close($ch); + +if ($userInfoResponse === false || $httpCode !== 200) { + http_response_code(500); + echo $debug ? 'Échec UserInfo.' : 'Erreur d\'authentification.'; + exit; +} + +$claims = json_decode((string)$userInfoResponse, true) ?: []; +$email = $claims['email'] ?? null; + +// Fallback : lire l'email depuis le payload du id_token +if (!$email && $idToken && substr_count($idToken, '.') === 2) { + [, $p, ] = explode('.', $idToken, 3); + $payload = json_decode((string)base64_decode(strtr($p, '-_', '+/'), true), true); + if (is_array($payload) && !empty($payload['email'])) { + $email = $payload['email']; + } +} + +if (!$email) { + http_response_code(400); + echo $debug ? 'Email non fourni par l\'IdP.' : 'Impossible de récupérer votre email.'; + exit; +} + +// Nom d'affichage depuis les claims SSO +$ssoName = ''; +if (!empty($claims['given_name']) || !empty($claims['family_name'])) { + $ssoName = trim(($claims['given_name'] ?? '') . ' ' . ($claims['family_name'] ?? '')); +} elseif (!empty($claims['name'])) { + $ssoName = trim($claims['name']); +} elseif (!empty($claims['preferred_username'])) { + $ssoName = trim($claims['preferred_username']); +} + +// Charge le nom personnalisé depuis la base (prioritaire sur le SSO) +require_once dirname(__DIR__, 2) . '/src/auth.php'; +$pdo = dbPdo(); +$dbName = ''; +if ($pdo) { + try { + $st = $pdo->prepare('SELECT display_name FROM user_profiles WHERE email = :e'); + $st->execute([':e' => strtolower(trim($email))]); + $dbName = (string)($st->fetchColumn() ?: ''); + } catch (\Throwable) { + } +} + +if ($dbName !== '') { + // Nom personnalisé existant → on le conserve, le SSO ne l'écrase pas + $sessionName = $dbName; +} else { + // Première connexion → on persiste le nom SSO + $sessionName = $ssoName; + if ($ssoName !== '' && $pdo) { + try { + $pdo->prepare( + 'INSERT INTO user_profiles (email, display_name, updated_at) + VALUES (:e, :n, now()) + ON CONFLICT (email) DO NOTHING' + )->execute([':e' => strtolower(trim($email)), ':n' => $ssoName]); + } catch (\Throwable) { + } + } +} + +// Ouvre la session authentifiée +session_regenerate_id(true); +$_SESSION['user_email'] = strtolower(trim($email)); +$_SESSION['user_display_name'] = $sessionName; +$_SESSION['oidc'] = [ + 'issuer' => $OIDC_ISSUER, + 'sub' => $claims['sub'] ?? null, + 'access_token' => $accessToken, + 'id_token' => $idToken, + 'expires_at' => time() + (int)($tokens['expires_in'] ?? 3600), +]; + +$target = $_SESSION['oidc_return_to'] ?? '/'; +unset($_SESSION['oidc_return_to'], $_SESSION['oidc_flow']); +if (!is_string($target) || $target === '' || $target[0] !== '/') { + $target = '/'; +} +header('Location: ' . $target, true, 303); +exit; diff --git a/public/oidc/me.php b/public/oidc/me.php new file mode 100644 index 0000000..91cf8f5 --- /dev/null +++ b/public/oidc/me.php @@ -0,0 +1,194 @@ + true, + CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $accTok], + CURLOPT_TIMEOUT => 6, + ]); + $resp = curl_exec($ch); + $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + if ($resp !== false && $code === 200) { + $tmp = json_decode((string)$resp, true); + if (is_array($tmp)) { + $claims = $tmp; + } + } +} + +// Extraire rôles groupés (Keycloak) +$roles = []; +if (isset($claims['realm_access']['roles']) && is_array($claims['realm_access']['roles'])) { + $roles = array_merge($roles, $claims['realm_access']['roles']); +} +if (isset($claims['resource_access']) && is_array($claims['resource_access'])) { + foreach ($claims['resource_access'] as $clientId => $data) { + if (!empty($data['roles']) && is_array($data['roles'])) { + foreach ($data['roles'] as $r) { + $roles[] = $clientId . ':' . $r; + } + } + } +} +$roles = array_values(array_unique($roles)); +?> + + + + +OIDC • Profil + + + + + +

Profil A5L

+ + +
Aucune session A5L. Connecte-toi via A5L d'abord.
+ +
+
Session / Jetons
+
+
+
Issuer
+
Subject (sub)
+
ID Token
+
Access Token
+
Expire à
+
Temps restant
+
+ +
+ Voir jetons non masqués (danger) +
+
ID Token
+
+
Access Token
+
+
+
+ +
+
+ +
+
Claims
+
+
+
Email
+
Preferred username
+
Given name
+
Family name
+
Name
+
Locale
+
Rôles
+
+ +
    + +
  • + +
+ + — + +
+
+ + +
Claims (JSON complet)
+
+ + + +
+ Aucun claim reçu. Vérifie que ton callback remplit bien $_SESSION['oidc_userinfo'] ou que l’ID Token contient les champs. +
+ +
+
+ + + Retour + + diff --git a/public/oidc/start.php b/public/oidc/start.php new file mode 100644 index 0000000..5461ec8 --- /dev/null +++ b/public/oidc/start.php @@ -0,0 +1,72 @@ + 'code', + 'client_id' => $clientId, + 'redirect_uri' => $redirectUri, + 'scope' => 'openid email profile', + 'state' => $state, + 'nonce' => $nonce, + 'code_challenge' => $codeChallenge, + 'code_challenge_method' => 'S256', + 'ui_locales' => 'fr', +]; + +header('Location: ' . $authEndpoint . '?' . http_build_query($params, '', '&', PHP_QUERY_RFC3986), true, 302); +exit; diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..f3362e1 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,16 @@ +User-agent: * +Disallow: /?action=edit +Disallow: /?action=create +Disallow: /?action=admin +Disallow: /?action=delete +Disallow: /?action=diff +Disallow: /?action=categories +Disallow: /?action=add_files +Disallow: /?action=import_image +Disallow: /?action=sources +Disallow: /?action=profile +Disallow: /login +Disallow: /logout.php +Disallow: /oidc/ + +Sitemap: https://varlog.a5l.fr/sitemap.xml diff --git a/public/route.php b/public/route.php new file mode 100644 index 0000000..9919296 --- /dev/null +++ b/public/route.php @@ -0,0 +1,7 @@ +getPrivateCategories(); + +$published = array_filter($articles->getAll(true), static function (array $a) use ($privateCats): bool { + $cat = trim($a['category'] ?? ''); + if ($cat !== '' && in_array($cat, $privateCats, true)) { + return false; + } + if (strtotime((string)($a['published_at'] ?? '')) > time()) { + return false; + } + return true; +}); + +header('Content-Type: application/xml; charset=UTF-8'); +header('X-Robots-Tag: noindex'); + +echo '' . "\n"; +echo '' . "\n"; + +// Homepage +echo ' ' . "\n"; +echo ' ' . htmlspecialchars(rtrim(APP_URL, '/') . '/') . '' . "\n"; +echo ' daily' . "\n"; +echo ' 1.0' . "\n"; +echo ' ' . "\n"; + +foreach ($published as $article) { + $loc = htmlspecialchars(rtrim(APP_URL, '/') . '/post/' . rawurlencode($article['slug'] ?? '')); + $lastmod = date('Y-m-d', strtotime((string)($article['updated_at'] ?? $article['published_at'] ?? 'now'))); + echo ' ' . "\n"; + echo ' ' . $loc . '' . "\n"; + echo ' ' . $lastmod . '' . "\n"; + echo ' monthly' . "\n"; + echo ' 0.8' . "\n"; + echo ' ' . "\n"; +} + +echo '' . "\n"; diff --git a/scripts/fetch-network-info.sh b/scripts/fetch-network-info.sh new file mode 100644 index 0000000..8dda75e --- /dev/null +++ b/scripts/fetch-network-info.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# Récupère l'IP publique et les infos AS du FAI courant. +# Résultat dans config/network-info.json — lu par templates/legal.php +set -euo pipefail + +OUT="/var/www/lan.acegrp.varlog/config/network-info.json" + +json=$(curl -sf --max-time 10 "https://ipinfo.io/json") + +if [ -z "$json" ]; then + echo "Erreur : réponse vide de ip-api.com" >&2 + exit 1 +fi + +echo "$json" > "$OUT" +echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) — network-info mis à jour : $json" diff --git a/src/ArticleManager.php b/src/ArticleManager.php new file mode 100644 index 0000000..b7c2711 --- /dev/null +++ b/src/ArticleManager.php @@ -0,0 +1,1213 @@ +allCache === null) { + $this->allCache = $this->loadAll(); + } + if ($publishedOnly) { + return array_values(array_filter($this->allCache, fn ($a) => $a['published'])); + } + return $this->allCache; + } + + private function loadAll(): array + { + $articles = []; + if (!is_dir($this->dataDir)) { + return $articles; + } + + foreach (scandir($this->dataDir) as $entry) { + if ($entry === '.' || $entry === '..') { + continue; + } + $dir = $this->dataDir . '/' . $entry; + if (!is_dir($dir) || !file_exists($dir . '/meta.json')) { + continue; + } + + $article = $this->loadArticle($dir); + if (!$article) { + continue; + } + $articles[] = $article; + } + + usort($articles, static fn ($a, $b) => strcmp($b['published_at'] ?? '', $a['published_at'] ?? '')); + + return $articles; + } + + public function getBySlug(string $slug): ?array + { + $path = $this->slugIndexPath(); + if (!file_exists($path)) { + $this->buildSlugIndex(); + } + $index = json_decode((string) file_get_contents($path), true) ?? []; + $uuid = $index[$slug] ?? null; + return $uuid !== null ? $this->getByUuid($uuid) : null; + } + + public function getByUuid(string $uuid): ?array + { + if (!$this->isValidUuid($uuid)) { + return null; + } + $dir = $this->dataDir . '/' . $uuid; + if (!is_dir($dir) || !file_exists($dir . '/meta.json')) { + return null; + } + return $this->loadArticle($dir); + } + + public function getRevisionContent(string $uuid, int $n): ?string + { + if (!$this->isValidUuid($uuid) || $n < 1) { + return null; + } + $path = sprintf('%s/%s/revisions/%04d.md', $this->dataDir, $uuid, $n); + if (!file_exists($path)) { + return null; + } + $c = file_get_contents($path); + return $c !== false ? $c : null; + } + + // ------------------------------------------------------------------ // + // Écriture + // ------------------------------------------------------------------ // + + public function create(string $title, string $content, bool $published, string $slug = '', string $publishedAt = '', string $author = '', string $seoTitle = '', string $seoDescription = '', string $ogImage = '', string $category = '', array $tags = []): string + { + $uuid = $this->generateUuid(); + $slug = $slug !== '' ? $this->sanitizeSlug($slug) : $this->generateSlug($title); + $slug = $this->uniqueSlug($slug, $uuid); + $now = date('Y-m-d H:i:s'); + $publishedAt = $publishedAt !== '' ? $publishedAt : $now; + + $dir = $this->dataDir . '/' . $uuid; + mkdir($dir, 0755, true); + mkdir($dir . '/files', 0755, true); + + $meta = [ + 'uuid' => $uuid, + 'slug' => $slug, + 'title' => $title, + 'author' => $author, + 'published' => $published, + 'featured' => false, + 'published_at' => $publishedAt, + 'created_at' => $now, + 'updated_at' => $now, + 'revisions' => [], + 'cover' => '', + 'files_meta' => [], + 'external_links' => [], + 'seo_title' => $seoTitle, + 'seo_description' => $seoDescription, + 'og_image' => $ogImage, + 'category' => $category, + 'tags' => $this->normalizeTags($tags), + ]; + $this->writeMeta($dir, $meta); + file_put_contents($dir . '/index.md', ltrim($content)); + $this->rebuildSearchIndex(); + $this->rebuildBacklinksCache(); + + return $uuid; + } + + public function update(string $uuid, string $title, string $content, bool $published, string $slug, string $publishedAt, string $revisionComment = '', string $seoTitle = '', string $seoDescription = '', string $ogImage = '', string $category = '', ?array $tags = null): void + { + $article = $this->getByUuid($uuid); + if (!$article) { + return; + } + + $slug = $slug !== '' ? $this->sanitizeSlug($slug) : $this->generateSlug($title); + $slug = $this->uniqueSlug($slug, $uuid); + + // Snapshot de l'état courant avant écrasement — uniquement si le contenu ou le titre a changé + $revisions = $article['revisions'] ?? []; + $contentChanged = ltrim($content) !== ($article['content'] ?? ''); + $titleChanged = $title !== ($article['title'] ?? ''); + + if ($contentChanged || $titleChanged) { + $revDir = $this->dataDir . '/' . $uuid . '/revisions'; + if (!is_dir($revDir)) { + mkdir($revDir, 0755, true); + } + $n = count($revisions) + 1; + $revFile = sprintf('%s/%04d.md', $revDir, $n); + file_put_contents($revFile, $article['content']); + + $revisions[] = [ + 'n' => $n, + 'date' => date('Y-m-d H:i:s'), + 'comment' => $revisionComment, + 'title' => $article['title'], + ]; + + // Limite à MAX_REVISIONS + if (count($revisions) > self::MAX_REVISIONS) { + $oldest = array_shift($revisions); + @unlink(sprintf('%s/%04d.md', $revDir, (int)($oldest['n'] ?? 0))); + } + } // fin if ($contentChanged || $titleChanged) + + $meta = [ + 'uuid' => $uuid, + 'slug' => $slug, + 'title' => $title, + 'author' => $article['author'] ?? '', + 'published' => $published, + 'featured' => (bool)($article['featured'] ?? false), + 'published_at' => $publishedAt !== '' ? $publishedAt : ($article['published_at'] ?? date('Y-m-d H:i:s')), + 'created_at' => $article['created_at'] ?? date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s'), + 'revisions' => $revisions, + 'cover' => $article['cover'] ?? '', + 'files_meta' => $article['files_meta'] ?? [], + 'external_links' => $article['external_links'] ?? [], + 'seo_title' => $seoTitle, + 'seo_description' => $seoDescription, + 'og_image' => $ogImage, + 'category' => $category, + 'tags' => $tags !== null ? $this->normalizeTags($tags) : ($article['tags'] ?? []), + ]; + $dir = $this->dataDir . '/' . $uuid; + $this->writeMeta($dir, $meta); + file_put_contents($dir . '/index.md', ltrim($content)); + $this->rebuildSearchIndex(); + $this->rebuildBacklinksCache(); + } + + public function autosave(string $uuid, string $title, string $content, string $slug): bool + { + if (!$this->isValidUuid($uuid)) { + return false; + } + $dir = $this->dataDir . '/' . $uuid; + $raw = @file_get_contents($dir . '/meta.json'); + if ($raw === false) { + return false; + } + $meta = json_decode($raw, true); + if (!is_array($meta)) { + return false; + } + + $slug = $slug !== '' ? $this->sanitizeSlug($slug) : $this->generateSlug($title); + $slug = $this->uniqueSlug($slug, $uuid); + + $meta['title'] = $title; + $meta['slug'] = $slug; + $meta['updated_at'] = date('Y-m-d H:i:s'); + + $this->writeMeta($dir, $meta); + @file_put_contents($dir . '/index.md', ltrim($content)); + return true; + } + + public function addFileMeta(string $uuid, string $filename, string $author, string $sourceUrl, string $title = '', array $extraMeta = []): void + { + if (!$this->isValidUuid($uuid)) { + return; + } + $filename = basename($filename); + + $raw = file_get_contents($this->dataDir . '/' . $uuid . '/meta.json'); + if ($raw === false) { + return; + } + $meta = json_decode($raw, true); + if (!is_array($meta)) { + return; + } + if (!isset($meta['files_meta']) || !is_array($meta['files_meta'])) { + $meta['files_meta'] = []; + } + $entry = ['author' => $author, 'source_url' => $sourceUrl]; + if ($title !== '') { + $entry['title'] = $title; + } + if (!empty($extraMeta)) { + $clean = $extraMeta; + unset($clean['title'], $clean['author'], $clean['credit'], $clean['source']); + $entry['meta'] = $clean; + } + $meta['files_meta'][$filename] = $entry; + $this->writeMeta($this->dataDir . '/' . $uuid, $meta); + } + + public function setCover(string $uuid, string $filename): void + { + $article = $this->getByUuid($uuid); + if (!$article) { + return; + } + $filename = basename($filename); + $filesDir = $this->dataDir . '/' . $uuid . '/files'; + $targetPath = $filesDir . '/' . $filename; + if (!file_exists($targetPath)) { + return; + } + + $mime = mime_content_type($targetPath) ?: ''; + $ext = $this->extFromMime($mime) ?? strtolower(pathinfo($filename, PATHINFO_EXTENSION)) ?: 'jpg'; + $coverName = 'cover.' . $ext; + + // Rename old cover back to hash name + $oldCover = $article['cover'] ?? ''; + if ($oldCover && $oldCover !== $filename && $oldCover !== $coverName) { + $oldPath = $filesDir . '/' . basename($oldCover); + if (file_exists($oldPath)) { + $hash = substr(hash_file('sha256', $oldPath), 0, 16); + $size = filesize($oldPath); + $oldExt = strtolower(pathinfo($oldCover, PATHINFO_EXTENSION)); + rename($oldPath, $filesDir . '/' . "{$hash}-{$size}.{$oldExt}"); + } + } + + // Rename target to cover.{ext} + $newPath = $filesDir . '/' . $coverName; + if ($targetPath !== $newPath) { + rename($targetPath, $newPath); + } + + $raw = file_get_contents($this->dataDir . '/' . $uuid . '/meta.json'); + if ($raw === false) { + return; + } + $meta = json_decode($raw, true); + if (!is_array($meta)) { + return; + } + $meta['cover'] = $coverName; + $this->writeMeta($this->dataDir . '/' . $uuid, $meta); + } + + public function addFileFromUrl(string $uuid, string $url, bool $isCover = false, string $author = '', string $sourceUrl = '', string $title = '', array $extraMeta = []): ?string + { + if (!$this->isValidUuid($uuid)) { + return null; + } + if (!filter_var($url, FILTER_VALIDATE_URL) || !preg_match('#^https?://#i', $url)) { + return null; + } + + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_MAXREDIRS => 3, + CURLOPT_TIMEOUT => 20, + CURLOPT_CONNECTTIMEOUT => 5, + CURLOPT_USERAGENT => 'varlog/1.0', + ]); + $body = curl_exec($ch); + $info = curl_getinfo($ch); + curl_close($ch); + + if ($body === false || (int)$info['http_code'] !== 200) { + return null; + } + + $tmp = tempnam(sys_get_temp_dir(), 'vl_'); + file_put_contents($tmp, $body); + + $mime = mime_content_type($tmp) ?: 'application/octet-stream'; + $isImage = str_starts_with($mime, 'image/'); + $filesDir = $this->dataDir . '/' . $uuid . '/files'; + if (!is_dir($filesDir)) { + mkdir($filesDir, 0755, true); + } + + if ($isImage) { + $ext = $this->extFromMime($mime) ?? strtolower(pathinfo(parse_url($url, PHP_URL_PATH) ?? '', PATHINFO_EXTENSION)) ?: 'jpg'; + + if ($isCover) { + // Gérer l'ancienne cover + $article = $this->getByUuid($uuid); + $oldCover = $article['cover'] ?? ''; + if ($oldCover) { + $oldPath = $filesDir . '/' . basename($oldCover); + if (file_exists($oldPath)) { + $hash = substr(hash_file('sha256', $oldPath), 0, 16); + $size = filesize($oldPath); + $oldExt = strtolower(pathinfo($oldCover, PATHINFO_EXTENSION)); + rename($oldPath, $filesDir . '/' . "{$hash}-{$size}.{$oldExt}"); + } + } + $filename = 'cover.' . $ext; + } else { + $hash = substr(hash_file('sha256', $tmp), 0, 16); + $size = strlen($body); + $filename = "{$hash}-{$size}.{$ext}"; + } + } else { + // Non-image : nom extrait de l'URL, sanitisé, dédupliqué + $urlPath = parse_url($url, PHP_URL_PATH) ?? ''; + $filename = preg_replace('/[^a-zA-Z0-9._-]/', '_', basename($urlPath)) ?: 'file'; + $i = 1; + $info = pathinfo($filename); + while (file_exists($filesDir . '/' . $filename)) { + $filename = $info['filename'] . '_' . $i . (isset($info['extension']) ? '.' . $info['extension'] : ''); + $i++; + } + } + + rename($tmp, $filesDir . '/' . $filename); + + if ($author !== '' || $sourceUrl !== '' || $title !== '' || !empty($extraMeta)) { + $this->addFileMeta($uuid, $filename, $author, $sourceUrl, $title, $extraMeta); + } + + if ($isCover && $isImage) { + $raw = file_get_contents($this->dataDir . '/' . $uuid . '/meta.json'); + if ($raw !== false) { + $meta = json_decode($raw, true); + if (is_array($meta)) { + $meta['cover'] = $filename; + $this->writeMeta($this->dataDir . '/' . $uuid, $meta); + } + } + } + + return $filename; + } + + public function addExternalLink(string $uuid, string $url, string $title = '', string $author = '', array $extraMeta = []): bool + { + if (!$this->isValidUuid($uuid) || !filter_var($url, FILTER_VALIDATE_URL)) { + return false; + } + $dir = $this->dataDir . '/' . $uuid; + $raw = file_get_contents($dir . '/meta.json'); + if ($raw === false) { + return false; + } + $meta = json_decode($raw, true); + if (!is_array($meta)) { + return false; + } + if (!isset($meta['external_links']) || !is_array($meta['external_links'])) { + $meta['external_links'] = []; + } + foreach ($meta['external_links'] as $link) { + if ($link['url'] === $url) { + return true; // already exists + } + } + $urlPath = parse_url($url, PHP_URL_PATH) ?? ''; + $name = $title !== '' ? $title : (rawurldecode(basename($urlPath)) ?: $url); + $entry = ['url' => $url, 'name' => $name, 'added_at' => date('Y-m-d H:i:s')]; + $resolvedAuthor = $author !== '' ? $author : ($extraMeta['author'] ?? ''); + if ($resolvedAuthor !== '') { + $entry['author'] = $resolvedAuthor; + } + if (!empty($extraMeta)) { + $clean = $extraMeta; + unset($clean['title'], $clean['author'], $clean['credit']); + $entry['meta'] = $clean; + } + $meta['external_links'][] = $entry; + $this->writeMeta($dir, $meta); + $this->rebuildBacklinksCache(); + return true; + } + + public function updateExternalLinkMeta(string $uuid, string $url, array $metaUpdates): bool + { + if (!$this->isValidUuid($uuid)) { + return false; + } + $dir = $this->dataDir . '/' . $uuid; + $raw = file_get_contents($dir . '/meta.json'); + if ($raw === false) { + return false; + } + $meta = json_decode($raw, true); + if (!is_array($meta)) { + return false; + } + $found = false; + foreach ($meta['external_links'] as &$link) { + if ($link['url'] === $url) { + $link['meta'] = array_merge($link['meta'] ?? [], $metaUpdates); + $found = true; + break; + } + } + unset($link); + if (!$found) { + return false; + } + $this->writeMeta($dir, $meta); + return true; + } + + public function removeExternalLink(string $uuid, string $url): bool + { + if (!$this->isValidUuid($uuid)) { + return false; + } + $dir = $this->dataDir . '/' . $uuid; + $raw = file_get_contents($dir . '/meta.json'); + if ($raw === false) { + return false; + } + $meta = json_decode($raw, true); + if (!is_array($meta)) { + return false; + } + $meta['external_links'] = array_values(array_filter( + $meta['external_links'] ?? [], + static fn ($l) => $l['url'] !== $url + )); + $this->writeMeta($dir, $meta); + $this->rebuildBacklinksCache(); + return true; + } + + public function getCategories(): array + { + $cats = []; + $source = $this->getSearchIndex() ?? $this->getAll(); + foreach ($source as $article) { + $cat = trim($article['category'] ?? ''); + if ($cat !== '') { + $cats[$cat] = ($cats[$cat] ?? 0) + 1; + } + } + ksort($cats); + return $cats; + } + + public function renameCategory(string $old, string $new): void + { + if (!is_dir($this->dataDir)) { + return; + } + foreach (scandir($this->dataDir) as $entry) { + if ($entry === '.' || $entry === '..') { + continue; + } + $metaPath = $this->dataDir . '/' . $entry . '/meta.json'; + if (!file_exists($metaPath)) { + continue; + } + $raw = file_get_contents($metaPath); + if ($raw === false) { + continue; + } + $meta = json_decode($raw, true); + if (!is_array($meta) || trim($meta['category'] ?? '') !== $old) { + continue; + } + $meta['category'] = $new; + $this->writeMeta($this->dataDir . '/' . $entry, $meta); + } + } + + public function deleteCategory(string $name): void + { + $this->renameCategory($name, ''); + } + + public function getPrivateCategories(): array + { + $path = $this->dataDir . '/private_cats.json'; + if (!file_exists($path)) { + return []; + } + $data = json_decode((string)file_get_contents($path), true); + return is_array($data) ? $data : []; + } + + public function togglePrivateCategory(string $cat): void + { + $cats = $this->getPrivateCategories(); + if (in_array($cat, $cats, true)) { + $cats = array_values(array_filter($cats, fn ($c) => $c !== $cat)); + } else { + $cats[] = $cat; + } + file_put_contents( + $this->dataDir . '/private_cats.json', + json_encode(array_values($cats), JSON_UNESCAPED_UNICODE) + ); + } + + // ─── Tag types ────────────────────────────────────────────────────────────── + + private function tagTypesPath(): string + { + return $this->dataDir . '/tag_types.json'; + } + + public function getTagTypes(): array + { + $p = $this->tagTypesPath(); + if (!file_exists($p)) { + return []; + } + $data = json_decode((string) file_get_contents($p), true); + return is_array($data) ? $data : []; + } + + public function saveTagTypes(array $types): void + { + file_put_contents( + $this->tagTypesPath(), + json_encode($types, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n" + ); + } + + /** Enregistre les tags d'un article directement (utile pour les scripts de migration). */ + public function setTags(string $uuid, array $tags): void + { + $dir = $this->dataDir . '/' . $uuid; + $metaPath = $dir . '/meta.json'; + if (!file_exists($metaPath)) { + return; + } + + $meta = json_decode((string) file_get_contents($metaPath), true); + if (!is_array($meta)) { + return; + } + + $meta['tags'] = $this->normalizeTags($tags); + $this->writeMeta($dir, $meta); + $this->rebuildSearchIndex(); + } + + /** @return list Toutes les valeurs distinctes d'un type de tag, triées. */ + public function getAllTagValues(string $type): array + { + $values = []; + foreach ($this->getSearchIndex() ?? $this->getAll() as $a) { + foreach (($a['tags'][$type] ?? []) as $v) { + $values[$v] = true; + } + } + ksort($values); + return array_keys($values); + } + + private function normalizeTags(array $raw): array + { + $out = []; + foreach ($raw as $type => $values) { + $type = trim((string)$type); + if ($type === '') { + continue; + } + $vals = array_values(array_filter(array_map('trim', (array)$values), fn ($v) => $v !== '')); + if ($vals !== []) { + $out[$type] = $vals; + } + } + return $out; + } + + public function setFeatured(string $uuid, bool $featured): void + { + if (!$this->isValidUuid($uuid)) { + return; + } + $dir = $this->dataDir . '/' . $uuid; + $raw = @file_get_contents($dir . '/meta.json'); + if ($raw === false) { + return; + } + $meta = json_decode($raw, true); + if (!is_array($meta)) { + return; + } + $meta['featured'] = $featured; + $this->writeMeta($dir, $meta); + $this->allCache = null; + @unlink($this->articleCachePath($uuid)); + } + + public function delete(string $uuid): void + { + if (!$this->isValidUuid($uuid)) { + return; + } + $dir = $this->dataDir . '/' . $uuid; + if (is_dir($dir)) { + $this->allCache = null; + @unlink($this->articleCachePath($uuid)); + @unlink($this->slugIndexPath()); + $this->removeDir($dir); + } + $this->rebuildSearchIndex(); + $this->rebuildBacklinksCache(); + } + + // ------------------------------------------------------------------ // + // Cache des rétroliens + // ------------------------------------------------------------------ // + + private function backlinksPath(): string + { + return $this->dataDir . '/_cache/backlinks.json'; + } + + private function articleCachePath(string $uuid): string + { + return $this->dataDir . '/_cache/articles/' . $uuid . '.json'; + } + + private function slugIndexPath(): string + { + return $this->dataDir . '/_cache/slug_index.json'; + } + + private function buildSlugIndex(): void + { + $cacheDir = $this->dataDir . '/_cache'; + if (!is_dir($cacheDir)) { + mkdir($cacheDir, 0755, true); + } + $index = []; + // Préférer le search_index (lecture unique) plutôt que loadAll() (N lectures) + $source = $this->getSearchIndex() ?? $this->loadAll(); + foreach ($source as $article) { + $slug = $article['slug'] ?? ''; + $uuid = $article['uuid'] ?? ''; + if ($slug !== '' && $uuid !== '') { + $index[$slug] = $uuid; + } + } + file_put_contents( + $this->slugIndexPath(), + json_encode($index, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) + ); + } + + /** + * Reconstruit le cache des rétroliens. + * Produit un index slug → [article minimal, ...] pour tous les articles publiés + * qui pointent vers un autre article interne via leurs external_links. + */ + public function rebuildBacklinksCache(): void + { + $cacheDir = $this->dataDir . '/_cache'; + if (!is_dir($cacheDir)) { + mkdir($cacheDir, 0755, true); + } + + $index = []; + foreach ($this->getAll(publishedOnly: true) as $article) { + foreach ($article['external_links'] ?? [] as $link) { + $path = rtrim(parse_url($link['url'] ?? '', PHP_URL_PATH) ?? '', '/'); + if (!preg_match('#^/post/([a-z0-9][a-z0-9-]*)$#', $path, $m)) { + continue; + } + $target = $m[1]; + $index[$target][] = [ + 'uuid' => $article['uuid'], + 'slug' => $article['slug'] ?? '', + 'title' => $article['title'] ?? '', + 'cover' => $article['cover'] ?? '', + 'category' => $article['category'] ?? '', + 'published_at' => $article['published_at'] ?? '', + 'created_at' => $article['created_at'] ?? '', + ]; + } + } + + file_put_contents( + $this->backlinksPath(), + json_encode($index, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) + ); + } + + /** + * Retourne les articles qui pointent vers /post/, depuis le cache. + * Reconstruit le cache si absent. + */ + public function getBacklinks(string $slug, string $excludeUuid = ''): array + { + $path = $this->backlinksPath(); + if (!file_exists($path)) { + $this->rebuildBacklinksCache(); + } + $index = json_decode((string) file_get_contents($path), true); + if (!is_array($index)) { + return []; + } + $result = $index[$slug] ?? []; + if ($excludeUuid !== '') { + $result = array_values(array_filter($result, static fn ($a) => $a['uuid'] !== $excludeUuid)); + } + return $result; + } + + // ------------------------------------------------------------------ // + // Index de recherche (fichier plat) + // ------------------------------------------------------------------ // + + /** + * Reconstruit search_index.json à partir de tous les articles. + * Appelé automatiquement après chaque create/update/delete. + */ + public function rebuildSearchIndex(): void + { + $index = []; + foreach ($this->getAll() as $article) { + $index[] = [ + 'uuid' => $article['uuid'], + 'slug' => $article['slug'] ?? '', + 'title' => $article['title'] ?? '', + 'category' => $article['category'] ?? '', + 'author' => $article['author'] ?? '', + 'cover' => $article['cover'] ?? '', + 'published' => $article['published'], + 'published_at' => $article['published_at'] ?? '', + 'created_at' => $article['created_at'] ?? '', + 'updated_at' => $article['updated_at'] ?? '', + 'tags' => $article['tags'] ?? [], + 'plain' => $this->stripForIndex($article['content'] ?? ''), + ]; + } + file_put_contents( + $this->dataDir . '/search_index.json', + json_encode($index, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) + ); + $this->searchIndexCache = $index; + $this->clearSearchResultsCache(); + } + + /** Vide le cache des résultats de recherche (appelé après chaque modification). */ + public function clearSearchResultsCache(): void + { + $dir = $this->dataDir . '/_cache/search'; + if (!is_dir($dir)) { + return; + } + foreach (scandir($dir) as $f) { + if (str_ends_with($f, '.json')) { + @unlink($dir . '/' . $f); + } + } + } + + /** + * Retourne les résultats mis en cache pour une requête anonyme, ou null si absent. + * @return array|null + */ + public function getSearchCache(string $query): ?array + { + $path = $this->searchCachePath($query); + if (!file_exists($path)) { + return null; + } + $data = json_decode((string) file_get_contents($path), true); + return is_array($data) ? $data : null; + } + + /** Persiste les résultats d'une requête anonyme dans le cache. */ + public function setSearchCache(string $query, array $results): void + { + $dir = $this->dataDir . '/_cache/search'; + if (!is_dir($dir)) { + mkdir($dir, 0755, true); + } + file_put_contents( + $this->searchCachePath($query), + json_encode($results, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) + ); + } + + private function searchCachePath(string $query): string + { + return $this->dataDir . '/_cache/search/' . md5(mb_strtolower(trim($query))) . '.json'; + } + + /** Retourne l'index pré-construit, ou null s'il n'existe pas encore. */ + public function getSearchIndex(): ?array + { + if ($this->searchIndexCache !== null) { + return $this->searchIndexCache; + } + $path = $this->dataDir . '/search_index.json'; + if (!file_exists($path)) { + return null; + } + $data = json_decode((string) file_get_contents($path), true); + if (!is_array($data) || empty($data)) { + return null; + } + // Rebuild automatique si le format est obsolète (champs cover/created_at absents) + if (!array_key_exists('cover', $data[0])) { + $this->rebuildSearchIndex(); + return $this->searchIndexCache; + } + $this->searchIndexCache = $data; + return $this->searchIndexCache; + } + + /** Retire la syntaxe Markdown pour stocker du texte brut dans l'index. */ + private function stripForIndex(string $md): string + { + $t = preg_replace('/!\[[^\]]*\]\([^)]+\)/', '', $md) ?? $md; + $t = preg_replace('/\[([^\]]+)\]\([^)]+\)/', '$1', $t) ?? $t; + $t = preg_replace('/```[\s\S]*?```/', '', $t) ?? $t; + $t = preg_replace('/`[^`]+`/', '', $t) ?? $t; + $t = preg_replace('/^#{1,6}\s*/m', '', $t) ?? $t; + $t = preg_replace('/[*_~]{1,3}([^*_~]+)[*_~]{1,3}/', '$1', $t) ?? $t; + $t = preg_replace('/^\s*[-*+|>]\s*/m', '', $t) ?? $t; + $t = preg_replace('/\n{2,}/', ' ', $t) ?? $t; + return trim($t); + } + + // ------------------------------------------------------------------ // + // Fichiers associés + // ------------------------------------------------------------------ // + + public function getFiles(string $uuid): array + { + $dir = $this->dataDir . '/' . $uuid . '/files'; + if (!is_dir($dir)) { + return []; + } + + $files = []; + foreach (scandir($dir) as $name) { + if ($name === '.' || $name === '..') { + continue; + } + $path = $dir . '/' . $name; + if (!is_file($path)) { + continue; + } + $mime = mime_content_type($path) ?: 'application/octet-stream'; + $files[] = [ + 'name' => $name, + 'size' => filesize($path), + 'mime' => $mime, + 'is_image' => str_starts_with($mime, 'image/'), + 'is_video' => str_starts_with($mime, 'video/'), + 'is_audio' => str_starts_with($mime, 'audio/'), + 'uploaded_at' => date('Y-m-d H:i:s', (int)filemtime($path)), + ]; + } + return $files; + } + + public function deleteFile(string $uuid, string $name): bool + { + if (!$this->isValidUuid($uuid)) { + return false; + } + $name = basename($name); + if ($name === '' || $name[0] === '.') { + return false; + } + $path = $this->dataDir . '/' . $uuid . '/files/' . $name; + if (!is_file($path)) { + return false; + } + return unlink($path); + } + + public function addFile(string $uuid, array $uploadedFile): ?string + { + if (!$this->isValidUuid($uuid)) { + return null; + } + $dir = $this->dataDir . '/' . $uuid . '/files'; + if (!is_dir($dir)) { + mkdir($dir, 0755, true); + } + + $mime = mime_content_type($uploadedFile['tmp_name']) ?: 'application/octet-stream'; + + if (str_starts_with($mime, 'image/')) { + // HEIC/HEIF : converti en JPEG (non supporté nativement par les navigateurs) + if (in_array($mime, ['image/heic', 'image/heif'], true) && extension_loaded('imagick')) { + try { + $img = new \Imagick($uploadedFile['tmp_name']); + $img->setImageFormat('jpeg'); + $img->setImageCompressionQuality(88); + $converted = tempnam(sys_get_temp_dir(), 'vl_heic_') . '.jpg'; + $img->writeImage($converted); + $img->destroy(); + $uploadedFile['tmp_name'] = $converted; + $mime = 'image/jpeg'; + } catch (\Exception $e) { + // Échec conversion → stocke tel quel + } + } + + $ext = $this->extFromMime($mime) + ?? strtolower(pathinfo($uploadedFile['name'], PATHINFO_EXTENSION)) + ?: 'jpg'; + $hash = substr(hash_file('sha256', $uploadedFile['tmp_name']), 0, 16); + $size = filesize($uploadedFile['tmp_name']); + $name = "{$hash}-{$size}.{$ext}"; + $dest = $dir . '/' . $name; + if (!rename($uploadedFile['tmp_name'], $dest) && !move_uploaded_file($uploadedFile['tmp_name'], $dest)) { + return null; + } + return $name; + } + + // Non-image : nom sanitisé + déduplication + $name = preg_replace('/[^a-zA-Z0-9._-]/', '_', basename($uploadedFile['name'])); + $dest = $dir . '/' . $name; + $i = 1; + $info = pathinfo($name); + while (file_exists($dest)) { + $dest = $dir . '/' . $info['filename'] . '_' . $i . (isset($info['extension']) ? '.' . $info['extension'] : ''); + $i++; + } + if (!move_uploaded_file($uploadedFile['tmp_name'], $dest)) { + return null; + } + return basename($dest); + } + + // ------------------------------------------------------------------ // + // Rendu + // ------------------------------------------------------------------ // + + public function resolveFileUrls(string $uuid, string $markdown): string + { + $base = '/file?uuid=' . rawurlencode($uuid) . '&name='; + + return preg_replace_callback( + '/(!?\[([^\]]*)\])\((?!https?:\/\/)(?!\/)([^)]+)\)/', + static function (array $m) use ($base): string { + return $m[1] . '(' . $base . rawurlencode($m[3]) . ')'; + }, + $markdown + ) ?? $markdown; + } + + // ------------------------------------------------------------------ // + // Helpers privés + // ------------------------------------------------------------------ // + + private function extFromMime(string $mime): ?string + { + return match($mime) { + 'image/jpeg' => 'jpg', + 'image/png' => 'png', + 'image/webp' => 'webp', + 'image/gif' => 'gif', + 'image/avif' => 'avif', + 'image/svg+xml' => 'svg', + 'image/heic' => 'jpg', + 'image/heif' => 'jpg', + default => null, + }; + } + + private function loadArticle(string $dir): ?array + { + $metaPath = $dir . '/meta.json'; + if (!file_exists($metaPath)) { + return null; + } + $uuid = basename($dir); + $cachePath = $this->articleCachePath($uuid); + + // Utiliser le cache si plus récent que meta.json + if (file_exists($cachePath) && filemtime($cachePath) >= filemtime($metaPath)) { + $cached = json_decode((string) file_get_contents($cachePath), true); + if (is_array($cached) && !empty($cached['uuid'])) { + return $cached; + } + } + + $raw = file_get_contents($metaPath); + if ($raw === false) { + return null; + } + $meta = json_decode($raw, true); + if (!is_array($meta) || empty($meta['uuid'])) { + return null; + } + + $contentPath = $dir . '/index.md'; + $meta['content'] = file_exists($contentPath) ? (string)file_get_contents($contentPath) : ''; + $meta['published'] = (bool)($meta['published'] ?? false); + $meta['featured'] = (bool)($meta['featured'] ?? false); + $meta['files_meta'] = $meta['files_meta'] ?? []; + $meta['external_links'] = $meta['external_links'] ?? []; + $meta['tags'] = $meta['tags'] ?? []; + + if (!empty($meta['cover'])) { + $coverPath = $dir . '/files/' . basename((string)$meta['cover']); + if (!file_exists($coverPath)) { + $meta['cover'] = ''; + } + } + + // Écrire le cache + $cacheDir = dirname($cachePath); + if (!is_dir($cacheDir)) { + mkdir($cacheDir, 0755, true); + } + file_put_contents($cachePath, json_encode($meta, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + + return $meta; + } + + public function deleteRevision(string $uuid, int $revN): void + { + if (!$this->isValidUuid($uuid) || $revN < 1) { + return; + } + $dir = $this->dataDir . '/' . $uuid; + $raw = @file_get_contents($dir . '/meta.json'); + $meta = $raw !== false ? json_decode($raw, true) : null; + if (!is_array($meta)) { + return; + } + $revisions = $meta['revisions'] ?? []; + $newRevisions = []; + foreach ($revisions as $rev) { + if ((int)($rev['n'] ?? 0) === $revN) { + @unlink(sprintf('%s/revisions/%04d.md', $dir, $revN)); + } else { + $newRevisions[] = $rev; + } + } + $meta['revisions'] = array_values($newRevisions); + $this->writeMeta($dir, $meta); + } + + public function deleteAllRevisions(string $uuid): void + { + if (!$this->isValidUuid($uuid)) { + return; + } + $dir = $this->dataDir . '/' . $uuid; + $revDir = $dir . '/revisions'; + if (is_dir($revDir)) { + foreach (glob($revDir . '/*.md') ?: [] as $f) { + @unlink($f); + } + } + $raw = @file_get_contents($dir . '/meta.json'); + $meta = $raw !== false ? json_decode($raw, true) : null; + if (is_array($meta)) { + $meta['revisions'] = []; + $this->writeMeta($dir, $meta); + } + } + + private function writeMeta(string $dir, array $meta): void + { + $this->allCache = null; + $this->searchIndexCache = null; + $uuid = $meta['uuid'] ?? basename($dir); + + // Invalider le cache article et le slug index + @unlink($this->articleCachePath($uuid)); + @unlink($this->slugIndexPath()); + + file_put_contents( + $dir . '/meta.json', + json_encode($meta, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n" + ); + } + + private function generateSlug(string $title): string + { + $map = [ + 'à' => 'a', 'â' => 'a', 'ä' => 'a', + 'é' => 'e', 'è' => 'e', 'ê' => 'e', 'ë' => 'e', + 'î' => 'i', 'ï' => 'i', + 'ô' => 'o', 'ö' => 'o', + 'ù' => 'u', 'û' => 'u', 'ü' => 'u', + 'ç' => 'c', 'æ' => 'ae', 'œ' => 'oe', + ]; + $slug = mb_strtolower($title, 'UTF-8'); + $slug = strtr($slug, $map); + $slug = preg_replace('/[^a-z0-9]+/', '-', $slug); + return trim((string)$slug, '-') ?: 'article'; + } + + private function sanitizeSlug(string $slug): string + { + $slug = mb_strtolower(trim($slug), 'UTF-8'); + $slug = preg_replace('/[^a-z0-9-]/', '-', $slug); + $slug = preg_replace('/-+/', '-', $slug); + return trim((string)$slug, '-') ?: 'article'; + } + + private function uniqueSlug(string $slug, string $excludeUuid): string + { + $taken = array_column( + array_filter($this->getAll(), static fn ($a) => $a['uuid'] !== $excludeUuid), + 'slug' + ); + + if (!in_array($slug, $taken, true)) { + return $slug; + } + $i = 2; + while (in_array($slug . '-' . $i, $taken, true)) { + $i++; + } + return $slug . '-' . $i; + } + + private function generateUuid(): string + { + $bytes = random_bytes(16); + $bytes[6] = chr(ord($bytes[6]) & 0x0f | 0x40); + $bytes[8] = chr(ord($bytes[8]) & 0x3f | 0x80); + return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($bytes), 4)); + } + + private function isValidUuid(string $uuid): bool + { + return (bool)preg_match( + '/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i', + $uuid + ); + } + + /** + * Retourne les articles publiés qui contiennent un lien vers /post/. + */ + private function removeDir(string $dir): void + { + foreach (scandir($dir) as $entry) { + if ($entry === '.' || $entry === '..') { + continue; + } + $path = $dir . '/' . $entry; + is_dir($path) ? $this->removeDir($path) : unlink($path); + } + rmdir($dir); + } +} diff --git a/src/CommentManager.php b/src/CommentManager.php new file mode 100644 index 0000000..87588ca --- /dev/null +++ b/src/CommentManager.php @@ -0,0 +1,202 @@ + UUID (dans l'URL), 'code' => 6 chiffres (saisi par le visiteur)]. + */ + public function submit( + string $articleUuid, + string $name, + string $email, + string $content, + string $ip, + string $ua + ): array { + $bytes = random_bytes(16); + $bytes[6] = chr(ord($bytes[6]) & 0x0f | 0x40); + $bytes[8] = chr(ord($bytes[8]) & 0x3f | 0x80); + $token = vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($bytes), 4)); + $code = sprintf('%06d', random_int(100000, 999999)); + $this->pdo->prepare( + 'INSERT INTO comments + (article_uuid, author_name, author_email, content, verify_token, verification_code, ip_address, user_agent) + VALUES (:uuid, :name, :email, :content, :token, :code, :ip, :ua)' + )->execute([ + ':uuid' => $articleUuid, + ':name' => $name, + ':email' => $email, + ':content' => $content, + ':token' => $token, + ':code' => $code, + ':ip' => $ip, + ':ua' => substr($ua, 0, 512), + ]); + return ['token' => $token, 'code' => $code]; + } + + /** + * Vérifie le code PIN pour un token donné. + * Retourne l'article_uuid en cas de succès. + * Retourne int > 0 : tentatives restantes (code incorrect). + * Retourne 0 : commentaire supprimé après 3 tentatives échouées. + * Retourne null : token introuvable ou expiré. + */ + public function verify(string $token, string $code): string|int|null + { + $st = $this->pdo->prepare( + "SELECT id, verification_code, verify_attempts, article_uuid + FROM comments + WHERE verify_token = :token + AND verified = FALSE + AND created_at >= NOW() - INTERVAL '24 hours' + LIMIT 1" + ); + $st->execute([':token' => $token]); + $row = $st->fetch(PDO::FETCH_ASSOC); + + if (!$row) { + return null; + } + + if ($row['verification_code'] !== $code) { + $newAttempts = (int)$row['verify_attempts'] + 1; + if ($newAttempts >= 3) { + $this->pdo->prepare('DELETE FROM comments WHERE id = :id') + ->execute([':id' => $row['id']]); + return 0; + } + $this->pdo->prepare('UPDATE comments SET verify_attempts = :a WHERE id = :id') + ->execute([':a' => $newAttempts, ':id' => $row['id']]); + return 3 - $newAttempts; + } + + $this->pdo->prepare( + 'UPDATE comments + SET verified = TRUE, published = TRUE, verification_code = NULL, verify_token = NULL + WHERE id = :id' + )->execute([':id' => $row['id']]); + + return (string)$row['article_uuid']; + } + + /** @return array> */ + public function forArticle(string $uuid): array + { + $st = $this->pdo->prepare( + 'SELECT id, author_name, content, created_at + FROM comments + WHERE article_uuid = :uuid AND verified = TRUE AND published = TRUE + ORDER BY created_at ASC' + ); + $st->execute([':uuid' => $uuid]); + return $st->fetchAll(PDO::FETCH_ASSOC); + } + + public function setPublished(int $id, bool $published): void + { + $this->pdo->prepare('UPDATE comments SET published = :pub WHERE id = :id') + ->execute([':pub' => $published, ':id' => $id]); + } + + public function delete(int $id): void + { + $this->pdo->prepare('DELETE FROM comments WHERE id = :id') + ->execute([':id' => $id]); + } + + /** @return array|null */ + public function getById(int $id): ?array + { + $st = $this->pdo->prepare( + 'SELECT id, article_uuid, author_name, author_email, content, + verify_token, verification_code, verify_attempts, verified, published, created_at, ip_address + FROM comments WHERE id = :id LIMIT 1' + ); + $st->execute([':id' => $id]); + $row = $st->fetch(PDO::FETCH_ASSOC); + return $row ?: null; + } + + /** @return array{all:int,pending:int,verified:int,hidden:int} */ + public function countsByStatus(): array + { + try { + $row = $this->pdo->query( + 'SELECT + COUNT(*) AS all, + COUNT(*) FILTER (WHERE verified = FALSE) AS pending, + COUNT(*) FILTER (WHERE verified = TRUE AND published = TRUE) AS verified, + COUNT(*) FILTER (WHERE verified = TRUE AND published = FALSE) AS hidden + FROM comments' + )->fetch(PDO::FETCH_ASSOC); + return [ + 'all' => (int)($row['all'] ?? 0), + 'pending' => (int)($row['pending'] ?? 0), + 'verified' => (int)($row['verified'] ?? 0), + 'hidden' => (int)($row['hidden'] ?? 0), + ]; + } catch (\Throwable) { + return ['all' => 0, 'pending' => 0, 'verified' => 0, 'hidden' => 0]; + } + } + + /** + * Retourne tous les commentaires pour l'admin, avec statut email depuis journal_smtp. + * + * @param string $filterStatus '' = tous, 'pending' = non vérifié, + * 'verified' = vérifié+publié, 'hidden' = vérifié+non publié + * @return array> + */ + public function allForAdmin(string $filterStatus = ''): array + { + $where = match($filterStatus) { + 'pending' => 'WHERE c.verified = FALSE', + 'verified' => 'WHERE c.verified = TRUE AND c.published = TRUE', + 'hidden' => 'WHERE c.verified = TRUE AND c.published = FALSE', + default => '', + }; + + $sqlWithJoin = " + SELECT c.id, c.article_uuid, c.author_name, c.author_email, c.content, + c.verification_code, c.verified, c.published, c.created_at, c.ip_address, + j.status AS mail_status, + j.error_message AS mail_error, + j.sent_at AS mail_sent_at + FROM comments c + LEFT JOIN LATERAL ( + SELECT status, error_message, sent_at + FROM journal_smtp + WHERE to_email = c.author_email + AND created_at BETWEEN c.created_at - INTERVAL '1 minute' + AND c.created_at + INTERVAL '10 minutes' + ORDER BY created_at ASC + LIMIT 1 + ) j ON TRUE + $where + ORDER BY c.created_at DESC + "; + + try { + return $this->pdo->query($sqlWithJoin)->fetchAll(PDO::FETCH_ASSOC); + } catch (\Throwable) { + // journal_smtp absent ou jointure échouée : requête de secours sans jointure + $sqlFallback = " + SELECT c.id, c.article_uuid, c.author_name, c.author_email, c.content, + c.verification_code, c.verified, c.published, c.created_at, c.ip_address, + NULL AS mail_status, NULL AS mail_error, NULL AS mail_sent_at + FROM comments c + $where + ORDER BY c.created_at DESC + "; + return $this->pdo->query($sqlFallback)->fetchAll(PDO::FETCH_ASSOC); + } + } +} diff --git a/src/ConfigRepo.php b/src/ConfigRepo.php new file mode 100644 index 0000000..bace310 --- /dev/null +++ b/src/ConfigRepo.php @@ -0,0 +1,114 @@ +query('SELECT * FROM app_config WHERE id=1')->fetch(PDO::FETCH_ASSOC); + if (!$row) { + return [ + 'allow_password' => true,'allow_oidc' => false,'registrations_open' => true, + 'oidc_issuer' => null,'oidc_name' => null,'oidc_client_id' => null,'oidc_client_secret' => null,'oidc_redirect_uri' => null + ]; + } + return $row; +} + +function config_repo_save(array $in): void +{ + $pdo = db(); + $sql = 'INSERT INTO app_config + (id, allow_password, allow_oidc, registrations_open, oidc_issuer, oidc_name, oidc_client_id, oidc_client_secret, oidc_redirect_uri, updated_at) + VALUES (1,:pw,:oidc,:open,:iss,:name,:cid,:sec,:redir, now()) + ON CONFLICT (id) DO UPDATE SET + allow_password=:pw, allow_oidc=:oidc, registrations_open=:open, + oidc_issuer=:iss, oidc_name=:name, oidc_client_id=:cid, oidc_client_secret=:sec, oidc_redirect_uri=:redir, + updated_at=now()'; + $stmt = $pdo->prepare($sql); + $stmt->execute([ + ':pw' => (bool)$in['allow_password'], + ':oidc' => (bool)$in['allow_oidc'], + ':open' => (bool)$in['registrations_open'], + ':iss' => trim((string)($in['oidc_issuer'] ?? '')) ?: null, + ':name' => trim((string)($in['oidc_name'] ?? '')) ?: null, + ':cid' => trim((string)($in['oidc_client_id'] ?? '')) ?: null, + ':sec' => trim((string)($in['oidc_client_secret'] ?? '')) ?: null, + ':redir' => trim((string)($in['oidc_redirect_uri'] ?? '')) ?: null, + ]); +} + +/** + * Met à jour le fichier .env en conservant les autres lignes. + * $pairs = ['KEY'=>'value', ...] ; value null => supprime la clé. + */ +function env_set_pairs(string $envPath, array $pairs): void +{ + if (!is_file($envPath)) { + file_put_contents($envPath, ''); + } + $lines = file($envPath, FILE_IGNORE_NEW_LINES); + $map = []; + foreach ($lines as $i => $line) { + if (preg_match('/^\s*#/', $line) || trim($line) === '') { + $map[$i] = $line; + continue; + } + if (!str_contains($line, '=')) { + $map[$i] = $line; + continue; + } + [$k,$v] = explode('=', $line, 2); + $k = trim($k); + if ($k === '') { + $map[$i] = $line; + continue; + } + if (array_key_exists($k, $pairs)) { + if ($pairs[$k] === null) { + $map[$i] = null; + } // supprimé + else { + $map[$i] = $k.'='.env_quote((string)$pairs[$k]); + } + unset($pairs[$k]); + } else { + $map[$i] = $line; + } + } + // append keys restantes + foreach ($pairs as $k => $v) { + if ($v === null) { + continue; + } + $map[] = $k.'='.env_quote((string)$v); + } + // re-écriture + $out = []; + foreach ($map as $line) { + if ($line === null) { + continue; + } $out[] = $line; + } + file_put_contents($envPath, implode(PHP_EOL, $out).PHP_EOL); +} + +function env_quote(string $v): string +{ + if ($v === '' || preg_match('/\s|[#"\'=]/', $v)) { + // met entre guillemets et échappe + $v = str_replace(['\\','"'], ['\\\\','\\"'], $v); + return "\"$v\""; + } + return $v; +} + +function ensure_admin(): void +{ + // adapte à ton système + if (empty($_SESSION['user']['is_admin'])) { + http_response_code(403); + exit('Forbidden'); + } +} diff --git a/src/Domain/User.php b/src/Domain/User.php new file mode 100644 index 0000000..da5e745 --- /dev/null +++ b/src/Domain/User.php @@ -0,0 +1,16 @@ +cacheRead($url); + if ($cached !== null && time() < (int)$cached['fetched_at'] + (int)$cached['ttl']) { + return $cached; + } + return $this->fetch($url); + } + + /** Force le refetch et met le cache à jour. */ + public function fetch(string $url): ?array + { + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_MAXREDIRS => 5, + CURLOPT_TIMEOUT => 10, + CURLOPT_USERAGENT => 'varlog/1.0 FeedFetcher (+' . (defined('APP_URL') ? APP_URL : '') . ')', + CURLOPT_HEADER => true, + ]); + $raw = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $hSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); + curl_close($ch); + + if ($raw === false || !is_int($httpCode) || $httpCode < 200 || $httpCode >= 400) { + return null; + } + + $headers = substr((string)$raw, 0, $hSize); + $body = substr((string)$raw, $hSize); + + libxml_use_internal_errors(true); + $xml = simplexml_load_string($body); + libxml_clear_errors(); + if ($xml === false) { + return null; + } + + $isAtom = ($xml->getName() === 'feed'); + $items = $isAtom ? $this->parseAtom($xml) : $this->parseRss($xml); + $feedTitle = $isAtom + ? (string)($xml->title ?? '') + : (string)($xml->channel->title ?? ''); + + $ttl = $this->resolveTtl($xml, $isAtom, $headers); + + $data = [ + 'feed_title' => $feedTitle, + 'fetched_at' => time(), + 'ttl' => $ttl, + 'items' => $items, + ]; + $this->cacheWrite($url, $data); + return $data; + } + + // ------------------------------------------------------------------ // + + private function parseRss(\SimpleXMLElement $xml): array + { + $items = []; + foreach ($xml->channel->item ?? [] as $item) { + $date = (string)($item->pubDate ?? ''); + $items[] = [ + 'title' => trim((string)($item->title ?? '')), + 'url' => trim((string)($item->link ?? '')), + 'summary' => $this->cleanSummary((string)($item->description ?? '')), + 'date' => $date !== '' ? (int)strtotime($date) : 0, + 'author' => trim((string)($item->author ?? '')), + ]; + } + return $this->sortItems($items); + } + + private function parseAtom(\SimpleXMLElement $xml): array + { + $ns = $xml->getNamespaces(true); + $items = []; + foreach ($xml->entry ?? [] as $entry) { + $url = ''; + foreach ($entry->link ?? [] as $link) { + $rel = (string)($link['rel'] ?? 'alternate'); + if ($rel === 'alternate' || $rel === '') { + $url = (string)($link['href'] ?? ''); + break; + } + } + $date = (string)($entry->published ?? $entry->updated ?? ''); + $author = (string)($entry->author->name ?? ''); + $summary = (string)($entry->summary ?? $entry->content ?? ''); + $items[] = [ + 'title' => trim((string)($entry->title ?? '')), + 'url' => trim($url), + 'summary' => $this->cleanSummary($summary), + 'date' => $date !== '' ? (int)strtotime($date) : 0, + 'author' => trim($author), + ]; + } + return $this->sortItems($items); + } + + private function cleanSummary(string $html): string + { + $text = strip_tags($html); + $text = preg_replace('/\s+/', ' ', $text) ?? $text; + return mb_strimwidth(trim($text), 0, 200, '…'); + } + + private function sortItems(array $items): array + { + usort($items, static fn ($a, $b) => $b['date'] <=> $a['date']); + return $items; + } + + private function resolveTtl(\SimpleXMLElement $xml, bool $isAtom, string $headers): int + { + // 1. TTL déclaré dans le flux RSS ( en minutes) + if (!$isAtom) { + $rssttl = (int)($xml->channel->ttl ?? 0); + if ($rssttl > 0) { + return $this->clampTtl($rssttl * 60); + } + } + + // 2. Cache-Control: max-age depuis les headers HTTP + if (preg_match('/max-age=(\d+)/i', $headers, $m)) { + return $this->clampTtl((int)$m[1]); + } + + // 3. Valeur par défaut : 1 heure + return 3600; + } + + private function clampTtl(int $seconds): int + { + return max(self::MIN_TTL, min(self::MAX_TTL, $seconds)); + } + + // ------------------------------------------------------------------ // + + private function cachePath(string $url): string + { + return $this->cacheDir . '/' . md5($url) . '.json'; + } + + private function cacheRead(string $url): ?array + { + $path = $this->cachePath($url); + if (!file_exists($path)) { + return null; + } + $data = json_decode((string)file_get_contents($path), true); + return is_array($data) ? $data : null; + } + + private function cacheWrite(string $url, array $data): void + { + if (!is_dir($this->cacheDir)) { + mkdir($this->cacheDir, 0755, true); + } + file_put_contents( + $this->cachePath($url), + json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) + ); + } +} diff --git a/src/FileManager.php b/src/FileManager.php new file mode 100644 index 0000000..990cc8c --- /dev/null +++ b/src/FileManager.php @@ -0,0 +1,85 @@ +db = $db; + $this->uploadDir = rtrim($uploadDir, '/'); + } + + public function upload(int $postId, array $file): ?int + { + if ($file['error'] !== UPLOAD_ERR_OK) { + return null; + } + + $type = $this->guessType($file['type']); + $originalName = basename($file['name']); + $ext = pathinfo($originalName, PATHINFO_EXTENSION); + $filename = uniqid('file_') . '.' . $ext; + $destination = $this->uploadDir . '/' . $filename; + + if (!move_uploaded_file($file['tmp_name'], $destination)) { + return null; + } + + $stmt = $this->db->prepare(' + INSERT INTO post_files (post_id, file_type, file_path, original_name) + VALUES (:post_id, :file_type, :file_path, :original_name) + '); + $stmt->execute([ + 'post_id' => $postId, + 'file_type' => $type, + 'file_path' => $filename, + 'original_name' => $originalName + ]); + + return (int)$this->db->lastInsertId(); + } + + public function getFilesForPost(int $postId): array + { + $stmt = $this->db->prepare('SELECT * FROM post_files WHERE post_id = :post_id ORDER BY uploaded_at'); + $stmt->execute(['post_id' => $postId]); + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + public function delete(int $fileId): bool + { + $stmt = $this->db->prepare('SELECT file_path FROM post_files WHERE id = :id'); + $stmt->execute(['id' => $fileId]); + $file = $stmt->fetch(PDO::FETCH_ASSOC); + + if (!$file) { + return false; + } + + $fullPath = $this->uploadDir . '/' . $file['file_path']; + if (file_exists($fullPath)) { + unlink($fullPath); + } + + $stmt = $this->db->prepare('DELETE FROM post_files WHERE id = :id'); + return $stmt->execute(['id' => $fileId]); + } + + private function guessType(string $mime): string + { + if (str_starts_with($mime, 'image/')) { + return 'image'; + } + if (str_starts_with($mime, 'video/')) { + return 'video'; + } + if (str_starts_with($mime, 'audio/')) { + return 'audio'; + } + return 'file'; + } +} diff --git a/src/Http/Csrf.php b/src/Http/Csrf.php new file mode 100644 index 0000000..ecf3fbf --- /dev/null +++ b/src/Http/Csrf.php @@ -0,0 +1,24 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, + ]); + return self::$pdo = $pdo; + } catch (PDOException $e) { + throw new RuntimeException('Connexion BDD échouée.', previous: $e); + } + } + + /** @deprecated Utiliser Database::get() */ + public static function pdo(): PDO + { + @trigger_error(__METHOD__.' est déprécié. Utiliser Database::get()', E_USER_DEPRECATED); + return self::get(); + } + + /** @deprecated Utiliser Database::get() */ + public static function getPdo(): PDO + { + @trigger_error(__METHOD__.' est déprécié. Utiliser Database::get()', E_USER_DEPRECATED); + return self::get(); + } + + /** @deprecated Utiliser Database::get() */ + public static function getInstance(): PDO + { + @trigger_error(__METHOD__.' est déprécié. Utiliser Database::get()', E_USER_DEPRECATED); + return self::get(); + } + + public static function transactional(callable $fn) + { + $pdo = self::get(); + try { + $pdo->beginTransaction(); + $ret = $fn($pdo); + $pdo->commit(); + return $ret; + } catch (\Throwable $e) { + if ($pdo->inTransaction()) { + $pdo->rollBack(); + } + throw $e; + } + } +} diff --git a/src/Infrastructure/DbAdapter.php b/src/Infrastructure/DbAdapter.php new file mode 100644 index 0000000..4230c86 --- /dev/null +++ b/src/Infrastructure/DbAdapter.php @@ -0,0 +1,36 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, + ]); + } +} diff --git a/src/Infrastructure/Session.php b/src/Infrastructure/Session.php new file mode 100644 index 0000000..86a6443 --- /dev/null +++ b/src/Infrastructure/Session.php @@ -0,0 +1,42 @@ + 0, + 'path' => '/', + 'domain' => '', + 'secure' => true, + 'httponly' => true, + 'samesite' => 'Strict', + ]); + session_start(); + + // Verrouillage basique contre session hijacking + $key = '_sess_fingerprint'; + $fp = hash('xxh3', ($_SERVER['REMOTE_ADDR'] ?? '') . '|' . ($_SERVER['HTTP_USER_AGENT'] ?? '')); + if (!isset($_SESSION[$key])) { + $_SESSION[$key] = $fp; + } elseif ($_SESSION[$key] !== $fp) { + session_regenerate_id(true); + $_SESSION = []; + $_SESSION[$key] = $fp; + } + } + + public static function regenerate(): void + { + session_regenerate_id(true); + } +} diff --git a/src/Parsedown.php b/src/Parsedown.php new file mode 100644 index 0000000..b7ca513 --- /dev/null +++ b/src/Parsedown.php @@ -0,0 +1,1828 @@ +textElements($text); + + # convert to markup + $markup = $this->elements($Elements); + + # trim line breaks + $markup = trim($markup, "\n"); + + return $markup; + } + + protected function textElements($text) + { + # make sure no definitions are set + $this->DefinitionData = array(); + + # standardize line breaks + $text = str_replace(array("\r\n", "\r"), "\n", $text); + + # remove surrounding line breaks + $text = trim($text, "\n"); + + # split text into lines + $lines = explode("\n", $text); + + # iterate through lines to identify blocks + return $this->linesElements($lines); + } + + # + # Setters + # + + public function setBreaksEnabled($breaksEnabled) + { + $this->breaksEnabled = $breaksEnabled; + + return $this; + } + + protected $breaksEnabled; + + public function setMarkupEscaped($markupEscaped) + { + $this->markupEscaped = $markupEscaped; + + return $this; + } + + protected $markupEscaped; + + public function setUrlsLinked($urlsLinked) + { + $this->urlsLinked = $urlsLinked; + + return $this; + } + + protected $urlsLinked = true; + + public function setSafeMode($safeMode) + { + $this->safeMode = (bool) $safeMode; + + return $this; + } + + protected $safeMode; + + public function setStrictMode($strictMode) + { + $this->strictMode = (bool) $strictMode; + + return $this; + } + + protected $strictMode; + + protected $safeLinksWhitelist = array( + 'http://', + 'https://', + 'ftp://', + 'ftps://', + 'mailto:', + 'tel:', + 'data:image/png;base64,', + 'data:image/gif;base64,', + 'data:image/jpeg;base64,', + 'irc:', + 'ircs:', + 'git:', + 'ssh:', + 'news:', + 'steam:', + ); + + # + # Lines + # + + protected $BlockTypes = array( + '#' => array('Header'), + '*' => array('Rule', 'List'), + '+' => array('List'), + '-' => array('SetextHeader', 'Table', 'Rule', 'List'), + '0' => array('List'), + '1' => array('List'), + '2' => array('List'), + '3' => array('List'), + '4' => array('List'), + '5' => array('List'), + '6' => array('List'), + '7' => array('List'), + '8' => array('List'), + '9' => array('List'), + ':' => array('Table'), + '<' => array('Comment', 'Markup'), + '=' => array('SetextHeader'), + '>' => array('Quote'), + '[' => array('Reference'), + '_' => array('Rule'), + '`' => array('FencedCode'), + '|' => array('Table'), + '~' => array('FencedCode'), + ); + + # ~ + + protected $unmarkedBlockTypes = array( + 'Code', + ); + + # + # Blocks + # + + protected function lines(array $lines) + { + return $this->elements($this->linesElements($lines)); + } + + protected function linesElements(array $lines) + { + $Elements = array(); + $CurrentBlock = null; + + foreach ($lines as $line) { + if (chop($line) === '') { + if (isset($CurrentBlock)) { + $CurrentBlock['interrupted'] = ( + isset($CurrentBlock['interrupted']) + ? $CurrentBlock['interrupted'] + 1 : 1 + ); + } + + continue; + } + + while (($beforeTab = strstr($line, "\t", true)) !== false) { + $shortage = 4 - mb_strlen($beforeTab, 'utf-8') % 4; + + $line = $beforeTab + . str_repeat(' ', $shortage) + . substr($line, strlen($beforeTab) + 1) + ; + } + + $indent = strspn($line, ' '); + + $text = $indent > 0 ? substr($line, $indent) : $line; + + # ~ + + $Line = array('body' => $line, 'indent' => $indent, 'text' => $text); + + # ~ + + if (isset($CurrentBlock['continuable'])) { + $methodName = 'block' . $CurrentBlock['type'] . 'Continue'; + $Block = $this->$methodName($Line, $CurrentBlock); + + if (isset($Block)) { + $CurrentBlock = $Block; + + continue; + } else { + if ($this->isBlockCompletable($CurrentBlock['type'])) { + $methodName = 'block' . $CurrentBlock['type'] . 'Complete'; + $CurrentBlock = $this->$methodName($CurrentBlock); + } + } + } + + # ~ + + $marker = $text[0]; + + # ~ + + $blockTypes = $this->unmarkedBlockTypes; + + if (isset($this->BlockTypes[$marker])) { + foreach ($this->BlockTypes[$marker] as $blockType) { + $blockTypes [] = $blockType; + } + } + + # + # ~ + + foreach ($blockTypes as $blockType) { + $Block = $this->{"block$blockType"}($Line, $CurrentBlock); + + if (isset($Block)) { + $Block['type'] = $blockType; + + if (! isset($Block['identified'])) { + if (isset($CurrentBlock)) { + $Elements[] = $this->extractElement($CurrentBlock); + } + + $Block['identified'] = true; + } + + if ($this->isBlockContinuable($blockType)) { + $Block['continuable'] = true; + } + + $CurrentBlock = $Block; + + continue 2; + } + } + + # ~ + + if (isset($CurrentBlock) and $CurrentBlock['type'] === 'Paragraph') { + $Block = $this->paragraphContinue($Line, $CurrentBlock); + } + + if (isset($Block)) { + $CurrentBlock = $Block; + } else { + if (isset($CurrentBlock)) { + $Elements[] = $this->extractElement($CurrentBlock); + } + + $CurrentBlock = $this->paragraph($Line); + + $CurrentBlock['identified'] = true; + } + } + + # ~ + + if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type'])) { + $methodName = 'block' . $CurrentBlock['type'] . 'Complete'; + $CurrentBlock = $this->$methodName($CurrentBlock); + } + + # ~ + + if (isset($CurrentBlock)) { + $Elements[] = $this->extractElement($CurrentBlock); + } + + # ~ + + return $Elements; + } + + protected function extractElement(array $Component) + { + if (! isset($Component['element'])) { + if (isset($Component['markup'])) { + $Component['element'] = array('rawHtml' => $Component['markup']); + } elseif (isset($Component['hidden'])) { + $Component['element'] = array(); + } + } + + return $Component['element']; + } + + protected function isBlockContinuable($Type) + { + return method_exists($this, 'block' . $Type . 'Continue'); + } + + protected function isBlockCompletable($Type) + { + return method_exists($this, 'block' . $Type . 'Complete'); + } + + # + # Code + + protected function blockCode($Line, $Block = null) + { + if (isset($Block) and $Block['type'] === 'Paragraph' and ! isset($Block['interrupted'])) { + return; + } + + if ($Line['indent'] >= 4) { + $text = substr($Line['body'], 4); + + $Block = array( + 'element' => array( + 'name' => 'pre', + 'element' => array( + 'name' => 'code', + 'text' => $text, + ), + ), + ); + + return $Block; + } + } + + protected function blockCodeContinue($Line, $Block) + { + if ($Line['indent'] >= 4) { + if (isset($Block['interrupted'])) { + $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']); + + unset($Block['interrupted']); + } + + $Block['element']['element']['text'] .= "\n"; + + $text = substr($Line['body'], 4); + + $Block['element']['element']['text'] .= $text; + + return $Block; + } + } + + protected function blockCodeComplete($Block) + { + return $Block; + } + + # + # Comment + + protected function blockComment($Line) + { + if ($this->markupEscaped or $this->safeMode) { + return; + } + + if (strpos($Line['text'], '') !== false) { + $Block['closed'] = true; + } + + return $Block; + } + } + + protected function blockCommentContinue($Line, array $Block) + { + if (isset($Block['closed'])) { + return; + } + + $Block['element']['rawHtml'] .= "\n" . $Line['body']; + + if (strpos($Line['text'], '-->') !== false) { + $Block['closed'] = true; + } + + return $Block; + } + + # + # Fenced Code + + protected function blockFencedCode($Line) + { + $marker = $Line['text'][0]; + + $openerLength = strspn($Line['text'], $marker); + + if ($openerLength < 3) { + return; + } + + $infostring = trim(substr($Line['text'], $openerLength), "\t "); + + if (strpos($infostring, '`') !== false) { + return; + } + + $Element = array( + 'name' => 'code', + 'text' => '', + ); + + if ($infostring !== '') { + /** + * https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes + * Every HTML element may have a class attribute specified. + * The attribute, if specified, must have a value that is a set + * of space-separated tokens representing the various classes + * that the element belongs to. + * [...] + * The space characters, for the purposes of this specification, + * are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab), + * U+000A LINE FEED (LF), U+000C FORM FEED (FF), and + * U+000D CARRIAGE RETURN (CR). + */ + $language = substr($infostring, 0, strcspn($infostring, " \t\n\f\r")); + + $Element['attributes'] = array('class' => "language-$language"); + } + + $Block = array( + 'char' => $marker, + 'openerLength' => $openerLength, + 'element' => array( + 'name' => 'pre', + 'element' => $Element, + ), + ); + + return $Block; + } + + protected function blockFencedCodeContinue($Line, $Block) + { + if (isset($Block['complete'])) { + return; + } + + if (isset($Block['interrupted'])) { + $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']); + + unset($Block['interrupted']); + } + + if (($len = strspn($Line['text'], $Block['char'])) >= $Block['openerLength'] + and chop(substr($Line['text'], $len), ' ') === '' + ) { + $Block['element']['element']['text'] = substr($Block['element']['element']['text'], 1); + + $Block['complete'] = true; + + return $Block; + } + + $Block['element']['element']['text'] .= "\n" . $Line['body']; + + return $Block; + } + + protected function blockFencedCodeComplete($Block) + { + return $Block; + } + + # + # Header + + protected function blockHeader($Line) + { + $level = strspn($Line['text'], '#'); + + if ($level > 6) { + return; + } + + $text = trim($Line['text'], '#'); + + if ($this->strictMode and isset($text[0]) and $text[0] !== ' ') { + return; + } + + $text = trim($text, ' '); + + $Block = array( + 'element' => array( + 'name' => 'h' . $level, + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $text, + 'destination' => 'elements', + ) + ), + ); + + return $Block; + } + + # + # List + + protected function blockList($Line, ?array $CurrentBlock = null) + { + list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]{1,9}+[.\)]'); + + if (preg_match('/^('.$pattern.'([ ]++|$))(.*+)/', $Line['text'], $matches)) { + $contentIndent = strlen($matches[2]); + + if ($contentIndent >= 5) { + $contentIndent -= 1; + $matches[1] = substr($matches[1], 0, -$contentIndent); + $matches[3] = str_repeat(' ', $contentIndent) . $matches[3]; + } elseif ($contentIndent === 0) { + $matches[1] .= ' '; + } + + $markerWithoutWhitespace = strstr($matches[1], ' ', true); + + $Block = array( + 'indent' => $Line['indent'], + 'pattern' => $pattern, + 'data' => array( + 'type' => $name, + 'marker' => $matches[1], + 'markerType' => ($name === 'ul' ? $markerWithoutWhitespace : substr($markerWithoutWhitespace, -1)), + ), + 'element' => array( + 'name' => $name, + 'elements' => array(), + ), + ); + $Block['data']['markerTypeRegex'] = preg_quote($Block['data']['markerType'], '/'); + + if ($name === 'ol') { + $listStart = ltrim(strstr($matches[1], $Block['data']['markerType'], true), '0') ?: '0'; + + if ($listStart !== '1') { + if ( + isset($CurrentBlock) + and $CurrentBlock['type'] === 'Paragraph' + and ! isset($CurrentBlock['interrupted']) + ) { + return; + } + + $Block['element']['attributes'] = array('start' => $listStart); + } + } + + $Block['li'] = array( + 'name' => 'li', + 'handler' => array( + 'function' => 'li', + 'argument' => !empty($matches[3]) ? array($matches[3]) : array(), + 'destination' => 'elements' + ) + ); + + $Block['element']['elements'] [] = & $Block['li']; + + return $Block; + } + } + + protected function blockListContinue($Line, array $Block) + { + if (isset($Block['interrupted']) and empty($Block['li']['handler']['argument'])) { + return null; + } + + $requiredIndent = ($Block['indent'] + strlen($Block['data']['marker'])); + + if ($Line['indent'] < $requiredIndent + and ( + ( + $Block['data']['type'] === 'ol' + and preg_match('/^[0-9]++'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches) + ) or ( + $Block['data']['type'] === 'ul' + and preg_match('/^'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches) + ) + ) + ) { + if (isset($Block['interrupted'])) { + $Block['li']['handler']['argument'] [] = ''; + + $Block['loose'] = true; + + unset($Block['interrupted']); + } + + unset($Block['li']); + + $text = isset($matches[1]) ? $matches[1] : ''; + + $Block['indent'] = $Line['indent']; + + $Block['li'] = array( + 'name' => 'li', + 'handler' => array( + 'function' => 'li', + 'argument' => array($text), + 'destination' => 'elements' + ) + ); + + $Block['element']['elements'] [] = & $Block['li']; + + return $Block; + } elseif ($Line['indent'] < $requiredIndent and $this->blockList($Line)) { + return null; + } + + if ($Line['text'][0] === '[' and $this->blockReference($Line)) { + return $Block; + } + + if ($Line['indent'] >= $requiredIndent) { + if (isset($Block['interrupted'])) { + $Block['li']['handler']['argument'] [] = ''; + + $Block['loose'] = true; + + unset($Block['interrupted']); + } + + $text = substr($Line['body'], $requiredIndent); + + $Block['li']['handler']['argument'] [] = $text; + + return $Block; + } + + if (! isset($Block['interrupted'])) { + $text = preg_replace('/^[ ]{0,'.$requiredIndent.'}+/', '', $Line['body']); + + $Block['li']['handler']['argument'] [] = $text; + + return $Block; + } + } + + protected function blockListComplete(array $Block) + { + if (isset($Block['loose'])) { + foreach ($Block['element']['elements'] as &$li) { + if (end($li['handler']['argument']) !== '') { + $li['handler']['argument'] [] = ''; + } + } + } + + return $Block; + } + + # + # Quote + + protected function blockQuote($Line) + { + if (preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches)) { + $Block = array( + 'element' => array( + 'name' => 'blockquote', + 'handler' => array( + 'function' => 'linesElements', + 'argument' => (array) $matches[1], + 'destination' => 'elements', + ) + ), + ); + + return $Block; + } + } + + protected function blockQuoteContinue($Line, array $Block) + { + if (isset($Block['interrupted'])) { + return; + } + + if ($Line['text'][0] === '>' and preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches)) { + $Block['element']['handler']['argument'] [] = $matches[1]; + + return $Block; + } + + if (! isset($Block['interrupted'])) { + $Block['element']['handler']['argument'] [] = $Line['text']; + + return $Block; + } + } + + # + # Rule + + protected function blockRule($Line) + { + $marker = $Line['text'][0]; + + if (substr_count($Line['text'], $marker) >= 3 and chop($Line['text'], " $marker") === '') { + $Block = array( + 'element' => array( + 'name' => 'hr', + ), + ); + + return $Block; + } + } + + # + # Setext + + protected function blockSetextHeader($Line, ?array $Block = null) + { + if (! isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted'])) { + return; + } + + if ($Line['indent'] < 4 and chop(chop($Line['text'], ' '), $Line['text'][0]) === '') { + $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2'; + + return $Block; + } + } + + # + # Markup + + protected function blockMarkup($Line) + { + if ($this->markupEscaped or $this->safeMode) { + return; + } + + if (preg_match('/^<[\/]?+(\w*)(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+(\/)?>/', $Line['text'], $matches)) { + $element = strtolower($matches[1]); + + if (in_array($element, $this->textLevelElements, true)) { + return; + } + + $Block = array( + 'name' => $matches[1], + 'element' => array( + 'rawHtml' => $Line['text'], + 'autobreak' => true, + ), + ); + + return $Block; + } + } + + protected function blockMarkupContinue($Line, array $Block) + { + if (isset($Block['closed']) or isset($Block['interrupted'])) { + return; + } + + $Block['element']['rawHtml'] .= "\n" . $Line['body']; + + return $Block; + } + + # + # Reference + + protected function blockReference($Line) + { + if (strpos($Line['text'], ']') !== false + and preg_match('/^\[(.+?)\]:[ ]*+?(?:[ ]+["\'(](.+)["\')])?[ ]*+$/', $Line['text'], $matches) + ) { + $id = strtolower($matches[1]); + + $Data = array( + 'url' => $matches[2], + 'title' => isset($matches[3]) ? $matches[3] : null, + ); + + $this->DefinitionData['Reference'][$id] = $Data; + + $Block = array( + 'element' => array(), + ); + + return $Block; + } + } + + # + # Table + + protected function blockTable($Line, ?array $Block = null) + { + if (! isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted'])) { + return; + } + + if ( + strpos($Block['element']['handler']['argument'], '|') === false + and strpos($Line['text'], '|') === false + and strpos($Line['text'], ':') === false + or strpos($Block['element']['handler']['argument'], "\n") !== false + ) { + return; + } + + if (chop($Line['text'], ' -:|') !== '') { + return; + } + + $alignments = array(); + + $divider = $Line['text']; + + $divider = trim($divider); + $divider = trim($divider, '|'); + + $dividerCells = explode('|', $divider); + + foreach ($dividerCells as $dividerCell) { + $dividerCell = trim($dividerCell); + + if ($dividerCell === '') { + return; + } + + $alignment = null; + + if ($dividerCell[0] === ':') { + $alignment = 'left'; + } + + if (substr($dividerCell, - 1) === ':') { + $alignment = $alignment === 'left' ? 'center' : 'right'; + } + + $alignments [] = $alignment; + } + + # ~ + + $HeaderElements = array(); + + $header = $Block['element']['handler']['argument']; + + $header = trim($header); + $header = trim($header, '|'); + + $headerCells = explode('|', $header); + + if (count($headerCells) !== count($alignments)) { + return; + } + + foreach ($headerCells as $index => $headerCell) { + $headerCell = trim($headerCell); + + $HeaderElement = array( + 'name' => 'th', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $headerCell, + 'destination' => 'elements', + ) + ); + + if (isset($alignments[$index])) { + $alignment = $alignments[$index]; + + $HeaderElement['attributes'] = array( + 'style' => "text-align: $alignment;", + ); + } + + $HeaderElements [] = $HeaderElement; + } + + # ~ + + $Block = array( + 'alignments' => $alignments, + 'identified' => true, + 'element' => array( + 'name' => 'table', + 'elements' => array(), + ), + ); + + $Block['element']['elements'] [] = array( + 'name' => 'thead', + ); + + $Block['element']['elements'] [] = array( + 'name' => 'tbody', + 'elements' => array(), + ); + + $Block['element']['elements'][0]['elements'] [] = array( + 'name' => 'tr', + 'elements' => $HeaderElements, + ); + + return $Block; + } + + protected function blockTableContinue($Line, array $Block) + { + if (isset($Block['interrupted'])) { + return; + } + + if (count($Block['alignments']) === 1 or $Line['text'][0] === '|' or strpos($Line['text'], '|')) { + $Elements = array(); + + $row = $Line['text']; + + $row = trim($row); + $row = trim($row, '|'); + + preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]++`|`)++/', $row, $matches); + + $cells = array_slice($matches[0], 0, count($Block['alignments'])); + + foreach ($cells as $index => $cell) { + $cell = trim($cell); + + $Element = array( + 'name' => 'td', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $cell, + 'destination' => 'elements', + ) + ); + + if (isset($Block['alignments'][$index])) { + $Element['attributes'] = array( + 'style' => 'text-align: ' . $Block['alignments'][$index] . ';', + ); + } + + $Elements [] = $Element; + } + + $Element = array( + 'name' => 'tr', + 'elements' => $Elements, + ); + + $Block['element']['elements'][1]['elements'] [] = $Element; + + return $Block; + } + } + + # + # ~ + # + + protected function paragraph($Line) + { + return array( + 'type' => 'Paragraph', + 'element' => array( + 'name' => 'p', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $Line['text'], + 'destination' => 'elements', + ), + ), + ); + } + + protected function paragraphContinue($Line, array $Block) + { + if (isset($Block['interrupted'])) { + return; + } + + $Block['element']['handler']['argument'] .= "\n".$Line['text']; + + return $Block; + } + + # + # Inline Elements + # + + protected $InlineTypes = array( + '!' => array('Image'), + '&' => array('SpecialCharacter'), + '*' => array('Emphasis'), + ':' => array('Url'), + '<' => array('UrlTag', 'EmailTag', 'Markup'), + '[' => array('Link'), + '_' => array('Emphasis'), + '`' => array('Code'), + '~' => array('Strikethrough'), + '\\' => array('EscapeSequence'), + ); + + # ~ + + protected $inlineMarkerList = '!*_&[:<`~\\'; + + # + # ~ + # + + public function line($text, $nonNestables = array()) + { + return $this->elements($this->lineElements($text, $nonNestables)); + } + + protected function lineElements($text, $nonNestables = array()) + { + # standardize line breaks + $text = str_replace(array("\r\n", "\r"), "\n", $text); + + $Elements = array(); + + $nonNestables = ( + empty($nonNestables) + ? array() + : array_combine($nonNestables, $nonNestables) + ); + + # $excerpt is based on the first occurrence of a marker + + while ($excerpt = strpbrk($text, $this->inlineMarkerList)) { + $marker = $excerpt[0]; + + $markerPosition = strlen($text) - strlen($excerpt); + + $Excerpt = array('text' => $excerpt, 'context' => $text); + + foreach ($this->InlineTypes[$marker] as $inlineType) { + # check to see if the current inline type is nestable in the current context + + if (isset($nonNestables[$inlineType])) { + continue; + } + + $Inline = $this->{"inline$inlineType"}($Excerpt); + + if (! isset($Inline)) { + continue; + } + + # makes sure that the inline belongs to "our" marker + + if (isset($Inline['position']) and $Inline['position'] > $markerPosition) { + continue; + } + + # sets a default inline position + + if (! isset($Inline['position'])) { + $Inline['position'] = $markerPosition; + } + + # cause the new element to 'inherit' our non nestables + + + $Inline['element']['nonNestables'] = isset($Inline['element']['nonNestables']) + ? array_merge($Inline['element']['nonNestables'], $nonNestables) + : $nonNestables + ; + + # the text that comes before the inline + $unmarkedText = substr($text, 0, $Inline['position']); + + # compile the unmarked text + $InlineText = $this->inlineText($unmarkedText); + $Elements[] = $InlineText['element']; + + # compile the inline + $Elements[] = $this->extractElement($Inline); + + # remove the examined text + $text = substr($text, $Inline['position'] + $Inline['extent']); + + continue 2; + } + + # the marker does not belong to an inline + + $unmarkedText = substr($text, 0, $markerPosition + 1); + + $InlineText = $this->inlineText($unmarkedText); + $Elements[] = $InlineText['element']; + + $text = substr($text, $markerPosition + 1); + } + + $InlineText = $this->inlineText($text); + $Elements[] = $InlineText['element']; + + foreach ($Elements as &$Element) { + if (! isset($Element['autobreak'])) { + $Element['autobreak'] = false; + } + } + + return $Elements; + } + + # + # ~ + # + + protected function inlineText($text) + { + $Inline = array( + 'extent' => strlen($text), + 'element' => array(), + ); + + $Inline['element']['elements'] = self::pregReplaceElements( + $this->breaksEnabled ? '/[ ]*+\n/' : '/(?:[ ]*+\\\\|[ ]{2,}+)\n/', + array( + array('name' => 'br'), + array('text' => "\n"), + ), + $text + ); + + return $Inline; + } + + protected function inlineCode($Excerpt) + { + $marker = $Excerpt['text'][0]; + + if (preg_match('/^(['.$marker.']++)[ ]*+(.+?)[ ]*+(? strlen($matches[0]), + 'element' => array( + 'name' => 'code', + 'text' => $text, + ), + ); + } + } + + protected function inlineEmailTag($Excerpt) + { + $hostnameLabel = '[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?'; + + $commonMarkEmail = '[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]++@' + . $hostnameLabel . '(?:\.' . $hostnameLabel . ')*'; + + if (strpos($Excerpt['text'], '>') !== false + and preg_match("/^<((mailto:)?$commonMarkEmail)>/i", $Excerpt['text'], $matches) + ) { + $url = $matches[1]; + + if (! isset($matches[2])) { + $url = "mailto:$url"; + } + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'a', + 'text' => $matches[1], + 'attributes' => array( + 'href' => $url, + ), + ), + ); + } + } + + protected function inlineEmphasis($Excerpt) + { + if (! isset($Excerpt['text'][1])) { + return; + } + + $marker = $Excerpt['text'][0]; + + if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches)) { + $emphasis = 'strong'; + } elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches)) { + $emphasis = 'em'; + } else { + return; + } + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => $emphasis, + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $matches[1], + 'destination' => 'elements', + ) + ), + ); + } + + protected function inlineEscapeSequence($Excerpt) + { + if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters, true)) { + return array( + 'element' => array('rawHtml' => $Excerpt['text'][1]), + 'extent' => 2, + ); + } + } + + protected function inlineImage($Excerpt) + { + if (! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[') { + return; + } + + $Excerpt['text'] = substr($Excerpt['text'], 1); + + $Link = $this->inlineLink($Excerpt); + + if ($Link === null) { + return; + } + + $Inline = array( + 'extent' => $Link['extent'] + 1, + 'element' => array( + 'name' => 'img', + 'attributes' => array( + 'src' => $Link['element']['attributes']['href'], + 'alt' => $Link['element']['handler']['argument'], + ), + 'autobreak' => true, + ), + ); + + $Inline['element']['attributes'] += $Link['element']['attributes']; + + unset($Inline['element']['attributes']['href']); + + return $Inline; + } + + protected function inlineLink($Excerpt) + { + $Element = array( + 'name' => 'a', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => null, + 'destination' => 'elements', + ), + 'nonNestables' => array('Url', 'Link'), + 'attributes' => array( + 'href' => null, + 'title' => null, + ), + ); + + $extent = 0; + + $remainder = $Excerpt['text']; + + if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches)) { + $Element['handler']['argument'] = $matches[1]; + + $extent += strlen($matches[0]); + + $remainder = substr($remainder, $extent); + } else { + return; + } + + if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*+"|\'[^\']*+\'))?\s*+[)]/', $remainder, $matches)) { + $Element['attributes']['href'] = $matches[1]; + + if (isset($matches[2])) { + $Element['attributes']['title'] = substr($matches[2], 1, - 1); + } + + $extent += strlen($matches[0]); + } else { + if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches)) { + $definition = strlen($matches[1]) ? $matches[1] : $Element['handler']['argument']; + $definition = strtolower($definition); + + $extent += strlen($matches[0]); + } else { + $definition = strtolower($Element['handler']['argument']); + } + + if (! isset($this->DefinitionData['Reference'][$definition])) { + return; + } + + $Definition = $this->DefinitionData['Reference'][$definition]; + + $Element['attributes']['href'] = $Definition['url']; + $Element['attributes']['title'] = $Definition['title']; + } + + return array( + 'extent' => $extent, + 'element' => $Element, + ); + } + + protected function inlineMarkup($Excerpt) + { + if ($this->markupEscaped or $this->safeMode or strpos($Excerpt['text'], '>') === false) { + return; + } + + if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w[\w-]*+[ ]*+>/s', $Excerpt['text'], $matches)) { + return array( + 'element' => array('rawHtml' => $matches[0]), + 'extent' => strlen($matches[0]), + ); + } + + if ($Excerpt['text'][1] === '!' and preg_match('/^/s', $Excerpt['text'], $matches)) { + return array( + 'element' => array('rawHtml' => $matches[0]), + 'extent' => strlen($matches[0]), + ); + } + + if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w[\w-]*+(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+\/?>/s', $Excerpt['text'], $matches)) { + return array( + 'element' => array('rawHtml' => $matches[0]), + 'extent' => strlen($matches[0]), + ); + } + } + + protected function inlineSpecialCharacter($Excerpt) + { + if (substr($Excerpt['text'], 1, 1) !== ' ' and strpos($Excerpt['text'], ';') !== false + and preg_match('/^&(#?+[0-9a-zA-Z]++);/', $Excerpt['text'], $matches) + ) { + return array( + 'element' => array('rawHtml' => '&' . $matches[1] . ';'), + 'extent' => strlen($matches[0]), + ); + } + + return; + } + + protected function inlineStrikethrough($Excerpt) + { + if (! isset($Excerpt['text'][1])) { + return; + } + + if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches)) { + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'del', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $matches[1], + 'destination' => 'elements', + ) + ), + ); + } + } + + protected function inlineUrl($Excerpt) + { + if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/') { + return; + } + + if (strpos($Excerpt['context'], 'http') !== false + and preg_match('/\bhttps?+:[\/]{2}[^\s<]+\b\/*+/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE) + ) { + $url = $matches[0][0]; + + $Inline = array( + 'extent' => strlen($matches[0][0]), + 'position' => $matches[0][1], + 'element' => array( + 'name' => 'a', + 'text' => $url, + 'attributes' => array( + 'href' => $url, + ), + ), + ); + + return $Inline; + } + } + + protected function inlineUrlTag($Excerpt) + { + if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w++:\/{2}[^ >]++)>/i', $Excerpt['text'], $matches)) { + $url = $matches[1]; + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'a', + 'text' => $url, + 'attributes' => array( + 'href' => $url, + ), + ), + ); + } + } + + # ~ + + protected function unmarkedText($text) + { + $Inline = $this->inlineText($text); + return $this->element($Inline['element']); + } + + # + # Handlers + # + + protected function handle(array $Element) + { + if (isset($Element['handler'])) { + if (!isset($Element['nonNestables'])) { + $Element['nonNestables'] = array(); + } + + if (is_string($Element['handler'])) { + $function = $Element['handler']; + $argument = $Element['text']; + unset($Element['text']); + $destination = 'rawHtml'; + } else { + $function = $Element['handler']['function']; + $argument = $Element['handler']['argument']; + $destination = $Element['handler']['destination']; + } + + $Element[$destination] = $this->{$function}($argument, $Element['nonNestables']); + + if ($destination === 'handler') { + $Element = $this->handle($Element); + } + + unset($Element['handler']); + } + + return $Element; + } + + protected function handleElementRecursive(array $Element) + { + return $this->elementApplyRecursive(array($this, 'handle'), $Element); + } + + protected function handleElementsRecursive(array $Elements) + { + return $this->elementsApplyRecursive(array($this, 'handle'), $Elements); + } + + protected function elementApplyRecursive($closure, array $Element) + { + $Element = call_user_func($closure, $Element); + + if (isset($Element['elements'])) { + $Element['elements'] = $this->elementsApplyRecursive($closure, $Element['elements']); + } elseif (isset($Element['element'])) { + $Element['element'] = $this->elementApplyRecursive($closure, $Element['element']); + } + + return $Element; + } + + protected function elementApplyRecursiveDepthFirst($closure, array $Element) + { + if (isset($Element['elements'])) { + $Element['elements'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['elements']); + } elseif (isset($Element['element'])) { + $Element['element'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['element']); + } + + $Element = call_user_func($closure, $Element); + + return $Element; + } + + protected function elementsApplyRecursive($closure, array $Elements) + { + foreach ($Elements as &$Element) { + $Element = $this->elementApplyRecursive($closure, $Element); + } + + return $Elements; + } + + protected function elementsApplyRecursiveDepthFirst($closure, array $Elements) + { + foreach ($Elements as &$Element) { + $Element = $this->elementApplyRecursiveDepthFirst($closure, $Element); + } + + return $Elements; + } + + protected function element(array $Element) + { + if ($this->safeMode) { + $Element = $this->sanitiseElement($Element); + } + + # identity map if element has no handler + $Element = $this->handle($Element); + + $hasName = isset($Element['name']); + + $markup = ''; + + if ($hasName) { + $markup .= '<' . $Element['name']; + + if (isset($Element['attributes'])) { + foreach ($Element['attributes'] as $name => $value) { + if ($value === null) { + continue; + } + + $markup .= " $name=\"".self::escape($value).'"'; + } + } + } + + $permitRawHtml = false; + + if (isset($Element['text'])) { + $text = $Element['text']; + } + // very strongly consider an alternative if you're writing an + // extension + elseif (isset($Element['rawHtml'])) { + $text = $Element['rawHtml']; + + $allowRawHtmlInSafeMode = isset($Element['allowRawHtmlInSafeMode']) && $Element['allowRawHtmlInSafeMode']; + $permitRawHtml = !$this->safeMode || $allowRawHtmlInSafeMode; + } + + $hasContent = isset($text) || isset($Element['element']) || isset($Element['elements']); + + if ($hasContent) { + $markup .= $hasName ? '>' : ''; + + if (isset($Element['elements'])) { + $markup .= $this->elements($Element['elements']); + } elseif (isset($Element['element'])) { + $markup .= $this->element($Element['element']); + } else { + if (!$permitRawHtml) { + $markup .= self::escape($text, true); + } else { + $markup .= $text; + } + } + + $markup .= $hasName ? '' : ''; + } elseif ($hasName) { + $markup .= ' />'; + } + + return $markup; + } + + protected function elements(array $Elements) + { + $markup = ''; + + $autoBreak = true; + + foreach ($Elements as $Element) { + if (empty($Element)) { + continue; + } + + $autoBreakNext = ( + isset($Element['autobreak']) + ? $Element['autobreak'] : isset($Element['name']) + ); + // (autobreak === false) covers both sides of an element + $autoBreak = !$autoBreak ? $autoBreak : $autoBreakNext; + + $markup .= ($autoBreak ? "\n" : '') . $this->element($Element); + $autoBreak = $autoBreakNext; + } + + $markup .= $autoBreak ? "\n" : ''; + + return $markup; + } + + # ~ + + protected function li($lines) + { + $Elements = $this->linesElements($lines); + + if (! in_array('', $lines, true) + and isset($Elements[0]) and isset($Elements[0]['name']) + and $Elements[0]['name'] === 'p' + ) { + unset($Elements[0]['name']); + } + + return $Elements; + } + + # + # AST Convenience + # + + /** + * Replace occurrences $regexp with $Elements in $text. Return an array of + * elements representing the replacement. + */ + protected static function pregReplaceElements($regexp, $Elements, $text) + { + $newElements = array(); + + while (preg_match($regexp, $text, $matches, PREG_OFFSET_CAPTURE)) { + $offset = $matches[0][1]; + $before = substr($text, 0, $offset); + $after = substr($text, $offset + strlen($matches[0][0])); + + $newElements[] = array('text' => $before); + + foreach ($Elements as $Element) { + $newElements[] = $Element; + } + + $text = $after; + } + + $newElements[] = array('text' => $text); + + return $newElements; + } + + # + # Deprecated Methods + # + + public function parse($text) + { + $markup = $this->text($text); + + return $markup; + } + + protected function sanitiseElement(array $Element) + { + static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/'; + static $safeUrlNameToAtt = array( + 'a' => 'href', + 'img' => 'src', + ); + + if (! isset($Element['name'])) { + unset($Element['attributes']); + return $Element; + } + + if (isset($safeUrlNameToAtt[$Element['name']])) { + $Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]); + } + + if (! empty($Element['attributes'])) { + foreach ($Element['attributes'] as $att => $val) { + # filter out badly parsed attribute + if (! preg_match($goodAttribute, $att)) { + unset($Element['attributes'][$att]); + } + # dump onevent attribute + elseif (self::striAtStart($att, 'on')) { + unset($Element['attributes'][$att]); + } + } + } + + return $Element; + } + + protected function filterUnsafeUrlInAttribute(array $Element, $attribute) + { + foreach ($this->safeLinksWhitelist as $scheme) { + if (self::striAtStart($Element['attributes'][$attribute], $scheme)) { + return $Element; + } + } + + $Element['attributes'][$attribute] = str_replace(':', '%3A', $Element['attributes'][$attribute]); + + return $Element; + } + + # + # Static Methods + # + + protected static function escape($text, $allowQuotes = false) + { + return htmlspecialchars($text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8'); + } + + protected static function striAtStart($string, $needle) + { + $len = strlen($needle); + + if ($len > strlen($string)) { + return false; + } else { + return strtolower(substr($string, 0, $len)) === strtolower($needle); + } + } + + public static function instance($name = 'default') + { + if (isset(self::$instances[$name])) { + return self::$instances[$name]; + } + + $instance = new static(); + + self::$instances[$name] = $instance; + + return $instance; + } + + private static $instances = array(); + + # + # Fields + # + + protected $DefinitionData; + + # + # Read-Only + + protected $specialCharacters = array( + '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', '~' + ); + + protected $StrongRegex = array( + '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*+[*])+?)[*]{2}(?![*])/s', + '_' => '/^__((?:\\\\_|[^_]|_[^_]*+_)+?)__(?!_)/us', + ); + + protected $EmRegex = array( + '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s', + '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us', + ); + + protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*+(?:\s*+=\s*+(?:[^"\'=<>`\s]+|"[^"]*+"|\'[^\']*+\'))?+'; + + protected $voidElements = array( + 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', + ); + + protected $textLevelElements = array( + 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont', + 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing', + 'i', 'rp', 'del', 'code', 'strike', 'marquee', + 'q', 'rt', 'ins', 'font', 'strong', + 's', 'tt', 'kbd', 'mark', + 'u', 'xm', 'sub', 'nobr', + 'sup', 'ruby', + 'var', 'span', + 'wbr', 'time', + ); +} diff --git a/src/PostManager.php b/src/PostManager.php new file mode 100644 index 0000000..4050dfd --- /dev/null +++ b/src/PostManager.php @@ -0,0 +1,69 @@ +db = $db; + } + + public function getAll(): array + { + $stmt = $this->db->query('SELECT * FROM posts ORDER BY created_at DESC'); + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + public function get(int $id): ?array + { + $stmt = $this->db->prepare('SELECT * FROM posts WHERE id = :id'); + $stmt->execute(['id' => $id]); + $post = $stmt->fetch(PDO::FETCH_ASSOC); + return $post ?: null; + } + + public function create(string $title, string $content, string $published_at): int + { + $stmt = $this->db->prepare(' + INSERT INTO posts (title, content, created_at, is_published) + VALUES (:title, :content, :published_at, true) + '); + $stmt->execute([ + 'title' => $title, + 'content' => $content, + 'published_at' => $published_at, + ]); + return (int)$this->db->lastInsertId(); + } + + + public function update(int $id, string $title, string $content, string $published_at, bool $published): bool + { + $stmt = $this->db->prepare(' + UPDATE posts + SET title = :title, + content = :content, + created_at = :published_at, + is_published = :published, + updated_at = NOW() + WHERE id = :id + '); + return $stmt->execute([ + 'id' => $id, + 'title' => $title, + 'content' => $content, + 'published_at' => $published_at, + 'published' => $published, + ]); + } + + + public function delete(int $id): bool + { + $stmt = $this->db->prepare('DELETE FROM posts WHERE id = :id'); + return $stmt->execute(['id' => $id]); + } +} diff --git a/src/RatingManager.php b/src/RatingManager.php new file mode 100644 index 0000000..b73eb99 --- /dev/null +++ b/src/RatingManager.php @@ -0,0 +1,46 @@ +pdo->prepare( + 'INSERT INTO article_ratings (article_uuid, user_email, rating) + VALUES (:uuid, :email, :r) + ON CONFLICT (article_uuid, user_email) + DO UPDATE SET rating = :r, rated_at = NOW()' + ); + $st->execute([':uuid' => $uuid, ':email' => strtolower($email), ':r' => $rating]); + } + + /** @return array{avg: float|null, count: int} */ + public function statsForArticle(string $uuid): array + { + $st = $this->pdo->prepare( + 'SELECT ROUND(AVG(rating)::numeric, 1) as avg, COUNT(*) as count + FROM article_ratings WHERE article_uuid = :uuid' + ); + $st->execute([':uuid' => $uuid]); + $row = $st->fetch(PDO::FETCH_ASSOC); + return [ + 'avg' => $row && $row['avg'] !== null ? (float)$row['avg'] : null, + 'count' => $row ? (int)$row['count'] : 0, + ]; + } + + public function userRating(string $uuid, string $email): ?int + { + $st = $this->pdo->prepare( + 'SELECT rating FROM article_ratings WHERE article_uuid = :uuid AND user_email = :email' + ); + $st->execute([':uuid' => $uuid, ':email' => strtolower($email)]); + $v = $st->fetchColumn(); + return $v !== false ? (int)$v : null; + } +} diff --git a/src/ReactionManager.php b/src/ReactionManager.php new file mode 100644 index 0000000..842b344 --- /dev/null +++ b/src/ReactionManager.php @@ -0,0 +1,63 @@ +pdo->prepare( + 'SELECT id FROM article_reactions + WHERE article_uuid = :uuid AND reaction_type = :type AND visitor_hash = :hash' + ); + $st->execute([':uuid' => $uuid, ':type' => $type, ':hash' => $visitorHash]); + if ($st->fetchColumn() !== false) { + $this->pdo->prepare( + 'DELETE FROM article_reactions + WHERE article_uuid = :uuid AND reaction_type = :type AND visitor_hash = :hash' + )->execute([':uuid' => $uuid, ':type' => $type, ':hash' => $visitorHash]); + return false; + } + $this->pdo->prepare( + 'INSERT INTO article_reactions (article_uuid, reaction_type, visitor_hash) + VALUES (:uuid, :type, :hash) ON CONFLICT DO NOTHING' + )->execute([':uuid' => $uuid, ':type' => $type, ':hash' => $visitorHash]); + return true; + } + + /** @return array */ + public function statsForArticle(string $uuid): array + { + $st = $this->pdo->prepare( + 'SELECT reaction_type, COUNT(*) AS cnt + FROM article_reactions WHERE article_uuid = :uuid GROUP BY reaction_type' + ); + $st->execute([':uuid' => $uuid]); + $stats = array_fill_keys(self::TYPES, 0); + foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $row) { + $stats[$row['reaction_type']] = (int) $row['cnt']; + } + return $stats; + } + + /** @return string[] */ + public function visitorReactions(string $uuid, string $visitorHash): array + { + $st = $this->pdo->prepare( + 'SELECT reaction_type FROM article_reactions + WHERE article_uuid = :uuid AND visitor_hash = :hash' + ); + $st->execute([':uuid' => $uuid, ':hash' => $visitorHash]); + return array_column($st->fetchAll(PDO::FETCH_ASSOC), 'reaction_type'); + } +} diff --git a/src/Repository/DictionnaryRepository.php b/src/Repository/DictionnaryRepository.php new file mode 100644 index 0000000..262aa69 --- /dev/null +++ b/src/Repository/DictionnaryRepository.php @@ -0,0 +1,54 @@ +pdo->prepare('SELECT * FROM dd_entities WHERE code = :c AND is_active IS TRUE'); + $st->execute([':c' => $code]); + $e = $st->fetch(PDO::FETCH_ASSOC); + if (!$e) { + return null; + } + + $e['fields'] = $this->getFields((int)$e['id']); + $e['rules'] = $this->getRules((int)$e['id']); + return $e; + } + + public function getFields(int $entityId): array + { + $st = $this->pdo->prepare('SELECT * FROM dd_fields WHERE entity_id = :id ORDER BY ui_order NULLS LAST, id'); + $st->execute([':id' => $entityId]); + return $st->fetchAll(PDO::FETCH_ASSOC); + } + + public function getRules(int $entityId): array + { + $st = $this->pdo->prepare('SELECT * FROM dd_rules WHERE entity_id = :id AND active IS TRUE'); + $st->execute([':id' => $entityId]); + return $st->fetchAll(PDO::FETCH_ASSOC); + } + + public function getEnum(string $name): array + { + $st = $this->pdo->prepare(' + SELECT ev.code, ev.label + FROM dd_enums e JOIN dd_enum_values ev ON ev.enum_id = e.id + WHERE e.name = :n AND ev.active IS TRUE + ORDER BY ev.sort_order, ev.id + '); + $st->execute([':n' => $name]); + return $st->fetchAll(PDO::FETCH_ASSOC); + } +} diff --git a/src/Repository/ProfileRepository.php b/src/Repository/ProfileRepository.php new file mode 100644 index 0000000..94c3abb --- /dev/null +++ b/src/Repository/ProfileRepository.php @@ -0,0 +1,158 @@ +pdo = $pdo; + return; + } + + // 1) App\Infrastructure\Database (si elle expose quelque chose) + if (class_exists(Database::class)) { + if (method_exists(Database::class, 'pdo')) { + $try = Database::pdo(); + if ($try instanceof PDO) { + $this->pdo = $try; + return; + } + } + if (method_exists(Database::class, 'getPdo')) { + $try = Database::getPdo(); + if ($try instanceof PDO) { + $this->pdo = $try; + return; + } + } + if (method_exists(Database::class, 'getInstance')) { + $db = Database::getInstance(); + if ($db instanceof PDO) { + $this->pdo = $db; + return; + } + if (is_object($db) && method_exists($db, 'pdo')) { + $try = $db->pdo(); + if ($try instanceof PDO) { + $this->pdo = $try; + return; + } + } + } + } + + // 2) Fonction globale éventuelle + if (function_exists('db')) { + $try = db(); + if ($try instanceof PDO) { + $this->pdo = $try; + return; + } + } + + // 3) Variable globale éventuelle + if (!empty($GLOBALS['pdo']) && $GLOBALS['pdo'] instanceof PDO) { + $this->pdo = $GLOBALS['pdo']; + return; + } + + // 4) Fallback env/const : compose un DSN pgsql si nécessaire + $dsn = getenv('DB_DSN') ?: ($_ENV['DB_DSN'] ?? null); + $user = getenv('DB_USER') ?: ($_ENV['DB_USER'] ?? null); + $pass = getenv('DB_PASS') ?: ($_ENV['DB_PASS'] ?? null); + if (!$dsn) { + $host = getenv('DB_HOST') ?: ($_ENV['DB_HOST'] ?? 'localhost'); + $port = getenv('DB_PORT') ?: ($_ENV['DB_PORT'] ?? '5432'); + $name = getenv('DB_NAME') ?: ($_ENV['DB_NAME'] ?? null); + if ($name) { + $dsn = sprintf('pgsql:host=%s;port=%s;dbname=%s', $host, $port, $name); + } + } + if ($dsn) { + $pdo = new PDO($dsn, (string)$user, (string)$pass, [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, + ]); + $this->pdo = $pdo; + return; + } + + throw new \RuntimeException('Impossible d’obtenir un PDO (aucune source valide trouvée).'); + } + + public function all(?bool $onlyActive = null): array + { + $sql = 'SELECT * FROM profiles'; + if ($onlyActive !== null) { + $sql .= ' WHERE is_active = :act'; + } + $sql .= ' ORDER BY slug'; + $stmt = $this->pdo->prepare($sql); + if ($onlyActive !== null) { + $stmt->bindValue(':act', $onlyActive, PDO::PARAM_BOOL); + } + $stmt->execute(); + return $stmt->fetchAll(PDO::FETCH_ASSOC) ?: []; + } + + public function findById(int $id): ?array + { + $stmt = $this->pdo->prepare('SELECT * FROM profiles WHERE id = :id'); + $stmt->execute([':id' => $id]); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + return $row ?: null; + } + + public function findBySlug(string $slug): ?array + { + $stmt = $this->pdo->prepare('SELECT * FROM profiles WHERE slug = :slug'); + $stmt->execute([':slug' => $slug]); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + return $row ?: null; + } + + public function create(string $slug, string $label, ?string $description, array $permissions, bool $isSystem, bool $isActive): int + { + $stmt = $this->pdo->prepare('INSERT INTO profiles(slug,label,description,permissions,is_system,is_active) VALUES(:slug,:label,:desc,CAST(:perms AS jsonb),:sys,:act) RETURNING id'); + $stmt->execute([ + ':slug' => $slug, + ':label' => $label, + ':desc' => $description, + ':perms' => json_encode($permissions, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + ':sys' => $isSystem, + ':act' => $isActive, + ]); + return (int)$stmt->fetchColumn(); + } + + public function update(int $id, string $slug, string $label, ?string $description, array $permissions, bool $isSystem, bool $isActive): void + { + $stmt = $this->pdo->prepare('UPDATE profiles SET slug=:slug,label=:label,description=:desc,permissions=CAST(:perms AS jsonb),is_system=:sys,is_active=:act WHERE id=:id'); + $stmt->execute([ + ':id' => $id, + ':slug' => $slug, + ':label' => $label, + ':desc' => $description, + ':perms' => json_encode($permissions, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + ':sys' => $isSystem, + ':act' => $isActive, + ]); + } + + public function delete(int $id): void + { + $stmt = $this->pdo->prepare('DELETE FROM profiles WHERE id=:id AND is_system = FALSE'); + $stmt->execute([':id' => $id]); + } +} diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php new file mode 100644 index 0000000..9c74b7f --- /dev/null +++ b/src/Repository/UserRepository.php @@ -0,0 +1,129 @@ +pdo->prepare('SELECT id FROM users WHERE email = :email LIMIT 1'); + $st->execute([':email' => $email]); + $id = $st->fetchColumn(); + if ($id !== false && $id !== null) { + return (string)$id; + } + + // 2) Création + // Génère un hash robuste sur une valeur aléatoire (aucune chance de connexion par mot de passe). + $randomSecret = bin2hex(random_bytes(32)); + $randomHash = password_hash($randomSecret, PASSWORD_DEFAULT); + + $sql = <<pdo->prepare($sql); + $st->execute([ + ':email' => $email, + ':hash' => $randomHash, + ]); + return (string)$st->fetchColumn(); + } catch (\PDOException $e) { + // Unique violation sur email (23505) → on relit l’id (race condition) + if ($e->getCode() === '23505') { + $st = $this->pdo->prepare('SELECT id FROM users WHERE email = :email LIMIT 1'); + $st->execute([':email' => $email]); + $id = $st->fetchColumn(); + if ($id !== false && $id !== null) { + return (string)$id; + } + } + throw $e; + } + } + + public function findByEmail(string $email): ?User + { + $sql = 'SELECT id, email, password_hash, is_active FROM users WHERE email = :email LIMIT 1'; + $st = $this->pdo->prepare($sql); + $st->execute([':email' => $email]); + $row = $st->fetch(PDO::FETCH_ASSOC); + if (!$row) { + return null; + } + + $isActive = $this->toBool($row['is_active']); + + return new User( + (string)$row['id'], + (string)$row['email'], + (string)$row['password_hash'], + $isActive + ); + } + + public function create(string $email, string $passwordHash): string + { + // PostgreSQL + $sql = 'INSERT INTO users (email, password_hash) VALUES (:email, :hash) RETURNING id'; + $st = $this->pdo->prepare($sql); + $st->execute([':email' => $email, ':hash' => $passwordHash]); + return (string)$st->fetchColumn(); + } + + public function updatePassword(string $userId, string $newHash): void + { + $sql = <<pdo->prepare($sql); + $st->execute([':h' => $newHash, ':id' => $userId]); + } + + /** + * Normalise un bool venant de PDO/pgsql ('t','f',1,0,true,false,'1','0','true','false') + */ + private function toBool(mixed $v): bool + { + if (is_bool($v)) { + return $v; + } + if (is_int($v)) { + return $v === 1; + } + if (is_string($v)) { + $v = strtolower($v); + return in_array($v, ['t', '1', 'true', 'on', 'yes'], true); + } + return (bool)$v; + } +} diff --git a/src/SearchEngine.php b/src/SearchEngine.php new file mode 100644 index 0000000..01f8f06 --- /dev/null +++ b/src/SearchEngine.php @@ -0,0 +1,276 @@ + $articles Liste brute d'articles (depuis ArticleManager) + * @return array + */ + public function search(string $query, array $articles): array + { + $tokens = $this->tokenize($query); + if (empty($tokens)) { + return []; + } + + $results = []; + foreach ($articles as $article) { + // 'plain' est pré-calculé dans search_index.json, sinon on stripe à la volée + $plain = $article['plain'] ?? $this->stripMarkdown($article['content'] ?? ''); + $tWords = $this->tokenize($article['title'] ?? ''); + $cWords = $this->tokenize($article['category'] ?? ''); + $pWords = $this->tokenize($plain); + + $score = $this->scoreArticle($tokens, $tWords, $cWords, $pWords); + if ($score > 0.0) { + $results[] = [ + 'article' => $article, + 'score' => $score, + 'snippet' => $this->buildSnippet($plain, $tokens), + 'tier' => $this->determineTier($tokens, $tWords, $cWords, $pWords), + ]; + } + } + + usort($results, static function (array $a, array $b): int { + if ($a['tier'] !== $b['tier']) { + return $a['tier'] <=> $b['tier']; + } + return $b['score'] <=> $a['score']; + }); + return $results; + } + + // ─── Scoring ───────────────────────────────────────────────────────────── + + private function scoreArticle(array $tokens, array $tWords, array $cWords, array $pWords): float + { + $total = 0.0; + foreach ($tokens as $token) { + $ts = $this->tokenScore($token, $tWords) * self::TITLE_WEIGHT + + $this->tokenScore($token, $cWords) * self::CAT_WEIGHT + + $this->tokenScore($token, $pWords) * self::CONTENT_WEIGHT; + + if ($ts <= 0.0) { + return 0.0; // AND strict : token introuvable → article exclu + } + $total += $ts; + } + return $total; + } + + /** + * Classe un résultat en tier : + * 1 → tous les tokens trouvés exactement dans le titre + * 2 → tous les tokens trouvés exactement dans titre, catégorie ou contenu + * 3 → au moins un token uniquement en correspondance floue + */ + private function determineTier(array $tokens, array $tWords, array $cWords, array $pWords): int + { + $inTitle = true; + foreach ($tokens as $token) { + if ($this->tokenScore($token, $tWords, false) < 0.75) { + $inTitle = false; + break; + } + } + if ($inTitle) { + return 1; + } + + $allWords = array_merge($tWords, $cWords, $pWords); + foreach ($tokens as $token) { + if ($this->tokenScore($token, $allWords, false) < 0.75) { + return 3; + } + } + return 2; + } + + /** + * Retourne un score 0–1 mesurant à quel point $token correspond + * au meilleur mot de la liste $words. + */ + private function tokenScore(string $token, array $words, bool $fuzzy = true): float + { + $best = 0.0; + $tLen = mb_strlen($token); + foreach ($words as $w) { + if ($w === $token) { + return 1.0; // exact + } + if ($tLen >= 3 && (str_contains($w, $token) || str_contains($token, $w))) { + $best = max($best, 0.75); // sous-chaîne (pluriels, conjugaisons) + } + if ($fuzzy && $tLen >= 4) { + $sim = $this->trigramSimilarity($token, $w); + if ($sim >= self::FUZZY_FLOOR) { + $best = max($best, $sim * 0.55); // fuzzy (fautes de frappe) + } + } + } + return $best; + } + + /** + * Calcule un score cumulé (OR) pour plusieurs tokens sur un ensemble d'articles. + * Tokenise chaque article une seule fois — évite N tokenisations avec N appels à search(). + * Le fuzzy (trigramme) est désactivé sur le contenu (poids 1.0) pour des raisons de perf. + * + * @param string[] $tokens Mots normalisés (lowercase, sans accents) + * @param array[] $articles Articles (doivent avoir uuid, title, category, plain|content) + * @return array{0: array, 1: array} + */ + public function scorePool(array $tokens, array $articles): array + { + if (empty($tokens) || empty($articles)) { + return [[], []]; + } + + $scoreMap = []; + $articleMap = []; + + foreach ($articles as $article) { + $plain = $article['plain'] ?? $this->stripMarkdown($article['content'] ?? ''); + $tWords = $this->tokenize($article['title'] ?? ''); + $cWords = $this->tokenize($article['category'] ?? ''); + $pWords = $this->tokenize($plain); + + $total = 0.0; + foreach ($tokens as $token) { + $ts = $this->tokenScore($token, $tWords, true) * self::TITLE_WEIGHT + + $this->tokenScore($token, $cWords, true) * self::CAT_WEIGHT + + $this->tokenScore($token, $pWords, false) * self::CONTENT_WEIGHT; + $total += $ts; + } + + if ($total > 0.0) { + $uuid = $article['uuid']; + $scoreMap[$uuid] = $total; + $articleMap[$uuid] = $article; + } + } + + return [$scoreMap, $articleMap]; + } + + // ─── Trigramme ─────────────────────────────────────────────────────────── + + private function trigramSimilarity(string $a, string $b): float + { + $tA = $this->trigrams($a); + $tB = $this->trigrams($b); + if (empty($tA) || empty($tB)) { + return 0.0; + } + $common = count(array_intersect($tA, $tB)); + return $common / max(count($tA), count($tB)); + } + + /** @return string[] */ + private function trigrams(string $s): array + { + $out = []; + $len = mb_strlen($s); + for ($i = 0; $i + 2 < $len; $i++) { + $out[] = mb_substr($s, $i, 3); + } + return array_unique($out); + } + + // ─── Snippet avec surbrillance ──────────────────────────────────────────── + + private function buildSnippet(string $text, array $tokens): string + { + $norm = $this->normalize($text); + $pos = 0; + foreach ($tokens as $token) { + $p = mb_strpos($norm, $token); + if ($p !== false) { + $pos = max(0, $p - 60); + break; + } + } + + $raw = mb_substr($text, $pos, self::SNIPPET_LEN); + if ($pos > 0) { + $raw = '…' . ltrim($raw); + } + if ($pos + self::SNIPPET_LEN < mb_strlen($text)) { + $raw .= '…'; + } + + $escaped = htmlspecialchars($raw, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + + // Surbrillance : on cherche les tokens dans le texte HTML-échappé + foreach ($tokens as $token) { + $escaped = (string) preg_replace( + '/(' . preg_quote(htmlspecialchars($token, ENT_QUOTES, 'UTF-8'), '/') . ')/iu', + '$1', + $escaped + ); + } + + return $escaped; + } + + // ─── Helpers texte ──────────────────────────────────────────────────────── + + /** Découpe en mots normalisés (min. 2 caractères). */ + private function tokenize(string $text): array + { + $norm = $this->normalize($text); + $words = preg_split('/\W+/u', $norm, -1, PREG_SPLIT_NO_EMPTY) ?: []; + return array_values(array_filter($words, fn ($w) => mb_strlen($w) >= 2)); + } + + /** Minuscule + translittération des accents français. */ + private function normalize(string $text): string + { + $text = mb_strtolower($text, 'UTF-8'); + return strtr($text, [ + 'à' => 'a', 'â' => 'a', 'ä' => 'a', + 'é' => 'e', 'è' => 'e', 'ê' => 'e', 'ë' => 'e', + 'î' => 'i', 'ï' => 'i', + 'ô' => 'o', 'ö' => 'o', + 'ù' => 'u', 'û' => 'u', 'ü' => 'u', + 'ç' => 'c', 'æ' => 'ae', 'œ' => 'oe', 'ñ' => 'n', + ]); + } + + /** Retire la syntaxe Markdown pour extraire le texte brut. */ + private function stripMarkdown(string $md): string + { + $t = preg_replace('/!\[[^\]]*\]\([^)]+\)/', '', $md) ?? $md; // images + $t = preg_replace('/\[([^\]]+)\]\([^)]+\)/', '$1', $t) ?? $t; // liens + $t = preg_replace('/```[\s\S]*?```/', '', $t) ?? $t; // blocs code + $t = preg_replace('/`[^`]+`/', '', $t) ?? $t; // code inline + $t = preg_replace('/^#{1,6}\s*/m', '', $t) ?? $t; // titres + $t = preg_replace('/[*_~]{1,3}([^*_~]+)[*_~]{1,3}/', '$1', $t) ?? $t; // gras/italique + $t = preg_replace('/^\s*[-*+|>]\s*/m', '', $t) ?? $t; // listes, citations, tableaux + $t = preg_replace('/\n{2,}/', ' ', $t) ?? $t; + return trim($t); + } +} diff --git a/src/SearchLogParser.php b/src/SearchLogParser.php new file mode 100644 index 0000000..cb7c3b5 --- /dev/null +++ b/src/SearchLogParser.php @@ -0,0 +1,127 @@ +logDir = rtrim($logDir, '/'); + $this->vhostBase = $vhostBase; + $this->cacheFile = $cacheFile !== '' + ? $cacheFile + : dirname(__DIR__) . '/_cache/search_terms.json'; + $this->cacheTtl = $cacheTtl; + } + + /** @return array terme => nombre d'occurrences, trié desc */ + public function topTerms(int $limit = 100): array + { + if ($this->cacheValid()) { + $data = json_decode((string) file_get_contents($this->cacheFile), true); + if (is_array($data)) { + return array_slice($data, 0, $limit, true); + } + } + + $counts = []; + foreach ($this->logFiles() as $file) { + $this->parseFile($file, $counts); + } + arsort($counts); + + @mkdir(dirname($this->cacheFile), 0755, true); + file_put_contents($this->cacheFile, json_encode($counts, JSON_UNESCAPED_UNICODE)); + + return array_slice($counts, 0, $limit, true); + } + + public function isReadable(): bool + { + $f = $this->logDir . '/' . $this->vhostBase; + return file_exists($f) && is_readable($f); + } + + private function cacheValid(): bool + { + return file_exists($this->cacheFile) + && (time() - filemtime($this->cacheFile)) < $this->cacheTtl; + } + + /** @return list */ + private function logFiles(): array + { + $base = $this->logDir . '/' . $this->vhostBase; + $files = []; + + if (file_exists($base) && is_readable($base)) { + $files[] = ['path' => $base, 'gz' => false]; + } + + for ($i = 1; $i <= 14; $i++) { + $plain = $base . '.' . $i; + $gz = $plain . '.gz'; + if (file_exists($plain) && is_readable($plain)) { + $files[] = ['path' => $plain, 'gz' => false]; + } elseif (file_exists($gz) && is_readable($gz)) { + $files[] = ['path' => $gz, 'gz' => true]; + } + } + + return $files; + } + + private function parseFile(array $file, array &$counts): void + { + if ($file['gz']) { + $h = @gzopen($file['path'], 'rb'); + if (!$h) { + return; + } + while (!gzeof($h)) { + $line = gzgets($h, 8192); + if ($line !== false) { + $this->parseLine($line, $counts); + } + } + gzclose($h); + } else { + $h = @fopen($file['path'], 'rb'); + if (!$h) { + return; + } + while (($line = fgets($h)) !== false) { + $this->parseLine($line, $counts); + } + fclose($h); + } + } + + private function parseLine(string $line, array &$counts): void + { + if (!str_contains($line, 'GET /search?')) { + return; + } + if (!preg_match('/"GET \/search\?([^"]*) HTTP\//', $line, $m)) { + return; + } + + parse_str($m[1], $params); + $q = trim(urldecode($params['q'] ?? '')); + + if ($q === '' || mb_strlen($q) > 200) { + return; + } + $q = mb_strtolower($q); + $counts[$q] = ($counts[$q] ?? 0) + 1; + } +} diff --git a/src/Service/AuthService.php b/src/Service/AuthService.php new file mode 100644 index 0000000..4169fe7 --- /dev/null +++ b/src/Service/AuthService.php @@ -0,0 +1,105 @@ + now() - interval '5 minutes' + and success = false"; + $st = \App\Infrastructure\Database::pdo()->prepare($sql); + $st->execute([':ip' => $ip]); + $fails = (int)$st->fetchColumn(); + return $fails < 10; // à ajuster + } + + + + public function login(string $email, string $password, string $ip): bool + { + $user = $this->users->findByEmail($email); + $ok = $user && $user->isActive && password_verify($password, $user->passwordHash); + + $pdo = \App\Infrastructure\Database::pdo(); + $st = $pdo->prepare('insert into login_attempts(email, ip, success) values(:e, :ip, :s)'); + $st->bindValue(':e', $email, \PDO::PARAM_STR); + $st->bindValue(':ip', $ip, \PDO::PARAM_STR); + $st->bindValue(':s', $ok, \PDO::PARAM_BOOL); + $st->execute(); + + if ($ok) { + \App\Infrastructure\Session::regenerate(); + $_SESSION['uid'] = $user->id; + $_SESSION['email'] = $user->email; + } + return $ok; + } + + + public function changePassword(string $userId, string $currentPassword, string $newPassword): bool + { + // Récupération de l’utilisateur (rapide : requête directe ; tu peux créer findById() si tu préfères) + $pdo = \App\Infrastructure\Database::pdo(); + $st = $pdo->prepare('select id, email, password_hash, is_active from users where id = :id'); + $st->execute([':id' => $userId]); + $row = $st->fetch(\PDO::FETCH_ASSOC); + if (!$row || !(bool)$row['is_active']) { + return false; + } + + // Vérifier l’ancien mot de passe + if (!password_verify($currentPassword, (string)$row['password_hash'])) { + return false; + } + + // Politique minimale : longueur uniquement (espaces autorisés) + if (mb_strlen($newPassword) < 7) { + return false; + } + // (optionnel) interdire seulement le caractère NUL + if (strpos($newPassword, "\0") !== false) { + return false; + } + + // Mettre à jour le hash + $newHash = password_hash($newPassword, PASSWORD_ARGON2ID); + (new \App\Repository\UserRepository(\App\Infrastructure\Database::get()))->updatePassword($row['id'], $newHash); + + // (Optionnel) rotation session + \App\Infrastructure\Session::regenerate(); + return true; + } + + public function register(string $email, string $password): string + { + $hash = password_hash($password, PASSWORD_ARGON2ID); + return $this->users->create($email, $hash); + } + + public static function requireAuth(): void + { + if (!isset($_SESSION['uid'])) { + header('Location: /login'); + exit; + } + } + + public static function logout(): void + { + $_SESSION = []; + session_destroy(); + } +} diff --git a/src/Service/MailQueue.php b/src/Service/MailQueue.php new file mode 100644 index 0000000..42fdc58 --- /dev/null +++ b/src/Service/MailQueue.php @@ -0,0 +1,218 @@ +pdo = $pdo; + $this->mailService = $mailService; + } + + /** + * Ajoute un email dans la file. + * + * @param array{delay?:int} $options delay en secondes avant éligibilité (facultatif) + */ + public function enqueue(string $to, string $subject, string $body, array $options = []): int + { + $delay = max(0, (int)($options['delay'] ?? 0)); + + $sql = <<pdo->prepare($sql); + $stmt->execute([ + ':to' => trim($to), + ':subject' => $subject, + ':body' => $body, + ':delay' => $delay, + ]); + + /** @var int $id */ + $id = (int) $stmt->fetchColumn(); + return $id; + } + + /** + * Traite la file en batch. + * - Récupère jusqu'à $max jobs "disponibles" + * - Pose un lease (locked_at) pour éviter les doublons de traitement + * - Envoie via MailService + * - Met à jour le statut (sent/failed) + planifie un retry si besoin + * + * @return array{processed:int, sent:int, failed:int, retried:int} + */ + public function process(int $max = 100): array + { + $max = max(1, min(1000, $max)); + + $jobs = $this->reserveBatch($max); + $processed = $sent = $failed = $retried = 0; + + foreach ($jobs as $job) { + $processed++; + $ok = false; + try { + // Anti-abus est appliqué par MailService::send() + $ok = $this->mailService->send($job['to_email'], $job['subject'], $job['body']); + } catch (Throwable $e) { + // On traitera en "retry" sous ce catch + $this->appendError((int)$job['id'], 'unexpected: ' . $e->getMessage()); + } + + if ($ok) { + $this->markAsSent((int)$job['id']); + $sent++; + } else { + // incrémente attempts et programme retry/backoff + $didRetry = $this->scheduleRetry((int)$job['id'], (int)$job['attempts'] + 1); + if ($didRetry) { + $retried++; + } else { + $this->markAsFailed((int)$job['id']); + $failed++; + } + } + } + + return compact('processed', 'sent', 'failed', 'retried'); + } + + /** + * Réserve jusqu'à $max jobs prêts (pending) en posant locked_at (lease). + * Utilise FOR UPDATE SKIP LOCKED pour le parallélisme côté SQL. + * + * @return array + */ + private function reserveBatch(int $max): array + { + // 1) Sélection des candidats + $this->pdo->beginTransaction(); + try { + $sql = <<pdo->prepare($sql); + $stmt->bindValue(':max', $max, PDO::PARAM_INT); + $stmt->execute(); + + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: []; + + if ($rows) { + // 2) Marquer locked_at + sending + $ids = array_map(static fn ($r) => (int)$r['id'], $rows); + $in = implode(',', array_fill(0, count($ids), '?')); + + $up = $this->pdo->prepare( + "UPDATE mail_queue + SET locked_at = (NOW() AT TIME ZONE 'UTC'), + status = 'sending' + WHERE id IN ($in)" + ); + $up->execute($ids); + } + + $this->pdo->commit(); + return $rows; + } catch (Throwable $e) { + $this->pdo->rollBack(); + return []; + } + } + + private function markAsSent(int $id): void + { + $stmt = $this->pdo->prepare("UPDATE mail_queue SET status='sent', locked_at=NULL WHERE id=:id"); + $stmt->execute([':id' => $id]); + } + + private function markAsFailed(int $id): void + { + $stmt = $this->pdo->prepare("UPDATE mail_queue SET status='failed', locked_at=NULL WHERE id=:id"); + $stmt->execute([':id' => $id]); + } + + /** + * Définit la prochaine tentative avec backoff exponentiel plafonné. + * Retourne false si on considère que c'est "trop" et qu'il faut passer en failed. + */ + private function scheduleRetry(int $id, int $nextAttempt): bool + { + // Politique simple : jusqu'à 8 tentatives (≈ ~4h de backoff cumulé, puis plafond 24h) + if ($nextAttempt > 8) { + return false; + } + + $delay = min(self::BASE_BACKOFF_SECONDS * (2 ** ($nextAttempt - 1)), self::MAX_BACKOFF_SECONDS); + + $sql = <<pdo->prepare($sql); + $stmt->execute([ + ':attempts' => $nextAttempt, + ':delay' => $delay, + ':id' => $id, + ]); + + return true; + } + + private function appendError(int $id, string $message): void + { + $sql = "UPDATE mail_queue SET last_error = COALESCE(last_error,'') || :e || E'\n' WHERE id = :id"; + $stmt = $this->pdo->prepare($sql); + $stmt->execute([ + ':e' => '[' . (new DateTimeImmutable('now', new \DateTimeZone('UTC')))->format('c') . '] ' . $message, + ':id' => $id, + ]); + } +} diff --git a/src/Service/MailService.php b/src/Service/MailService.php new file mode 100644 index 0000000..e554280 --- /dev/null +++ b/src/Service/MailService.php @@ -0,0 +1,287 @@ + */ + private array $smtpConfig; + + /** + * @param \PDO $pdo PDO connecté (PostgreSQL recommandé) + * @param array $smtpConfig [ + * 'host' => 'smtp.example.tld', + * 'port' => 587, + * 'username' => 'user', + * 'password' => 'pass', + * 'encryption' => 'tls'|'ssl'|null, + * 'from' => 'no-reply@example.tld', + * 'from_name' => 'Mon appli', + * 'reply_to' => 'contact@example.tld' (optionnel), + * 'reply_to_name' => 'Support' (optionnel), + * 'smtp_options' => [...] (optionnel, cf. PHPMailer::SMTPOptions) + * ] + */ + public function __construct(\PDO $pdo, array $smtpConfig) + { + $this->pdo = $pdo; + $this->smtpConfig = $smtpConfig; + + $this->mailer = new PHPMailer(true); + $this->configureMailer($this->mailer, $smtpConfig); + } + + /** + * Envoie un mail et journalise l'opération dans journal_smtp + */ + public function send(string $to, string $subject, string $body): bool + { + $to = trim($to); + if (!filter_var($to, FILTER_VALIDATE_EMAIL)) { + $this->log('blocked', $to, $subject, $body, 'invalid_email'); + return false; + } + + if (!$this->passesRateLimits($to)) { + $this->log('blocked', $to, $subject, $body, 'rate_limited'); + return false; + } + + try { + $this->mailer->clearAddresses(); + $this->mailer->clearReplyTos(); + + $this->mailer->addAddress($to); + if (!empty($this->smtpConfig['reply_to'])) { + $this->mailer->addReplyTo( + (string) $this->smtpConfig['reply_to'], + (string)($this->smtpConfig['reply_to_name'] ?? '') + ); + } + + $this->mailer->Subject = $subject; + $this->mailer->Body = $body; + $this->mailer->AltBody = $this->buildAltBody($body); + $this->mailer->isHTML($this->looksLikeHtml($body)); + + $sent = $this->mailer->send(); + + $this->log( + $sent ? 'sent' : 'error', + $to, + $subject, + $body, + $sent ? null : 'phpmailer_send_returned_false', + $this->mailer->getLastMessageID() ?: null + ); + + return $sent; + } catch (MailException $e) { + $this->log('error', $to, $subject, $body, 'phpmailer_exception: ' . $e->getMessage()); + return false; + } catch (\Throwable $e) { + $this->log('error', $to, $subject, $body, 'unexpected: ' . $e->getMessage()); + return false; + } + } + + /** + * Retourne les derniers envois (journal) + * @return array> + */ + public function list(int $limit = 50): array + { + $limit = max(1, min(500, $limit)); + $sql = <<pdo->prepare($sql); + $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); + $stmt->execute(); + + /** @var array> $rows */ + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + return $rows ?: []; + } + + // ---------- Internals ---------- + + /** + * @param array $cfg + */ + private function configureMailer(PHPMailer $m, array $cfg): void + { + $m->isSMTP(); + $m->Host = (string) $cfg['host']; + $m->Port = (int) ($cfg['port'] ?? 587); + $m->SMTPAuth = true; + $m->Username = (string) $cfg['username']; + $m->Password = (string) $cfg['password']; + $m->SMTPSecure = $cfg['encryption'] ?? PHPMailer::ENCRYPTION_STARTTLS; + if (!empty($cfg['smtp_options']) && is_array($cfg['smtp_options'])) { + $m->SMTPOptions = $cfg['smtp_options']; + } + + $from = (string) ($cfg['from'] ?? $cfg['username']); + $fromName = (string) ($cfg['from_name'] ?? ''); + $m->setFrom($from, $fromName); + + // Hygiène SMTP + $m->CharSet = 'UTF-8'; + $m->Encoding = 'base64'; + $m->Timeout = 15; // secondes + } + + /** Règles anti-abus : per-recipient + garde-fous globaux */ + private function passesRateLimits(string $recipient): bool + { + // 1) Min interval per destinataire (5 min) + $sql1 = <<pdo->prepare($sql1); + $stmt1->execute([':r' => $recipient]); + $last = $stmt1->fetchColumn(); + if ($last) { + $lastTs = (new DateTimeImmutable((string) $last))->getTimestamp(); + $delta = time() - $lastTs; + if ($delta < self::MIN_INTERVAL_BETWEEN_SENDS_SECONDS) { + return false; + } + } + + // 2) Max 5 en 12h par destinataire + $sql2 = <<= (NOW() AT TIME ZONE 'UTC') - INTERVAL '12 hours' + AND status = 'sent' + SQL; + $stmt2 = $this->pdo->prepare($sql2); + $stmt2->execute([':r' => $recipient]); + $count12h = (int) $stmt2->fetchColumn(); + if ($count12h >= self::MAX_SENDS_PER_12H_PER_RECIPIENT) { + return false; + } + + // 3) Garde-fou global / heure + $sql3 = <<= (NOW() AT TIME ZONE 'UTC') - INTERVAL '1 hour' + AND status = 'sent' + SQL; + $stmt3 = $this->pdo->query($sql3); + $global1h = (int) $stmt3->fetchColumn(); + if ($global1h >= self::MAX_GLOBAL_PER_HOUR) { + return false; + } + + return true; + } + + /** + * Journalise un envoi / tentative + * + * @param 'sent'|'error'|'blocked' $status + */ + private function log( + string $status, + string $recipient, + string $subject, + string $body, + ?string $error = null, + ?string $messageId = null + ): void { + $script = $this->detectScript(); + $host = (string) ($this->smtpConfig['host'] ?? ''); + $user = (string) ($this->smtpConfig['username'] ?? ''); + $subjectDb = mb_strimwidth($subject, 0, self::MAX_SUBJECT_LEN, '…', 'UTF-8'); + $bodyDb = mb_strimwidth($body, 0, self::MAX_BODY_LEN_FOR_LOG, '…', 'UTF-8'); + + $sql = <<pdo->prepare($sql); + $stmt->execute([ + ':script' => $script, + ':recipient' => $recipient, + ':subject' => $subjectDb, + ':body' => $bodyDb, + ':status' => $status, + ':error' => $error, + ':smtp_host' => $host, + ':smtp_user' => $user, + ':message_id' => $messageId, + ]); + } + + private function detectScript(): string + { + // Exemple : /public/pages/notifications/send.php + $script = $_SERVER['SCRIPT_NAME'] ?? ($_SERVER['PHP_SELF'] ?? ''); + if ($script === '' && \PHP_SAPI === 'cli') { + $script = $_SERVER['argv'][0] ?? 'cli'; + } + return (string) $script; + } + + private function looksLikeHtml(string $body): bool + { + return (bool) preg_match('~<(?:html|body|div|p|span|table|br|h[1-6]|a)\b~i', $body); + } + + private function buildAltBody(string $body): string + { + if (!$this->looksLikeHtml($body)) { + // Déjà texte brut + return $body; + } + // Version simplifiée : strip tags (on peut faire mieux selon besoins) + $text = trim((string) @html_entity_decode(strip_tags($body), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')); + return $text !== '' ? $text : '[Voir version HTML]'; + } +} diff --git a/src/Service/UiFormRenderer.php b/src/Service/UiFormRenderer.php new file mode 100644 index 0000000..566405a --- /dev/null +++ b/src/Service/UiFormRenderer.php @@ -0,0 +1,76 @@ +dict->getEntityByCode($entityCode); + if (!$e) { + return '
Entité inconnue
'; + } + + $html = ''; + foreach ($e['fields'] as $f) { + if (!$f['form_visible']) { + continue; + } + if ($f['read_only']) { + continue; + } + + $name = $f['code']; + $label = $f['label']; + $help = $f['help_text'] ?? ''; + $widget = $f['ui_widget'] ?? 'text'; + $val = $values[$name] ?? ''; + + $html .= '
'; + $html .= ''; + + if ($widget === 'select' && $f['enum_domain']) { + $opts = $this->dict->getEnum($f['enum_domain']); + $html .= ''; + } else { + $type = match ($widget) { + 'email' => 'email', + 'number' => 'number', + 'date' => 'date', + 'checkbox' => 'checkbox', + default => 'text', + }; + if ($type === 'checkbox') { + $chk = $val ? ' checked' : ''; + $html .= ''; + } else { + $placeholder = $f['placeholder'] ?? ''; + $html .= ''; + } + } + + if ($help) { + $html .= '
'.htmlspecialchars($help).'
'; + } + $html .= '
'; + } + return $html; + } +} diff --git a/src/Service/Validator.php b/src/Service/Validator.php new file mode 100644 index 0000000..5214a2a --- /dev/null +++ b/src/Service/Validator.php @@ -0,0 +1,79 @@ +dict->getEntityByCode($entityCode); + if (!$e) { + return ['_global' => ['Entité inconnue']]; + } + + // Index les champs + $fields = []; + foreach ($e['fields'] as $f) { + $fields[$f['code']] = $f; + } + + foreach ($e['rules'] as $r) { + $code = $r['field_code']; + $type = $r['rule_type']; + $val = $r['rule_value']; + $msg = $r['message']; + + $v = $code ? ($payload[$code] ?? null) : null; + + switch ($type) { + case 'required': + if ($code && ($v === null || $v === '')) { + $errors[$code][] = $msg; + } + break; + case 'regex': + if ($code && $v !== null && $v !== '' && !preg_match('#'.$val.'#u', (string)$v)) { + $errors[$code][] = $msg; + } + break; + case 'min': + if ($code && is_numeric($v) && (float)$v < (float)$val) { + $errors[$code][] = $msg; + } + break; + case 'max': + if ($code && is_numeric($v) && (float)$v > (float)$val) { + $errors[$code][] = $msg; + } + break; + case 'between': + if ($code && is_numeric($v)) { + [$a,$b] = array_map('floatval', explode(',', $val)); + $fv = (float)$v; + if ($fv < $a || $fv > $b) { + $errors[$code][] = $msg; + } + } + break; + case 'unique': + // à implémenter côté repo (SELECT COUNT(*) FROM table WHERE col=:v AND id<>:id) + // Laisse un hook ici. + break; + case 'custom': + // point d’extension si tu veux appeler une callable par nom + break; + } + } + + return $errors; + } +} diff --git a/src/SiteSettings.php b/src/SiteSettings.php new file mode 100644 index 0000000..6c4bbc1 --- /dev/null +++ b/src/SiteSettings.php @@ -0,0 +1,84 @@ + 0) { + $current['posts_per_page'] = $val; + } + } + file_put_contents( + siteSettingsPath(), + json_encode($current, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) + ); +} diff --git a/src/SmtpSettings.php b/src/SmtpSettings.php new file mode 100644 index 0000000..a054959 --- /dev/null +++ b/src/SmtpSettings.php @@ -0,0 +1,53 @@ +}> + */ + public function suggest( + string $markdown, + array $existingValues = [], + array $currentTags = [] + ): array { + $plain = $this->stripMarkdown($markdown); + + $candidates = []; + + // ── 1. Valeurs connues dans le système ────────────────────────────── + foreach ($existingValues as $val) { + $cnt = $this->countOccurrences($plain, $val); + if ($cnt > 0) { + $this->add($candidates, $val, $cnt, true, in_array($val, $currentTags, true), 'known'); + } + } + + // ── 2. Abréviations : 2-7 majuscules consécutives (avec chiffres/tirets) ── + preg_match_all('/\b([A-Z][A-Z0-9]{1,6}(?:-[A-Z0-9]+)?)\b/', $plain, $m); + foreach (array_count_values($m[1]) as $abbr => $cnt) { + if (!isset($candidates[$abbr])) { + $this->add($candidates, $abbr, $cnt, false, in_array($abbr, $currentTags, true), 'abbrev'); + } + } + + // ── 3. CamelCase / PascalCase (ex: Zigbee2MQTT, OpenWrt, HomeAssistant) ── + preg_match_all('/\b([A-Z][a-z]+(?:[A-Z0-9][a-z0-9]*)+)\b/', $plain, $m); + foreach (array_count_values($m[1]) as $word => $cnt) { + if (!isset($candidates[$word])) { + $this->add($candidates, $word, $cnt, false, in_array($word, $currentTags, true), 'camel'); + } + } + + // ── 4. Noms propres multi-mots (ex: "Home Assistant", "Raspberry Pi") ── + preg_match_all( + '/\b([A-ZÀÂÄÉÈÊËÎÏÔÙÛÜŸÇ][a-zàâäéèêëîïôùûüÿç]{1,}(?:\s+[A-ZÀÂÄÉÈÊËÎÏÔÙÛÜŸÇ][a-zàâäéèêëîïôùûüÿç]{1,})+)\b/u', + $plain, + $m + ); + foreach (array_count_values($m[1]) as $phrase => $cnt) { + if (!isset($candidates[$phrase]) && !$this->isStop($phrase)) { + $this->add($candidates, $phrase, $cnt, false, in_array($phrase, $currentTags, true), 'proper'); + } + } + + // ── 5. Mots capitalisés simples présents ≥ 2 fois ─────────────────── + preg_match_all('/\b([A-ZÀÂÄÉÈÊËÎÏÔÙÛÜŸÇ][a-zàâäéèêëîïôùûüÿç]{2,})\b/u', $plain, $m); + foreach (array_count_values($m[1]) as $word => $cnt) { + if ($cnt >= 2 && !isset($candidates[$word]) && !in_array($word, self::STOP, true)) { + $this->add($candidates, $word, $cnt, false, in_array($word, $currentTags, true), 'proper'); + } + } + + // ── Filtrage ───────────────────────────────────────────────────────── + foreach (array_keys($candidates) as $key) { + if (mb_strlen($key) < 2 || is_numeric($key)) { + unset($candidates[$key]); + } + } + + // ── Tri : actuels > connus > fréquence ─────────────────────────────── + uasort( + $candidates, + fn ($a, $b) => + ((int)$b['current'] <=> (int)$a['current']) + ?: ((int)$b['known'] <=> (int)$a['known']) + ?: ($b['count'] <=> $a['count']) + ); + + return $candidates; + } + + /** + * Suggestions spéciales pour le champ « catégorie » : + * retourne les catégories existantes classées par fréquence dans le texte. + * + * @param string[] $allCats Clés = nom de catégorie, valeurs = nb articles + * @return array catégorie => nb occurrences dans le texte + */ + public function suggestCategory(string $markdown, array $allCats, string $currentCat = ''): array + { + $plain = $this->stripMarkdown($markdown); + $result = []; + foreach (array_keys($allCats) as $cat) { + $cnt = $this->countOccurrences($plain, $cat); + $result[$cat] = $cnt; + } + // Trier : catégorie courante en premier, puis par occurrence décroissante, puis alphabétique + uksort($result, function ($a, $b) use ($result, $currentCat) { + if ($a === $currentCat) { + return -1; + } + if ($b === $currentCat) { + return 1; + } + return ($result[$b] <=> $result[$a]) ?: strcmp($a, $b); + }); + return $result; + } + + // ── Helpers ────────────────────────────────────────────────────────────── + + private function add(array &$c, string $key, int $cnt, bool $known, bool $current, string $group): void + { + $c[$key] = [ + 'count' => $cnt, + 'known' => $known, + 'current' => $current, + 'group' => $group, + ]; + } + + private function countOccurrences(string $haystack, string $needle): int + { + if ($needle === '') { + return 0; + } + return substr_count(mb_strtolower($haystack), mb_strtolower($needle)); + } + + private function isStop(string $phrase): bool + { + $words = explode(' ', $phrase); + // Phrase stop si tous les mots sont des stop words + foreach ($words as $w) { + if (!in_array($w, self::STOP, true)) { + return false; + } + } + return true; + } + + public function stripMarkdown(string $md): string + { + // Blocs de code → on retire pour éviter les faux positifs de variables/commandes + $md = preg_replace('/```[\s\S]*?```/m', ' ', $md); + $md = preg_replace('/`[^`\n]+`/', ' ', $md); + // En-têtes + $md = preg_replace('/^#{1,6}\s+/m', '', $md); + // Gras/italique + $md = preg_replace('/\*{1,3}([^*]+)\*{1,3}/', '$1', $md); + $md = preg_replace('/_{1,3}([^_]+)_{1,3}/', '$1', $md); + // Liens et images + $md = preg_replace('/!\[[^\]]*\]\([^\)]*\)/', ' ', $md); + $md = preg_replace('/\[([^\]]+)\]\([^\)]+\)/', '$1', $md); + // URLs brutes + $md = preg_replace('/https?:\/\/\S+/', ' ', $md); + // Balises HTML + $md = strip_tags($md); + // Marqueurs de liste + $md = preg_replace('/^[\*\-\+]\s+/m', '', $md); + // Lignes horizontales + $md = preg_replace('/^[-*_]{3,}\s*$/m', '', $md); + // Espaces multiples + return trim((string)preg_replace('/\s+/', ' ', $md)); + } +} diff --git a/src/auth.php b/src/auth.php new file mode 100644 index 0000000..a4efb25 --- /dev/null +++ b/src/auth.php @@ -0,0 +1,285 @@ + '', 'url' => '', 'slug' => '']; + } + if (array_key_exists($key, $cache)) { + return $cache[$key]; + } + $pdo = dbPdo(); + if ($pdo) { + try { + $st = $pdo->prepare('SELECT display_name, profile_url, profile_slug, bio FROM user_profiles WHERE email = :e'); + $st->execute([':e' => $key]); + $row = $st->fetch(PDO::FETCH_ASSOC); + if ($row) { + $cache[$key] = [ + 'name' => ($row['display_name'] !== '') ? $row['display_name'] : explode('@', $key)[0], + 'url' => $row['profile_url'] ?? '', + 'slug' => $row['profile_slug'] ?? '', + 'bio' => $row['bio'] ?? '', + ]; + return $cache[$key]; + } + } catch (\Throwable) { + } + } + $cache[$key] = ['name' => explode('@', $key)[0], 'url' => '', 'slug' => '']; + return $cache[$key]; +} + +function authorSlug(string $email): string +{ + return authorProfile($email)['slug']; +} + +function profileBySlug(string $slug): ?array +{ + if ($slug === '') { + return null; + } + $pdo = dbPdo(); + if (!$pdo) { + return null; + } + try { + $st = $pdo->prepare('SELECT email, display_name, profile_url, profile_slug, bio FROM user_profiles WHERE profile_slug = :s'); + $st->execute([':s' => $slug]); + $row = $st->fetch(PDO::FETCH_ASSOC); + return $row ?: null; + } catch (\Throwable) { + return null; + } +} + + +function dbPdo(): ?PDO +{ + static $pdo = null; + static $failed = false; + if ($failed) { + return null; + } + if ($pdo !== null) { + return $pdo; + } + $dsn = $_ENV['DB_DSN'] ?? (getenv('DB_DSN') ?: ''); + $user = $_ENV['DB_USER'] ?? (getenv('DB_USER') ?: ''); + $pass = $_ENV['DB_PASS'] ?? (getenv('DB_PASS') ?: ''); + if (!$dsn) { + $failed = true; + return null; + } + try { + $pdo = new PDO($dsn, $user ?: null, $pass ?: null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); + } catch (\Throwable) { + $failed = true; + return null; + } + return $pdo; +} + +function currentUserRoles(): array +{ + if (!isLoggedIn()) { + return []; + } + if (isset($_SESSION['user_roles'])) { + return $_SESSION['user_roles']; + } + $pdo = dbPdo(); + if (!$pdo) { + $_SESSION['user_roles'] = []; + return []; + } + try { + $st = $pdo->prepare( + 'SELECT r.name FROM roles r + JOIN user_roles ur ON ur.role_id = r.id + WHERE ur.user_email = :e' + ); + $st->execute([':e' => strtolower(currentUserEmail() ?? '')]); + $_SESSION['user_roles'] = $st->fetchAll(PDO::FETCH_COLUMN) ?: []; + } catch (\Throwable) { + $_SESSION['user_roles'] = []; + } + return $_SESSION['user_roles']; +} + +function hasRole(string $role): bool +{ + return in_array($role, currentUserRoles(), true); +} + +// Capacités connues — clé => label affiché dans l'admin +const KNOWN_CAPABILITIES = [ + 'propose_articles' => 'Proposer des articles', + 'validate_articles_all' => 'Valider des articles', + 'validate_articles_own' => 'Valider ses articles uniquement', + 'publish_articles_all' => 'Publier des articles', + 'publish_articles_own' => 'Publier ses articles uniquement', + 'edit_articles_all' => 'Modifier des articles', + 'edit_articles_own' => 'Modifier ses articles uniquement', + 'rate_articles' => 'Noter des articles', + 'view_previews' => 'Lire des avant-premières', + 'view_drafts_all' => 'Voir tous les brouillons', + 'view_drafts_own' => 'Voir ses brouillons', + 'view_sources_all' => 'Voir les sources (tous les articles)', + 'view_sources_own' => 'Voir les sources de ses articles', +]; + +// Groupes pour l'interface d'administration +const CAPABILITY_GROUPS = [ + 'Articles' => [ + 'propose_articles', + 'validate_articles_all', + 'validate_articles_own', + 'publish_articles_all', + 'publish_articles_own', + 'edit_articles_all', + 'edit_articles_own', + ], + 'Accès & lecture' => [ + 'rate_articles', + 'view_previews', + 'view_drafts_all', + 'view_drafts_own', + 'view_sources_all', + 'view_sources_own', + ], +]; + +function currentUserCapabilities(): array +{ + if (!isLoggedIn()) { + return []; + } + if (isset($_SESSION['user_capabilities'])) { + return $_SESSION['user_capabilities']; + } + $pdo = dbPdo(); + if (!$pdo) { + $_SESSION['user_capabilities'] = []; + return []; + } + try { + $st = $pdo->prepare( + 'SELECT DISTINCT rc.capability + FROM role_capabilities rc + JOIN user_roles ur ON ur.role_id = rc.role_id + WHERE ur.user_email = :e' + ); + $st->execute([':e' => strtolower(currentUserEmail() ?? '')]); + $_SESSION['user_capabilities'] = $st->fetchAll(PDO::FETCH_COLUMN) ?: []; + } catch (\Throwable) { + $_SESSION['user_capabilities'] = []; + } + return $_SESSION['user_capabilities']; +} + +function hasCapability(string $cap): bool +{ + if (isAdmin()) { + return true; + } + return in_array($cap, currentUserCapabilities(), true); +} + +function canDoOnArticle(string $baseCap, array $article): bool +{ + if (isAdmin()) { + return true; + } + if (hasCapability($baseCap . '_all')) { + return true; + } + if (hasCapability($baseCap . '_own')) { + $owner = strtolower($article['author'] ?? ''); + return $owner !== '' && $owner === strtolower(currentUserEmail() ?? ''); + } + return false; +} + +function isAdmin(): bool +{ + $email = currentUserEmail(); + if (!$email) { + return false; + } + // Fallback bootstrap : var d'env + $rawAdmin = $_ENV['ADMIN_EMAIL'] ?? (getenv('ADMIN_EMAIL') ?: ''); + $allowed = array_filter(array_map('trim', explode(',', (string)$rawAdmin))); + if (in_array(strtolower($email), array_map('strtolower', $allowed), true)) { + return true; + } + return hasRole('admin'); +} + +function ssoLogoutUrl(): string +{ + $issuer = rtrim((string)($_ENV['OIDC_ISSUER'] ?? (getenv('OIDC_ISSUER') ?: '')), '/'); + $clientId = (string)($_ENV['OIDC_CLIENT_ID'] ?? (getenv('OIDC_CLIENT_ID') ?: '')); + $baseUrl = rtrim((string)($_ENV['APP_URL'] ?? (getenv('APP_URL') ?: '/')), '/'); + + $params = [ + 'client_id' => $clientId, + 'post_logout_redirect_uri' => $baseUrl . '/', + ]; + if (!empty($_SESSION['oidc']['id_token'])) { + $params['id_token_hint'] = $_SESSION['oidc']['id_token']; + } + + if (!$issuer) { + return $baseUrl . '/'; + } + + return $issuer . '/protocol/openid-connect/logout?' . http_build_query($params); +} diff --git a/src/db.php b/src/db.php new file mode 100644 index 0000000..16a2030 --- /dev/null +++ b/src/db.php @@ -0,0 +1,18 @@ +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); +} catch (PDOException $e) { + die('Connexion échouée : ' . $e->getMessage()); +} diff --git a/src/helpers.php b/src/helpers.php new file mode 100644 index 0000000..fc22b88 --- /dev/null +++ b/src/helpers.php @@ -0,0 +1,135 @@ +$output"; +} + +function slugify(string $s): string +{ + $map = ['à' => 'a','â' => 'a','ä' => 'a','é' => 'e','è' => 'e','ê' => 'e','ë' => 'e','î' => 'i','ï' => 'i','ô' => 'o','ö' => 'o','ù' => 'u','û' => 'u','ü' => 'u','ç' => 'c','æ' => 'ae','œ' => 'oe']; + $s = mb_strtolower($s); + $s = strtr($s, $map); + $s = (string)preg_replace('/[^a-z0-9]+/', '-', $s); + return trim($s, '-'); +} + +/** + * Diff ligne-à-ligne via LCS. Retourne un tableau de [op, line] où + * op est '=' (inchangé), '-' (supprimé), '+' (ajouté). + */ +function lineDiff(string $old, string $new): array +{ + $a = explode("\n", $old); + $b = explode("\n", $new); + $n = count($a); + $m = count($b); + + if ($n * $m > 300000) { + return [['!', "Diff trop grand ({$n}×{$m} lignes), affichage brut."], ['-', $old], ['+', $new]]; + } + + $dp = array_fill(0, $n + 1, array_fill(0, $m + 1, 0)); + for ($i = $n - 1; $i >= 0; $i--) { + for ($j = $m - 1; $j >= 0; $j--) { + $dp[$i][$j] = $a[$i] === $b[$j] + ? 1 + $dp[$i + 1][$j + 1] + : max($dp[$i + 1][$j], $dp[$i][$j + 1]); + } + } + + $diff = []; + $i = 0; + $j = 0; + while ($i < $n || $j < $m) { + if ($i < $n && $j < $m && $a[$i] === $b[$j]) { + $diff[] = ['=', $a[$i]]; + $i++; + $j++; + } elseif ($j < $m && ($i >= $n || $dp[$i][$j + 1] >= $dp[$i + 1][$j])) { + $diff[] = ['+', $b[$j++]]; + } else { + $diff[] = ['-', $a[$i++]]; + } + } + return $diff; +} + +// 16 couleurs RGB de base — distribuées sur le spectre, visuellement distinctes +const COLOR_PALETTE_16 = [ + [220, 38, 38], // rouge + [234, 88, 12], // orange + [217, 119, 6], // ambre + [161, 142, 14], // jaune-olive + [77, 124, 15], // citron + [22, 163, 74], // vert + [4, 120, 87], // émeraude + [15, 118, 110], // sarcelle + [8, 145, 178], // cyan + [3, 105, 161], // ciel + [37, 99, 235], // bleu + [79, 70, 229], // indigo + [109, 40, 217], // violet + [147, 51, 234], // pourpre + [192, 38, 211], // fuchsia + [219, 39, 119], // rose +]; + +/** + * Génère un dégradé CSS pour une catégorie. + * Avec $allCats, l'assignation est séquentielle (par ordre alpha) ; + * au-delà de 16, un décalage de teinte et d'angle différencie les palettes. + * Sans $allCats, fallback par hachage sur la palette. + */ +function coverGradient(string $seed, array $allCats = []): string +{ + $key = strtolower(trim($seed)); + + if (!empty($allCats)) { + $keys = array_map(fn ($k) => strtolower(trim((string)$k)), array_keys($allCats)); + $pos = array_search($key, $keys, true); + if ($pos !== false) { + $idx = (int) $pos; + $tier = (int) floor($idx / 16); + $ci = $idx % 16; + return _paletteGradient(COLOR_PALETTE_16[$ci], $tier); + } + } + + // Hachage déterministe en l'absence de liste + $ci = abs(crc32($key)) % 16; + return _paletteGradient(COLOR_PALETTE_16[$ci], 0); +} + +function _paletteGradient(array $rgb, int $tier): string +{ + [$r, $g, $b] = $rgb; + + // Tier 0 : dégradé standard clair → foncé, 135° + // Tier 1 : plus saturé, angle inversé, 315° + // Tier 2+ : plus sombre encore, 225° + $tintMix = match ($tier) { + 0 => 0.65, 1 => 0.48, default => 0.35 + }; + $shadeK = match ($tier) { + 0 => 0.35, 1 => 0.25, default => 0.18 + }; + $angle = match ($tier) { + 0 => 135, 1 => 315, default => 225 + }; + + $tr = (int) round($r * (1 - $tintMix) + 255 * $tintMix); + $tg = (int) round($g * (1 - $tintMix) + 255 * $tintMix); + $tb = (int) round($b * (1 - $tintMix) + 255 * $tintMix); + + $sr = (int) round($r * $shadeK); + $sg = (int) round($g * $shadeK); + $sb = (int) round($b * $shadeK); + + return "linear-gradient({$angle}deg,rgb($tr,$tg,$tb) 0%,rgb($sr,$sg,$sb) 100%)"; +} diff --git a/src/mailer.php b/src/mailer.php new file mode 100644 index 0000000..ed75539 --- /dev/null +++ b/src/mailer.php @@ -0,0 +1,181 @@ +0) + if ($coolMin > 0) { + $q1 = "SELECT 1 FROM journal_smtp + WHERE to_email = :e AND created_at >= NOW() - INTERVAL :cool + AND status IN ('sent','queued') + LIMIT 1"; + $stmt = $pdo->prepare($q1); + $stmt->execute([':e' => $email, ':cool' => sprintf('%d minutes', $coolMin)]); + if ($stmt->fetchColumn()) { + return [false, "Un email vient d’être envoyé. Réessayez dans {$coolMin} min."]; + } + } + + // Plafond 12h (actif seulement si >0) + if ($maxPer12h > 0) { + $q2 = "SELECT COUNT(*) FROM journal_smtp + WHERE to_email = :e AND created_at >= NOW() - INTERVAL '12 hours' + AND status IN ('sent','queued')"; + $stmt = $pdo->prepare($q2); + $stmt->execute([':e' => $email]); + if ((int)$stmt->fetchColumn() >= $maxPer12h) { + return [false, 'Quota atteint. Réessayez plus tard.']; + } + } + + return [true, '']; +} + + +/** + * Envoi immédiat SMTP avec PHPMailer + journalisation. + * @param string $to destinataire + * @param string $subject objet + * @param string $html corps HTML + * @param string|null $text corps texte brut (optionnel, auto-généré si null) + * @param array $opts ['reply_to'=>['email','name']] + */ +function envoyer_mail_smtp(string $to, string $subject, string $html, ?string $text = null, array $opts = []): bool +{ + if (!($opts['bypass_rate_limit'] ?? false)) { + [$ok, $msg] = mailer_can_send($to, (int)env('SMTP_COOLDOWN_MINUTES', '5'), (int)env('SMTP_MAX_PER_12H', '5')); + if (!$ok) { + throw new RuntimeException($msg); + } + } + + $pdo = db(); + $pdo->beginTransaction(); + try { + $stmt = $pdo->prepare("INSERT INTO journal_smtp + (created_at, script_path, to_email, subject, content_html, content_text, status, ip, user_agent) + VALUES (NOW(), :script, :to, :subj, :html, :text, 'queued', :ip, :ua) + RETURNING id"); + $stmt->execute([ + ':script' => ($_SERVER['SCRIPT_NAME'] ?? ''), + ':to' => $to, + ':subj' => $subject, + ':html' => $html, + ':text' => $text ?? trim(html_entity_decode(strip_tags($html), ENT_QUOTES)), + ':ip' => ($_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? ''), + ':ua' => substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 512), + ]); + $rowId = (int)$stmt->fetchColumn(); + $pdo->commit(); + } catch (\Throwable $e) { + if ($pdo->inTransaction()) { + $pdo->rollBack(); + } + throw $e; + } + + $mail = new PHPMailer(true); + try { + $mail->isSMTP(); + $_smtpRead = function_exists('smtpCfg'); + $mail->Host = $_smtpRead ? smtpCfg('host', 'SMTP_HOST', 'localhost') : (string)env('SMTP_HOST', 'localhost'); + $mail->Port = (int)($_smtpRead ? smtpCfg('port', 'SMTP_PORT', '587') : env('SMTP_PORT', '587')); + $_smtpUser = $_smtpRead ? smtpCfg('user', 'SMTP_USER') : (string)env('SMTP_USER', ''); + $_smtpPass = $_smtpRead ? smtpCfg('pass', 'SMTP_PASS') : (string)env('SMTP_PASS', ''); + $mail->SMTPAuth = ($_smtpUser !== '' || $_smtpPass !== ''); + $mail->Username = $_smtpUser; + $mail->Password = $_smtpPass; + $secure = strtolower($_smtpRead ? smtpCfg('secure', 'SMTP_SECURE', 'tls') : (string)env('SMTP_SECURE', 'tls')); + if ($secure === 'ssl') { + $mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS; + } elseif ($secure === 'tls') { + $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; + } + + $mail->SMTPKeepAlive = true; // réutilise la connexion + $mail->Timeout = 30; // évite les blocages longs + $mail->SMTPOptions = ['ssl' => ['verify_peer' => true,'verify_peer_name' => true,'allow_self_signed' => false]]; + + $mail->CharSet = 'UTF-8'; + $mail->isHTML(true); + + // Expéditeur + $from = $_smtpRead ? smtpCfg('from', 'SMTP_FROM', 'no-reply@varlog.a5l.fr') : (string)env('SMTP_FROM', 'no-reply@varlog.a5l.fr'); + $fromName = $_smtpRead ? smtpCfg('from_name', 'SMTP_FROM_NAME', 'varlog') : (string)env('SMTP_FROM_NAME', 'varlog'); + $mail->setFrom($from, $fromName); + + // Reply-To + if (!empty($opts['reply_to']) && is_array($opts['reply_to']) && filter_var($opts['reply_to'][0] ?? '', FILTER_VALIDATE_EMAIL)) { + $mail->addReplyTo($opts['reply_to'][0], $opts['reply_to'][1] ?? ''); + } elseif ($rt = env('SMTP_REPLY_TO')) { + $mail->addReplyTo($rt, (string)env('SMTP_REPLY_TO_NAME', 'Support')); + } + + // DKIM optionnel + if ($d = env('DKIM_DOMAIN')) { + $mail->DKIM_domain = $d; + $mail->DKIM_selector = (string)env('DKIM_SELECTOR', 'default'); + $mail->DKIM_private = (string)env('DKIM_PRIVATE_KEY_PATH', ''); + $mail->DKIM_passphrase = (string)env('DKIM_PASSPHRASE', ''); + $mail->DKIM_identity = $from; + } + + $mail->addAddress($to); + $mail->Subject = $subject; + $mail->Body = $html; + $mail->AltBody = $text ?? trim(html_entity_decode(strip_tags($html), ENT_QUOTES)); + + $mail->send(); + + $pdo->prepare("UPDATE journal_smtp SET status='sent', sent_at=NOW() WHERE id=:id")->execute([':id' => $rowId]); + return true; + } catch (Exception $e) { + $pdo->prepare("UPDATE journal_smtp SET status='error', error_message=:err, sent_at=NOW() WHERE id=:id") + ->execute([':id' => $rowId, ':err' => substr($e->getMessage(), 0, 1000)]); + throw new RuntimeException('Envoi email impossible: '.$e->getMessage()); + } +} diff --git a/templates/about.php b/templates/about.php new file mode 100644 index 0000000..13d8065 --- /dev/null +++ b/templates/about.php @@ -0,0 +1,10 @@ + +
+ +
+getFiles($addFilesArticle['uuid']); +$articleUuid = $addFilesArticle['uuid']; +$articleTitle = $addFilesArticle['title']; + +// Extraire 1-3 mots significatifs du titre pour l'auto-recherche +$_sfStop = ['ou','et','un','une','le','la','les','de','du','des','en','au','aux','ce','cet', + 'cette','ces','que','qui','par','sur','dans','son','sa','ses','mon','ton','nos', + 'vos','leur','leurs','voir','comment','quoi','dont','votre','notre','selon','car', + 'mais','donc','puis','plus','très','avec','pour','pas','est','sont','était', + 'être','avoir','faire','tout','tous','toute','toutes']; +$_sfWords = preg_split('/[^a-zA-ZÀ-ÿ0-9]+/u', $articleTitle) ?: []; +$_sfKw = []; +foreach ($_sfWords as $_w) { + if (mb_strlen($_w) >= 3 && !in_array(mb_strtolower($_w), $_sfStop, true)) { + $_sfKw[] = $_w; + if (count($_sfKw) >= 3) { + break; + } + } +} +$autoSearchQuery = !empty($_sfKw) ? implode(' ', $_sfKw) : $articleTitle; +unset($_sfStop, $_sfWords, $_sfKw, $_w); +?> + + +
+ + +
+ ← Retour +

Ajouter des fichiers

+
+ +

+ Article : +

+ +
+ + +
+
+
+
Uploader
+
+
+ +
+ Images → sha256-taille.ext
+ Vidéos, PDF, autres → nom sanitisé +
+
+
+ + Annuler +
+
+
+
+ + + +
+
+
Fichiers existants
+
+ +
+ + + + + '🎬', + str_starts_with($f['mime'], 'audio/') => '🎵', + $f['mime'] === 'application/pdf' => '📑', + default => '📄', + } ?> + + +
+ + Ko +
+ + cover + +
+ +
+
+
+ +
+ + +
+
+
+
Fichiers d'autres articles
+
+ + +
+
+
+
+
+ +
+ + + +Brouillon'; + } + if (strtotime((string)($a['published_at'] ?? '')) > $now) { + return 'Avant-première'; + } + return 'Publié'; +} +?> + +
+

Administration

+ + Nouvel article +
+ + + + + + + +
+ 'Publiés', 'value' => $adminData['published'], 'color' => 'success'], + ['label' => 'Avant-premières', 'value' => $adminData['previews'], 'color' => 'warning'], + ['label' => 'Brouillons', 'value' => $adminData['drafts'], 'color' => 'secondary'], + ['label' => 'Total', 'value' => $adminData['total'], 'color' => 'primary'], + ]; + foreach ($stats as $s): ?> +
+
+
+
+
+
+
+
+ +
+ +
Activité récente
+ + + + + + + + + + + + + + + + + + + +
TitreAuteurStatutModifié le
+ + + + + +
+ + + + + +
+ +
+ +
+ + +
+ +
+ +
+ +
+
+ + + + Réinitialiser + +
+ +
+ résultat(s) +
+ +
+ + +

Aucun article.

+ +
+
+
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
TitreAuteurCatégorieStatutDate
+ + + + + + + + + Modifier +
+
+ + + + + + + +
+ Impossible de retirer le rôle Administrateur : il doit rester au moins un administrateur. +
+ + + +
+
Attribuer un rôle
+
+
+
+ + +
+
+ + +
+
+ +
+
+
+
+ + + +

Aucun utilisateur.

+ + + + + + + + + + + + + + + + + + + + +
EmailStatutRôles
+ + Pré-inscrit + + Actif + + Inactif + + + + +
+ + + +
+ + + + +
+ + !in_array($r['name'], $currentRoleNames, true)); + ?> + +
+ + + +
+ +
+ + + + + +
+ + +
+ +

Aucun rôle défini.

+ + + + + + + + + + + + + + + + + + +
RôleUtilisateurs
+ + +
+ KNOWN_CAPABILITIES[$c] ?? $c, + $r['capabilities'] + ); + echo htmlspecialchars(implode(', ', $capLabels) ?: '–'); + ?> +
+ +
Toutes les permissions
+ +
+ + + Éditer + +
+ + +
+ +
+ +
+ + +
+
+
Nouveau rôle
+
+
+
+ + +
+ + +
+
+
+
+ +
+ + + + +
Paramètres enregistrés.
+ + +
+
Paramètres du site
+
+
+
+ + +
Affiché dans la barre de navigation et les onglets.
+
+
+ + +
Affiché sous le titre dans la navbar et dans le pied de page.
+
+
+ + +
Format BCP 47 (ex : fr, fr-FR). Utilisé dans <html lang>, og:locale, RSS et JSON-LD.
+
+
+ + +
+
+ + +
+
+ + +
Affiché dans le footer.
+
+ +
+
+
+ + + + + + + + +
+ + +
+
Catégories existantes
+ +

Aucune catégorie définie.

+ +
+ $_count): + $_gradient = coverGradient($_cat, $_cats); ?> +
+
+ +
+ +
+ + article 1 ? 's' : '' ?> +
+ +
+ + + +
+ + +
+ + +
+ +
+ + +
+ +
+
+ +
+ +
+ + +
+
Prochaine couleur
+
+
+

+ La prochaine catégorie reçoit la couleur n°. +

+
+ $_rgb): + $_g = _paletteGradient($_rgb, 0); + $_active = $_i === $_nextIdx; + ?> +
+
+ +
+
+
+
+ +
+ + +
+ +
+ +
+
Types de tags
+ +

Aucun type de tag défini.

+ +
+ $_label): ?> +
+
+ + +
+ + +
+
+
+ +
+ +
+ +
+
Nouveau type
+
+
+
+
+ + +
Minuscules, chiffres, _
+
+
+ + +
+ +
+
+
+
+ +
+ + + + + + + + + + + + +
Paramètres SMTP enregistrés.
+ + +
+ + +
+
+
Configuration SMTP
+
+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + + +
Laisser vide pour conserver le mot de passe actuel.
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+ + +
+
+
+
+ + +
+ + +
+
+
+
+ + + +
+
+ + + ✓ Succès + + ✗ Échec + + — + + + +
+ +
+ +
+ + +
+ +
+ +
+ +
+ + + + + + + + 0, 'sent' => 0, 'error' => 0, 'queued' => 0]; + $emlFilters = [ + '' => ['label' => 'Tous', 'count' => $emlCounts['all']], + 'sent' => ['label' => 'Envoyés', 'count' => $emlCounts['sent']], + 'error' => ['label' => 'Erreurs', 'count' => $emlCounts['error']], + 'queued' => ['label' => 'En file', 'count' => $emlCounts['queued']], + ]; + ?> + +
+
Logs emails + +
+
+ $fInfo): ?> + + + + + +
+
+ + +

Aucun email enregistré.

+ +
+ + + + + + + + + + + + + Envoyé' . ($emSentAt ? ' ' . $emSentAt : '') . ''; + } elseif ($em['status'] === 'error') { + $emBadge = 'Erreur'; + } else { + $emBadge = 'En file'; + } + ?> + + + + + + + + + +
DateDestinataireSujetStatutContenu
+
+ Voir +
+ +

Erreur :

+ + +
+ +
+
+
+
+ + 0 || count($adminData['emails']) === 50): ?> + + + + + + + + + + + 0, 'pending' => 0, 'verified' => 0, 'hidden' => 0]; + $cmtFilters = [ + '' => ['label' => 'Tous', 'count' => $cmtCounts['all']], + 'pending' => ['label' => 'En attente', 'count' => $cmtCounts['pending']], + 'verified' => ['label' => 'Vérifiés', 'count' => $cmtCounts['verified']], + 'hidden' => ['label' => 'Masqués', 'count' => $cmtCounts['hidden']], + ]; + ?> + +
+
Commentaires + +
+
+ $fInfo): ?> + + + + + +
+
+ + +

Aucun commentaire pour ce filtre.

+ +
+ + + + + + + + + + + + + + + Envoyé'; + } elseif ($mailStatus === 'error') { + $mailBadge = 'Erreur'; + } elseif ($mailStatus === 'queued') { + $mailBadge = 'En file'; + } else { + $mailBadge = '-'; + } + ?> + + + + + + + + + + + +
ArticleAuteurCommentaireDateEmailStatut
+ + + + + + + + +
+
+
+ + + + +
Code : + +
+ + En attente + + Vérifié + + Publié + + Masqué + + + + +
+ + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + +
+
+
+ + + + + + + + +
+
Termes recherchés + +
+ Derniers 14 jours de logs · cache 10 min +
+ + +
+ Les logs Apache ne sont pas lisibles par PHP. Vérifiez que www-data appartient au groupe adm. +
+ +

Aucune recherche trouvée dans les logs.

+ + +
+ + + + + + + + + + + $count): ?> + + + + + + + + + +
#Terme recherchéFois
+ + + + +
+
+
+
+
+ + + + + + +
+ ← Retour +

Rôle :

+ +
+ +
+ +
+ + +
+ + +
+ Le rôle admin a toutes les permissions implicitement — les cases à cocher sont ignorées. +
+ + +
+
+ $groupCaps): ?> +
+
+ + +
+ > + +
+ +
+ +
+
+ + +
+ + +
+
+
+
+ + +
+ + Annuler +
+
+
+ +
+
+ + + +
+ +

Articles de

+
+ + +

Aucun article publié.

+ +
+text($post['content']); + $preview = mb_strimwidth(strip_tags($html), 0, 120, '…'); + $category = trim((string)($post['category'] ?? '')); + $gradient = coverGradient($category !== '' ? $category : $post['uuid'], $allCats ?? []); + $postUrl = '/post/' . rawurlencode($post['slug']); + $coverFile = $post['cover'] ?? ''; + $coverStyle = $coverFile !== '' + ? 'background-image: url(\'/file?uuid=' . rawurlencode($post['uuid']) . '&name=' . rawurlencode($coverFile) . '\')' + : 'background: ' . $gradient; + $isAvantPremiere = $post['published'] && strtotime((string)($post['published_at'] ?? '')) > time(); + $isLocked = $isAvantPremiere && !hasCapability('view_previews'); + ?> +
+ +
Avant-première
+ +
+ + + +
+
+

+ + + + + +

+

+ +
+ + + +
+ +
+ + + + + + + + +
+
+
+

+ + + ↗ + + + Mes liens +
+ +
+

+ +
+ + +
+ + +

Aucun article publié.

+ +
+ text($post['content']); + $preview = mb_strimwidth(strip_tags($html), 0, 120, '…'); + $category = trim((string)($post['category'] ?? '')); + $gradient = coverGradient($category !== '' ? $category : $post['uuid'], $allCats ?? []); + $postUrl = '/post/' . rawurlencode($post['slug']); + $coverFile = $post['cover'] ?? ''; + $coverStyle = $coverFile !== '' + ? 'background-image: url(\'/file?uuid=' . rawurlencode($post['uuid']) . '&name=' . rawurlencode($coverFile) . '\')' + : 'background: ' . $gradient; + ?> +
+
+ + + +
+
+

+ +

+

+ +
+ +
+ +
+ 6): ?> +

+ Voir tous les articles → +

+ + + + + +
+

Catégories

+ ← Retour +
+ +
+ + +
+
Catégories existantes
+ + +

Aucune catégorie définie.

+ +
+ $count): + $gradient = coverGradient($cat, $cats); ?> +
+
+ + +
+ + +
+ + article 1 ? 's' : '' ?> +
+ + +
+ + + +
+ + + +
+ + +
+ + +
+ + +
+ +
+
+ +
+ +
+ + +
+
Nouvelle catégorie
+
+
+

+ Créez une catégorie en l'assignant à un article. + La prochaine reçoit la couleur n°. +

+ + +
+ $rgb): + $g = _paletteGradient($rgb, 0); + $active = $i === $nextIdx; + ?> +
+
+ +
+
+
+
+ +
+ + +
+ +
+

Types de tags

+
+ +
+ + +
+ +

Aucun type de tag défini.

+ +
+ $_label): ?> +
+
+ + +
+ + +
+
+
+ +
+ +
+ + +
+
Nouveau type
+
+
+
+
+ + +
Minuscules, chiffres, _ (sans accent)
+
+
+ + +
+ +
+
+
+
+ +
+ + + +// $visitorReactions — string[] (types déjà cliqués par ce visiteur) +// $comments — array de commentaires publiés +// $commentFlash — bool|null (commentaire soumis, email envoyé) +// $commentVerified — bool|null (commentaire vérifié et publié) +// $commentError — string|null (message d'erreur) + +$_reactionDefs = [ + 'useful' => ['👍', 'Utile'], + 'important' => ['🔥', 'Important'], + 'interesting' => ['🤔', 'À creuser'], +]; + +$_csrfToken = bin2hex(random_bytes(16)); +$_SESSION['comment_csrf'] = $_csrfToken; +?> + + + +
+
À lire aussi
+ +
+ + + +
+ +
+ Commentaires + + + +
+ + +
+ Un code de confirmation vous a été envoyé par email. + Cliquez sur le lien reçu, puis saisissez le code à 6 chiffres pour publier votre commentaire. +
+ + + +
Votre commentaire a été publié. Merci !
+ + + +
+ + + +
+
+
+ + + + +
+
+ +
+
+ + + +
+
+ +
+
+ + + +

Aucun commentaire pour l'instant. Soyez le premier !

+ + + +
+
+
Laisser un commentaire
+
+ + + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + Un code de vérification sera envoyé à votre adresse email. +
+
+
+
+
+ +
diff --git a/templates/contact.php b/templates/contact.php new file mode 100644 index 0000000..ca88305 --- /dev/null +++ b/templates/contact.php @@ -0,0 +1,145 @@ + 100) { + $error = 'Nom invalide.'; + } elseif (!filter_var($from, FILTER_VALIDATE_EMAIL)) { + $error = 'Adresse e-mail invalide.'; + } elseif ($body === '' || mb_strlen($body) > 5000) { + $error = 'Message vide ou trop long (max 5000 caractères).'; + } + } + + if (!$error && $contactEmail !== '') { + $subjectClean = mb_encode_mimeheader( + '[' . siteTitle() . ' contact] ' . mb_strimwidth($subject, 0, 100, '…'), + 'UTF-8', + 'B' + ); + $nameClean = mb_encode_mimeheader($name, 'UTF-8', 'B'); + + $fromEmail = $_ENV['CONTACT_FROM_EMAIL'] ?? ('noreply@' . (parse_url(APP_URL, PHP_URL_HOST) ?? 'localhost')); + $headers = 'From: =?UTF-8?B?' . base64_encode(siteTitle() . ' contact') . "?= <{$fromEmail}>\r\n"; + $headers .= "Reply-To: {$nameClean} <{$from}>\r\n"; + $headers .= "Content-Type: text/plain; charset=UTF-8\r\n"; + $headers .= "Content-Transfer-Encoding: 8bit\r\n"; + + $fullBody = "De : {$name} <{$from}>\n\n{$body}\n\n---\nEnvoyé depuis varlog"; + + if (@mail($contactEmail, $subjectClean, $fullBody, $headers)) { + $_SESSION['contact_last_sent'] = time(); + $success = true; + } else { + $error = 'Erreur lors de l\'envoi. Veuillez réessayer plus tard.'; + } + } elseif (!$error) { + $error = 'Formulaire de contact non configuré.'; + } +} + +// Génère un nouveau token CSRF à chaque affichage du formulaire +if (!$success) { + $_SESSION['contact_csrf'] = bin2hex(random_bytes(16)); +} + +ob_start(); +?> + +
+

Contact

+

Envoyez-moi un message. Votre adresse e-mail ne sera pas publiée.

+ + + + + + + + + + + +
+
+
+ + + + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+
+ + +
+ + + +
+
+ +
+ ← Retour +

Confirmation — droits d'auteur

+
+ +
+ +
+ Vous êtes sur le point de copier ce fichier sur votre serveur : + +
+
+ + +
+
Ce que dit la loi française
+
+

En France, toute œuvre de l'esprit est protégée dès sa création sans + formalité d'enregistrement (art. L.111-1 CPI). Cela inclut les photographies, illustrations, + textes, vidéos et musiques.

+ +

Reproduire ou diffuser publiquement une œuvre sans l'autorisation de son auteur constitue + une contrefaçon (art. L.335-2 CPI), passible de :

+
    +
  • 3 ans d'emprisonnement
  • +
  • 300 000 € d'amende
  • +
+ +

L'exception d'usage privé (art. L.122-5 1° CPI) est strictement + personnelle et ne couvre pas la publication sur un blog, même non commercial + et même à audience restreinte.

+
+
+ + +
+
+ ✓ Cas où vous pouvez légalement télécharger ce fichier +
+
    +
  • + Vous êtes l'auteur ou co-auteur du fichier +
  • +
  • + Le fichier est distribué sous une licence libre compatible avec la + reproduction publique : CC0, + CC BY, + CC BY-SA, + CC BY-ND, + domaine public, etc.
    + ⚠ CC BY-NC ne suffit pas si le blog génère des revenus, + même indirects. +
  • +
  • + Vous disposez d'une autorisation écrite explicite de l'auteur ou + du titulaire des droits patrimoniaux +
  • +
  • + L'œuvre est dans le domaine public : 70 ans révolus après le décès + de l'auteur en Union Européenne (art. L.123-1 CPI) +
  • +
+
+ + +
+ + + + + + + + + + + +
+
+
+ + +
+
+
+ +
+ + Annuler +
+
+ +
+
+ + + +
+ ← Retour +
+ + — révision # + + du + + +
+
+ +
+ − Supprimé + + Ajouté + = Inchangé +
+ + +
Aucune différence — le contenu est identique.
+ + + +
+ + + + +
+ + + +
+ +
− 
+ +
+ +
  
+ + +
+ + + + + + + +
+ ← Retour à l'édition +
+

+ Suggestions : +

+

+ +

+
+
+ +
+ + + +

+ Sélectionnez une catégorie. Les chiffres indiquent combien de fois le mot apparaît dans le texte. +

+ +
+ $_cnt): ?> + +
+ +
+ +
+ + + + $s['current']); + $_known = array_filter($suggestions, fn ($s) => !$s['current'] && $s['known']); + $_abbrevs = array_filter($suggestions, fn ($s) => !$s['current'] && !$s['known'] && $s['group'] === 'abbrev'); + $_camel = array_filter($suggestions, fn ($s) => !$s['current'] && !$s['known'] && $s['group'] === 'camel'); + $_proper = array_filter($suggestions, fn ($s) => !$s['current'] && !$s['known'] && in_array($s['group'], ['proper'], true)); + ?> + +

+ Cochez les termes à associer à cet article. Les termes déjà taggués sont présélectionnés. +

+ + + +
+ 10): ?> +
+ + + + +
+ +
+ +
+ + +
+ $_info): ?> + + +
+ + 10): ?> +
+
+ +
+ + + + + + + + +

Aucun terme détecté dans cet article.

+ + + + +
+ + Annuler +
+ +
+ + + +
+

Flux agrégés

+
+ + +

Aucun article disponible pour l'instant.

+ +
+ 0 ? date('d/m/Y', $_item['date']) : ''; + $_authorName = $_item['author_name'] ?? ''; + $_authorSlug = $_item['author_slug'] ?? ''; + ?> +
+
+ + + + + + + + + + + +
+

+ + ↗ + +

+ +

+ +
+ +
+ + + + + + diff --git a/templates/header.php b/templates/header.php new file mode 100644 index 0000000..7fec33a --- /dev/null +++ b/templates/header.php @@ -0,0 +1,49 @@ + + +
+
+ + + Mug ALPINUX + + + + + +sessionAlready()) { + ?> + + + + + +
+
+ + diff --git a/templates/import_image.php b/templates/import_image.php new file mode 100644 index 0000000..7e48911 --- /dev/null +++ b/templates/import_image.php @@ -0,0 +1,41 @@ + + +
+ ← Retour +

Importer un fichier depuis une URL

+
+ +

+ Article : +

+ + +
+ URL invalide ou inaccessible — vérifiez que le lien est correct et que le serveur peut y accéder. +
+ + +
+
+
+
+ + +
Les métadonnées seront récupérées automatiquement à l'étape suivante.
+
+
+ + Annuler +
+
+
+
+ + 'Type', + 'size' => 'Taille', + // PDF + 'pages' => 'Pages', + 'page_size' => 'Format', + 'pdf_version' => 'Version PDF', + // Image + 'width' => 'Dimensions', + 'camera' => 'Appareil', + // HTML + 'site_name' => 'Site', + 'og_type' => 'Type', + 'language' => 'Langue', + // Commun + 'author' => $isHtml ? 'Auteur' : ($isPdf ? 'Auteur' : 'Auteur EXIF'), + 'date' => $isPdf ? 'Créé le' : ($isHtml ? 'Publié le' : 'Prise de vue'), + 'description' => 'Description', + 'subject' => 'Sujet', + 'keywords' => 'Mots-clés', + 'copyright' => 'Copyright', + // PDF logiciel + 'creator' => 'Créé avec', + 'producer' => 'Produit par', + // HTML liens + 'canonical' => 'URL canonique', + 'og_image' => 'Image OG', +]; + +$hasTitle = !empty($step2Meta['title']); +$preAuthor = $step2Meta['author'] ?? $step2Meta['credit'] ?? ''; +$preSource = $step2Meta['canonical'] ?? $step2Meta['source'] ?? $step2Url; +?> + +
+ ← Retour +

Importer un fichier

+
+ +

+ Article : +

+ + +
+ Le site a bloqué la récupération automatique des métadonnées (protection anti-bot). + Renseignez le titre manuellement ci-dessous. +
+ + + + +
+

Aperçu de la page

+ + Aperçu +
+ + + + !empty($step2Meta[$key]), ARRAY_FILTER_USE_BOTH); +if ($visibleRows): ?> +
+
Métadonnées du fichier
+
+ + + $label): ?> + htmlspecialchars(round($val / 1024) . ' Ko'), + 'width' => htmlspecialchars($val . ' × ' . ($step2Meta['height'] ?? '?') . ' px'), + 'og_image' => '', + 'canonical' => '' . htmlspecialchars((string)$val) . '', + default => htmlspecialchars((string)$val), + }; + ?> + + + + + + +
+
+
+ + + +
+
+
+ + + + + $v !== null && $v !== '' + ); +?> + + + +
+ + + +
+ Titre non trouvé dans les métadonnées — saisie requise. +
+ +
+ + +
+

Mode

+
+ + +
+
+ + +
+ +
+ + +
+ + +
+ + +
> + +
+ + +
+ + +
+ + +
Laissé vide → URL du fichier utilisée comme source.
+
+ + +
+
+ + +
+
+ +
+ +
+ + Annuler +
+
+
+
+ + + + + + <?= htmlspecialchars(($seoTitle ?? '') ?: ($title ?? siteTitle())) ?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + class=""> + +
+ +
+ +
+ +
+ + + + + + + + + + + + diff --git a/templates/legal.php b/templates/legal.php new file mode 100644 index 0000000..ee6f011 --- /dev/null +++ b/templates/legal.php @@ -0,0 +1,10 @@ + +
+ +
+ +
+ +
+ + +
+ +
+
+

+ +

+ +
+ + +
+ + +
+ $_link): + $_btnBg = $_palette[$_i % count($_palette)]; + ?> + + + + + + + +
+ + + + + +
+ + + +
+ ← Retour à l'édition +

Confirmer les modifications

+
+ +
+ + + + + + + + + + + $_ctv): ?> + + + + + + $fname): ?> + + + + + +
+
+ + +
+ +
+ +
Aucune modification détectée.
+ + + +
+

Diff du contenu

+ +
Contenu identique.
+ +
+ − Supprimé + + Ajouté +
+
+ + + + +
+ + + +
+ +
− 
+ +
+ +
  
+ + +
+ +
+ + + +
+ + + +
+ Slug recalculé depuis le nouveau titre. Slug initial : + + +
+ +
+ + +
+ + +
+ +
+ + ← Retour +
+ +
+ + +
+
+
+ SEO — titre, description, image +
+
+
+ + +
+ Idéal : 30–60 car. + 0 / 60 +
+
+ +
+ + +
+ Idéal : 120–155 car. + 0 / 155 +
+
+ +
+
+ +
+
+ Aperçu dans les moteurs de recherche +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Auteur
Publication
Modification (après enreg.)
Languefr-FR
Catégorie—' ?>
og:image + ' . htmlspecialchars($coverFilename) . '' : '— (pas de couverture)' ?> +
+
+
+
+ +
+
+ + + + + + + + + +
+
+
+ +

Modifier l'article

+ + + +

Nouvel article

+ + + + + + +
+
    + +
  • + +
+
+ + +
+ + +
+ + + + +
+ + +
+ + + +
+ +
+ +
+
+ +
+
+ + +
+

Tags

+ $_tagLabel): + $_tagVal = implode(', ', $articleTags[$_tagKey] ?? []); + $_suggestId = 'tag-suggest-create-' . htmlspecialchars($_tagKey); + $_tagVals = $allTagValues[$_tagKey] ?? []; + ?> +
+ + + + + +
+ +
+ + + +
+ + Écris en Markdown — les fichiers uploadés sont référençables dans le contenu : + ![alt](nom-du-fichier.jpg) + +
+ +
+ + +
+ + +
+
+ + +
+
+
+ > + +
+
+
+ +
+ + +
Images → nommées sha256-taille.ext. Vidéos, PDF → nom sanitisé.
+
+ + + +
+ +
+ +
+
+ + ⚡ Suggestions +
+
+ +
+
+ +
+
+ + +
+

Tags

+ $_tagLabel): + $_tagVal = implode(', ', $articleTags[$_tagKey] ?? []); + $_suggestId = 'tag-suggest-' . htmlspecialchars($_tagKey); + $_tagVals = $allTagValues[$_tagKey] ?? []; + ?> +
+
+ + + ⚡ Suggestions + +
+ + + + +
+ +
+ + +
+ + +
+ > + +
+
+ + +
+ + + + +
+ + +
+ + Annuler + +
+ +
+ + + + +
+

Fichiers existants

+
+ $f): ?> + +
+
+ + + + + + + + '🎬', + str_starts_with($f['mime'], 'audio/') => '🎵', + $f['mime'] === 'application/pdf' => '📑', + default => '📄', + } ?> + + + + +
+
+ + Ko + + cover + +
+
+ + + +
+
+ +
+ + +
+ + + ✓ Cover + +
+ + +
+
+
+
+
+ +
+
+
+ + + $f['is_image']); ?> + + +
+

+ Images disponibles + (clic → insère dans le contenu) +

+
+ + + <?= htmlspecialchars($img['name']) ?> + +
+
+ + + + +
+

Liens externes

+
    + + +
  • + + + + + + +
    + + +
    +
  • + +
+
+ + + + +
+
+ + + + $f): ?> +
+ +
+ + + + + +
+
+
+ +
+ +
+
+
+ + + + + + + + + + + + + + + + +
#DateTitre à l'époqueCommentaire
+ + + + + –' ?> + + + Diff + +
+ + +
+
+
+
+ + + + +
+
+ +
+
+
+
+ + +
+ Idéal : 30–60 caractères + 0 / 60 +
+
+ +
+ + +
+ Idéal : 120–155 caractères + 0 / 155 +
+
+ +
+ + +
+
+
+
+ +
+ + Annuler +
+ + + + + +text($post['content'])), 0, $len, '…'); +} + +function _renderCard(array $post, array $privateCats, array $allCats, \Parsedown $pd): void +{ + $postUrl = '/post/' . rawurlencode($post['slug']); + $isDraft = !$post['published']; + $isAvantPremiere = $post['published'] && strtotime((string)($post['published_at'] ?? '')) > time(); + $postCat = trim($post['category'] ?? ''); + $isPrivate = $postCat !== '' && in_array($postCat, $privateCats, true); + $isLocked = $isAvantPremiere && !hasCapability('view_previews'); + $coverStyle = _cardCoverStyle($post, $allCats); + $preview = _cardExcerpt($post, $pd); + ?> +
+ +
Brouillon
+ +
Avant-première
+ +
Privé
+ +
+ + + +
+
+

+ + + + + +

+

+ +
+ + + +
+ + + + + + + + + + +
+

+ Derniers articles + + À la une + +

+ + time() && !hasCapability('view_previews'); + ?> + +
+ + À la une + +
+ +
+ +

+

+
 → lire
+
+
+
+ + +
+ + + +
+ + + + 6): ?> + + +
+ + + + +
+

+ Tendances · 10 derniers jours +

+
+ + + +
+
+ + + + +
+

Récemment mis à jour

+ +
+ + + + +
+

Redécouvertes

+ +
+ + + + + + + + +
+ + + +
+ + + + + + + + isLoggedIn() || !in_array($cat, $privateCats ?? [], true), + ARRAY_FILTER_USE_KEY +); +arsort($_tagCats); +if (!empty($_tagCats)): + $_minCount = min($_tagCats); + $_maxCount = max($_tagCats); + ?> + + + + ++ + + + 'https://schema.org', + '@type' => 'WebSite', + 'name' => siteTitle(), + 'url' => rtrim(APP_URL, '/') . '/', + 'description' => siteClaim(), + 'inLanguage' => siteLang(), + ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); +} + +$mainClass = 'container-fluid'; +include __DIR__ . '/layout.php'; diff --git a/templates/post_view.php b/templates/post_view.php new file mode 100644 index 0000000..b14ef62 --- /dev/null +++ b/templates/post_view.php @@ -0,0 +1,354 @@ + 'a','â' => 'a','ä' => 'a','á' => 'a','é' => 'e','è' => 'e','ê' => 'e','ë' => 'e', + 'î' => 'i','ï' => 'i','í' => 'i','ô' => 'o','ö' => 'o','ó' => 'o','ù' => 'u','û' => 'u', + 'ü' => 'u','ú' => 'u','ç' => 'c','ñ' => 'n','æ' => 'ae','œ' => 'oe', +]; +$_tocItems = []; +$_tocSeen = []; +$_renderedContent = preg_replace_callback( + '/<(h[23])>(.+?)<\/h[23]>/i', + function ($m) use (&$_tocItems, &$_tocSeen, $_accentMap) { + $tag = $m[1]; + $inner = $m[2]; + $level = (int) substr($tag, 1); + $plain = strip_tags($inner); + $slug = trim(preg_replace( + '/[^a-z0-9]+/', + '-', + mb_strtolower(strtr($plain, $_accentMap), 'UTF-8') + ), '-') ?: 'section'; + if (isset($_tocSeen[$slug])) { + $_tocSeen[$slug]++; + $id = $slug . '-' . $_tocSeen[$slug]; + } else { + $_tocSeen[$slug] = 0; + $id = $slug; + } + $_tocItems[] = ['level' => $level, 'text' => $plain, 'id' => $id]; + return "<{$tag} id=\"" . htmlspecialchars($id) . "\">{$inner}"; + }, + $Parsedown->text($rawContent) +); + +ob_start(); + +$coverFile = $article['cover'] ?? ''; +$ogImage = $coverFile !== '' + ? url('file?uuid=' . rawurlencode($article['uuid']) . '&name=' . rawurlencode($coverFile)) + : null; + +$category = trim((string)($article['category'] ?? '')); +$gradient = coverGradient($category !== '' ? $category : $article['uuid'], $allCats ?? []); + +// Pièces jointes (hors fichiers intégrés, thumbs et cover) +$attachments = []; +if ($files) { + $referenced = []; + preg_match_all('/\(\/file\?uuid=[^&]+&name=([^)]+)\)/', $rawContent, $m); + foreach ($m[1] as $encodedName) { + $referenced[rawurldecode($encodedName)] = true; + } + $attachments = array_values(array_filter( + $files, + static fn ($f) => + !isset($referenced[$f['name']]) + && !str_starts_with($f['name'], '_thumb_') + && $f['name'] !== $coverFile + )); +} + +$externalLinks = $article['external_links'] ?? []; +?> +
+ + +
+ +
+ +
Brouillon
+ +
Privé
+ + +
> + + <?= htmlspecialchars($article['title']) ?> + +
+ + +
+ ← Retour + + ✎ Modifier + 🗑 Supprimer + +
+ + +
+
+ + + +

+

+ + + + + + + · + + +

+
+
+ + ℹ Sources + + ['👍', 'Utile'], + 'important' => ['🔥', 'Important'], + 'interesting' => ['🤔', 'À creuser'], + ]; +?> +
+ [$icon, $label]): ?> + +
+ + + + +
+ +
+
+
+ +
+
+
+
+ +
+
+
+ + + + +
+ + + +
+ + + +text($article['content'])); + $plain = preg_replace('/\s+/', ' ', $plain); + $seoDescription = mb_strimwidth(trim((string)$plain), 0, 155, '…'); +} + +// og:image : cover puis fallback og_image du meta +if ($ogImage === null || $ogImage === '') { + $ogImage = $article['og_image'] ?? ''; +} + +// Auteur : nom et URL de profil résolus depuis le champ author du JSON de l'article +$metaAuthor = $authorName; +$metaAuthorUrl = $authorProfileUrl; + +// JSON-LD Article +$jsonLdData = [ + '@context' => 'https://schema.org', + '@type' => 'BlogPosting', + 'headline' => $seoTitle, + 'description' => $seoDescription, + 'url' => $canonical, + 'datePublished' => date('c', strtotime((string)$articlePublishedAt)), + 'dateModified' => date('c', strtotime((string)($article['updated_at'] ?? $articlePublishedAt))), + 'author' => array_filter([ + '@type' => 'Person', + 'name' => $metaAuthor !== '' ? $metaAuthor : siteTitle(), + 'url' => $metaAuthorUrl !== '' ? $metaAuthorUrl : null, + ]), + 'publisher' => [ + '@type' => 'Blog', + 'name' => siteTitle(), + 'url' => rtrim(APP_URL, '/'), + ], + 'inLanguage' => siteLang(), +]; +if (!empty($ogImage)) { + $jsonLdData['image'] = $ogImage; +} +$jsonLd = json_encode($jsonLdData, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + +include __DIR__ . '/layout.php'; diff --git a/templates/profile.php b/templates/profile.php new file mode 100644 index 0000000..778ec69 --- /dev/null +++ b/templates/profile.php @@ -0,0 +1,227 @@ + + +
+
+

Mon profil

+ +
+ + +
Profil mis à jour.
+ + +
+ +
+ + +
+
+
Identité
+
+
+ + +
+ Affiché comme auteur sur vos articles. + +
Page publique : /profil/ + +
+
+
+ + +
+
+
+
+ + +
+
+
Page publique
+
+
+ + +
Affichée sur votre page de profil public.
+
+
+ + +
Lien vers un site ou profil externe (utilisé dans les métadonnées article:author, JSON-LD).
+
+
+
+
+ +
+
+ + + + + +
+

Flux RSS

+
+
+ +
+
    + +
  • +
    +
    +
    +
    +
    + + +
    +
  • + +
+
+ +
+
+
+
Ajouter un flux
+
+
+
+ +
+
+ +
+ +
+
+
+
+
+
+ +prepare( + 'SELECT r.name, r.label, COALESCE(array_agg(rc.capability) FILTER (WHERE rc.capability IS NOT NULL), \'{}\') AS caps + FROM user_roles ur + JOIN roles r ON r.id = ur.role_id + LEFT JOIN role_capabilities rc ON rc.role_id = r.id + WHERE ur.user_email = :email + GROUP BY r.id, r.name, r.label + ORDER BY r.name' + ); + $st->execute([':email' => currentUserEmail()]); + $_profileRoles = $st->fetchAll(PDO::FETCH_ASSOC); +} +if (!empty($_profileRoles)): ?> +
+

Rôles & droits

+
+ $c !== '' + ); + ?> +
+
+
+ + +
+ +
    + +
  • + +
+ +
Aucun droit associé à ce rôle.
+ +
+
+ +
+
+ + + + +
+ + + + + + +

Aucun résultat pour .

+ +

+ résultat 1 ? 's' : '' ?> + pour +

+ + 'Dans le titre', 2 => 'Dans le texte', 3 => 'À peu près']; + $byTier = []; + foreach ($searchResults as $r) { + $byTier[$r['tier'] ?? 3][] = $r; + } + $multiTier = count($byTier) > 1; + ?> + +
+ $tierResults): ?> + +

+ + +
+
+ + + + + + +
+

+ +

+ +

+ +
+ + +
+ + + + +
+ + 'Type MIME', + 'size' => 'Taille originale', + 'pages' => 'Pages', + 'page_size' => 'Format', + 'pdf_version' => 'Version PDF', + 'width' => 'Dimensions', + 'camera' => 'Appareil photo', + 'site_name' => 'Site', + 'og_type' => 'Type OG', + 'language' => 'Langue', + 'date' => 'Date', + 'description' => 'Description', + 'subject' => 'Sujet', + 'keywords' => 'Mots-clés', + 'copyright' => 'Copyright', + 'credit' => 'Crédit', + 'creator' => 'Créé avec', + 'producer' => 'Produit par', + 'canonical' => 'URL canonique', + 'og_image' => 'Image OG', +]; + +function renderMetaCell(string $key, mixed $val, array $row = []): string +{ + return match($key) { + 'size' => htmlspecialchars(number_format((float)$val / 1024, 1)) . ' Ko', + 'width' => htmlspecialchars((string)$val) . ' × ' . htmlspecialchars((string)($row['height'] ?? '?')) . ' px', + 'og_image' => str_starts_with((string)$val, '/') + ? '' + : '' . htmlspecialchars((string)$val) . '', + 'canonical' => '' . htmlspecialchars((string)$val) . '', + default => htmlspecialchars((string)$val), + }; +} +?> + +
+ ← Modifier +

Sources & médias

+
+

+ + +
+

+ Liens & sources externes + +

+ + +

Aucun lien externe enregistré.

+ +
+ +
+
+
+ + + + + +
+ +
+ + +
+ +
+ + + + + +
+ + Auteur : + + + Ajouté le + +
+ + + isset($metaLabels[$k]) && $v !== null && $v !== '' && $k !== 'height', ARRAY_FILTER_USE_BOTH); + ?> + + + + $label): + if (!isset($lMeta[$key]) || $lMeta[$key] === '' || $lMeta[$key] === null) { + continue; + } + ?> + + + + + + +
+ + +
+
+
+
+ +
+ +
+ + !str_starts_with($f['name'], '_thumb_'))); +?> +
+

+ Pièces jointes + +

+ + +

Aucun fichier joint.

+ +
+ +
+
+
+ + + + + + + +
+ '🎬', + str_starts_with($f['mime'], 'audio/') => '🎵', + $f['mime'] === 'application/pdf' => '📑', + default => '📄', + } ?> +
+ + +
+ +
+ + + + + + + + cover + +
+ + +
+ Ko + + Auteur : + + + + Source : + + + + + +
+ + + isset($metaLabels[$k]) && $v !== null && $v !== '' && $k !== 'height', ARRAY_FILTER_USE_BOTH); + ?> + + + + $label): + if (!isset($fExtra[$key]) || $fExtra[$key] === '' || $fExtra[$key] === null) { + continue; + } + ?> + + + + + + +
+ + + + Pas de métadonnées enregistrées. + +
+
+
+
+ +
+ +
+ +