WP-Cache – rozwiązania problemów z semget i działaniem

WP-Cache to bardzo przydatny plugin do Word Press’a. Plugin ten zapisuje każdą wygenerowaną stronę w postaci statycznego pliku HTML na serwerze przez co znacznie zmniejsza obciążenie serwera i bazy danych. Długość cache, jak i reguły opisujące pliki/strony, które mają być cache’owane można ustawić z poziomu panelu WP.

Ostatnio zetknąłem się z dwoma problemami w użytkowaniu tego plugina, poniżej rozwiązania:

Problem 1:
Za każdym razem, gdy strona generowana jest po raz pierwszy pojawia się komunikat w górze strony:

Warning: semget() failed for key 0x152b: Permission denied in /XXX/wp-content/plugins/wp-cache/wp-cache-phase2.php on line 98

Rozwiązanie:
Trzeba zedytować plik: wp-content/plugins/wp-cache/wp-cache-phase2.php w linii 98:
zamienić:

$mutex = sem_get($sem_id, 1, 0644 | IPC_CREAT, 1);

na

$mutex = @sem_get($sem_id, 1, 0644 | IPC_CREAT, 1);

Znak @ przed funkcją php blokuje komunikaty o błędach, spowodowanych daną funkcją.

Alternatywą tego rozwiązania jest dopisanie

ini_set( 'display_errors', 0 );

w pliku wp-config.php W tym wypadku zablokujemy wyświetlania się komunikatów o błędach globalnie w całym Word Press’ie.

Problem 2:
WP-Cache jest poprawnie zainstalowane, włączone, jednak strony się nie cache’ują.

Sprawdz czy w katalogu wp-content znajduje się plik

!advanced-cache.php

Jeżeli tak to oznacza, że WP-Cache ma źle podlinkowany plik wp-cache-phase1.php

Rozwiązanie:
Najpierw należy skasować plik !advanced-cache.php.

Następnie przypadku gdy mamy dostęp ssh trzeba z poziomu katalogu wp-content wykonać polecenie unix’owe, które stworzy link symboliczny do tego pliku wykorzystywany przez skrypty WP:

ln -s   plugins/wp-cache/wp-cache-phase1.php advanced-cache.php

Gdy nie mamy ssh musimy zainstalować wtyczkę jeszcze raz (zdeaktywować i aktywować).

Przyjemna komunikacja php – flash czyli AMFPHP

Z pewnością każdy programista php, który w swoim projekcie musiał komunikować się z flash’em na własnej skórze doświadczył, że nie jest to zadanie ani przyjemne ani przyjazne w implementacji.

Aby przekazać dane flash’owi, skrypty muszą generować XML’e, które następnie zasysa flash i przetwarza Action Script’em. Bolączek tego rozwiązania jest wiele, poczynając od dodatkowego czasu potrzebnego na implementacje, wygenerowania XML i późniejszego jego zdekodowania a skończywszy na generowaniu sztucznego transferu, ograniczeniach w wielkości plików XML i trudności w debugowaniu.

Znamy zatem bolączki standardowej komunikacji php-flash, zatem trzeba zadać sobie pytanie czy możemy ich uniknąć? Oczywiście!

AMFPHP czyli przyjazna komunikacja na lini php – flash

AMFPHP to open source’owa implementacja Action Message Format (AMF). Bibliteka ta poprzez RPC (Remote Procedure Call) pośredniczy w komunikacji skryptów Action Script ze skryptami serwerowymi, przesyłane dane są serializowane do postaci binarnej. Wymiana danych następuje poprzez plik gateway.php. AMFPHP zachowuje typu danych: obiekt, tablica, wartości typu int, null, bool będą w tej samej postaci dostępne w Action Script w php.

Powyższy opis może nieco odstraszać egzotycznymi nazwami, to tylko teoria w praktyce nie ma się czego obawiać, zaraz na przykładzie pokaże, że instalacja jak i konfiguracja jest banalna.

1. Ściągamy najnowszą wersję AMFPHP, aktualnie jest to wersja 1.9 beta 2. Znaczkiem beta nie ma się co zanadto przejmować, gdyż wersja ta jest stabilna i dobrze przetestowana.
2. Rozpakowywujemy archiwum powyżej document_root gdyż katalog browser i plik gateway.php muszą być dostępne przez HTTP. Resztę plików można by schować poniżej document_root, jednak na potrzeby tej prezentacji nie będę się w takie szczgóły zagłębiać.
3. W katalogu services tworzymy usługi, które maja postac klas PHP. Kod przykładowej usługi:

<?php
require_once( '/my_application/library/core_classes/game.php' );

class FlashGame {

	private $game;
	
	public function __construct() {
		$this->game = new Game();
	}
	
	/**
	 * Metoda zwraca informacje o grze i uzytkowniku
	 *
	 * @param integer - id gry
	 * @param integer - id usera
	 * @return array
	 */
	public function getResults( $gameID, $userID ) {
		$response['info'] = $this->game->getGameInfo( $gameID );
		$response['user'] = $this->game->getUserInfo( $userID );

		return $response;
	}

	/**
	 * Metoda usua uzytkownika
	 *
	 * @param integer - id usera
	 * @return bool
	 */
	public function remove( $userID ) {
		return (bool)$this->game->remove( $userID );
	}
	
	/**
	 * Metoda zwraca obiekt gry
	 *
	 * @return Object
	 */
	public function getGameObject() {
		return $this->game->getGameObject();
	}
}
?>

Klasy usług powinny być maksymalnie proste i jedynie odwoływać się do klas logiki biznesowej aplikacji. Taka konstrukcja zapewnia jasny i przejrzysty kod aplikacji i jest zgodna z kanonami programowania.

Jeżeli poprawnie wykonaliśmy powyższe operacje to będziemy mogli odpalić browsera i z jego poziomu przetestować działanie. Odpalamy browsera (tutaj przykład), przy pierwszym odpaleniu pyta się nas o ścieżkę do gateway i prezentacje wyników, wybierzmy 'tree view’, i klikamy OK.

Po lewej stronie widzimy dostępne usługi, po dwukrotnym kliknięciu na nazwę usługi pojawiają nam się dostępne metody. Domyślnie widoczne są wszystkie metody, można to zmienić pisząc znak podkreślenia na początku jej nazwy. Każda metoda opisana jest komentarzem wyciągniętym z klasy oraz zawiera pola na wprowadzenie testowych danych. Pola te odpowiadają parametrom przyjmowanym przez tą metodę.

Z poziomu browsera możemy przetestować działanie metod poprzez kliknięcie na przycisk call, wyniki prezentowane są w oknie poniżej. Prezentowany wynik jest identyczny z tym co dostaje Flash. Po publikacji aplikacji należy pamiętać o skasowaniu lub zabezpieczeniu hasłem dostępu katalog browsera.

Zatem podsumujmy co daje nam AMFPHP:

  • oszczędność w implementacji
  • łatwe testowanie i debugowanie aplikacji
  • mniejszy transfer serwera
  • większa szybkość działania aplikacji
  • przejrzystość i skalowalność
  • bezpieczeństwo (dane przesyłane binarnie a XML w postaci jawnej)
  • proste wdrożenie do aplikacji

Tutaj do ściągnięcia jest cała paczka AMFPHP wraz z przykładowymi serwisami.

Automatyczne przeglądanie kont na naszej klasie

Ostatnio zapragnąłem trochę posurfować po największym portalu społecznościowym w Polsce jakim jest Nasza-Klasa. Poniżej prościutki kod odwiedzający losowe konta.

Deklarujemy zmienne do logowania, plik przetrzymujący ciasteczka i nazwę przeglądarki z jaką będzie się przedstawiał nasz automat.

$cookie = 'cookie.txt';

$loginUrl = 'http://nasza-klasa.pl/login';
$login = 'login';
$password = 'pass';
$useragent = 'Mozilla/5.0 (X11; U; Linux i686; pl; rv:1.8.0.3) Gecko/20060426 Firefox/1.5.0.3';

Logujemy się na konto n-k:

$ch = curl_init( $loginUrl );
curl_setopt( $ch, CURLOPT_COOKIEFILE, $cookie );
curl_setopt( $ch, CURLOPT_COOKIEJAR, $cookie );
curl_setopt( $ch, CURLOPT_COOKIE, $cookie);
curl_setopt( $ch, CURLOPT_USERAGENT, $useragent );
curl_setopt( $ch, CURLOPT_POST, true );
curl_setopt( $ch, CURLOPT_POSTFIELDS, 'login='.$login.'&password='.$password );
curl_setopt( $ch, CURLOPT_REFERER, 'http://nasza-klasa.pl/' );
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, 1);
$afterLogin = curl_exec($ch);
curl_close($ch);

Wywołujemy adresy losowych kont:

for( $i=1; $i<=100; $i++ ) {
	$profileID = rand( 1000, 15000000 ); // rand
	
	$ch = curl_init( 'http://nasza-klasa.pl/profile/'.$profileID );
	curl_setopt( $ch, CURLOPT_COOKIEJAR, $cookie);
	curl_setopt( $ch, CURLOPT_COOKIEFILE, $cookie);
	curl_setopt( $ch, CURLOPT_COOKIE, $cookie);
	curl_setopt( $ch, CURLOPT_USERAGENT, $useragent );
	curl_setopt( $ch, CURLOPT_HEADER, 0);
	curl_setopt( $ch, CURLOPT_REFERER, 'http://nasza-klasa.pl/' );
	curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
        curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, 1);
	$profile = curl_exec($ch);
	curl_close($ch);
	
	if( !preg_match( '/Nie znaleziono strony \(404\)/', $profile ) ) {
		// konto istnieje
		// z danymi konta możemy zrobić co chcemy...
	} else {
		// nie ma konta takiego konta
	}
}

Ustawiamy w contab'ie wywołanie skryptu co 10 minut:

*/10 * * * * curl -s -o /dev/null PATH_TO_SCRIPT

Z uwagi na bardzo dużą liczbę kont prawdopodobieństwo odwiedzenia w niedługim czasie 2 razy tego samego konta jest stosunkowo niewielkie.

Jednak jeżeli chcemy odwiedzić każde konto jedynie raz to należy zmodyfikować skrypt tak aby przy każdym wywołaniu iterował po kolejnej setce kont. Informacje o ID od którego mamy zacząć możemy zapisywać w bazie danych lub w pliku.

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

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.