Files
gitea 338ec24e90
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m20s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Successful in 9m26s
Testy / Testy astrodemo (pull_request) Successful in 9m25s
Testy / Testy astroklient (pull_request) Successful in 9m25s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 6s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 5s
build / build (push) Successful in 12s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m21s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m25s
Testy / Testy astrodemo (push) Successful in 9m25s
Testy / Testy astroklient (push) Successful in 9m25s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 6s
Testy / Kontrola składni wszystkich warstw (push) Successful in 4s
astroklient: własna nazwa produktu zamiast odziedziczonej po rdzeniu
Astroklient przedstawiał się na stronie głównej jako „astrololo" — w tytule
karty i w nagłówku. To najpoważniejszy z możliwych śladów: mówi klientowi
wprost, że trzyma okrojoną wersję czegoś większego, i podaje frazę, po której da
się wyszukać, czego mu nie dano. Cała reszta izolacji przestaje mieć znaczenie,
gdy produkt sam się przedstawia nazwą rodzica.

Wzięło się stąd, że nazwa była wpisana na sztywno w szablony, a szablony są
WSPÓLNE — dokładnie ta sama przyczyna, co przy zaszytym w base.html warunku na
administratora. Nazwa mieszka teraz w module `produkt.py`, który nakładka
produktu nadpisuje; szablony o nią pytają, zamiast ją znać.

astrodemo NIE było dotknięte: ma własne kopie szablonów i pokazuje „astrodemo".

DRUGIE ŹRÓDŁO WYCIEKU, mniej oczywiste: nazwa siedziała też w identyfikatorach
JavaScriptu, które widzi każdy, kto otworzy narzędzia deweloperskie — zdarzenie
`astrololo:coords` i klucz `astrololo.form.v1` w localStorage. Przemianowane na
neutralne `kosmogram:*`; te identyfikatory nie mają powodu nosić nazwy rodziny
produktów. SKUTEK UBOCZNY: zmiana klucza w localStorage kasuje raz zapamiętane
wersje robocze formularza.

Sprawdzone, że NIE wyciekają: nagłówki X-Astrololo-* jadą wyłącznie między
warstwami, a dokumentacja OpenAPI (gdzie zostaje tytuł aplikacji FastAPI) oddaje
404 także zalogowanemu.

TEST rozdziela to, co widzi człowiek, od identyfikatorów wewnętrznych. Nazwa
rdzenia wolno występować w nazwach loggerów, materiale do wyprowadzania kluczy
i nagłówkach międzywarstwowych; nie wolno w szablonach, zasobach ani w treści
odpowiedzi. Zakaz na wszystko byłby albo obejściem przez zmianę nazw kluczy
kryptograficznych, albo martwą regułą wyłączaną przy pierwszej okazji.

Sprawdzony w obie strony: po cofnięciu nakładki test pada.

Testy: astroklient 15, presentation 368, astrodemo 28, logic 342, data 42,
render 41.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 16:37:03 +02:00

129 lines
5.1 KiB
JavaScript

// Wspólne dane formularza między zakładkami (PRE-21).
//
// Problem: każdą zakładkę (Horoskop / Interpretacje / Kalendarz) wypełniało się
// od nowa — te same imię, data, godzina, strefa i miejsce. Teraz raz wpisane
// dane wędrują za użytkownikiem.
//
// Dlaczego localStorage, a nie sesja na serwerze: serwer zostaje BEZSTANOWY
// (żadnego magazynu sesji, żadnych danych urodzeniowych trzymanych po stronie
// usługi), a przy okazji dane synchronizują się też między osobnymi kartami
// przeglądarki — zdarzenie `storage` daje to za darmo.
//
// UWAGA na kolejność: ten skrypt ładujemy w <head> z `defer`, więc wykonuje się
// PO sparsowaniu dokumentu, ale PRZED geo.js (skrypty defer idą w kolejności
// dokumentu). Dzięki temu mapa startuje już z odtworzonymi współrzędnymi i nie
// trzeba jej potem przestawiać.
(function () {
'use strict';
var KEY = 'kosmogram.form.v1';
// Pola wspólne dla zakładek. Nazwa pola formularza -> jak je znaleźć.
// `geoSearch` (nazwa miejsca) nie ma atrybutu name — szukamy po id.
// type: brak = zwykła wartość (.value); 'check' = pojedynczy checkbox (.checked);
// 'multi' = kilka checkboxów o tej samej nazwie (lista zaznaczonych wartości).
// Opcje (stacje, tabele, porównanie domów) są tu, żeby wybór z „Horoskopu"
// wędrował na kolejny ekran — inaczej podsumowanie liczyłoby się bez nich.
var FIELDS = [
{ key: 'person', sel: 'input[name=person]' },
{ key: 'date', sel: 'input[name=date]' },
{ key: 'time', sel: 'input[name=time]' },
{ key: 'tz_offset', sel: 'input[name=tz_offset]' },
{ key: 'lat', sel: 'input[name=lat]' },
{ key: 'lon', sel: 'input[name=lon]' },
{ key: 'house_system', sel: 'select[name=house_system]' },
{ key: 'zodiac', sel: 'select[name=zodiac]' },
{ key: 'place', sel: '#geoSearch' },
{ key: 'stations', sel: 'input[name=stations]', type: 'check' },
{ key: 'tables', sel: 'input[name=tables]', type: 'check' },
{ key: 'house_systems', name: 'house_systems', type: 'multi' },
{ key: 'aspect_orb', sel: 'input[name=aspect_orb]' },
{ key: 'aspect_luminary_bonus', sel: 'input[name=aspect_luminary_bonus]' },
{ key: 'aspect_minor', sel: 'input[name=aspect_minor]', type: 'check' }
];
function elementsOf(f) {
if (f.type === 'multi') return Array.prototype.slice.call(document.querySelectorAll('[name=' + f.name + ']'));
var el = document.querySelector(f.sel);
return el ? [el] : [];
}
function load() {
try {
return JSON.parse(localStorage.getItem(KEY) || '{}') || {};
} catch (e) {
return {}; // uszkodzony wpis nie może zablokować strony
}
}
function save(state) {
try {
localStorage.setItem(KEY, JSON.stringify(state));
} catch (e) {
/* tryb prywatny albo brak miejsca — trudno, formularz ma działać dalej */
}
}
function apply(state) {
FIELDS.forEach(function (f) {
var els = elementsOf(f);
if (!els.length) return;
var v = state[f.key];
if (v === undefined || v === null) return;
if (f.type === 'multi') {
if (!Array.isArray(v)) return;
els.forEach(function (el) { el.checked = v.indexOf(el.value) !== -1; });
} else if (f.type === 'check') {
els[0].checked = !!v;
} else {
if (v === '') return;
if (els[0].value !== v) els[0].value = v;
}
});
}
function collect() {
var state = load();
FIELDS.forEach(function (f) {
var els = elementsOf(f);
if (!els.length) return; // pola nieobecnego na tej zakładce nie kasujemy
if (f.type === 'multi') {
state[f.key] = els.filter(function (el) { return el.checked; }).map(function (el) { return el.value; });
} else if (f.type === 'check') {
state[f.key] = els[0].checked;
} else {
if (els[0].value === '' || els[0].value === null) return;
state[f.key] = els[0].value;
}
});
return state;
}
// 1) odtwórz to, co użytkownik wpisał gdzie indziej
apply(load());
// 2) zapisuj każdą zmianę (input dla wpisywania, change dla list, checkboxów, kalendarzy)
FIELDS.forEach(function (f) {
elementsOf(f).forEach(function (el) {
['input', 'change'].forEach(function (ev) {
el.addEventListener(ev, function () { save(collect()); });
});
});
});
// 3) zapisz stan po wysłaniu formularza — po przeładowaniu strony pola mają
// pokazywać to, co faktycznie policzyliśmy
document.addEventListener('submit', function () { save(collect()); }, true);
// 4) druga otwarta karta zmieniła dane -> dociągnij je tutaj na żywo
window.addEventListener('storage', function (e) {
if (e.key === KEY) apply(load());
});
// 5) geo.js ustawia lat/lon z mapy poza polami formularza — po takiej zmianie
// też chcemy zapamiętać (zdarzenie leci z „Tu i teraz" oraz z mapy)
document.addEventListener('kosmogram:coords', function () {
setTimeout(function () { save(collect()); }, 0);
});
})();