# Démo Oracle — architecture technique

## Le principe qui gouverne tout le module

**Une action retirée de la démonstration est retirée du serveur, pas de
l'interface.**

Griser un bouton ne protège rien : un `POST` forgé atteint le gestionnaire quel
que soit l'état du bouton. Le module Oracle complet compte une vingtaine de
gestionnaires d'action dans son `report.php` — arrêt de session, `ALTER
SYSTEM`, redimensionnement de fichier, `DBMS_STATS`, profils SQL, points de
restauration, politiques d'audit. Ici, `report.php` n'en contient **aucun**, et
le fichier le dit en en-tête.

Ce que fait l'interface — griser, expliquer, renvoyer vers la version complète
— est de la **communication**, jamais de la sécurité. Les deux ne doivent pas
être confondues, et le code est écrit pour qu'on ne puisse pas les confondre.

## Autonomie : trois contraintes, trois conséquences

Le module vit dans son propre dépôt public et doit s'installer seul, sans
modifier le dépôt du cœur. Trois points où le module complet s'appuie sur le
cœur, et ce qui les remplace ici :

| Le module Oracle complet… | Démo Oracle… |
| --- | --- |
| reçoit sa barre latérale de `mon-appli/includes/header.php`, qui la code en dur dès que `oracle_url()` existe | rend sa **propre** navigation, deux rangées sans JavaScript, dans `views/report/layout.php` |
| sert des URL propres `/oracle/report/config/tablespaces` grâce à un front-controller et à une règle de réécriture posées dans le cœur | route lui-même par `report.php?section=…`, comme le module PostgreSQL |
| voit ses tables `oracle_*` créées par `lib/Database.php` | crée `demo_oracle_databases` lui-même, via `DemoOracleSchema::ensure()` |

Une quatrième conséquence, plus discrète : le `.htaccess` du cœur protège
`modules/oracle/_*` par une règle nominative. Le module porte donc **son
propre** `.htaccess`, qui refuse `manifest.json`, `module.php` et tout fichier
commençant par `_`.

## Arborescence

```
modules/demo_oracle/
  module.php              descripteur (name, roles, docs, nav_*)
  manifest.json           metadonnees de paquet (icone, docs, profils)
  .htaccess               refus des partiels et des fichiers de description
  index.php               accueil : liste des bases, connexion
  connect.php             selection d'une base -> session
  disconnect.php          relachement
  databases.php           CRUD des bases (role _admin)
  test_connection.php     test de connexion (POST -> JSON, role _admin)
  admin.php               configuration du module (role _admin)
  report.php              routeur des rapports — AUCUN gestionnaire d'action
  _report_<section>.php   un partiel par section servie (thin controller)
  _data_<section>.php     point de donnees JSON des rapports rafraichis
  lib/
    boot.php              require_once des bibliotheques de base + ensure()
    DemoOracleConfig.php      constantes de plafonnement
    DemoOracleSchema.php      creation de la table, comptage, hasRoom()
    DemoOracleOciConnect.php  ouverture OCI8 (toutes fonctions prefixees demo_)
    DemoOracleAccess.php      lecture des bases, is_manager()
    DemoOracleUrls.php        carte des sections et des menus, demo_oracle_url()
    DemoOracleDemo.php        badge / disabled / page « version complete »
    DemoOracleAshSource.php   licence Diagnostic Pack ou repli sw$bash_snapper
    DemoOracleRealtimeFilters.php  plafonnement des filtres du temps reel
    DemoOracleSqlXml.php      analyse d'OTHER_XML (hors controleur : testable)
    DemoOracleConnectionTest.php   la part Oracle du test de connexion
    DemoOracleDsn.php         construction du DSN, direct ou portefeuille
    DemoOracleWallet.php      extraction et menage des portefeuilles OCI
    DemoOracle*Repository.php      requetes de lecture, portees du module complet
  views/
    index.php, databases.php, admin.php
    report/layout.php     navigation + aiguillage overview|locked|partial|wip
    report/overview.php   vue d'ensemble de la base connectee
    report/<section>.php  une vue par rapport porte
    report/_nav_end.php   point d'extension, vide a dessein
  assets/img/             logos 64 et 512
  assets/js/              JS des rapports, ampute de tout appel d'ecriture
  i18n/                   fr, en, it, pl reels ; de, es, ro copies de en
  docs/                   ce document et les deux guides
```

## La barre latérale

Le cœur (`mon-appli/includes/header.php`) code en dur un rail d'icônes pour
Oracle et pour PostgreSQL, chacun conditionné à `function_exists('oracle_url')`
ou `pg_url`. Un troisième module ne peut pas s'y ajouter sans modifier le dépôt
principal.

`views/report/layout.php` rend donc **son propre** `<aside>`, en réutilisant
les classes du cœur (`oracle-sidebar`, `oracle-dropdown`…). Ce n'est pas un
détournement : le module s'appuie déjà sur `card`, `btn`, `data-table`,
`cfg-*`, `sql-tab-*`, toutes définies dans le même `main.css`. Réutiliser plutôt
que dupliquer garantit que la démonstration suivra l'apparence du module complet
sans entretien.

Deux pièces manquaient, que le cœur ne pose que pour ses deux modules :

| Pièce | Comment elle est remplacée |
| --- | --- |
| `has-oracle-sidebar` sur le `<body>` — décale `.site-main` de 52 px | une règle CSS équivalente dans le `<style>` du layout. En CSS et non en JavaScript, pour qu'elle s'applique **avant** le premier rendu et non après |
| le bouton ☰ de repli, qui vit dans la barre de titre | injecté par un script du layout, qui s'exécute **avant** `main.js` — celui-ci le retrouve par son identifiant et lui greffe la persistance |

Le reste était déjà générique : `ociToggle()` ouvre n'importe quel
`.oracle-dropdown`, et `--ora-header-h` est calculé pour tout le monde.

Si la barre de titre change de structure, l'injection échoue sans bruit et le
rail reste simplement déplié — dégrader, pas casser.

`layout.php` **ne rend aucune ligne de titre** : les vingt vues de rapport
portent déjà la leur, et le nom de la base figure dans la barre du haut. En
poser une de plus produisait deux titres l'un au-dessus de l'autre.

Les liens « retour » et « déconnexion » qu'elle portait ne sont pas relogés
ailleurs : le retour passe par l'entrée du module dans la barre de navigation,
et la page d'accueil du module offre déjà la déconnexion sous le rappel de la
base connectée. Le rail ne porte donc que les menus.

Trois modes n'ont pas de vue à eux et reçoivent donc leur en-tête du layout :
la vue d'ensemble (qui a gagné le sien), la section verrouillée et la section
non portée. `test_render.php` rend réellement le layout dans ces cinq
situations et **compte les en-têtes** : un seul, toujours.

## Les points de donnees JSON

Quatre rapports vont chercher leurs données en JSON : temps réel, sessions
actives et diagnostic des verrous se rafraîchissent seuls toutes les dix
secondes ; le rapport ASH détaillé réclame sa période par tranches
successives. Dans le module complet, le même
fichier sert la page et ses données, en testant `?format=json` en tête.

Ici c'est impossible : le partiel est inclus depuis `views/report/layout.php`,
donc **après** l'envoi des en-têtes et du `<head>`. Émettre du JSON à cet
endroit produirait une page HTML contenant du JSON, que le JavaScript ne
saurait pas lire.

`report.php` intercepte donc `?format=json` **avant** toute mise en page et
délègue à `_data_<section>.php`, qui se termine par `exit`. Une section
verrouillée ou sans point de données répond 404 en JSON, pas une page HTML.

Le rapport ASH détaillé montre pourquoi le plafonnement doit vivre côté
serveur : son point de données reçoit les bornes de **chaque tranche** depuis
le navigateur, en boucle. Borner la période dans le seul formulaire aurait
laissé une suite de tranches forgées reconstituer n'importe quelle amplitude.
`DemoOracleAshDetailFilters::clamp()` est donc appelé des deux côtés.

## Le plafonnement, côté serveur

`DemoOracleRealtimeFilters::parse()` lit `$_GET` et rend des valeurs **déjà
plafonnées** : fenêtre figée à 30 minutes, dimension ramenée à `wait_class` si
elle n'est pas servie, service forcé sur « tous », état forcé sur « actives ».

C'est le pendant obligatoire du grisage. La page et son point de données
appellent la même méthode, si bien qu'une URL forgée à la main —
`?window=90&dim=sql_id&state=all` — rend exactement ce que rend un clic normal.
Griser les boutons sans cela n'aurait protégé personne : `_data_realtime.php`
est joignable directement.

## La carte des sections

`demo_oracle_sections()`, dans `lib/DemoOracleUrls.php`, est la **référence
unique** du module : la barre de navigation, le routeur et la documentation la
lisent tous les trois. Chaque entrée porte son menu, sa clé i18n de titre, et
un booléen `demo` qui dit si la section est réellement servie.

Une section ajoutée là apparaît partout ; une section oubliée n'apparaît nulle
part. C'est voulu : le module complet a vu ses menus et son routeur diverger
plus d'une fois.

Une entrée peut porter `'nav' => false` : elle est **servie par le routeur mais
absente des deux rangées de menu**. Le détail d'un SQL_ID est dans ce cas — sans
`sql_id` la page n'a rien à montrer, un lien de menu n'aurait mené qu'à un
message d'erreur. On y arrive depuis les rapports.

## Le routage

```
report.php?section=config/tablespaces
   |
   +-- section inconnue      -> mode « overview » (un lien perime degrade, il ne casse pas)
   +-- section['demo'] false -> mode « locked »   -> DemoOracleDemo::page()
   +-- partiel absent        -> mode « wip »      -> message franc
   +-- sinon                 -> mode « partial »  -> include _report_<section>.php
```

Le mode est résolu dans `report.php`, puis `View::display()` rend
`views/report/layout.php` qui aiguille sur ces quatre cas. La mise en page reste
ainsi un seul fichier.

Le nom du partiel dérive mécaniquement de la section :
`config/tablespaces` → `_report_config_tablespaces.php`.

## Le mécanisme « version complète »

Tout passe par `DemoOracleDemo`, et par lui seul — un seul endroit décide de
l'apparence, du libellé et du lien. Trois formes :

| Méthode | Usage |
| --- | --- |
| `badge($extra)` | pastille jaune à côté d'un contrôle désactivé |
| `disabled($extra)` | les attributs `disabled` + `title` à coller sur le contrôle |
| `page($titre, $detail)` | corps complet d'un rapport absent de la démonstration |

L'URL cible vient de la configuration du module
(`full_version_url`, dans `securite/modules/demo_oracle.json`). Vide, les
mentions restent des explications sans lien.

## Les plafonnements

Dans `lib/DemoOracleConfig.php`, **hors de portée de l'interface** :

| Constante | Valeur | Ce qu'elle borne |
| --- | --- | --- |
| `DEMO_ORACLE_MAX_DATABASES` | 5 | bases déclarables |
| `DEMO_ORACLE_HISTORY_DAYS` | 7 | profondeur du rapport d'historique |
| `DEMO_ORACLE_ASH_MAX_HOURS` | 24 | amplitude d'un rapport ASH |
| `DEMO_ORACLE_RT_DIMENSIONS` | `wait_class`, `module`, `inst_id` | dimensions du temps réel |
| `DEMO_ORACLE_BASE` | `/modules/demo_oracle` | racine des URL |

`DEMO_ORACLE_BASE` est défini **avant** le garde d'inclusion de
`DemoOracleUrls.php`. Les déclarations de fonctions étant hoistées, le garde
`function_exists` court-circuite le reste du fichier dès le premier `include` :
un `define` placé après serait ignoré. Le piège a déjà coûté un bogue dans le
module `audit_oracle`.

## Conventions reprises du cœur

- **SQLite natif**, jamais PDO : `Database::getInstance()` et ses helpers.
- **Repositories sans état** : pas de constructeur, `$conn` en premier argument,
  lectures renvoyant systématiquement un `array` (vide en cas d'échec OCI).
- **Toutes les chaînes UI** passent par `t()`.
- **Préfixe `demo_`** sur chaque fonction globale : le module peut cohabiter
  avec le module Oracle complet sans collision de noms.
- **CSRF** sur chaque `POST`, jeton en session, comparaison par `hash_equals`.

## Aucun appel OCI hors de `lib/`

C'est la règle qui rend **vérifiable** l'affirmation « la démonstration n'écrit
rien » : il suffit d'auditer un seul répertoire. Le banc de test la fait
respecter, et elle a déjà servi — le test de connexion a d'abord été écrit avec
son `oci_connect` directement dans `test_connection.php`, et le banc l'a
refusé. La partie Oracle est donc partie dans
`lib/DemoOracleConnectionTest.php`, le point d'entrée ne gardant que les
gardes, les paramètres, l'i18n et le JSON.

Une exception aurait suffi à rendre la règle inutile : c'est tout l'intérêt de
la faire tenir par le banc plutôt que par la discipline.

## Un seul constructeur de DSN

Le module complet reconstruit sa chaîne de connexion dans **trois** fichiers —
`databases.php` pour son test à l'enregistrement, `OracleOciConnect` pour les
rapports, `test_connection.php` pour le bouton — et ces trois copies ont
divergé : celle de `databases.php` passe l'alias TNS brut là où les deux autres
résolvent le descripteur complet depuis le `tnsnames.ora`.

Ici, `lib/DemoOracleDsn.php` est le seul endroit qui sait construire un DSN, et
le banc vérifie que personne ne le refait à la main. La conséquence pratique :
le bouton **Tester la connexion** éprouve exactement la chaîne que les rapports
utiliseront ensuite — tester autre chose n'aurait rien prouvé.

## Le portefeuille Oracle Cloud

`lib/DemoOracleWallet.php` reprend les deux helpers de
`modules/oracle/databases.php` avec trois durcissements, le module étant fait
pour être exposé :

| Durcissement | Pourquoi |
| --- | --- |
| l'archive est **inspectée avant** extraction ; une entrée contenant `..` fait tout rejeter | `ZipArchive::extractTo()` ne protège pas de la traversée de répertoire |
| seuls les fichiers attendus d'un portefeuille sont extraits, **à plat** | un `.php` glissé dans l'archive atterrirait sinon sur le disque |
| la taille décompressée est plafonnée | contre l'archive qui gonfle |

Une archive sans `tnsnames.ora` est refusée : ce n'est pas un portefeuille. Le
répertoire d'extraction vit sous `securite/`, hors DocumentRoot, et
`delete()` refuse tout chemin qui n'y mène pas — la valeur vient de la base, une
donnée corrompue ne doit pas pouvoir faire effacer autre chose.

## Comment un rapport est porté

Le portage est mécanique, et volontairement traçable : chaque fichier porte en
tête d'où il vient et ce qui en a été retiré.

1. **Le repository** — recopié depuis `modules/oracle/lib/`, tronqué à la
   première méthode d'action. Les méthodes qui renvoient `array{ok, msg}` ne
   sont pas reprises. C'est la coupure qui compte : le reste n'est que de
   l'interface.
2. **Le contrôleur** `_report_<section>.php` — mêmes lectures, sans le calcul
   de droit DBA ni le jeton CSRF, qui n'ont plus d'objet.
3. **La vue** — les blocs réservés au DBA sont soit supprimés (quand ils ne
   contiennent qu'un formulaire d'action), soit **conservés désactivés** (quand
   le bouton dit au visiteur ce qu'offre la version complète).
4. **Le JavaScript** — amputé de tout appel réseau en écriture. Le filtrage,
   le tri et les graphiques restent.

### Supprimer ou griser ?

Les deux, selon ce que le visiteur y gagne :

| Situation | Choix |
| --- | --- |
| Un bouton nomme une fonction de la version complète | **grisé**, avec la pastille |
| Une fenêtre modale, un formulaire d'édition | **supprimé** — un formulaire vide n'apprend rien |
| Une colonne entière dédiée aux actions | **conservée**, ses boutons grisés |
| Un lien vers une page absente de la démonstration | **supprimé** |
| Une donnée qui *arme* une action retirée (les outlines) | **non transmise** |

Dans tous les cas le gestionnaire serveur est absent. Le grisage informe ; il
ne protège pas, et le module ne lui demande pas de le faire.

## Ce qui reste à porter

Les partiels `_report_*.php` sont livrés par étapes. Une section annoncée dans
la carte mais dont le partiel manque affiche un message franc (mode « wip »)
plutôt qu'une page vide.

| Étape | Contenu | État |
| --- | --- | --- |
| 1 | squelette, navigation, gestion des bases, configuration | livrée |
| 2 | menu Configuration (6 rapports), menu Temps réel (3 rapports) | livrée |
| 2b | détail d'un SQL_ID, hors menu | livrée |
| 2c | test de connexion dans la gestion des bases | livrée |
| 3 | Historique, Diagnostic, Tableaux de bord ; navigation en barre latérale | livrée |
| 4 | SQL Monitor natif, reconstruit sans licence Oracle | livrée |

Les vingt sections servies sont portées ; il ne reste en « wip » aucune entrée
de la carte.

## Le SQL Monitor natif

Le seul rapport de la démonstration qui ne soit pas un simple portage tronqué :
c'est une **reconstruction**.

`DBMS_SQL_MONITOR.REPORT_SQL_MONITOR` n'est qu'un metteur en page au-dessus de
`GV$SQL_MONITOR` et `GV$SQL_PLAN_MONITOR` — deux vues qui *sont* le Real-Time
SQL Monitoring, donc le Tuning Pack. Contourner le paquetage ne contourne pas
la licence. Le module ne les lit donc pas du tout, et reconstruit à partir de
sources libres.

```
DemoOracleSqlMonSource       resout la source ; ici toujours « session »,
                             et sonde V$SESSION.SQL_PLAN_LINE_ID (ORA-00904)
DemoOracleSqlMonFilters      plafonne AVANT toute requete — page et JSON
DemoOracleSqlMonitorRepository  GV$SESSION, GV$SQL_WORKAREA_ACTIVE,
                             GV$SESSION_LONGOPS, GV$SQL_PLAN, GV$SESSTAT
DemoOracleSqlMonAccumulator  l'histogramme, d'un rafraichissement a l'autre
DemoOracleSqlMonPayload      assemble ; n'emet aucun appel OCI
```

Trois points de conception méritent d'être retenus.

**L'accumulateur n'est pas un cache.** Un cache expire et se reconstruit ;
celui-ci ne se reconstruit pas. Un appel à `GV$SESSION` ne donne qu'un
instantané — la ligne de plan active à l'instant du clic. C'est
l'accumulation, poll après poll, qui reconstitue « où le temps est passé ».
D'où l'absence délibérée du cache disque de `_data_realtime.php` : servir une
réponse mise en cache reviendrait à compter deux fois le même instant.

**Le JavaScript cesse d'interroger quand l'onglet passe en arrière-plan.**
Ailleurs ce serait une économie ; ici c'est une question de justesse — un
polling invisible ajouterait des échantillons à un histogramme que personne ne
regarde.

**`OPERATION_ID` de `GV$SQL_WORKAREA_ACTIVE` *est* l'identifiant de ligne de
plan.** C'est la seule attribution ligne à ligne qui survive même quand
`SQL_PLAN_LINE_ID` n'est pas alimentée — d'où le repli documenté dans le
README.

Le module complet porte les trois branches (`pack`, `ash`, `session`) dans
`OracleSqlMonSource` ; la démonstration ne porte que la troisième, et les
méthodes des deux premières sont **absentes** du repository, pas désactivées.
Le banc l'assure : le fichier ne contient aucune des chaînes `GV$SQL_MONITOR`,
`SQL_PLAN_MONITOR`, `DBMS_SQL_MONITOR`, `ACTIVE_SESSION_HISTORY`.
