Uwaga, blog przeniesiony

Posty na tym blogu już nie będą się pojawiać. Zapraszam gorąco pod nowy adres: blog.grzegorzpawlik.com



Subskrybuj ten blog...

czwartek, 21 lutego 2008

CakePHP + nuSOAP serwer + autoryzacja othAuth

Autoryzacja serwera soap jest o tyle niewdzięczna, o ile nie została zaimplementowana w samym protokole. Do tego sam cake nie ułatwia nam sprawy. Jednak po wielu dniach zmagań udało mi się nagiąć tą materię i z chęcią się doświadczeniami podzielę (tak naprawdę po uporaniu się z tym problemem postanowiłem założyć ten blog).
Załóżmy, że serwer, który opisałem w poprzednim poście już stoi. Załóżmy też, że jest zrobione wszystko, co musiało być zrobione, aby autoryzacja othAuth działała w naszej aplikacji (jeśli nie- przed kontynuowaniem zapraszam tutaj )
Po pierwsze zmieńmy w /app/config/core.php definicję stałej CAKE_SESSION_COOKIE na 'PHPSESSID' bez tego nie uda nam się "odzyskać" sesji znając jej id.
Dalej... logowanie "normalnego" usera wygląda mniej więcej tak:
/app/controllers/user
function login()
{

if(isset($this->params['data']))
{

$auth_num = $this->othAuth->login($this->params['data']['User']);

return $auth_num;// potrzebne dla soap-owego logowania (1 jeśli udane)
}
}


Wracajmy teraz szybko do naszego oczka w głowie: /app/controllers/soap_controller.php.
W metodzie _defineTypes dodajmy definicję złożonych typów:

$this->server->wsdl->addComplexType (
'LoginData',
'complexType',
'struct',
'all',
'',
array(
'Login' => array('name' => 'Login',
'type' => 'xsd:string'), //
'Password' => array('name' => 'Password',
'type' => 'xsd:string'), //
)
);
/**
* odpowiedĹş na logowanie
*/
$this->server->wsdl->addComplexType (
'LoginResponse',
'complexType',
'struct',
'all',
'',
array(
'Result' => array('name' => 'Result',
'type' => 'xsd:int'), //
'SID' => array('name' => 'SID',
'type' => 'xsd:string'), //
)
);


A w metodzie _registerMethods() zarejestrujmy metodę SoapController.login:

$this->server->register('SoapController.login',
array(
'SoapParam' => 'tns:LoginData'),
array(
'return' => 'tns:LoginResponse') ,
$this->namespace,
$this->namespace . '#login',
'logowanie'
);


Teraz właściwe logowanie:

function login($login_data) {
$login_details['User'] = array('username' => $login_data['Login'],
'password' => $login_data['Password'],
'cookie' => "0");
$result = $this->requestAction('users/login', array('data' => $login_details));
return array(
'Result' => $result, 'SID' => session_id() );
}


Kilka słów wyjaśnienia: w pierwszej linii tworzymy trochę sztucznie tablicę $login_details, która jest taka jaką oczekuje metoda UsersController::login() (zgodnie z zasadą DRY nie będziemy tworzyć osobnej metody dla logowania przez soap). Wywołujemy akcję users/login przekazując do niej spreparowane dane i zwracamy zgodnie z definicją typu LoginResponse- tablicę z danymi.


To była ta łatwiejsza część. Teraz to nad czym spędziłem kilka intensywnych dni- jak odzyskać sesję??
Od razu sprostowanie: wygląda na to, że sesja "trzyma się" tak długo, jak długo klient się nie rozłączy (testowałem to za pomocą również nuSOAP + php i w okresie trwania skryptu - było ok). Jednak ja potrzebowałem możliwości podania przez klienta id sesji, którą otrzymał podczas logowania. Jak to zrobić?
Po kolei:
Dodajmy typ, który będzie służył do przekazania informacji na temat zalogowanego usera:
$this->server->wsdl->addComplexType (
'UserDetails',
'complexType',
'struct',
'all',
'',
array(
'Id' => array('name' => 'id',
'type' => 'xsd:int'), //
'Username' => array('name' => 'Username',
'type' => 'xsd:string'), //
'Email' => array('name' => 'Email',
'type' => 'xsd:string'), //
'GroupId' => array('name' => 'GroupId',
'type' => 'xsd:string'), //
'Error' => array('name' => 'Error',
'type' => 'xsd:string')
)
);


Zarejestrujmy metodę SoapController.check():

$this->server->register('SoapController.check',
array(
'data' => 'xsd:string'),
array(
'return' => 'tns:UserDetails') ,
$this->namespace,
$this->namespace . '#check',
'logowanie'
);


I zdefiniujmy ją:

function check($data="") {
$data = $this->requestAction('/users/soapCheck', array('data' => $data, 'SID'=>$data));
$this->log($data, LOG_DEBUG);

if(
$data){
$return = array ( 'Id' => $data['User']['id'],
'Username' => $data['User']['username'],
'Email' => $data['User']['email'],
'GroupId' => $data['Group']['id'],
'Error' => 'ewrifing ok');
}else{
$return = array ( 'Id' => -1,
'Username' => ''
'Email' => '',
'GroupId' => '',
'Error' => 'not logged in');
}

return
$return ;
}


Wyjaśnienia: oprócz danych jak przy logowaniu pojawiło się jeszcze pole w tablicy 'SID', to tędy przekażemy informację, że chcemy "wymusić" jakieś id sesji.
Teraz metoda UsersController::soapCheck():


function
soapCheck($data) {
return $this->othAuth->getData();
}


No i na koniec gdzieś musimy wymusić inne id sesji (no bo nie ma przeglądarki, ani kochanych cookies, które zrobią to za nas). W app/app_controller.php:

if(isset($this->params['SID'])){
session_id($this->params['SID']);
}


I ok... jaaasne. Nie działa, prawda? Dlaczego? A to dlatego, że w momencie kiedy klient SOAP łączy się z naszym ukochanym serwerem SOAP wygląda to tak:
request (soap/index) -> app_controller -> soap_contropper -> check -> request (/users/soapCheck) -> app_controller*->users_controller -> soapCheck ...

miejsce oznaczone gwiazdką oznacza moment, kiedy app_controller dostaje info o tym, że jest jakieś SID... tylko że wtedy to już jest za późno (jak to mówią ślązocy: "po ptokach" :P) $this->requestAction to moment, kiedy ciasteczka już nie są wysyłane i szanowna pani Sesja ma już głęboko w ... nosie fakt, że tam jakieś id jest przesyłane.
Nie lękaj się jednak, jest na to rada:
zmień w app_controller poprzednio dodany blok na:
if(isset($this->params['url']['SID'])){
session_id($this->params
['url']['SID']);
}


I teraz łącząc się z serwerem do linku /twoja_aplikacja/soap/ doklejaj parametr SID=. W takim wypadku już przy pierwszym request->app_controller id sesji trafia do "świadomości" Sesji i zazwyczaj rozpatruje je pozytywnie.

Nie jest to może najbardziej eleganckie rozwiązanie. Jednak najlepsze na jakie teraz mnie stać ;) Jeśli masz coś fajnieszego- pisz w komentarzach, z chęcią się podszkolę :)

wtorek, 19 lutego 2008

CakePHP + serwer SOAP (pierwsze kroki)

Mimo tego, że cake chwali się, że jest WebServices ready tyczy się to jedynie tak zwanego routingu (np. gdy użyjesz users/index - będzie "normalnie" czyli wyrenderuje widok /vews/users/index.thtml, a gdy "xml/user/index" - /views/users/xml/index.thtml) ale nie jest to prawdziwe WebSerwice. Nie wiem ile jest sposobów na zmianę CakePHP w prawdziwy serwer SOAP, ja znam jeden i Wam go pokażę.

1. Potrzebna nam biblioteka SOAP (jako prawdziwy programiści pozwalamy, żeby najtrudniejsze rzeczy pisali za nas inni). Ja wybrałem NuSOAP. Wrzuć pliki do /app/vendors/nusoap/*.
2. Stwórz kontroler do obsługi Soap. W moim przypadku soap_controller.php
/**
* Soap Serwer
* @package package
*/
vendor('nusoap/nusoap');
class
SoapController extends AppController {
var
$name = 'Soap';
var
$server;
var
$namespace;
var
$layout = 'blank';
var
$components = array('othAuth','Conf');
var
$othAuthRestrictions = null;

/**
* Inicjalizacja ustawień serwera
*
*/
function _init() {
$this->namespace = $this->Session->host;
$this->server = new soap_server();
$this->server->debug_flag = false;
$this->server->configureWSDL('MyWsdl', $this->namespace, 'http://'.$_SERVER['HTTP_HOST'] . $this->webroot . $this->params['controller'] ); // należy nadpisać endpoint, gdyż domyślnie ustawi się na /twoja_aplikacja/app/index.php
$this->server->wsdl->schemaTargetNamespace = $this->namespace;
$this->_defineTypes();
$this->_registerMethods();
}

/**
* Serwer endpoint handler
*
*/
function index(){
Configure::write('debug', 0);

$this->_init();

$HTTP_RAW_POST_DATA = isset($GLOBALS['HTTP_RAW_POST_DATA']) ? $GLOBALS['HTTP_RAW_POST_DATA'] : '';
$this->server->service($HTTP_RAW_POST_DATA);
exit();
}


/**
* define types required by this server
* przyda się później
*/
function _defineTypes() {

}

function
_registerMethods() {

$this->server->register(
'SoapController.hello', // method name
array('name' => 'xsd:string'), // input parameters
array('return' => 'xsd:string'),
$this->namespace,
$this->namespace . '#hello',
'document', // style
'encoded' // use
);

}

function
hello($name){
return array(
'return' => 'hello, '.$name);
}

}

?>

3. Do tego stwórz model, który nic nie robi :)
class Soap extends AppModel {
var $name = 'Soap';
var $useTable = false;
}
?>


Teraz objaśnienia:
SoapController::index() - to twój endpoint serwera SOAP. SoapController::_registerMethods() zajmuje się rejestrowaniem metod dostępnych przez serwer. W tym wypadku tylko jednej: SoapController::hello (notacja metod soap to Klasa.metoda).
Metoda hello przyjmuje string jako parametr i zrwaca 'Hello, '+ to co zostało jej przekazane.
Przykładowy klient (poza cake.php):
require('./nusoap/nusoap.php');

function
pr($var) {
echo
"<pre>";
var_dump($var);
echo
"&lt/pre>";
}

/* create client */
$endpoint = "http://localhost/meta/application_in_cakePHP/soap/index";

$mynamespace = "";
$client = new soapclient($endpoint);

$err = $client->getError();
if (
$err) {
// Display the error
echo '

Constructor error: '

. $err . '

'
;
// At this point, you know the call that follows will fail
}

$response = $client->call('SoapController.hello', array('name' =>"Greg"));
echo
"Call SoapController.hello";
if (
$client->fault) {
echo
'

Fault: '

;
print_r($response);
echo
'

'
;
} else {
// Check for errors
$err = $client->getError();
if (
$err) {
// Display the error
echo '

Error: '

. $err . '

'
;
} else {
echo(
'response:');
pr($response);
}
}

?>


I ładny response:
string(11) "hello, Greg"
Dodatkowo, gdy w przeglądarce wpiszesz ścieżkę do kontrolera Soap zobaczysz ładną dokumentację Twojego serwera. A dodając do ścieżki ?wsdl - dokument wsdl.

żródła:
http://www.scottnichol.com/nusoapprog.htm
http://dietrich.ganx4.com/nusoap/faq.php


poniedziałek, 18 lutego 2008

AppController::beforeFilter() - nagle nie działa?

Jeśli od jakiegoś czasu używasz CakePHP, to wiesz, że istnieje metoda kontrolera beforeFilter(). Jest to tak zwany callback wywoływany tuż po wykonaniu akcji kontrolera, ale przed renderowaniem widoku (źródło).
Dość częstą praktyką jest definiowanie metody w pliku app/app_controller.php, w klasie AppController po to, aby wykonywać w niej operacje, które zawsze muszą być wykonane przed każdą akcją kontrolera. Jednak czasem zdarza się, że jednocześnie inny programista zdefiniuje metodę beforeFilter() w swoim kontrolerze, wtedy niedoświadczony programista może mieć problemy ze znalezieniem przyczyny "dlaczego mój beforeFilter() w AppController się nie odpala?".
Zapraszam do przeczytania całości tego wpisu pod nowym adresem: http://blog.grzegorzpawlik.com/2008/02/appcontrollerbeforefilter-nagle-nie-dziala/

Uwaga! blog przeniesiony

Posty na tym blogu już nie będą się pojawiać. Zapraszam gorąco pod nowy adres: blog.grzegorzpawlik.com
Komentowanie artykułów możliwe jest pod nowym adresem.