1.2.0 zaczęło się jako porządki, przerodziło w pełny audyt i skończyło jako najbardziej znaczące wydanie notACMS do tej pory. Około 170 poprawek w rdzeniu, obu drzewach szablonów, dokumentacji i JS — z kilkoma celowymi zmianami niekompatybilnymi po drodze.


Kilka miesięcy temu pisałem o uruchomieniu audytu AI na notACMS i znalezieniu 79 bugów. 1.2.0 to kontynuacja: kolejne pełne przejście, tym razem z projektem w lepszym stanie i bogatszym AGENTS.md prowadzącym przegląd. Znalazło się sporo oczywistych rzeczy i kilka nieoczywistych. Oto te warte opisania.

Bug w nginx który po cichu psował formularze

Najbardziej frustrujące znalezisko: formularz kontaktowy który działał perfekcyjnie w każdym środowisku poza produkcją w Dockerze.

Konfiguracja nginx używała lokalizacji z regexem do routowania API kontaktowego:

location ~ ^/[a-z]{2}/api/contact$ {

Tylko że nie działała. W składni nginx {2} w pewnych kontekstach jest traktowane dosłownie — kwantyfikator był dopasowywany jako ciąg znaków, nie jako "dokładnie dwa znaki". Regex nigdy nie pasował, więc POST /pl/api/contact nigdy nie trafiał do PHP. Każda niedomyślna lokalizacja dostawała ciche 404 przy wysłaniu formularza.

Działało w DDEV (który ma własną konfigurację nginx) i lokalnie (gdzie PHP obsługuje routing inaczej). Docker produkcyjny, korzystający z commitowanego nginx.conf.template, był zepsuty od samego początku. Poprawka: jeden znak, zaescapowanie nawiasów klamrowych.

locale-redirect: pomocność szkodliwa

Skrypt locale-redirect.js czyta preferencje językowe przeglądarki i przekierowuje nowych użytkowników na ich lokalizację. Sensowny pomysł, ale logika miała dziurę.

Kiedy ktoś trafia na /pl/ pierwszy raz bez ciasteczka lang — powiedzmy, klikając w link — skrypt czytał navigator.language, znajdował en-US, ustawiał ciasteczko na en i przekierowywał go z dala od strony, na którą celowo wszedł.

Poprawka jest oczywista z perspektywy czasu: jeśli aktualny URL jest już na niedomyślnej lokalizacji, sam URL jest preferencją. Ustaw ciasteczko i stop. Bez przekierowania. Obejmuje to też środowiska HTTP — flaga Secure po cichu traciła ciasteczko w non-HTTPS (domyślna konfiguracja DDEV), uniemożliwiając działanie całego mechanizmu.

Dorzuciłem też zabezpieczenie przed scenariuszem którego wcześniej nie przewidziałem: przestarzałe ciasteczko z lokalizacją która zniknęła z _site.yaml. Bez niego usunięcie języka ze strony wpędzałoby każdego odwiedzającego ze starym ciasteczkiem w nieskończoną pętlę przekierowań na 404.

Excerpty wyszukiwarki i podwójne escapowanie

Przegląd bezpieczeństwa z 1.1.2 znalazł XSS w wyszukiwarce: excerpty z pagefind były wstrzykiwane do innerHTML bez escapowania. Naprawione przez owinięcie w esc().

Tyle że excerpty pagefind zawierają tagi <mark> — tak pagefind zaznacza dopasowania. Poprawka zepsuła wyróżnianie: <mark>term</mark> zamieniło się w dosłowny tekst &lt;mark&gt;term&lt;/mark&gt;. Właściwa odpowiedź: treść pagefind to autorski statyczny HTML, nie dane wejściowe użytkownika. Tagi <mark> są wstrzykiwane przez silnik wyszukiwania na etapie budowania, nie przez odwiedzających. Usuń esc() z excerpta, zostaw wszędzie indziej. Niewidoczne dopóki nie zauważysz że wyróżnienia nie wyróżniają.

Bezpieczeństwo i utwardzenie

ImageMagick działa teraz przez Symfony\Process z tablicami argumentów zamiast ciągów shell — exec() zniknął. Turnstile weryfikuje hostname w odpowiedzi siteverify względem skonfigurowanego base_url, więc tokeny wygenerowane na domenie testowej nie mogą być odtworzone w produkcji. Output JSON-LD jest hex-escapowany — </script> w tytule posta nie może już wyłamać się ze skryptu. Nagłówki bezpieczeństwa nginx (X-Frame-Options, CSP itd.) nie docierały do odpowiedzi /assets/ i /media/add_header na poziomie lokalizacji wyciszał dziedziczone nagłówki serwera.

/llms.txt

Mały dodatek: trasa /llms.txt dołączona do statycznego builda, wylistowująca najnowsze wpisy w formacie czytelnym maszynowo — per lokalizacja, konfigurowalne przez llms_limit w _site.yaml, nadpisywalne per-motyw przez przestrzeń nazw Twig @base. Statyczne strony nie są tradycyjnie łatwe do nawigowania dla modeli językowych — to niskokosztowy sposób na dostarczenie kontekstu komukolwiek (lub czemukolwiek) kto to czyta.

Breaking changes

Kilka zmian wymagających jednolinijkowej migracji w local/src/:

  • ContentItem::directoryKey() zwraca teraz pełną ścieżkę treści (pages/about nie about), naprawiając ciche kolizje URL między katalogami o tej samej nazwie w różnych sekcjach
  • getTree() przeniesione do ContentTreeProviderInterface — z ContentServiceInterface
  • structured_data().blogPosting() przyjmuje nazwaną mapę (było 13 pozycyjnych argumentów)
  • Klucz kontekstu lang_switch_url usunięty — użyj lang_switch
  • docs/customization/old-template/ usunięty z repo — pobierz z taga v1.1.x jeśli nadal potrzebujesz

Pełny przewodnik migracji: UPGRADE-1.2.md.

Linki

Pełny changelog: CHANGELOG.md.

Repozytorium: GitHub / holas1337/notACMS — Apache 2.0.