venerdì, maggio 17, 2013

Applicazioni web lato server, stato dell'arte

Riporto una serie di considerazioni utili per la progettazione di un'applicazione web, che nelle intenzioni dovrebbero delineare lo "stato dell'arte". Sicuramente si possono considerare altri aspetti (organizzazione del codice, unit testing, internazionalizzazione e localizzazione, ecc.), ma su questi aspetti magari tornerò un'altra volta.

In sintesi:
  1. lo sviluppo dell’applicazione dovrebbe differenziare il codice relativo al trattamento dei dati, quello relativo alla presentazione degli stessi e quello dedicato invece al controllo dell’accesso ai moduli applicativi; questa differenziazione viene comunemente indicata con la sigla MVC (ModelViewController), dove per model / modello si intende ciò che serve a rappresentare le informazioni (quindi il trattamento dei dati), per view / vista il codice che serve per la rappresentazione, e per controller / controllore il codice dove viene sostanzialmente deciso quali (categorie di) utenti possono svolgere quali operazioni;
  2. l’applicazione dovrebbe basarsi quanto più possibile sul rispetto dello standard HTTP in merito al significato dei “verbi” espressi nelle richieste (GET, POST, PUT*, DELETE*), con identificazione corretta delle risorse tramite ID univoci, gestione di rappresentazione multiple delle stesse risorse, risposte del server con i codici corretti (200 Ok, 401 Unauthorized, 403 Forbidden, 404 File not found, ecc.), e dopo l'esecuzione di operazioni tramite POST il browser dell'utente dovrebbe essere ridirezionato ad una pagina ottenuta con il metodo GET;
  3. l’accesso a dati presenti in una base di dati dovrebbe avvenire, ogni qualvolta ciò sia possibile, mediante l’uso di librerie / interfacce che forniscano un’astrazione rispetto allo specifico DBMS utilizzato; in questo modo, qualora si decidesse di cambiare il DBMS scelto (o di usare diversi DBMS con le diverse istanze dell’applicazione) non si avrà la necessità di modificare il codice sorgente;
  4. l’esecuzione delle query sul DBMS dovrebbero sfruttare metodologie, quali ad esempio i prepared statements, che consentono di renderle più efficienti e più flessibili nell’uso da parte del programmatore, con il vantaggio aggiuntivo di proteggere da attacchi di tipo SQL injection;
  5. le connessioni al DBSM da parte dell’applicazione dovrebbero essere ridotte al minimo, per cui, se per produrre una pagina web si devono effettuare più query, è bene che vengano eseguite le diverse query nell’ambito della stessa connessione; questo si può ottenere facilmente usando il design pattern denominato Singleton, che fa sì che venga assicurata l’esistenza di una sola istanza di una determinata classe (nel nostro caso, la classe per l’accesso ai dati);
  6. in un sistema software progettato in maniera object-oriented, sarà molto probabilmente naturale definire una classe per ogni relazione presente nella base di dati, creando di fatto un legame tra gli oggetti del mondo dell’applicazione e le corrispondenti tuple del mondo della base di dati; per evitare inutili riscritture di codice, è consigliabile sfruttare strumenti software, denominati ORM (object-relational mappers) che:
    1. generano automaticamente il codice sorgente delle classi necessarie, a partire dallo schema relazionale (o viceversa);
    2. forniscono funzioni per accedere ai dati in modalità object-oriented;
    3. permettono di astrarre dall’uso del codice SQL, in maniera tale da eseguire le query più comuni senza bisogno di scrivere istruzioni SQL (consentendone l’uso, però, in caso di necessità sofisticate, magari con modalità che permettano la costruzione di queryanziché la generazione di stringhe);
    4. mettono a disposizione funzioni predefinite per tutte le operazioni CRUD (Create, Retrieve, Update e Delete), nonché, spesso, per le ricerche, la paginazione dei risultati, la validazione dell’input, ecc.;
  7. l’applicazione dovrebbe avere un unico punto di ingresso (ossia, indipendentemente dall’URL della risorsa richiesta o inviata, dobbiamo essere sicuri che venga sempre eseguito un insieme di istruzioni specifico di controllo del flusso di esecuzione).

Nelle applicazioni sviluppate con il linguaggio di programmazione PHP, è quindi altamente consigliato l’uso di PDO anziché, ad esempio, delle funzioni della serie mysql_*(), che sono deprecate (vedi in merito l'interessante PDO tutorial for MySQL developers) in quanto:
  1. non supportano concetti avanzati quali prepared statements e transazioni;
  2. non mettono a disposizione la possibilità di scrivere codice object-oriented;
  3. necessitano di codice aggiuntivo per la creazione delle query (ad esempio per l’escape di caratteri speciali), che complicano la vita del programmatore e portano spesso a bug;
  4. non permettono una valida gestione delle condizioni di errore;
  5. non sono più mantenute (il che vuol dire che eventuali nuove vulnerabilità scoperte non verranno sistemate).
Inoltre, in una logica di separazione secondo il design pattern MVC, sarà da considerare errata la scrittura di codice che mescola l’accesso ai dati e la visualizzazione dei risultati, e sarà preferibile utilizzare un approccio object-oriented anche per gli oggetti recuperati.

Un esempio “quick and dirty” di codice PHP per la visualizzazione dei dati di un’ipotetica tabella customers potrebbe essere il seguente:

<?php

$host     = 'localhost';
$user     = 'foobar';
$password = 'pXC4FUb6bNFusDBV';
$dbname   = 'foobar';

$type = 'A';
//$type = "'";

// Instaurazione della connessione con il DBMS
$cn = mysql_connect($host, $user, $password);

// Selezione del database su cui verrà eseguita la query
mysql_select_db($dbname, $cn);

// Esecuzione della query
$result = mysql_query("SELECT id, name, city FROM customers WHERE type = '" . $type . "'", $cn);

// Ciclo sulle tuple ritornate dalla query
while ($row = mysql_fetch_array($result))
{
    printf("<p>id=%s, name=«%s», city=%s</p>", $row[0], $row[1], $row[2]);
}

// Liberazione della memoria associata al risultato della query e chiusura della connessione
mysql_free_result($result);
mysql_close($cn);
 

Analizzando il codice, notiamo i seguenti problemi:
  1. la connessione viene aperta e chiusa nel frammento individuato; è però estremamente probabile che, in un’applicazione web, si debbano effettuare più query per ottenere il risultato desiderato (ad esempio per controllare chi è l’utente connessio, se ha le autorizzazioni necessarie, ecc.), e quindi sarebbe bene spostare la gestione della connessione altrove;
  2. non c’è nessuna gestione dell’errore (cosa succederebbe se la password per l’accesso al database non fosse corretta, o se per qualche motivo il DBMS non dovesse rispondere?);
  3. la query è composta direttamente come stringa “hard-coded”, e questo comporta una serie di problemi (che cosa succede se il nome della tabella dovesse cambiare, ad esempio per l’aggiunta di un prefisso? come può essere gestito un log delle query eseguite? ecc.);
  4. l’uso di array indicizzati da numeri interi pone problemi nel caso di modifica della query;
  5. il tipo viene concatenato alla stringa e, a parte la seccatura di dover gestire le virgolette, c’è il piccolo problema che si otterrebbe un errore nel caso esso stesso contenesse delle virgolette all’interno;
  6. se il valore del tipo ci venisse dato in input, saremmo soggetti ad attacchi di tipo SQL-injection;
  7. viene mescolato il codice per la visualizzazione dei dati con quello per il loro recupero (e se un giorno volessimo visualizzare dati provenienti da fonti diverse, come un file XML o JSON?);
  8. siamo vincolati all’uso del DBMS MySQL.
Insomma, ci sono abbastanza motivi per riscrivere il codice utilizzando un po’ di buon senso e buone pratiche.


<?php

require_once('db.php');  // qui ci sarà il codice per la gestione della connessione al DBMS

$type = 'A';

$stmt = $dbh->prepare("SELECT id, name, city FROM customers WHERE type = ?");

$stmt->bindParam(1, $type);

$customers = array();
if ($stmt->execute()) {
  while ($row = $stmt->fetch(PDO::FETCH_OBJ)) {
    $customers[]=$row;
  }
}
else
{
  print_r($stmt->errorInfo());
}

?>

<?php foreach($customers as $customer): ?>
  <p>id=<?php echo $customer->id ?>, name=«<?php echo $customer->name ?>», city=<?php echo $customer->city ?></p>
<?php endforeach ?>

Da notare l'uso della sintassi alternativa per il codice PHP del livello view (in alternativa, molti preferiscono utilizzare template engines che non usano istruzioni PHP, quali smarty e i suoi derivati,
twig, ecc.).
Il file db.php conterrà il codice necessario per l’attivazione della connessione:


<?php

require_once('config.php');

try {
    $dbh = new PDO($connectionString, $user, $password, array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION));}
catch(PDOException $e) {
    echo 'Error: ' . $e->getMessage();
}

e il file config.php conterrà le
credenziali per l’accesso al DBMS (in cui, per garantire la compatibilità con più DBMS, viene messa una stringa di connessione, come nell'esempio seguente).



$connectionString = "mysql:host=localhost;dbname=foobar";

Nel costruttore dell'oggetto PDO è possibile, come nell'esempio, impostare il modo con cui devono essere gestiti eventuali errori (a parte quello relativo alla connessione), come ad esempio violazioni di vincoli di integrità referenziale. Il default è che PHP rimanga in silenzio, per cui se si vuole gestire eventuali errori è bene impostare il lancio di eccezioni.

Un classico caso di necessità di controllo delle eccezioni si ha quando si deve gestire una transazione con più query delle quali una potrebbe fallire, e si vuole fare in modo che in caso di fallimento tutte le query eseguite vengano annullate. Con PDO si può gestire facilmente la cosa tramite una transazione:

try {
    $dbh->beginTransaction();
    // prima query
    // seconda query
    // terza query
    $dbh->commit();
}
catch (Exception $e)
{
    $dbh->rollBack();
}


Rimane da dire che usando un ORM quale, ad esempio, CActiveRecord, l’estrazione dei dati da una tabella del database diventa poi ancora più semplice, visto che l’esecuzione della query e il recupero dei dati potrebbe ridursi ad una semplice riga di codice:

$customers = Customer::model()->findAllByAttributes(array('type'=>$type));


Note e chiarimenti

* I metodi PUT e DELETE non sono consentiti dall'HTML come valore per l'attributo "method" dell'elemento "form". Una loro implementazione può essere utile in caso di definizione di un'interfaccia RESTful o per rispondere a chiamate AJAX.

lunedì, maggio 13, 2013

"Da: no-replay@" anziché "Da: no-reply@"

Fateci caso, se siete iscritti a qualche newsletter: è più comune di quanto uno potrebbe pensare.


Sto parlando di quelli che mandano le email da un indirizzo fittizio al quale non si desidera ricevere risposte. Legittimo, solo che "rispondere" in inglese si dice reply, non replay (che invece significa "ripetere", "risuonare", "rimandare in onda").