Ostatnio spędziłem parę godzin szukając, dlaczego nie mogę includwać plików w swoich serwisach - okazało się, że winny jest open_basedir, który ustawiony jest dla konkretnej ścieżki, i include() (oraz inne funkcja otwierające pliki) tam właśnie zaczynają szukać plików, które mają wczytać - i nie przeszukują podkatalogów.
Popularne, i wszędzie sugerowane rozwiązanie:
<?
#a.php
include('podkatalog/b.php');
#podkatalog/b.php
include('plik.php');
Gdzie plik.php znajduje się w tym samym katalogu co a.php mimo, że wydaje się logiczne i w większości wypadków działa, może okazać się, że dzięki open_basedir przestało.
Rozwiązania?
a) wyłączyć dyrektywę open_basedir w php.ini (i masie podrzędnych, jeżeli masz dużo hostów virtualnych)
b) podawać ścieżki względne lub bezwzględne do dołączanych plików, np. tak:
<?
include('../plik.php');
# albo lepiej
include(dirname(__FILE__) . '/../plik.php');
Czas temu napisałem skrypt, który konwertował tytuły postów na linki SEO-friendly, czyli wywalał pliterki i zamieniał na bez-ogonkowe odpowiedniki, wywalał znaki specjalne etc. Niestety, poległ na czymś wklejonym z Worda. Nowa wersja jest nieco krótsza i wygląda tak:
It’s How Everyone Builds It zażółć gęślą jaźń !@#$%^&*()_+=-[]{}\\|;:\'<>?,./
<?
$string = iconv('UTF-8', 'UTF-8//IGNORE', $string);
$string = iconv('UTF-8', 'ASCII//TRANSLIT', $string);
$string = str_replace(' ', '-', trim(preg_replace('/([^a-zA-Z\s])/Us','', $string)));
echo $string;
W wyniku da nam ładne
Its-How-Everyone-Builds-It-zazolc-gesla-jazn-zazolc
Celem kolejnego ze zleceń jakie otrzymaliśmy jest napisanie rozbudowanego systemu ankiet z możliwością powrotu i zmiany odpowiedzi a co za tym idzie śledzeniem zmian.
Zanim talen wspomniał coś przy innej okazji nt. TRIGGERów w MySQL zapewne zrobił bym to używając kilku SELECT połączonych z UPDATE i INSERT. Jednak dzięki mechanizmowi TRIGGERów możemy to spłycić do jednego zapytania. Jak? Za chwilkę, najpierw o mechanizmie.
TRIGGERy to coś na kształt EVENTów - odpalają się w bazie kiedy zajdzie opisana przez programistę podczas ich tworzenia sytuacja - np, jakiś wiersz zostanie zmodyfikowany, usunięty lub dodany - pozawala nam to na jednokrotne zaprogramowanie takiego zdarzenia w MySQL i nie zawracanie sobie już tym głowy w warstwie aplikacji.
Przejdźmy do zadania - tworzymy sobie prostą tabelę:
CREATE TABLE IF NOT EXISTS `answers` (
`question_id` int(11) NOT NULL,
`user_id` int(11) NOT NULL,
`answer_id` int(11) NOT NULL,
UNIQUE KEY `question_id` (`question_id`,`user_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
Jak widać, założyłem klucz UNIQUE na pola question_id i user_id - zapobiega to dodaniu wielu odpowiedzi na jedno pytanie przez tego samego użytkownika. Następnym krokiem jest stworzenie tabeli, która przechowywać będzie archiwalne odpowiedzi - kod jest prawie identyczny:
CREATE TABLE IF NOT EXISTS `answers_old` (
`question_id` int(11) NOT NULL,
`user_id` int(11) NOT NULL,
`answer_id` int(11) NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
Tabela nie posiada żadnych indexów, ponieważ każda odpowiedź mogła być zmieniona więcej niż raz, więc będziemy mieli więcej niż jedno archiwum.
Następnym krokiem jest stworzenie samego TRIGGERa:
DELIMITER //
CREATE TRIGGER `test`.`archiwizuj` BEFORE UPDATE ON `test`.`answers`
FOR EACH ROW BEGIN
INSERT INTO answers_old SET question_id = OLD.question_id, answer_id = OLD.answer_id, user_id = OLD.user_id;
END
//
DELIMITER ;
Przed utworzeniem samego TRIGERRA zmieniamy znak końca funkcji na coś innego niż standardowy średnik, by średniki w treści zapytania nie wykrzaczyły nam operacji. Jeżeli robicie to przez phpMyAdmin i po wydaniu polecenia zwrócony zostanie błąd, a macie pewność, że wasza baza obsługuje TRIGERRy sprawdźcie czy mimo wszystko nie został on pomyślnie utworzony - u mnie tak właśnie było, a ja jak głupi szukałem błędu. Cóż.
Teraz demonstracja:
INSERT INTO `answers` SET `question_id` = 1, `user_id` = 1, `answer_id` = 1 ON DUPLICATE KEY UPDATE `answer_id` = 1;
Proste zapytanie, ustawia odpowiedź “1″ dla użytkownika “1″ i pytania “1″ - jeżeli istnieje już wpis użytkownika dla tego pytania, zaktualizuj odpowiedź na 1.
Po tym wywołaniu mamy jedynki w tabeli. Następne podejście:
INSERT INTO `answers` SET `question_id` = 1, `user_id` = 1, `answer_id` = 2 ON DUPLICATE KEY UPDATE `answer_id` = 2;
Użytkownik aktualizuje odpowiedź - pole `answer_id` zmieniło się na 2. Jak dotąd - nic nowego. Jednak jeżeli zajrzymy do tabeli `answers_old` zobaczymy tam wpis, który widzieliśmy poprzednio w `answers`. Jeszcze jedno zapytanie:
INSERT INTO `answers` SET `question_id` = 1, `user_id` = 1, `answer_id` = 3 ON DUPLICATE KEY UPDATE `answer_id` = 3;
W tabeli `answers` mamy odpowiedź numer 3, zaś w `answers_old` są już dwa wpisy z odpowiedziami 1 i 2.
Jak widać całość działa świetnie i zostało nam już tylko napisanie mechanizmu obsługującego informowanie administratora o zmianie odpowiedzi i wyświetlanie historii. Oczywiście ten przykład odnosi się nie tylko do ankiet, ale do wszelkich rodzaju danych, których stare wersje chcemy zachowywać w bazie danych - np. na potrzeby archiwizowania starej wersji strony w celu jej późniejszego odtworzenia etc etc.
Większość z programistów zdaje sobie sprawę z tego, jakim zagrożeniem może być błędnie napisany skrypt pozwalający na upload plików na serwer. Dlatego, większość ogranicza możliwość wgrywania plików przez odpowiednie filtrowanie na podstawie mime-type np. w taki sposób:
<?php
if($_FILES["type"] == "image/jpeg") {
// logika wgrywania dla plików JPEG
}
else {
echo "Nie można wgrać pliku takiego typu - tylko JPEG!";
}
Wszystko ładnie - wiemy, że wgrywany plik, jest de-facto plikiem jpeg.
Na pewno?
Otóż nie - dane, które znajdujemy w tablicy _FILES (name, size, type) są danymi, które podała nam przeglądarka podczas wysyłania pliku! Nie daje to nam w żaden sposób pewności, że złośliwy użytkownik nie zmienił swojej przeglądarki / nie użył innego programu który wysłał nam plik.php przy okazji dodając, że jest to format image/jpeg !
Jeżeli nie zabezpieczymy się przed tego typu zagraniami otwieramy serwer na wszelkiego rodzaju ataki. Zostają dwa rozwiązania, które najlepiej stosować jednocześnie
- sprawdź rozszerzenie pliku - rozszerzenie wyciągamy w następujący sposób:
<?
$ext = substr($file, strrpos($file,".") + 1);
- upewnij się, że plik rzeczywiście jest typu image/jpeg - np. przy użyciu funkcji getimagesize która jako wynik zwraca tablicę asocjacyjną - pod kluczem mime znajduje się typ pliku odczytany już na naszym serwerze