Skip to content

Repository files navigation

Progetto Classe Fullstack PHP MYSL


Architettura

Layout_Website/
├── public/              ← document root Apache 
│   ├── index.php        ← front controller: routing → handler
│   ├── .htaccess        ← mod_rewrite → index.php
│   ├── assets/          ← CSS, JS, immagini, bootstrap vendor
│   └── pages/           ← view templates (home, login, admin/*, customer/*)
├── src/                 ← classi PSR-4 namespace App\
│   ├── Db.php           ← App\Db (Singleton PDO)
│   └── Models/          ← App\Models\{User,Product,Purchase,Booking}
├── bootstrap.php        ← autoload + config + session + exception handler
├── config/
│   ├── config.php       ← credenziali DB, BASE_URL, APP_ENV
│   ├── config.example.php
│   └── routes.php       ← mappa METHOD /path → pages/handler.php
├── helpers/auth.php     ← funzioni globali: requireLogin, requireAdmin, logout
└── ui/components/       ← partial: header.php, footer.php, modal_confirm.php

Autoload: spl_autoload_register in bootstrap.php risolve App\Xsrc/X.php. Routing: array statico in config/routes.php, risolto da public/index.php.


Mappa URL

URL
BASE_URL/
BASE_URL/login
BASE_URL/register
BASE_URL/dashboard
BASE_URL/logout
BASE_URL/buyproject
BASE_URL/customer/bookings
BASE_URL/customer/purchases
BASE_URL/admin/bookings
BASE_URL/admin/products
BASE_URL/admin/purchases
BASE_URL/admin/users
BASE_URL/admin/edit-product?id=X
BASE_URL/admin/edit-user?id=X

Configurazione Apache (XAMPP)

Scenario A — subfolder (rapido, niente vhost): Aggiornare config/config.php:

define('BASE_URL', '/Layout_Website/public');

L'app risponde a http://localhost/Layout_Website/public/. Comodo per dev.

Scenario B — virtual host (raccomandato, URL puliti): In C:\xampp\apache\conf\extra\httpd-vhosts.conf:

<VirtualHost *:80>
    ServerName mentoring.local
    DocumentRoot "C:/xampp/htdocs/Layout_Website/public"
    <Directory "C:/xampp/htdocs/Layout_Website/public">
        AllowOverride All
        Require all granted
    </Directory>
</VirtualHost>

In C:\Windows\System32\drivers\etc\hosts:

127.0.0.1 mentoring.local

Aggiornare config/config.php:

define('BASE_URL', '');

L'app risponde a http://mentoring.local/.


Setup ambiente locale

  1. Clona il repository
  2. Copia il template di configurazione e popola i valori reali:
    cp config/config.example.php config/config.php
    
  3. Modifica config/config.php con le tue credenziali MySQL/MariaDB, il BASE_URL corretto e APP_ENV
  4. Configura Apache per usare public/ come document root (vedi sezione "Configurazione Apache" sopra)
  5. Crea il database e applica le migration in ordine:
    mysql -u root -p -e "CREATE DATABASE mentoring_platform CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
    mysql -u root -p mentoring_platform < database/migrations/001_create_users.sql
    mysql -u root -p mentoring_platform < database/migrations/002_create_products.sql
    mysql -u root -p mentoring_platform < database/migrations/003_create_purchases.sql
    mysql -u root -p mentoring_platform < database/migrations/004_create_bookings.sql
    mysql -u root -p mentoring_platform < database/migrations/005_indexes_and_constraints.sql
  6. (Opzionale) Carica i dati di sviluppo:
    mysql -u root -p mentoring_platform < database/seeds/001_dev_seed.sql
  7. Avvia XAMPP (Apache + MySQL)
  8. config/config.php è escluso da .gitignore e non deve mai essere committato

Vedi database/README.md per la procedura via phpMyAdmin e il reset completo.


Sicurezza — Sessioni e CSRF

Parametri cookie di sessione

Il nome del cookie è MENTORING_SID. Parametri applicati all'avvio di ogni sessione:

Attributo Valore
HttpOnly true
SameSite Lax
Secure true in HTTPS, false in HTTP locale
Lifetime 0 (cookie di sessione, eliminato alla chiusura del browser)

Timeout idle e rigenerazione ID

  • Timeout idle: 30 minuti di inattività → logout automatico con redirect a /login?expired=1
  • Regeneration policy: ID di sessione rigenerato al login, al logout, e periodicamente ogni 15 minuti (_created_at tracking)
  • Anti session fixation: session_regenerate_id(true) al login garantisce che il vecchio ID non rimanga valido

CSRF — token per-sessione

  • Token: bin2hex(random_bytes(32)) — 64 caratteri hex, generato alla prima request della sessione
  • Confronto: hash_equals (timing-safe), mai == o ===
  • Validazione: automatica nel front controller public/index.php su tutti i POST
  • Campo form: <?= \App\Csrf::field() ?><input type="hidden" name="_csrf" value="...">
  • Rigenerato a ogni login e logout tramite Session::regenerate()Csrf::regenerate()
  • Whitelist endpoint esenti (vuota, placeholder per webhook Stripe in MOD7): $csrfExempt = [] in public/index.php

File implementativi

File Classe Responsabilità
src/Session.php App\Session start, regenerate, destroy, touch, isExpired
src/Csrf.php App\Csrf token, regenerate, check, field, enforce
public/pages/403.php Pagina errore CSRF/accesso negato

Session & CSRF

Test manuali da eseguire dopo la configurazione Apache (vedi sezione "Configurazione Apache"):

Verifica cookie — aprire DevTools → Application → Cookies dopo il login:

  • MENTORING_SID deve avere HttpOnly: ✅, SameSite: Lax
  • Secure: true solo in HTTPS; false in HTTP locale (XAMPP)

Verifica CSRF bloccato (deve restituire HTTP 403):

curl -X POST http://localhost/Layout_Website/public/login \
     -d "email=test@test.com&password=test" \
     -c cookies.txt -b cookies.txt
# Atteso: HTTP 403

Verifica CSRF funzionante:

  1. GET /login → estrarre il valore _csrf dall'input hidden del form
  2. POST /login con _csrf=<valore> e credenziali valide → HTTP 302 a /dashboard

Verifica timeout idle:

  • Effettuare il login, lasciare la pagina inattiva per 31 minuti
  • Ricaricare una pagina autenticata → redirect a /login?expired=1 con messaggio "La sessione è scaduta per inattività"

Verifica anti session fixation:

  1. Prendere nota del valore di MENTORING_SID prima del login
  2. Effettuare il login
  3. Verificare che MENTORING_SID sia cambiato

Sicurezza — Error handler e Logging

App\ErrorHandler (in src/ErrorHandler.php) è registrato in bootstrap.php tramite ErrorHandler::register(). Converte errori PHP in ErrorException e cattura tutte le eccezioni non gestite.

Comportamento per tipo:

  • RuntimeException('CSRF_TOKEN_INVALID') → 403 + pagina dedicata + log compatto
  • PDOException (connessione DB rotta) → 500 + stack trace in dev, pagina generica in prod
  • Qualsiasi altro Throwable → 500 + log completo

Log:

  • Path: LOG_DIR/app-YYYY-MM-DD.log (rotazione naturale per data)
  • Formato riga: [2026-06-25 14:23:11] [LEVEL] messaggio | ip=x uri=/path ua="..."
  • Stack trace su riga successiva per gli errori gravi
  • Retention manuale: cancellare file più vecchi di 90 giorni. Implicazioni GDPR (IP nei log): da gestire in MOD futuro
  • LOG_DIR configurabile in config/config.php, default Layout_Website/logs/ (fuori da public/)

Sicurezza — Security Headers

App\SecurityHeaders::apply() (in src/SecurityHeaders.php) è chiamato in public/index.php come primo middleware, prima del controllo CSRF, così i security headers sono presenti anche sulle pagine di errore.

Header Valore
Content-Security-Policy[-Report-Only] vedi sotto
X-Frame-Options DENY
X-Content-Type-Options nosniff
Referrer-Policy strict-origin-when-cross-origin
Permissions-Policy geolocation=(), camera=(), microphone=(), payment=(self)
Strict-Transport-Security max-age=31536000; includeSubDomains (solo HTTPS)

CSP — modalità report-only vs enforce:

La costante CSP_ENFORCE in config/config.php controlla la modalità:

  • 'report-only' (default) → header Content-Security-Policy-Report-Only: le violazioni compaiono in DevTools Console come warning ma non bloccano nulla. Usare per testare senza rompere l'app.
  • 'enforce' → header Content-Security-Policy: le violazioni bloccano il caricamento delle risorse.

Procedura passaggio a enforce:

  1. Impostare CSP_ENFORCE = 'report-only' e usare l'app normalmente per 1-2 sessioni
  2. Aprire DevTools → Console: se ci sono warning CSP, rilassare la policy in src/SecurityHeaders.php
  3. Quando la console è pulita, impostare CSP_ENFORCE = 'enforce'

⚠️ HSTS (Strict-Transport-Security): emesso solo in HTTPS. Una volta che il browser riceve questo header, non comunicherà più con il sito in HTTP per max-age=31536000 (1 anno). Non abilitare HTTPS + HSTS in produzione senza un piano di rinnovo certificato.

TODO (MOD futuro): rimuovere 'unsafe-inline' da script-src e style-src dopo il cleanup di tutti gli <style> e <script> inline sparsi nel codice.


Sicurezza — Rate limiting

App\RateLimit (in src/RateLimit.php) usa la tabella DB rate_limit_attempts per tracciare i tentativi per IP/endpoint. Controllato dalla costante RATE_LIMIT_ENABLED in config/config.php (impostare false per disabilitare in test/dev).

Endpoint Max tentativi Finestra
POST /login 5 15 minuti
POST /register 3 60 minuti
POST /buyproject 10 60 minuti

Al superamento della soglia: risposta HTTP 429 con header Retry-After (secondi) e pagina public/pages/429.php.

Nota: il rate limit è per IP (REMOTE_ADDR), non per account. Non legge X-Forwarded-For senza un reverse proxy validato (rischio spoofing). Per deploy dietro Nginx/Cloudflare: aggiornare RateLimit::clientIp().

Migration: applicare database/migrations/006_create_rate_limit_attempts.sql manualmente (vedi database/README.md).

Manutenzione: pulizia periodica (mensile):

DELETE FROM rate_limit_attempts WHERE attempted_at < DATE_SUB(NOW(), INTERVAL 30 DAY);

Asset esterni — versioning e SRI

Risorsa Versione Hosting Hash SRI
Font Awesome 6.5.2 CDN (cdnjs) sha384-PPIZEGYM1v8zp5Py7UjFb79S58UeqCL9pYVnVPURKEqvioPROaVAJKKLzvH2rDnI
Animate.css 4.1.1 CDN (cdnjs) sha384-Gu3KVV2H9d+yA4QDpVB7VcOyhJlAVrcXd0thEjr4KznfaFPLe0xQJyonVxONa4ZC
Bootstrap 5.x Locale (public/assets/vendor/bootstrap/) n/a
Chart.js 4.4.0 Locale (public/assets/vendor/chartjs/) n/a

Ricalcolo hash SRI (se aggiorni una versione CDN):

curl -sL <URL_RISORSA> | openssl dgst -sha384 -binary | openssl base64 -A

Poi aggiornare integrity="sha384-<nuovo_hash>" nel tag HTML corrispondente in ui/components/header.php.


Verifica

Test manuali da eseguire dopo aver applicato la migration 006 e configurato Apache:

1. Error handler — connessione DB rotta:

  • Cambiare temporaneamente DB_PASS in config.php con un valore errato
  • Aprire / nel browser
  • Atteso in dev: pagina con stack trace. In prod: pagina 500 generica
  • Verificare che esista una riga in logs/app-<data>.log con dettaglio PDOException
  • Ripristinare DB_PASS

2. Security headers — DevTools → Network → selezionare una request → tab Headers:

  • Deve essere presente Content-Security-Policy-Report-Only (con la policy completa)
  • X-Frame-Options: DENY
  • X-Content-Type-Options: nosniff
  • Referrer-Policy: strict-origin-when-cross-origin
  • Permissions-Policy: geolocation=(), ...
  • X-Powered-By deve essere assente

3. Rate limiting — login:

  • Tentare login con password errata 6 volte di fila dallo stesso IP
  • Il 6° tentativo deve restituire HTTP 429 con pagina dedicata e header Retry-After

4. SRI funzionante — DevTools → Console dopo aver caricato una pagina con Font Awesome:

  • Nessun errore "Failed to find a valid digest"
  • Test manomissione: modificare integrity con valore errato → le icone non si caricano (browser blocca)

5. CSP in report-only — DevTools → Console:

  • Eventuali violazioni compaiono come warning, non bloccano
  • Se ci sono anomalie, rilassare la policy in src/SecurityHeaders.php prima di passare a enforce

Sicurezza — rotazione credenziali

Procedura di rotazione password MySQL

  1. Apri phpMyAdmin o una sessione MySQL CLI
  2. Esegui:
    ALTER USER 'utente'@'localhost' IDENTIFIED BY 'nuova_password_robusta';
    FLUSH PRIVILEGES;
  3. Aggiorna config/config.php con la nuova password
  4. Riavvia Apache da XAMPP Control Panel

Rimozione delle credenziali dallo storico git (operazione manuale)

Il comando git rm --cached config/config.php rimuove il file dal tracking git ma non cancella lo storico: le vecchie credenziali sono ancora ricostruibili da chiunque abbia accesso alla history del repository.

Per rimuoverle completamente dallo storico è necessario riscrivere la history con uno dei seguenti tool:

  • git filter-repo (raccomandato): git filter-repo --path config/config.php --invert-paths
  • BFG Repo-Cleaner: java -jar bfg.jar --delete-files config.php

⚠️ Dopo la riscrittura dello storico:

  1. Eseguire git push --force su tutti i remote
  2. Tutti i collaboratori devono ri-clonare il repository (i clone esistenti avranno storico divergente)

Questa operazione non è stata eseguita automaticamente: deve essere eseguita manualmente da Diego.

Limite noto degli .htaccess

Le cartelle config/, classes/, helpers/, sql/, docs/ sono protette da file .htaccess con Require all denied (Apache 2.4+). Questa è una difesa in profondità che funziona solo se Apache ha AllowOverride All o AllowOverride AuthConfig Limit attivo nel virtual host. Su server con AllowOverride None gli .htaccess vengono ignorati. La soluzione definitiva è spostare queste cartelle fuori dal document root (pianificato in MOD3).


Validazione centralizzata — App\Validator

App\Validator (in src/Validator.php) sostituisce i controlli manuali sparsi (if ($name === '') ..., filter_var(...)) con un sistema unificato e testabile.

Utilizzo

$v = \App\Validator::make($_POST, [
    'name'  => 'required|string|min:2|max:100',
    'email' => 'required|email|max:190',
    'price' => 'required|numeric|min:0|max:99999.99',
]);

if ($v->fails()) { /* mostra errori */ }
if ($v->passes()) { /* procedi */ }

Regole disponibili

Regola Esempio Descrizione
required required Campo non vuoto
string string Deve essere una stringa
email email Formato email valido (filter_var)
integer integer Valore intero
numeric numeric Valore numerico (intero o float)
min:N min:2 Lunghezza minima (string) o valore minimo (numeric)
max:N max:100 Lunghezza massima (string) o valore massimo (numeric)
in:a,b,c in:client,admin Valore in whitelist
date date Formato Y-m-d
time time Formato HH:MM
same:field same:password Uguale al valore di un altro campo
regex:/pattern/ regex:/^\d{4}$/ Match regexp

Helper form (helpers/form.php)

Funzione Utilizzo
field_error($v, 'field') Stampa <div class="invalid-feedback">messaggio</div> se il campo ha errori
field_invalid_class($v, 'field') Restituisce 'is-invalid' se il campo ha errori, altrimenti ''
old('field', $default) Ripopola il valore dal $_POST precedente; i campi sensibili (password, _csrf, ecc.) restituiscono sempre ''

Pagine

File Azione validata
public/pages/login.php POST login (formato email + password)
public/pages/register.php POST registrazione (name, email, password, confirm)
public/pages/customer/bookings.php POST nuova prenotazione (date, time, notes)
public/pages/admin/users.php POST crea utente (name, email, password, role)
public/pages/admin/edit_user.php POST modifica utente (name, email, password opzionale, role)
public/pages/admin/products.php POST crea prodotto (name, description, price)
public/pages/admin/edit_product.php POST modifica prodotto (name, description, price)

Comportamento campi opzionali

Un campo senza required e assente o vuoto in $_POST supera automaticamente tutte le regole (es. password in modifica utente). La logica applicativa controlla poi se il valore è presente per decidere se aggiornare.

Test

tests/manual/validator_test.php contiene 33 test che coprono tutti i casi edge. Eseguire con:

php tests/manual/validator_test.php
# Atteso: === 33 test eseguiti, 0 falliti ===

Verifica

1. Test validator suite:

php tests/manual/validator_test.php

Tutti e 33 i test devono passare.

2. Form login — messaggio generico:

  • Inviare il form con email malformata → messaggio "Credenziali non valide." (nessun dettaglio per anti-enumeration)

3. Form registrazione — errori inline:

  • Inviare con name di 1 carattere → errore inline sotto il campo Name
  • Inviare con password_confirm diversa da password → errore inline sotto Confirm Password
  • Verificare che i campi name/email mantengano il valore inserito dopo l'errore (form repopulation via old())

4. Form prenotazione (cliente):

  • Selezionare data passata → errore "La data non può essere nel passato."
  • Inviare senza data → errore inline sul campo Date

5. Form crea utente (admin):

  • Inviare con email già esistente → errore di business (email duplicata) sotto il form
  • Inviare con password < 8 caratteri → errore inline sul campo Password

6. Form modifica utente (admin):

  • Pagina mostra i valori attuali dell'utente pre-popolati
  • Campo password opzionale: lasciare vuoto → password non aggiornata
  • Campo password compilato (≥ 8 char) → password aggiornata al submit

7. Form prodotti (admin):

  • Inviare prezzo negativo → errore inline sul campo Price
  • Descrizione > 5000 caratteri → errore inline sul campo Description

Database

Lo schema del database è versionato in database/.

  • Migration DDL (ordine di esecuzione): database/migrations/001005
  • Seed di sviluppo: database/seeds/001_dev_seed.sql
  • Divergenze rilevate: database/DIVERGENZE.md

Troubleshooting — CSS non si carica / link 404

Sintomi

  • Pagine "nude" senza stile Bootstrap o style.css
  • Click su "Accedi" / "Registrati" restituisce 404 o pagina directory listing Apache

Diagnosi rapida (DevTools → Network)

  1. Ricaricare la pagina con DevTools aperti
  2. Filtrare per .css: controllare l'URL richiesto
    • URL senza /public/: es. /Layout_Website/assets/css/style.cssBASE_URL manca di /public
    • URL con doppio slash: es. /Layout_Website/public//assets/...BASE_URL ha slash finale in eccesso

Soluzione — config/config.php

Verificare che BASE_URL corrisponda allo scenario di deploy:

// Scenario A — subfolder XAMPP (URL: http://localhost/Layout_Website/public/)
define('BASE_URL', '/Layout_Website/public');

// Scenario B — virtual host (URL: http://mentoring.local/)
define('BASE_URL', '');

Regola: BASE_URL senza slash finale. I template aggiungono già / prima di ogni path (<?= BASE_URL ?>/login).

Verifica mod_rewrite

In C:\xampp\apache\conf\httpd.conf la riga:

LoadModule rewrite_module modules/mod_rewrite.so

non deve essere commentata (#). Dopo ogni modifica a httpd.conf riavviare Apache dal XAMPP Control Panel.

Verifica AllowOverride

Nella stessa httpd.conf, la sezione <Directory "C:/xampp/htdocs"> deve contenere:

AllowOverride All

Senza questa direttiva il file public/.htaccess (con RewriteEngine On) viene ignorato e tutte le route restituiscono 404.


UX e i18n italiano

Timezone

  • date_default_timezone_set('Europe/Rome') aggiunto come prima istruzione in bootstrap.php
  • SET time_zone = '+01:00' eseguito subito dopo la connessione PDO in src/Db.php
  • Tutte le date vengono ora visualizzate nel fuso italiano (CET/CEST secondo l'ora di sistema); le DATETIME sono salvate in DB come inserite (ora locale)

Formato date e prezzi

  • Date: d/m/Y (es. 25/06/2026)
  • Datetime: d/m/Y H:i
  • Prezzi: number_format($price, 2, ',', '.')€ 1.234,56

Loading state — public/assets/js/forms.js

File JS vanilla (IIFE) centralizzato, incluso da footer.php e footer_dashboard.php in sostituzione degli script inline.

  • Loading state: al submit disabilita il bottone e mostra spinner Bootstrap + testo "Attendere...". Safety reset automatico dopo 10 secondi se la navigazione non avviene.
  • Modal confirm: intercetta click su bottoni con attributo data-confirm-target, popola il modal Bootstrap #confirmModal e al confermato sottomette il form target.

Modal confirm — attributi richiesti

<button type="button"
        data-confirm-target="form-id"
        data-confirm-title="Titolo modale"
        data-confirm-body="Testo di descrizione"
        data-confirm-btn="Testo bottone conferma"
        data-confirm-btn-class="btn-danger">
    Azione
</button>

Pagina dettaglio prodotto

Route: GET /product?id=Npublic/pages/product_detail.php

  • Redirect a /buyproject se id <= 0
  • 404 se prodotto non trovato o inattivo
  • Mostra immagine, nome, prezzo €X.XXX,XX, descrizione completa
  • Bottone Acquista ora (con modal confirm) → solo utenti loggati non-admin
  • Link Accedi per acquistare → utenti non loggati
  • Link Modifica prodotto → solo admin
  • Bottone "Vedi dettagli" aggiunto su ogni card in buyproject.php

6. Pagina prodotto:

  • GET /product?id=1 → visualizza dettaglio
  • GET /product?id=0 o GET /product?id=999 → redirect o 404
  • Bottone acquisto: presente se loggato come cliente, assente se admin, "Accedi" se non loggato

About

Progetto didattico per studenti

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages