Modella! è un sistema di prototipizzazione che, a partire da uno schema dati SQL, permette di generare il codice necessario alla sua gestione all’interno di un sistema Web. Con questo sistema di prototipizzazione, che consente di produrre codice con alti standard di sicurezza riducendo i tempi di realizzazione del software, sono stati realizzati diversi sistemi, fra cui:
- il CMS Publio, utilizzato per la gestione del sito Web globalist.it;
- alcuni siti Web con alti carichi di lavoro;
- un sistema di gestione degli eventi per un’associazione farmaceutica;
- il sistema di gestione delle istanze per il CORECOM del Lazio;
- un sistema per la gestione dei test di sicurezza applicativa (Seguro!);
- il sistema di gestione degli iscritti per un Movimento politico.
Nella versione attuale, Modella! richiede la definizione dei campi dell’entità da gestire, ma dalla prossima versione sarà il sistema a leggere la tabella direttamente dal DB.
L’output del sistema per la gestione di un’ipotetica classe utente comprende i file PHP:
utente.inc | dichiarazioni per la classe |
utente.api.php | funzioni di interfaccia per la generazione delle pagine |
utente.db.php | funzioni di interfaccia DB |
utente.delete.php | script di cancellazione del record |
utente.elenco.php | pagina con elenco dei record |
utente.form.php | maschera di modifica del record |
utente.insert.php | script di inserimento del record |
utente.new.php | maschera di inserimento del record |
utente.update.php | script per la modifica del record |
e gli script SQL per la generazione delle entità del DB:
t_utente.sql | tabella |
f_utente_delete.sql | stored-procedure di eliminazione del record |
f_utente_insert.sql | stored-procedure di inserimento del record |
f_utente_update.sql | stored-procedure di modifica del record |
v_utente_elenco.sql | vista per la pagina di elenco record |
v_utente_load.sql | vista per le pagine di modifica dati |
Tutti gli script sono generati a partire da dei modelli predefiniti nei quali il sistema inserisce, di volta in volta, i valori opportuni per i dati dell’entità da gestire.
Flusso del programma per sistemi di back-end
Lo schema di funzionamento per un’interfaccia gestionale, che permetta le operazioni di inserimento, modifica e cancellazione delle entità, è:
Inserimento nuova entità
utente.elenco.php | mostra l’elenco (inizialmente vuoto) delle entità e un link alla pagina di inserimento nuovo utente |
utente.new.php | mostra la maschera di inserimento nuova entità |
utente.insert.php | richiama la stored-procedure di inserimento nuova entità |
utente.form.php | mostra la maschera di modifica entità |
Modifica dati entità
utente.elenco.php | mostra l’elenco delle entità con link alle maschere di modifica dati |
utente.form.php | mostra la maschera di modifica dati entità |
utente.update.php | richiama la stored-procedure di aggiornamento dei dati dell’entità |
utente.form.php | torna alla maschera di modifica entità |
Cancellazione entità
utente.elenco.php | mostra l’elenco delle entità con link alle maschere di modifica dati |
utente.form.php | mostra la maschera di modifica dati entità |
utente.delete.php | richiama la stored-procedure per la cancellazione dell’entità |
utente.elenco.php | torna all’elenco entità |
Flusso del programma per sistemi di front-end
Lo schema di funzionamento per un’interfaccia in sola lettura dei dati può essere un sotto-insieme delle funzioni del sistema di gestione, tipicamente un elenco delle entità e una scheda con i dati in sola lettura.
Applicazioni pratiche
Questo paradigma è stato applicato con buon successo a siti di natura differente:
Interfaccia di amministrazione del sito Globalist
Interfaccia pubblica del sito iVid
Caratteristiche del sistema
Ciascuna funzione è svolta da uno script specifico, in modo da rendere più rapido il debug del codice.
Le funzioni di interfaccia sono slegate dalle funzioni di gestione della base-dati che, al momento, possono gestire sia mySQL che PostgreSQL e possono venire estese ad altri RDBMS.
Queste sono le funzioni PHP di cancellazione e lettura di un record:
function utente_delete($id)
{
global $sessione;
$esito = ERR_UTENTE_NONE;
// verifica che l'ID sia un numero intero
if(is_id($id)) {
// definisce la query per chiamare la stored-procedure
$query = "SELECT utente_delete(".$id.",".$sessione->id_utente().")";
// esegue la query e torna l'esito dell'operazione
$esito = db_exec_result($query);
// verifica se il codice indichi un errore
$errore = utente_errore($esito);
if($errore)
throw new UIException(S_ERR_UTENTE, S_ERR_UTENTE_DELETE, $errore);
}
return $esito;
}
function utente_load($id)
{
$utente = null;
// legge i dati dell'utente */
if(is_id($id)) {
$utente = db_exec_object("SELECT * FROM utente_load WHERE id = ".$id);
}
// verifica che i dati ci siano..
if(is_null($utente))
throw new UIException(S_ERR_UTENTE, S_ERR_UTENTE_LOAD, S_ERR_UTENTE_NONE);
return $utente;
}
Il sistema Web si limita a richiamare la stored-procedure utente_delete
, passandole gli opportuni parametri, senza sapere né cosa faccia la funzione né su quale DB agisca.
Allo stesso modo, richiama i dati dalla vista utente_load
, senza doversi preoccupare di quali campi debbano essere richiamati.
Questo approccio ha diversi lati positivi:
- permette di assegnare il lavoro di creazione/modifica dell’interfaccia utente e delle procedure di interfaccia con il DB a risorse con differente esperienza;
- permette di isolare l’interfaccia utente del sistema dall’interfaccia DB: il passaggio del sito iVid dalla libreria
mysql
amysqli
ha richiesto la modifica di un solo file; - permette di non far conoscere ai programmatori che si occupano dell’interfaccia la struttura e il contenuto del DB sottostante;
- rende più complesso un attacco di SQL injection al sistema.
Il codice SQL per la generazione della vista utente_load
, così come viene prodotto dal sistema è:
CREATE VIEW utente_load AS
SELECT
f.*
-- , u.username AS modificato_da
-- , x.descrizione AS xxx
-- , date_format(f.ts_creazione, '%d-%m-%y') AS creato_fmt
FROM
utente f
-- utente u
-- WHERE
-- f.id_modifica = u.id
;
Le righe a commento, inserite automaticamente dal sistema, possono essere utilizzate per aggiungere delle informazioni alla vista, come la decodifica di eventuali codici numerici o delle date in formato g/m/a.
Il codice della stored-procedure di inserimento nuovo utente è:
CREATE FUNCTION utente_insert (
_username VARCHAR(80)
, _password VARCHAR(80)
, _nome VARCHAR(80)
, _cognome VARCHAR(80)
, _e_mail VARCHAR(250)
, _attivo BOOLEAN
, _id_modifica INTEGER
)
RETURNS INTEGER DETERMINISTIC
BEGIN
DECLARE _id INTEGER DEFAULT -1;
DECLARE _esito INTEGER DEFAULT 0;
DECLARE _count INTEGER DEFAULT 0;
-- verifica eventuali duplicazioni
SELECT count(*) INTO _count FROM utente WHERE (username = _username);
IF _count > 0 THEN
RETURN -2;
END IF;
-- verifica che l'utente esista
SELECT count(*) INTO _count FROM utente WHERE (id = _id_modifica) AND (attivo = true);
IF _count = 0 THEN
RETURN -3;
END IF;
-- verifica lo username
IF _username IS NULL OR _username = '' THEN
RETURN -4;
END IF;
-- verifica la password
IF _password IS NULL OR _password = '' THEN
RETURN -5;
END IF;
-- verifica il nome
IF _nome IS NULL OR _nome = '' THEN
RETURN -6;
END IF;
-- verifica il cognome
IF _cognome IS NULL OR _cognome = '' THEN
RETURN -7;
END IF;
-- verifica l'indirizzo e-mail
IF _e_mail IS NULL OR _e_mail = '' OR e_mail_valida(_e_mail) = FALSE THEN
RETURN -8;
END IF;
-- inserisce i dati
INSERT INTO utente(
username
, password
, nome
, cognome
, e_mail
, attivo
, id_modifica
)
VALUES(
_username
, sha1(_password)
, _nome
, _cognome
, _e_mail
, _attivo
, _id_modifica
);
-- definisce l'ID del record
SET _id = last_insert_id();
RETURN _id;
END;
La sequenza di controlli:
IF _campo IS NULL OR _campo = '' THEN
RETURN errore;
END IF;
non è particolarmente elegante, ma è più facile da generare automaticamente. :-)
Ciascun codice di errore è riportato nella funzione PHP utente_errore
:
function utente_errore($result)
{
$errore = null;
switch($result){
case ERR_UTENTE_COGNOME : $errore = S_ERR_UTENTE_COGNOME ; break;
case ERR_UTENTE_DUE : $errore = S_ERR_UTENTE_DUE ; break;
case ERR_UTENTE_E_MAIL : $errore = S_ERR_UTENTE_E_MAIL ; break;
case ERR_UTENTE_GRANT : $errore = S_ERR_UTENTE_GRANT ; break;
case ERR_UTENTE_NOME : $errore = S_ERR_UTENTE_NOME ; break;
case ERR_UTENTE_NONE : $errore = S_ERR_UTENTE_NONE ; break;
case ERR_UTENTE_PASSWORD: $errore = S_ERR_UTENTE_PASSWORD; break;
case ERR_UTENTE_SOSPESO : $errore = S_ERR_UTENTE_SOSPESO ; break;
case ERR_UTENTE_USERNAME: $errore = S_ERR_UTENTE_USERNAME; break;
}
return $errore;
}
Le costanti numeriche per i codici di errore e le corrispondenti stringhe per i messaggi di notifica sono definite nel file utente.inc.
Questo è un esempio di codice per la pagina di modifica dati utente:
try {
// verifica che l'utente abbia i privilegi necessari
// per accedere alla pagina
acl_check(SLUG_UTENTE_FORM);
// aggiorna la sequenza delle pagine visitate
$sessione->path_aggiorna();
// legge l'ID dell'utente dai parametri in GET
// la funzione param esegue gli opportuni controlli
// per la bonifica dei dai in ingresso
$id = param($_GET, 'p_id', $sessione->id_utente());
// legge i dati dell'utente *
$utente = utente_load($id);
if(is_null($utente))
throw new Exception(S_ERR_UTENTE_NONE);
// definisce il titolo della pagina
$titolo = S_UTENTE_DATI." ".$id;
// apre la pagina
html_begin();
html_head(SLUG_UTENTE_FORM, $titolo);
html_body_begin(SLUG_UTENTE_FORM);
html_header($titolo);
// visualizza la maschera di modifica dati utente
utente_form($utente);
// chiude la pagina
html_body_end();
html_end();
} catch (UIException $e) {
errore_eccezione($e);
}
Come è facile immaginare, Modella genera un numero elevato di file (almeno quindici per ciascuna entità gestita), ma questo è un problema relativo, perché, in fase di creazione del sistema, il codice è in gran parte già pronto e richiede solo un numero limitato di modifiche, mentre in fase di modifica/debug, l’associazione diretta di ciascuna funzione a uno script permette di identificare rapidamente il punto in cui è necessario intervenire.
Un altro lato positivo di questo paradigma è che tutte le entità di tutti i sistemi sono gestite pressocché nello stesso modo; quindi, una volta imparato il funzionamento del sistema per una determinata entità si è in condizione di operare con (relativa) facilità anche su altre entità o altri sistemi.
Un’ultima nota, relativa alla sicurezza: il paradigma su cui è basato Modella implementa in maniera automatica, tutta una serie di procedure e di accorgimenti che incrementano la sicurezza del sistema, indipendentemente dalla specifica esperienza o dal livello di consapevolezza dei programmatori.