Blogrys

Comecon

W 1949 r. Stalin, zniechęcony demokratycznymi wymogami Planu Marshalla oraz organizacjnym piekiełkiem Banku Światowego i Międzynarodowego Funduszu Walutowego, stworzył z niewielkim udziałem środkowoeuropejskich towarzyszy Radę Wzajemnej Pomocy Gospodarczej, czyli Comecon.

W 2023 r. ja, zniechęcony kapitalistycznymi wymogami Wordpressa i obiektowym piekiełkiem popularnych bibliotek i frameworków, stowrzyłem z niewielkim udziałem chatbota Comecon, czyli Receptor Wtrąceń, Przypuszczeń i Glos. Idąc za przykładem Stalina nie skorzystałem ani z Reacta, ani z Node’a.

Odrzućmy historyczne paralele i akronimowe zabawy. Mówiąc po ludzku, to znaczy po informatycznemu: Skleciłem w PHP fulficzerowy system komentarzy.

Najnowsze wcielenie Blogrysa jest blogiem statycznym, czyli HTML-owym. Blogerzy statyczni wzbraniają się przed PHP jak diabeł przed wodą święconą, tak jakby dodanie kilku pehapowych skryptów zamieniało Jekylla w pokracznego braciszka Blogspota. Jednocześnie marzą o funkcji komentowania uciekając się do pomysłowych, choć zagmatwanych rozwiązań. Ja zaś jestem pragmatykiem. Opanowałem podstawy PHP w stopniu wystarczającym do wyrzeźbienia Comeconu, który:

  • zapisuje i wyświetla komentarze pod notkami (wiadomo);
  • pozwala komentującym na dodanie odnośnika do zewnętrznej, zapewne własnej, witryny internetowej;
  • informuje drogą mailową o nowych komentarzach oferując możliwość wymiksowania się w dowolnej chwili z listy subskrybentów;
  • pozwala na edycję lub skasowanie komentarza w ciągu 20 minut po jego umieszczeniu;
  • broni się przed botami prostą kapczą wykorzystującą homoglify i typoglikemię;
  • wyłącza rzeczoną kapczę zarejestrowanym użytkownikom, którzy dostają ponadto chłodny kolorek, możliwość podłączenia na stałe odnośnika pod nick oraz uruchomienia domyślnych powiadomień o nowych komciach.

Comecon robi więc wszystko to, czego wymagamy od przyzwoitego systemu blogowych komentarzy. Działa przy tym błyskawicznie dzięki swojej prostej, może nawet prostackiej, opartej na czystym PHP konstrukcji. Czystym – czyli nie wykorzystującym w najmniejszym stopniu JavaScriptu, ani do dodawania, ani do wyświetlania komciów1. Poza tym – nie podłączonym pod żadną bazę daną. Tak jest, dobrze przeczytaliście: Komentarze, subskrybenci i użytkownicy zapisywani są w płaskich plikach tekstowych. Nie ma to jak zmyślne chałupnictwo. Binarni relacyjni smakosze muszą mi wybaczyć, SQL-a nie zdążyłem się jeszcze nauczyć.

Bonusowe funkcje, nie związane z komentowaniem, ale także napisane w czystym PHP – które dla uproszczenia również włożyłem do worka z napisem Comecon – to:

  • blogowa wyszukiwarka;
  • mailowa subskrybcja blogu (alternatywa dla RSS-a);
  • przekierowanie na losowy wpis;
  • wyświetlanie losowego aforyzmu na dole strony głównej.

Comecon każdy zainteresowany może zgitować lub zassać i wykorzystywać bądź rozwijać go według własnego widzimisię na mocy licencji CC BY-NC-SA 4.0.

Dołączam do niego techniczną instrukcję tłumaczącą, jak należy podczepić system pod własny (statyczny) blog – znajomość PHP nie jest wymagana.

Jeżeli ktoś ma propozycje, jak Comecon można by ulepszyć – albo jeżeli chce wyjaśnić mi, dlaczego mój kod w gruncie rzeczy posysa (piszę bez ironii, ponieważ nie wykluczam, że skrypty posiadają poważne strukturalne wady, których nawet dostrzec nie potrafię) – niech napisze w komentarzach albo wyśle mi maila albo zrobi pull requesta. Tylko proszę pamiętać, że Comecon z założenia jest waniliowy. Proszę też nie sugerować, żebym przeorientował go obiektowo!

Oto uogólniony opis działania komciolubnej maszyny:

  • Ustawienia, ścieżki katalogów, zahaszowane hasła itd. znajdują się w settings.php, który wgrywamy na serwer poza publiczną część witryny.
  • W tym samym niepublicznym miejscu znajduje się także vip.php, który zawiera informacje o zarejestrowanych użytkownikach.
  • Komentarze dodawane są oczywiście poprzez HTML-owy formularz sprzężony z save_comment.php, główny elementem Comeconu. save_comment.php waliduje pola, oczyszcza je z niebezpiecznych znaków specjalnych, sprawdza kapczę, rozpoznaje zarejestrowanych użytkowników, dodaje datę i czas, parsuje minimalistyczny wariant Markdownu, prasuje koszule, rejestruje mailową subskrybcję i zapisuje komentarz w odpowiednim pliku tekstowym na serwerze.
  • Hodowla prenumeratorów przebiega następująco: w momencie dodania przez kogoś komentarza odpowiednia pętla przelatuje przez wszystkie linijki subskrybcyjnego pliku tekstowego – i email_sending.php wysyła treściwe wiadomości przy pomocy osobnego modułu.
  • unsubscribe.php usuwa subskrybenta z listy przyjmując z internetu GET-a. Subskrybent identyfikowany jest poprzez adres mail i klucz. Skąd nadawca GET-a zna swój klucz? Nie musi się wcale nim przejmować. Klucz został wygenerowany losowo w momencie dodania prenumeratora. Potem, za każdym razem gdy system wysyła mail z powiadomieniem, generuje zarazem link do zrezygnowania z subskrybcji – sformatowany jako GET dla unsubscribe.php – który tenże klucz zawiera. Odnośnik zostaje umieszczony w stopce maila. Wystarczy kliknąć.
  • email_notification.php służy webmasterowi do rozesłania powiadomień o nowej notce przy pomocy „tajnego” URL-a. Tajność polega na tym, że mowa o kolejnym GET-owym adresie zabezpieczonym hasłem.
  • Napociłem się przy edit_comment.php, ponieważ przez dłuższą chwilę nie mogłem ogarnąć, jak pogodzić ze sobą dwa wymogi: Wyłącznie autor komentarza może go zmienić lub skasować2, ale wolno mu zrobić to tylko przez X minut po dokonaniu wpisu (X = 20). Teraz logika kodu łącząca oba kryteria wydaje się oczywista, ale na początku wcale taka nie była. Poza tym musiałem nauczyć się tworzyć i odczytywać ciasteczka, aby komentującemu przez 20 minut wyświetlał się na stronie link do edycji. Ciasteczka kosztowały mnie kilka siwych włosów, gdyż nie zdawałem sobie sprawy, że w ich tytule nie mogą znajdować się łączniki, które przechodzą wszak w nazwie plików w dowolnym systemie operacyjny. Wyskakiwał więc bug, którego kompletnie nie rozumiałem3.
  • Wyszukiwarka blogowa działa łopatologicznie: search.php przetrząsa katalog z notkami w formacie tekstowym tropiąc zadaną frazę. Blog liczy ponad 800 wpisów, więc i plików do przeszukania mamy ponad 800. Myślałem, że takie brutalne rozwiązanie będzie irytująco wolne. Okazało się przyjemnie szybkie.
  • random_post.php losuje linijkę z pliku tekstowego zawierającego adresy wszystkich notek, zaś random_quote.php losuje linijkę z pliku tekstowego mieszczącego moją kolekcją aforyzmów.

Comeconowi przydałoby się kilka ulepszeń, ale w najbliższej przyszłości nie planuję się za nie zabierać (choć spamerskie okoliczności mogą zmusić mnie do zrealizowania ostatniej poprawki z listy):

  1. Wypadałoby zmienić metodę przechowywania nicków, haseł i danych zarejestrowanych użytkowników. Obecnie wrzucam ich do tabeli w osobnym pliku PHP. Hasła nie są nawet zahaszowane. Tabelę uaktualniam w razie potrzeby ręcznie, więc przydałby się też system automatycznej rejestracji. Na razie nie przewiduję jednak, żeby nowi komentatorzy walili na Blogrys drzwiami i oknami.
  2. Serce Comeconu, save_comment.php, jest w miarę schludny (czytelne nazwy zmiennych to moja specjalność), niemniej ciągnie się na 130 linijek i przydałby mu się refactoring — tym bardziej, że jego algorytm częściowo zazębia się z add_subscriber.php.
  3. Rozsyłanie powiadomień o nowych notkach to pięta achillesowa Comeconu. URL z GET-em wymagający parametru-hasła? Hmmm… Nie miałem niestety innego pomysłu, a chodziło o to, żebym mógł rozsyłać powiadomienia automatycznie w momencie publikacji blogu. Skrypt aktualizujący Blogrysa curluje po prostu na sam koniec ów „magiczny” URL, a podłączony podeń skrypt PHP generuje i wysyła wszystkie maile samodzielnie.
  4. Captcha wykorzystywana przez Comecon jest tyleż pomysłowa co prostacka. Jeżeli boty przewiercą postawiony przeze mnie mur obronny, będę musiał w trybie pilnym wymyślić coś bardziej różnorodnego.
  1. Okej, to malutkie kłamstewko: JavaScript przyczepiony jest do przycisku formularza uniemożliwiając użytkownikowi kliknięcie go kilka razy pod rząd, co mogłoby skonsternować skrypt PHP odbierający komentarz. I owszem, w swojej pierwotnej wersji, przez kilka miesięcy Comecon wyświetlał komentarze przy pomocy JS. Obecnie pehap nie tylko dodaje, ale i pokazuje komentarze – i tak już pozostanie, na pohybel asynkom

  2. Oczywiście, że zostawiłem sobie administracyjny backdoor. 

  3. Inna sprawa, że zamiast napinać mózgównicę powinienem od razu wczytać się w serwerowe logi z błędami. 






Komentarze

a1 (2024-04-27 07:45:16)

https://www.youtube.com/watch?v=ObxIosyyi4g

SpeX (2024-04-27 22:59:22)

Czyli dla ewangelistów Jekyllrb, ten system komentarzy będzie nie przydatny. Bo w końcu nie uruchomisz go na "darmowym" gitsite czy inny rozwiązaniu hostującym tylko statyczne pliki bez żadnego engine skryptowego, po stronie serwera.

SpeX (2024-04-27 23:03:41)

A co do samego systemu komentarzy:
- czyli najlepiej na razie ze względów bezpieczeństwa nie rejestrować sobie nicka (bo przechowujesz w otwartym tekście)
- subskrypcja jest obligatoryjna, jeśli "klasycznym" zwyczajem chcesz pozostawić mail do siebie
- względem funkcjonalności zwykłego WP to brakuje np. feedu RSS do ostatnich komentarzy (nie korzystam) i feedu do komentarzy w konkretnym wpisie (z tego dość mocno korzystam),
- nie integrujesz się z systemami typu "grawatar",
- i nie wiem czemu, ale nie korzystasz też z cookis, więc za każdym razem muszę wypełniać formularz.


OK, tyle wyczekałem się na ten wpisy, więc mogłem się w końcu wynarzekać.

Cichy (2024-04-28 19:12:43)

Kilka uwag o dobrych praktykach:
1) Wszystko, co nie musi być publiczne, powinno być prywatne - najlepiej, żeby publiczny był tylko jeden plik PHP, który robi include czego trzeba z katalogu private. W szczególności plik z "bazą danych" nie powinien być publiczny, bo jak urośnie, to boty mogą łatwo natłuc sporo transferu.
2) Pomysł trzymania bazy w pliku txt jest słaby z wielu względów - jeśli koniecznie chcesz uniknąć korzystania z prawdziwej bazy danych, to lepiej chociaż użyć sqlite czy czegoś podobnego.
3) Osobny plik dla każdej akcji to zło, bo jakakolwiek przeróbka routingu oznacza konieczność powtarzania zmian we wszystkich tych plikach. Cały ruch powinien iść przez jeden plik, który podpina co trzeba.
4) Zamiast konstrukcji "if (warunek) { ... } else return false" lepiej robić "if (not warunek) { return false; }" i potem ciąg dalszy - unikniesz w ten sposób mnożenia zagnieżdżeń i kaskady else-ów na końcu.
5) Zmienne globalne to zło najgorsze - nie zapraszam do dyskusji, bo nie ma o czym. Nawet jeśli nie da się ich uniknąć, to trzeba przynajmniej ograniczyć ich liczbę do minimum - plik settings.php powinien zawierać jedną tablicę asocjacyjną, tak jak vip.php, a nie fafnaście zmiennych, które aż się proszą o przypadkową kolizję nazw, natomiast exit_messages.php powinien być zbiorem stałych.
6) PHPMailer, skoro jest wymagany, powinien być podpięty za pomocą composera.

Borys (2024-04-28 20:51:55)

@Spex
Dzięki za ciekawe propozycje!

Ad ewangeliści Jekylla na GitHubie: Nie, absolutnie, moje rozwiązanie ma łączyć prostotę i funkcjonalność (i amatorskie pehapowanie, jak wykazał Cichy, ale o tym za chwilę :)). Jeżeli ktoś uważa, że PHP jest sprzeczne z ideą statycznego blogowania, musi sięgnąć np. po Staticmana. Według mnie taka kombinacja jest udziwniona, ale każdemu jego regeneracja plików HTML.

Ad haszowanie haseł użytkowników: Po Twojej uwadze je raz-dwa zahaszowałem, stwierdziwszy, że moja początkowa decyzja o niehaszowaniu akurat ich była bezsensowna. Hasła w drodze do `vip.php` przechodzą jednak przez moje haszujące ręce, więc zalecam wybranie hasła w rodzaju „kot8”, żeby za cenę wstukania kilka znaków wyeliminować sobie kapczę i móc automatycznie wklejać własną stronę internetową.

Ad mail do siebie: Ale po co chciałbyś zostawiać mail do siebie? Jako swoją wizytówkę? No tak, mógłbym zrobić, żeby na życzenie komentatora jego mail się wyświetlał jako część podpisu, ale przecież inne systemy komentowania, np. ten WordPressowy, nie pokazują maili osób zostawiających komcia. Maila używa się bodajże tylko jako identyfikatora...?

Ad feed do najnowszych komentarzy i komentarzy pod wpisem: Ciekawa praca domowa! Zastanawiałem się, jak to najłatwiej zrealizować, mam pewien pomysł i spróbuję go niezadługo wprowadzić w życie.

Ad Gravatar: Przejrzałem ich API, sprawia wrażenie developer-friendly. Też pomyślę niebawem o podczepieniu. ALE gravatary nie są zabezpieczone hasłem, więc każda osoba znająca Twój mail będzie mogła się pod Ciebie podszyć. Natomiast mój prostacki system „fałszywe komentowanie” w teorii uniemożliwia. Grawatarowanie służyłoby więc w zasadzie tylko jako dodatek do awatarków, co samo w sobie jest, owszem, przyjemnym usprawnieniem.

Ad cookies: Tej uwagi nie rozumiem.

Borys (2024-04-28 21:13:56)

@Cichy
Bardzo dziękuję, że chciało Ci się przejrzeć kod i skomentować strukturę.

Ad publiczne vs prywatne: Kapuję. Rozumiem, że takie zalecenie podyktowane jest faktem, iż prawdopodobieństwo awarii serweru i pokazywania przez kilka godzin kodu PHP w przeglądarce jest zdecydowanie większe od awarii, w której katalog niepubliczny na serwerze nagle robi się publiczny?

Ad publiczna kopia pliku ze wszystkimi komentarzami: Tak, słuszna uwaga. Nie pomyślałem o tym. Ta publiczna kopia jest mi potrzebna dlatego, że w momencie publikacji nowej notki mój lokalny skrypcik bashowy robi wget, sort, uniq, cut i uaktualnia liczbę komentarzy w spisach treści. Gdy urośnie, będę musiał pomyśleć nad jakimś usprawnieniem, chociaż w tej chwili nic rozsądnego mi do głowy nie przychodzi.

Ad baza danych: Moja wiedza na temat SQL-a itp. jest w tej chwili absolutnie zerowa. Nie znam składni i nie wiem, jak spina się PHP z prawdziwą bazą danych. Domyślam się, że nie jest to trudne; prędzej czy później się poduczę i będę w stanie Comecona pod tym kątem przerobić. Ale ryzykując posądzenie o bycie przekornym: Dlaczego w takim bądź co bądź prostym systemie baza danych miałaby być lepsza od płaskich plików tekstowych? Do głowy przychodzi mi tylko kwestia kolejkowania komentarzy w sytuacji, gdy ich częstotliwość wzrośnie (chociaż aż tak nigdy nie wzrośnie :)). Czy są jakieś inne powody?

Ad „osobny plik dla każdej akcji”: Rozwiń, proszę, bo nie kapuję.

Ad konstrukcje warunkowe, zmienne globalne, composer: Dzięki, cenne rady, dodałem do TODO.

Cichy (2024-04-28 23:09:07)

Ad publiczne vs prywatne: ewentualna widoczność kodu nie jest w tym przypadku problemem, skoro i tak go udostępniłeś - chodzi generalnie o to, że do prywatnego katalogu trudniej się dobrać, trudniej wykorzystać jakieś bugi do zmodyfikowania jego zawartości albo uruchomić schowany tam plik w nieprawomyślny sposób. Oczywiście wiele zależy od konfiguracji serwera i od samego kodu, ale jeszcze nikt nigdy nie pożałował, że dał potencjalnym szkodnikom za małe uprawnienia.

Ad baza danych: narzędzie, którego używają rzesze programistów w warunkach bojowych od wielu lat, jest na pewno lepiej przetestowane i bardziej kuloodporne niż własny kod, który żadnych ostrych testów nie przeszedł, więc nie masz gwarancji, że pewnego dnia jakiś bug nie spowoduje wyczyszczenia albo zepsucia pliku z komentarzami. Relacyjność danych też na różne sposoby ułatwia życie - w tak prostym przypadku jak ten, może i zdołasz się bez niej obejść, ale o jakiejkolwiek większej rozbudowie systemu nie będzie mowy.

Ad „osobny plik dla każdej akcji”: chodzi o to, że zamiast URLi wskazujących bezpośrednio na konkretne pliki (np. blogrys.pl/assets/save_comment.php), lepiej mieć jeden plik obsługujący cały routing (np. blogrys.pl/assets/index.php?action=save_comment - a z pomocą ustawień w .htaccess możesz sprawić, że URLe typu blogrys.pl/assets/comment/save będą kierowane na index.php). Dzięki temu, że każdy request przechodzi przez jedno miejsce, dużo łatwiej jest debugować i modyfikować działanie systemu.

SpeX (2024-04-29 00:37:25)

Ad haszowanie haseł użytkowników:
W sumie to mi się jeszcze nigdy tu Capcha nie pojawiła, albo już jestem do nich tak przyzwyczajony, iż nie zwracam na nie uwagi.

Na początku, nie wiedziałem o co ci chodzi z tym automatycznym adresem strony, ale domyślam się. Iż w takim przypadku pobiera url z bazy danych, i nie muszę go za każdym razem dodawać.

Ad mail do siebie:
W sumie to fakt, chyba z przyzwyczajenie iż wordpress tego wymaga. Stało to się dla mnie "naturalne", iż przy komentowaniu trzeba podać mail, a nie koniecznie by dostać subskrypcje odpowiedzi.

PS. A da się "prenumerować" bez pozostawienie, pierwszego komentarza?

Ad cookies:
Chodzi, by plik cookies w przeglądarce pamiętał moje dane z formularza, jak podpis, (nie koniecznie hasło), url i mail.

Borys (2024-04-29 10:29:44)

@Cichy:
Okej, dzięki raz jeszcze, teraz wszystko zrozumiałem. O tej ostatniej sztuczce z routowaniem przez .htaccess nie miałem bladego pojęcia.


@SpeX:
Kapcza pojawia Ci się przecież na dole; za każdym razem musisz wstukać pewną datę. :)

Żeby zaprenumerować komentarze bez zostawiania własnego musisz wykorzystać następujący hack: Dodaj jednoznakowy „taktyczny” komentarz podpisując go adresem mail, a potem od razu skasuj. Twój mail pozostanie w bazie subskrybentów postu.

Co do pamięci formularza, to po raz kolejny uwidacznia się moja ignorancja, bo ja zawsze sądziłem, że przeglądarka kojarzy takie rzeczy sama rozpoznając adres odwiedzonej strony i wykorzystane już kiedyś pola. Tak czy owak, więcej ciasteczek nie będę dodawał, bo chcę ścinać takie „inwazyjne” elementy do minimum. Swoją drogą, wypróbuj program Autokey (Linux) albo Autohotkey (Win). Możesz tam podczepiać dowolne skróty i skrypty pod skróty klawiaturowe. Ja np. wklejam zawsze adres swojej strony kombinacją CTRL-F2.