Aktualizacja nginx do 1.8.0 i problem z PHP

Dzisiaj zespół dotdeb.org wypuścił nginxa 1.8.0 na Debiana, a ja niezwłocznie dokonałem aktualizacji z wersji 1.6.3. Początkową radość szybko zastąpiła panika, kiedy okazało się, że wszystkie strony oparte o PHP przestały działać, a konkretnie zaczęły zwracać pustą treść. W logach nginxa, PHP-FPM ani nigdzie indziej nie znalazłem błędów, w konfiguracji nginxa zmiany były tylko kosmetyczne, konfiguracja PHP w ogóle nie była ruszana, po prostu czeski film.

Rozwiązanie okazało się trywialne: w pliku /etc/nginx/fastcgi_params należy dodać linijkę:

fastcgi_param  SCRIPT_FILENAME    $document_root$fastcgi_script_name;

Potem już tylko przeładowanie konfiguracji nginxa i wszystko powinno działać.

Tytułem uzupełnienia dodam, że na właściwy trop trafiłem, kiedy spróbowałem połaczyć się z PHP-FPM bezpośrednio przez FastCGI. Wedle przepisu z tej strony w konsoli zapodałem:

SCRIPT_NAME=/index.php \
SCRIPT_FILENAME=/index.php \
REQUEST_METHOD=GET \
DOCUMENT_ROOT=/var/www/anime.com.pl/ \
cgi-fcgi -bind -connect /var/run/php5-fpm.sock

W odpowiedzi otrzymałem:

Primary script unknown
Status: 404 Not Found
Content-type: text/html

File not found.

Dalej już było z górki. Ciekawe tylko dlaczego to się nigdzie nie zalogowało?

Jako lekturę uzupełniającą polecam wpis z bloga Martina Fjordvalda, który tłumaczy czym różni się plik fastcgi_params od fastcgi.conf i skąd się ta różnica wzięła.

Wyłączanie filtrów w Doctrine2

Filtry w Doctrine to poniekąd pożyteczna funkcja, na filtrach jest oparte m.in miękkie kasowanie (ang. soft delete), które można podpiąć z paczki gedmo/doctrine-extensions. Filtry przeważnie są włączane na stałe w konfiguracji ORM-a w pliku config.yml, ale w razie potrzeby można je włączać i wyłączać w kodzie. Obiekt FilterCollection pobieramy z entity managera, a na nim można wykonać m.in. poniższe metody:

// sprawdzenie czy filtr jest zainstalowany
$em->getFilters()->has("soft-deleteable");

// sprawdzenie czy filtr jest włączony
$em->getFilters()->isEnabled("soft-deleteable");

// włączenie filtra
$em->getFilters()->enable("soft-deleteable");

// wyłączenie filtra
$em->getFilters()->disable("soft-deleteable");

Cała ta notka wzięła się stąd, że dłużej niż powinienem szukałem błędu w kodzie, a jego źródłem było właśnie miękkie kasowanie. Zapytanie, które powinno zwracać encję zwracało NULL, a taki przypadek nie był obsłużony w kodzie.

Roundcube przez HTTPS czyli nginx i self-signed certificate

Powinienem to zrobić dawno temu, ale dopiero teraz zabrałem się za zabezpieczenie SSL-em mojego webmaila opartego na Roundcube. Ponieważ poza mną nikt z niego nie korzysta, a sam sobie zazwyczaj ufam, to zdecydowałem się nie korzystać z żadnego centrum certyfikacji i wygenerować certyfikat własnoręcznie.

W sieci nie brakuje instrukcji generowania certyfikatów, ja staram się robić wszystko po debianowemu, więc kierowałem się opisem z debianowej wiki. Dlatego certyfikat wylądował w katalogu /etc/ssl/localcerts, który musiałem stworzyć:

mkdir -p /etc/ssl/localcerts

Polecenie generowania klucza i certyfikatu:

openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/ssl/localcerts/mail.komitywa.net.key -out /etc/ssl/localcerts/mail.komitywa.net.crt

Przed wygenerowaniem certyfikatu zostaniemy zapytani o dane certyfikowanego podmiotu:

You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:PL
State or Province Name (full name) [Some-State]:Mazowieckie
Locality Name (eg, city) []:Warsaw
Organization Name (eg, company) [Internet Widgits Pty Ltd]:komitywa.net
Organizational Unit Name (eg, section) []:.
Common Name (eg, YOUR name) []:mail.komitywa.net
Email Address []:moj-adres-email@komitywa.net

Na koniec warto ograniczyć dostęp do wygenerowanych plików:

chmod 600 /etc/ssl/localcerts/mail.komitywa.net.*

Teraz można już SSL do konfiguracji nginx’a. Do działającego wirtualnego hosta dodałem:

listen 443 ssl;
ssl_certificate /etc/ssl/localcerts/mail.komitywa.net.crt;
ssl_certificate_key /etc/ssl/localcerts/mail.komitywa.net.key;

Po przeładowaniu nginx’a i przetestowaniu, że połaczenie po HTTPS działa postanowiłem jeszcze dodać przekierowanie z HTTP na HTTPS:

server {
       listen 80;
       server_name mail.komitywa.net;
       return 301 https://mail.komitywa.net$request_uri;
}

Wygenerowany certyfikat jest ważny przez rok, więc w kwietniu 2016 będę musiał wygenerować nowy certyfikat.

WHERE warunek vs LEFT JOIN warunek

Być może nie powinienem publicznie przyznawać się, że popełniłem tak podstawowy błąd w konstruowaniu zapytania SQL, ale po jego odkryciu szczerze się uśmiałem, a skoro śmiech to zdrowie to się nim podzielę.

W uproszczeniu: mam tabelkę z danymi klientów (client) i tabelkę produktów (product), pomiędzy którymi istnieje relacja jeden do wielu. Wyciąganie danych do pokazania jest zrealizowane w Doctrine w następujący sposób:

$qb
    ->select('customer, product')
    ->from($this->getEntityName(), 'customer')
    ->leftJoin('customer.products', 'product')
    ->where('customer.identifier = :identifier')
    ->setParameter('identifier', $identifier);

Dostałem za zadanie zmienić wyświetlanie danych w ten sposób by pokazywać jedynie aktywne produkty. Zrobiłem to tak:

$qb
    ->select('customer, product')
    ->from($this->getEntityName(), 'customer')
    ->leftJoin('customer.products', 'product')
    ->where('customer.identifier = :identifier')
    ->andWhere('product.isActive = 1')
    ->setParameter('identifier', $identifier);

Ku memu zdziwieniu w wypadku klienta, który ma tylko jeden nieaktywny produkt wynik zapytania był pusty. No jakże to?! Przecież klient jak najbardziej istnieje, a ponieważ nie ma produktów spełniających kryteria wyszukiwania (isActive = 1) to w miejscu danych produktu powinny być NULLe.

Swój błąd zrozumiałem dopiero, kiedy spojrzałem na generowane przez Doctrine zapytanie SQL. W uproszczeniu wygląda ono tak:

SELECT 
  c.name,
  p.product_type
FROM 
  customer c 
  LEFT JOIN product p ON c.id = p.customer_id 
WHERE 
  c.identifier = '65092900214'
  AND p.is_active = 1

Ponieważ warunek p.is_active = 1 sprawdzamy w części WHERE zapytania to eliminujemy z zapytania wszystkie wiersze, w których on nie jest spełniony. Zatem jeśli klient ma tylko jeden nieaktywny produkt to wynik zapytania będzie pusty.

Oczekiwany efekt (wiersz z NULLami w miejscu danych produktu) uzyskamy przenosząc warunek do części LEFT JOIN zapytania:

SELECT 
  c.name,
  p.product_type
FROM customer c
  LEFT JOIN product p ON c.id = p.customer_id 
  AND p.is_active = 1
WHERE 
  c.identifier = '65092900214'

Ponieważ warunek dotyczy złączenia to jeśli nie zostanie on spełniony to złączenie nie zostanie wykonane, ale dane klienta zostaną zwrócone. Dokładnie tak jak chciałem.

W Doctrine poprawne zapytanie należy zbudować tak:

$qb
    ->select('customer, product')
    ->from($this->getEntityName(), 'customer')
    ->leftJoin('customer.products', 'product', 'WITH', 'product.isActive = 1')
    ->where('customer.identifier = :identifier')
    ->setParameter('identifier', $identifier);

Twig Extensions w aplikacji Symfony2

Twig sam z siebie ma sporo wygodnych funkcji, ale kilka bardzo przydatnych udogodnień nie trafiło do głównej biblioteki lecz do osobnej paczki Twig Extensions. W wypadku standardowej aplikacji Symfony2 te rozszerzenia już są uwzględnione w composer.json, ale jeśli nie to można jest zainstalować poleceniem:

composer require twig/extensions ~1.1.0

Trzeba je także włączyć w konfiguracji aplikacji, tak jak każde inne rozszerzenie Twiga:

services:
    twig.extension.intl:
        class: Twig_Extensions_Extension_Intl
        tags:
            - { name: twig.extension }

Powyższy przykład dodaje rozszerzenie Intl, które podobnie jak biblioteka Intl ułatwia formatowanie danych zgodnie z ustawieniami lokalizacji. Obecnie w Twig Extensions dostępne są następujące klasy:

  • Twig_Extensions_Extension_Array
  • Twig_Extensions_Extension_Date
  • Twig_Extensions_Extension_I18n
  • Twig_Extensions_Extension_Intl
  • Twig_Extensions_Extension_Text

Przy założeniu, że aplikacja ma ustawione polską lokalizację to wyświetlenie daty w postaci „13 listopada 2014” wygląda tak:

{{ meeting.date|localizeddate('long', 'none') }}

Wyświetlenie liczby z częścią dziesiętną:

{{ player.power|round(2)|localizednumber }}

Przy korzystaniu z filtra localizednumber tylko liczby z częścią ułamkową będą wyświetlane z przecinkiem, a liczby całkowite są wyświetlane bez przecinka.

Klucz kluczem do sukcesu

Mechanizm planowania zapytań w MySQL jest mniej sprytny niż sądziłem. Chciałem wykonać z pozoru łatwe zapytanie: z bazy użytkowników liczącej ok 130 tys. wierszy chciałem wyłuskać osoby mieszkające pod określonymi kodami pocztowymi (ok. 20 tys.). Dodatkowym warunkiem było podanie numeru telefonu, który znajduje się w innej tabelce oraz wyrażenie zgody na marketing, ale to nie jest istotne dla głównego problemu. Napisałem takie zapytanie:

SELECT
	k.imie,
	k.nazwisko,
	k.kod_pocztowy,
	u.numer_telefonu
FROM klient k
INNER JOIN ustawienia u ON k.id = u.klient_id
INNER JOIN tmp_kody t ON k.kod_pocztowy = t.kod_pocztowy
WHERE k.zgoda_na_marketing = 1
ORDER BY k.id

Szukane kody pocztowe wrzuciłem sobie do tabelki, bo przy próbie umieszczenia ich w treści zapytania phpMyAdmin odmówił współpracy, a wklejanie zapytania na zdalnej konsoli trwało ponad 10 minut.

Pierwszą próbę wykonania zapytania przerwałem po kilku minutach. EXPLAIN pokazał, że MySQL nie bardzo chce korzystać z indeksów i najchętniej przeskanowałby całą tabelkę tmp_kody i ustawienia. Próbowałem przepisać zapytanie na kilka sposobów by skłonić MySQL do ułożenia innego planu, ale bezskutecznie. Skutecznym rozwiązaniem okazało się tymczasowe dodanie indeksu na kolumnę kod_pocztowy. Oryginalne zapytanie wykonało się w mgnieniu oka, a EXPLAIN pokazał, że wszystkie etapy planu wykonania zaczęły korzystać z indeksów.

Wniosek na przyszłość: jeśli MySQL sobie nie radzi to można mu pomóc dodając tymczasowy indeks. To może okazać się szybsze niż czekanie na wynik nieoptymalnego zapytania.

Import dużych liczb z pliku CSV

Od czasu do czasu jestem proszony o wyciąganie z bazy różnych raportów. Prosta sprawa: zapytanie w phpMyAdminie, eksport wyniku do pliku CSV i wysyłamy… o ile w pliku nie ma kolumny zawierającej dużą liczbę. W takim wypadku zarówno LibreOffice Calc jak i Excel mają nieprzyjemny zwyczaj zaokrąglania jej. Przykładowo w 16 cyfrowym numerze karty po którym identyfikujemy klienta i w którym ostatnia cyfra jest sumą kontrolną dostajemy na końcu zawsze 0, co oczywiście skutecznie niweczy zarówno sprawdzanie sumy kontrolnej jak i identyfikację klienta.

Na szczęście w Calcu można to naprawić. W Excelu zapewne też, ale odbiorcy moich raportów nie wiedzą jak, a ja nie mam Excela, żeby eksperymentować. Dlatego przed wysłaniem raportu do działu marketingu importuję plik do Calca. W okienku z opcjami importu należy zaznaczyć kolumnę zawierającą dużą liczbę i zmienić jej typ na Tekst.

Import pliku CSV

Potem należy zapisać arkusz w formacie Excela. Taki plik można dostarczyć do marketingu.

Podpisywanie dokumentów profilem zaufanym w PUE ZUS

Ilekroć próbuję cokolwiek załatwić przez Platformę Usług Elektronicznych Zakładu Ubezpieczeń Społecznych czuję się jakbym próbował wbić gwoździa starą skarpetą – może i da się to zrobić, ale na pewno nie jest to łatwe ani wygodne. Zupełnie jakby ZUS chciał odstraszyć ludzi od załatwiania spraw online.

Teraz próbowałem złożyć deklaracje ZUS ZWUA i ZUS ZUA. O ile wypełnienie deklaracji przez kreatora było w miarę łatwe (oczywiście przez Firefoksa, bo wbudowana w Chrome’a wtyczka Flash jest nieobsługiwana), o tyle próba wysłania deklaracji kończyła się fiaskiem. Na ekranie pojawiał się kręcioł, Java odpalała jakiś SZAFIR SDK instalator i mogiła. W akcie desperacji spróbowałem całą operację przeprowadzić z komputera z Windowsem. Tutaj sytuacja była o tyle lepsza, że wyskoczyło okno podpisywania dokumentów podpisem elektronicznym. To naprowadziło mnie na właściwy trop: mimo, że do PUE loguję się przez profil zaufany ePUAP i mimo, że jako ubezpieczony podpisuję dokumenty rzeczonym profilem zaufanym to w ePłatniku domyślnie włączone jest podpisywanie dokumentów podpisem elektronicznym. Żeby to zmienić należy w zakładce Płatnik wybrać w menu po lewej Ustawienia, a następnie Ustawienia konta. Tam należy wybrać „Sposób podpisywania dokumentów w aplikacji ePłatnik” jako „Podpis profilem zaufanym ePuap”.

ZUS PUE podpis

Tworzenie pliku CSV w pamięci

Nie tak dawno pisałem o funkcjach fgetcsv() i fputcsv() w kontekście nakładania filtrów na strumienie PHP. Również z pomocą strumieni można rozwiązać problem tworzenia plików CSV w pamięci. Wystarczy otworzyć taki niby-plik w pamięci:

$fh = fopen('php://temp', 'rw');

i już można do niego zapisywać dane przez fputcsv():

fputcsv($fh, ["Imię", "Nazwisko", "Login", "PESEL"]);

Na koniec wystarczy tylko przewinąć wskaźnik pozycji w pliku na początek i pobrać jego zawartość:

rewind($fh);
$csv = stream_get_contents($fh);
fclose($fh);

Katalogi przed plikami w Gnome Shell

Jestem otwarty na nowe rozwiązania w dziedzinie interfejsu graficznego aplikacji, ale wyświetlanie katalogów przed plikami w managerze plików jest dla mnie aksjomatem. Tymczasem po którejś aktualizacji Debiana Nautilus zaczął pokazywać pliki wymieszane z katalogami (w kolejności alfabetycznej). Co gorsza, nie dało się tego zmienić przez wyklikanie bezpośrednio w Nautilusie. Musiałem uciec się do googlania, który podpowiedział w użycie dconf-editor i kluczu org.gnome.nautilus.preferences zaznaczenie opcji sort-directories-first.

dconf