// base temporaire tant qu'on ne connait pas son vrai nom // pour tester la connexion $db = "_sqlite" . $sqlite_version . "_install"; $tmp = _DIR_DB . $db . ".sqlite"; if ($sqlite_version == 3) { $ok = $link = new PDO("sqlite:$tmp"); } else { $ok = $link = sqlite_open($tmp, _SQLITE_CHMOD, $err); } } else { // Ouvrir (eventuellement creer la base) // si pas de version fourni, on essaie la 3, sinon la 2 if ($sqlite_version == 3) { $ok = $link = new PDO("sqlite:$f"); } else { $ok = $link = sqlite_open($f, _SQLITE_CHMOD, $err); } } if (!$ok) { $e = sqlite_last_error($db); spip_log("Impossible d'ouvrir la base SQLite($sqlite_version) $f : $e", 'sqlite.' . _LOG_HS); return false; } if ($link) { $last_connect = array( 'addr' => $addr, 'port' => $port, 'login' => $login, 'pass' => $pass, 'db' => $db, 'prefixe' => $prefixe, ); // etre sur qu'on definit bien les fonctions a chaque nouvelle connexion include_spip('req/sqlite_fonctions'); _sqlite_init_functions($link); } return array( 'db' => $db, 'prefixe' => $prefixe ? $prefixe : $db, 'link' => $link, 'total_requetes' => 0, ); } /** * Fonction de requete generale, munie d'une trace a la demande * * @param string $query * Requete a executer * @param string $serveur * Nom du connecteur * @param bool $requeter * Effectuer la requete ? * - true pour executer * - false pour retourner le texte de la requete * @return bool|SQLiteResult|string * Resultat de la requete */ function spip_sqlite_query($query, $serveur = '', $requeter = true) { #spip_log("spip_sqlite_query() > $query",'sqlite.'._LOG_DEBUG); #_sqlite_init(); // fait la premiere fois dans spip_sqlite $query = spip_sqlite::traduire_requete($query, $serveur); if (!$requeter) { return $query; } return spip_sqlite::executer_requete($query, $serveur); } /* ordre alphabetique pour les autres */ /** * Modifie une structure de table SQLite * * @param string $query Requête SQL (sans 'ALTER ') * @param string $serveur Nom de la connexion * @param bool $requeter inutilisé * @return bool * False si erreur dans l'exécution, true sinon */ function spip_sqlite_alter($query, $serveur = '', $requeter = true) { $query = spip_sqlite_query("ALTER $query", $serveur, false); // traduire la requete pour recuperer les bons noms de table $query = spip_sqlite::traduire_requete($query, $serveur); /* * la il faut faire les transformations * si ALTER TABLE x (DROP|CHANGE) y * * 1) recuperer "ALTER TABLE table " * 2) spliter les sous requetes (,) * 3) faire chaque requete independemment */ // 1 if (preg_match("/\s*(ALTER(\s*IGNORE)?\s*TABLE\s*([^\s]*))\s*(.*)?/is", $query, $regs)) { $debut = $regs[1]; $table = $regs[3]; $suite = $regs[4]; } else { spip_log("SQLite : Probleme de ALTER TABLE mal forme dans $query", 'sqlite.' . _LOG_ERREUR); return false; } // 2 // il faudrait une regexp pour eviter de spliter ADD PRIMARY KEY (colA, colB) // tout en cassant "ADD PRIMARY KEY (colA, colB), ADD INDEX (chose)"... en deux // ou revoir l'api de sql_alter en creant un // sql_alter_table($table,array($actions)); $todo = explode(',', $suite); // on remet les morceaux dechires ensembles... que c'est laid ! $todo2 = array(); $i = 0; $ouverte = false; while ($do = array_shift($todo)) { $todo2[$i] = isset($todo2[$i]) ? $todo2[$i] . "," . $do : $do; $o = (false !== strpos($do, "(")); $f = (false !== strpos($do, ")")); if ($o and !$f) { $ouverte = true; } elseif ($f) { $ouverte = false; } if (!$ouverte) { $i++; } } // 3 $resultats = array(); foreach ($todo2 as $do) { $do = trim($do); if (!preg_match('/(DROP PRIMARY KEY|DROP KEY|DROP INDEX|DROP COLUMN|DROP' . '|CHANGE COLUMN|CHANGE|MODIFY|RENAME TO|RENAME' . '|ADD PRIMARY KEY|ADD KEY|ADD INDEX|ADD UNIQUE KEY|ADD UNIQUE' . '|ADD COLUMN|ADD' . ')\s*([^\s]*)\s*(.*)?/i', $do, $matches) ) { spip_log("SQLite : Probleme de ALTER TABLE, utilisation non reconnue dans : $do \n(requete d'origine : $query)", 'sqlite.' . _LOG_ERREUR); return false; } $cle = strtoupper($matches[1]); $colonne_origine = $matches[2]; $colonne_destination = ''; $def = $matches[3]; // eluder une eventuelle clause before|after|first inutilisable $defr = rtrim(preg_replace('/(BEFORE|AFTER|FIRST)(.*)$/is', '', $def)); $defo = $defr; // garder la def d'origine pour certains cas // remplacer les definitions venant de mysql $defr = _sqlite_remplacements_definitions_table($defr); // reinjecter dans le do $do = str_replace($def, $defr, $do); $def = $defr; switch ($cle) { // suppression d'un index case 'DROP KEY': case 'DROP INDEX': $nom_index = $colonne_origine; spip_sqlite_drop_index($nom_index, $table, $serveur); break; // suppression d'une pk case 'DROP PRIMARY KEY': if (!_sqlite_modifier_table( $table, $colonne_origine, array('key' => array('PRIMARY KEY' => '')), $serveur) ) { return false; } break; // suppression d'une colonne case 'DROP COLUMN': case 'DROP': if (!_sqlite_modifier_table( $table, array($colonne_origine => ""), array(), $serveur) ) { return false; } break; case 'CHANGE COLUMN': case 'CHANGE': // recuperer le nom de la future colonne // on reprend la def d'origine car _sqlite_modifier_table va refaire la translation // en tenant compte de la cle primaire (ce qui est mieux) $def = trim($defo); $colonne_destination = substr($def, 0, strpos($def, ' ')); $def = substr($def, strlen($colonne_destination) + 1); if (!_sqlite_modifier_table( $table, array($colonne_origine => $colonne_destination), array('field' => array($colonne_destination => $def)), $serveur) ) { return false; } break; case 'MODIFY': // on reprend la def d'origine car _sqlite_modifier_table va refaire la translation // en tenant compte de la cle primaire (ce qui est mieux) if (!_sqlite_modifier_table( $table, $colonne_origine, array('field' => array($colonne_origine => $defo)), $serveur) ) { return false; } break; // pas geres en sqlite2 case 'RENAME': $do = "RENAME TO" . substr($do, 6); case 'RENAME TO': if (_sqlite_is_version(3, '', $serveur)) { if (!spip_sqlite::executer_requete("$debut $do", $serveur)) { spip_log("SQLite : Erreur ALTER TABLE / RENAME : $query", 'sqlite.' . _LOG_ERREUR); return false; } // artillerie lourde pour sqlite2 ! } else { $table_dest = trim(substr($do, 9)); if (!_sqlite_modifier_table(array($table => $table_dest), '', array(), $serveur)) { spip_log("SQLite : Erreur ALTER TABLE / RENAME : $query", 'sqlite.' . _LOG_ERREUR); return false; } } break; // ajout d'une pk case 'ADD PRIMARY KEY': $pk = trim(substr($do, 16)); $pk = ($pk[0] == '(') ? substr($pk, 1, -1) : $pk; if (!_sqlite_modifier_table( $table, $colonne_origine, array('key' => array('PRIMARY KEY' => $pk)), $serveur) ) { return false; } break; // ajout d'un index case 'ADD UNIQUE KEY': case 'ADD UNIQUE': $unique = true; case 'ADD INDEX': case 'ADD KEY': if (!isset($unique)) { $unique = false; } // peut etre "(colonne)" ou "nom_index (colonnes)" // bug potentiel si qqn met "(colonne, colonne)" // // nom_index (colonnes) if ($def) { $colonnes = substr($def, 1, -1); $nom_index = $colonne_origine; } else { // (colonne) if ($colonne_origine[0] == "(") { $colonnes = substr($colonne_origine, 1, -1); if (false !== strpos(",", $colonnes)) { spip_log(_LOG_GRAVITE_ERREUR, "SQLite : Erreur, impossible de creer un index sur plusieurs colonnes" . " sans qu'il ait de nom ($table, ($colonnes))", 'sqlite'); break; } else { $nom_index = $colonnes; } } // nom_index else { $nom_index = $colonnes = $colonne_origine; } } spip_sqlite_create_index($nom_index, $table, $colonnes, $unique, $serveur); break; // pas geres en sqlite2 case 'ADD COLUMN': $do = "ADD" . substr($do, 10); case 'ADD': default: if (_sqlite_is_version(3, '', $serveur) and !preg_match(',primary\s+key,i', $do)) { if (!spip_sqlite::executer_requete("$debut $do", $serveur)) { spip_log("SQLite : Erreur ALTER TABLE / ADD : $query", 'sqlite.' . _LOG_ERREUR); return false; } break; } // artillerie lourde pour sqlite2 ! // ou si la colonne est aussi primary key // cas du add id_truc int primary key // ajout d'une colonne qui passe en primary key directe else { $def = trim(substr($do, 3)); $colonne_ajoutee = substr($def, 0, strpos($def, ' ')); $def = substr($def, strlen($colonne_ajoutee) + 1); $opts = array(); if (preg_match(',primary\s+key,i', $def)) { $opts['key'] = array('PRIMARY KEY' => $colonne_ajoutee); $def = preg_replace(',primary\s+key,i', '', $def); } $opts['field'] = array($colonne_ajoutee => $def); if (!_sqlite_modifier_table($table, array($colonne_ajoutee), $opts, $serveur)) { spip_log("SQLite : Erreur ALTER TABLE / ADD : $query", 'sqlite.' . _LOG_ERREUR); return false; } } break; } // tout est bon, ouf ! spip_log("SQLite ($serveur) : Changements OK : $debut $do", 'sqlite.' . _LOG_INFO); } spip_log("SQLite ($serveur) : fin ALTER TABLE OK !", 'sqlite.' . _LOG_INFO); return true; } /** * Crée une table SQL * * Crée une table SQL nommee `$nom` à partir des 2 tableaux `$champs` et `$cles` * * @note Le nom des caches doit être inferieur à 64 caractères * * @param string $nom Nom de la table SQL * @param array $champs Couples (champ => description SQL) * @param array $cles Couples (type de clé => champ(s) de la clé) * @param bool $autoinc True pour ajouter un auto-incrément sur la Primary Key * @param bool $temporary True pour créer une table temporaire * @param string $serveur Nom de la connexion * @param bool $requeter Exécuter la requête, sinon la retourner * @return array|null|resource|string * - string Texte de la requête si demandée * - true si la requête réussie, false sinon. */ function spip_sqlite_create( $nom, $champs, $cles, $autoinc = false, $temporary = false, $serveur = '', $requeter = true ) { $query = _sqlite_requete_create($nom, $champs, $cles, $autoinc, $temporary, $ifnotexists = true, $serveur, $requeter); if (!$query) { return false; } $res = spip_sqlite_query($query, $serveur, $requeter); // SQLite ne cree pas les KEY sur les requetes CREATE TABLE // il faut donc les faire creer ensuite if (!$requeter) { return $res; } $ok = $res ? true : false; if ($ok) { foreach ($cles as $k => $v) { if (preg_match(',^(KEY|UNIQUE)\s,i', $k, $m)) { $index = trim(substr($k, strlen($m[1]))); $unique = (strlen($m[1]) > 3); $ok &= spip_sqlite_create_index($index, $nom, $v, $unique, $serveur); } } } return $ok ? true : false; } /** * Crée une base de données SQLite * * @param string $nom Nom de la base (sans l'extension de fichier) * @param string $serveur Nom de la connexion * @param string $option Options * * @return bool true si la base est créee. **/ function spip_sqlite_create_base($nom, $serveur = '', $option = true) { $f = $nom . '.sqlite'; if (strpos($nom, "/") === false) { $f = _DIR_DB . $f; } if (_sqlite_is_version(2, '', $serveur)) { $ok = sqlite_open($f, _SQLITE_CHMOD, $err); } else { $ok = new PDO("sqlite:$f"); } if ($ok) { unset($ok); return true; } unset($ok); return false; } /** * Crée une vue SQL nommée `$nom` * * @param string $nom * Nom de la vue a creer * @param string $query_select * Texte de la requête de sélection servant de base à la vue * @param string $serveur * Nom du connecteur * @param bool $requeter * Effectuer la requete, sinon la retourner * @return bool|SQLiteResult|string * - true si la vue est créée * - false si erreur ou si la vue existe déja * - string texte de la requête si $requeter vaut false */ function spip_sqlite_create_view($nom, $query_select, $serveur = '', $requeter = true) { if (!$query_select) { return false; } // vue deja presente if (sql_showtable($nom, false, $serveur)) { spip_log("Echec creation d'une vue sql ($nom) car celle-ci existe deja (serveur:$serveur)", 'sqlite.' . _LOG_ERREUR); return false; } $query = "CREATE VIEW $nom AS " . $query_select; return spip_sqlite_query($query, $serveur, $requeter); } /** * Fonction de création d'un INDEX * * @param string $nom * Nom de l'index * @param string $table * Table SQL de l'index * @param string|array $champs * Liste de champs sur lesquels s'applique l'index * @param string|bool $unique * Créer un index UNIQUE ? * @param string $serveur * Nom de la connexion sql utilisee * @param bool $requeter * true pour executer la requête ou false pour retourner le texte de la requête * @return bool|string * string : requête, false si erreur, true sinon. */ function spip_sqlite_create_index($nom, $table, $champs, $unique = '', $serveur = '', $requeter = true) { if (!($nom or $table or $champs)) { spip_log("Champ manquant pour creer un index sqlite ($nom, $table, (" . join(',', $champs) . "))", 'sqlite.' . _LOG_ERREUR); return false; } // SQLite ne differentie pas noms des index en fonction des tables // il faut donc creer des noms uniques d'index pour une base sqlite $nom = $table . '_' . $nom; // enlever d'eventuelles parentheses deja presentes sur champs if (!is_array($champs)) { if ($champs[0] == "(") { $champs = substr($champs, 1, -1); } $champs = array($champs); // supprimer l'info de longueur d'index mysql en fin de champ $champs = preg_replace(",\(\d+\)$,", "", $champs); } $ifnotexists = ""; $version = spip_sqlite_fetch(spip_sqlite_query("select sqlite_version() AS sqlite_version", $serveur), '', $serveur); if (!function_exists('spip_version_compare')) { include_spip('plugins/installer'); } if ($version and spip_version_compare($version['sqlite_version'], '3.3.0', '>=')) { $ifnotexists = ' IF NOT EXISTS'; } else { /* simuler le IF EXISTS - version 2 et sqlite < 3.3a */ $a = spip_sqlite_showtable($table, $serveur); if (isset($a['key']['KEY ' . $nom])) { return true; } } $query = "CREATE " . ($unique ? "UNIQUE " : "") . "INDEX$ifnotexists $nom ON $table (" . join(',', $champs) . ")"; $res = spip_sqlite_query($query, $serveur, $requeter); if (!$requeter) { return $res; } if ($res) { return true; } else { return false; } } /** * Retourne le nombre de lignes d’une ressource de sélection obtenue * avec `sql_select()` * * En PDO/sqlite3, il faut calculer le count par une requete count(*) * pour les resultats de SELECT * cela est fait sans spip_sqlite_query() * * @param Ressource|Object $r Ressource de résultat * @param string $serveur Nom de la connexion * @param bool $requeter Inutilisé * @return int Nombre de lignes */ function spip_sqlite_count($r, $serveur = '', $requeter = true) { if (!$r) { return 0; } if (_sqlite_is_version(3, '', $serveur)) { // select ou autre (insert, update,...) ? // (link,requete) a compter if (is_array($r->spipSqliteRowCount)) { list($link, $query) = $r->spipSqliteRowCount; // amelioration possible a tester intensivement : pas de order by pour compter ! // $query = preg_replace(",ORDER BY .+(LIMIT\s|HAVING\s|GROUP BY\s|$),Uims","\\1",$query); $query = "SELECT count(*) as zzzzsqlitecount FROM ($query)"; $l = $link->query($query); $i = 0; if ($l and $z = $l->fetch()) { $i = $z['zzzzsqlitecount']; } $r->spipSqliteRowCount = $i; } if (isset($r->spipSqliteRowCount)) { // Ce compte est faux s'il y a des limit dans la requete :( // il retourne le nombre d'enregistrements sans le limit return $r->spipSqliteRowCount; } else { return $r->rowCount(); } } else { return sqlite_num_rows($r); } } /** * Retourne le nombre de lignes d'une sélection * * @param array|string $from Tables à consulter (From) * @param array|string $where Conditions a remplir (Where) * @param array|string $groupby Critère de regroupement (Group by) * @param array $having Tableau des des post-conditions à remplir (Having) * @param string $serveur Nom de la connexion * @param bool $requeter Exécuter la requête, sinon la retourner * @return int|bool|string * - String Texte de la requête si demandé * - int Nombre de lignes * - false si la requête a échouée **/ function spip_sqlite_countsel( $from = array(), $where = array(), $groupby = '', $having = array(), $serveur = '', $requeter = true ) { $c = !$groupby ? '*' : ('DISTINCT ' . (is_string($groupby) ? $groupby : join(',', $groupby))); $r = spip_sqlite_select("COUNT($c)", $from, $where, '', '', '', $having, $serveur, $requeter); if ((is_resource($r) or is_object($r)) && $requeter) { // ressource : sqlite2, object : sqlite3 if (_sqlite_is_version(3, '', $serveur)) { list($r) = spip_sqlite_fetch($r, SPIP_SQLITE3_NUM, $serveur); } else { list($r) = spip_sqlite_fetch($r, SPIP_SQLITE2_NUM, $serveur); } } return $r; } /** * Supprime des enregistrements d'une table * * @param string $table Nom de la table SQL * @param string|array $where Conditions à vérifier * @param string $serveur Nom du connecteur * @param bool $requeter Exécuter la requête, sinon la retourner * @return bool|string * - int : nombre de suppressions réalisées, * - Texte de la requête si demandé, * - False en cas d'erreur. **/ function spip_sqlite_delete($table, $where = '', $serveur = '', $requeter = true) { $res = spip_sqlite_query( _sqlite_calculer_expression('DELETE FROM', $table, ',') . _sqlite_calculer_expression('WHERE', $where), $serveur, $requeter); // renvoyer la requete inerte si demandee if (!$requeter) { return $res; } if ($res) { $link = _sqlite_link($serveur); if (_sqlite_is_version(3, $link)) { return $res->rowCount(); } else { return sqlite_changes($link); } } else { return false; } } /** * Supprime une table SQL * * @param string $table Nom de la table SQL * @param string $exist True pour ajouter un test d'existence avant de supprimer * @param string $serveur Nom de la connexion * @param bool $requeter Exécuter la requête, sinon la retourner * @return bool|string * - string Texte de la requête si demandé * - true si la requête a réussie, false sinon */ function spip_sqlite_drop_table($table, $exist = '', $serveur = '', $requeter = true) { if ($exist) { $exist = " IF EXISTS"; } /* simuler le IF EXISTS - version 2 */ if ($exist && _sqlite_is_version(2, '', $serveur)) { $a = spip_sqlite_showtable($table, $serveur); if (!$a) { return true; } $exist = ''; } if (spip_sqlite_query("DROP TABLE$exist $table", $serveur, $requeter)) { return true; } else { return false; } } /** * Supprime une vue SQL * * @param string $view Nom de la vue SQL * @param string $exist True pour ajouter un test d'existence avant de supprimer * @param string $serveur Nom de la connexion * @param bool $requeter Exécuter la requête, sinon la retourner * @return bool|string * - string Texte de la requête si demandé * - true si la requête a réussie, false sinon */ function spip_sqlite_drop_view($view, $exist = '', $serveur = '', $requeter = true) { if ($exist) { $exist = " IF EXISTS"; } /* simuler le IF EXISTS - version 2 */ if ($exist && _sqlite_is_version(2, '', $serveur)) { $a = spip_sqlite_showtable($view, $serveur); if (!$a) { return true; } $exist = ''; } return spip_sqlite_query("DROP VIEW$exist $view", $serveur, $requeter); } /** * Fonction de suppression d'un INDEX * * @param string $nom : nom de l'index * @param string $table : table sql de l'index * @param string $serveur : nom de la connexion sql utilisee * @param bool $requeter : true pour executer la requête ou false pour retourner le texte de la requête * * @return bool ou requete */ function spip_sqlite_drop_index($nom, $table, $serveur = '', $requeter = true) { if (!($nom or $table)) { spip_log("Champ manquant pour supprimer un index sqlite ($nom, $table)", 'sqlite.' . _LOG_ERREUR); return false; } // SQLite ne differentie pas noms des index en fonction des tables // il faut donc creer des noms uniques d'index pour une base sqlite $index = $table . '_' . $nom; $exist = " IF EXISTS"; /* simuler le IF EXISTS - version 2 */ if (_sqlite_is_version(2, '', $serveur)) { $a = spip_sqlite_showtable($table, $serveur); if (!isset($a['key']['KEY ' . $nom])) { return true; } $exist = ''; } $query = "DROP INDEX$exist $index"; return spip_sqlite_query($query, $serveur, $requeter); } /** * Retourne la dernière erreur generée * * @uses sql_error_backtrace() * * @param string $query * Requête qui était exécutée * @param string $serveur * Nom de la connexion * @return string * Erreur eventuelle **/ function spip_sqlite_error($query = '', $serveur = '') { $link = _sqlite_link($serveur); if (_sqlite_is_version(3, $link)) { $errs = $link->errorInfo(); /* $errs[0] numero SQLState ('HY000' souvent lors d'une erreur) http://www.easysoft.com/developer/interfaces/odbc/sqlstate_status_return_codes.html $errs[1] numéro d'erreur SQLite (souvent 1 lors d'une erreur) http://www.sqlite.org/c3ref/c_abort.html $errs[2] Le texte du message d'erreur */ $s = ''; if (ltrim($errs[0], '0')) { // 00000 si pas d'erreur $s = "$errs[2]"; } } elseif ($link) { $s = sqlite_error_string(sqlite_last_error($link)); } else { $s = ": aucune ressource sqlite (link)"; } if ($s) { $trace = debug_backtrace(); if ($trace[0]['function'] != "spip_mysql_error") { spip_log("$s - $query - " . sql_error_backtrace(), 'sqlite.' . _LOG_ERREUR); } } return $s; } /** * Retourne le numero de la dernière erreur SQL * * Le numéro (en sqlite3/pdo) est un retour ODBC tel que (très souvent) HY000 * http://www.easysoft.com/developer/interfaces/odbc/sqlstate_status_return_codes.html * * @param string $serveur * nom de la connexion * @return int|string * 0 pas d'erreur * 1 ou autre erreur (en sqlite 2) * 'HY000/1' : numéro de l'erreur SQLState / numéro d'erreur interne SQLite (en sqlite 3) **/ function spip_sqlite_errno($serveur = '') { $link = _sqlite_link($serveur); if (_sqlite_is_version(3, $link)) { $t = $link->errorInfo(); $s = ltrim($t[0], '0'); // 00000 si pas d'erreur if ($s) { $s .= ' / ' . $t[1]; } // ajoute l'erreur du moteur SQLite } elseif ($link) { $s = sqlite_last_error($link); } else { $s = ": aucune ressource sqlite (link)"; } if ($s) { spip_log("Erreur sqlite $s", 'sqlite.' . _LOG_ERREUR); } return $s ? $s : 0; } /** * Retourne une explication de requête (Explain) SQLite * * @param string $query Texte de la requête * @param string $serveur Nom de la connexion * @param bool $requeter Exécuter la requête, sinon la retourner * @return array|string|bool * - array : Tableau de l'explication * - string si on retourne le texte de la requête * - false si on a pas pu avoir d'explication */ function spip_sqlite_explain($query, $serveur = '', $requeter = true) { if (strpos(ltrim($query), 'SELECT') !== 0) { return array(); } $query = spip_sqlite::traduire_requete($query, $serveur); $query = 'EXPLAIN ' . $query; if (!$requeter) { return $query; } // on ne trace pas ces requetes, sinon on obtient un tracage sans fin... $r = spip_sqlite::executer_requete($query, $serveur, false); return $r ? spip_sqlite_fetch($r, null, $serveur) : false; // hum ? etrange ca... a verifier } /** * Rècupère une ligne de résultat * * Récupère la ligne suivante d'une ressource de résultat * * @param Ressource $r Ressource de résultat (issu de sql_select) * @param string $t Structure de résultat attendu (défaut ASSOC) * @param string $serveur Nom de la connexion * @param bool $requeter Inutilisé * @return array Ligne de résultat */ function spip_sqlite_fetch($r, $t = '', $serveur = '', $requeter = true) { $link = _sqlite_link($serveur); $is_v3 = _sqlite_is_version(3, $link); if (!$t) { $t = ($is_v3 ? SPIP_SQLITE3_ASSOC : SPIP_SQLITE2_ASSOC); } $retour = false; if ($r) { $retour = ($is_v3 ? $r->fetch($t) : sqlite_fetch_array($r, $t)); } // les version 2 et 3 parfois renvoie des 'table.titre' au lieu de 'titre' tout court ! pff ! // suppression de 'table.' pour toutes les cles (c'est un peu violent !) // c'est couteux : on ne verifie que la premiere ligne pour voir si on le fait ou non if ($retour and strpos(implode('', array_keys($retour)), '.') !== false ) { foreach ($retour as $cle => $val) { if (($pos = strpos($cle, '.')) !== false) { $retour[substr($cle, $pos + 1)] = &$retour[$cle]; unset($retour[$cle]); } } } return $retour; } /** * Place le pointeur de résultat sur la position indiquée * * @param Ressource $r Ressource de résultat * @param int $row_number Position. Déplacer le pointeur à cette ligne * @param string $serveur Nom de la connexion * @param bool $requeter Inutilisé * @return bool True si déplacement réussi, false sinon. **/ function spip_sqlite_seek($r, $row_number, $serveur = '', $requeter = true) { if ($r) { $link = _sqlite_link($serveur); if (_sqlite_is_version(3, $link)) { // encore un truc de bien fichu : PDO ne PEUT PAS faire de seek ou de rewind... // je me demande si pour sqlite 3 il ne faudrait pas mieux utiliser // les nouvelles fonctions sqlite3_xx (mais encore moins presentes...) return false; } else { return sqlite_seek($r, $row_number); } } } /** * Libère une ressource de résultat * * Indique à SQLite de libérer de sa mémoire la ressoucre de résultat indiquée * car on n'a plus besoin de l'utiliser. * * @param Ressource|Object $r Ressource de résultat * @param string $serveur Nom de la connexion * @param bool $requeter Inutilisé * @return bool True si réussi */ function spip_sqlite_free(&$r, $serveur = '', $requeter = true) { unset($r); return true; //return sqlite_free_result($r); } /** * Teste si le charset indiqué est disponible sur le serveur SQL (aucune action ici) * * Cette fonction n'a aucune action actuellement * * @param array|string $charset Nom du charset à tester. * @param string $serveur Nom de la connexion * @param bool $requeter inutilisé * @return void */ function spip_sqlite_get_charset($charset = array(), $serveur = '', $requeter = true) { //$c = !$charset ? '' : (" LIKE "._q($charset['charset'])); //return spip_sqlite_fetch(sqlite_query(_sqlite_link($serveur), "SHOW CHARACTER SET$c"), NULL, $serveur); } /** * Prépare une chaîne hexadécimale * * Par exemple : FF ==> 255 en SQLite * * @param string $v * Chaine hexadecimale * @return string * Valeur hexadécimale pour SQLite **/ function spip_sqlite_hex($v) { return hexdec($v); } /** * Retourne une expression IN pour le gestionnaire de base de données * * IN (...) est limité à 255 éléments, d'où cette fonction assistante * * @param string $val * Colonne SQL sur laquelle appliquer le test * @param string|array $valeurs * Liste des valeurs possibles (séparés par des virgules si string) * @param string $not * - '' sélectionne les éléments correspondant aux valeurs * - 'NOT' inverse en sélectionnant les éléments ne correspondant pas aux valeurs * @param string $serveur * Nom du connecteur * @param bool $requeter * Inutilisé * @return string * Expression de requête SQL **/ function spip_sqlite_in($val, $valeurs, $not = '', $serveur = '', $requeter = true) { $n = $i = 0; $in_sql = ""; while ($n = strpos($valeurs, ',', $n + 1)) { if ((++$i) >= 255) { $in_sql .= "($val $not IN (" . substr($valeurs, 0, $n) . "))\n" . ($not ? "AND\t" : "OR\t"); $valeurs = substr($valeurs, $n + 1); $i = $n = 0; } } $in_sql .= "($val $not IN ($valeurs))"; return "($in_sql)"; } /** * Insère une ligne dans une table * * @param string $table * Nom de la table SQL * @param string $champs * Liste des colonnes impactées, * @param string $valeurs * Liste des valeurs, * @param array $desc * Tableau de description des colonnes de la table SQL utilisée * (il sera calculé si nécessaire s'il n'est pas transmis). * @param string $serveur * Nom du connecteur * @param bool $requeter * Exécuter la requête, sinon la retourner * @return bool|string|int|array * - int|true identifiant de l'élément inséré (si possible), ou true, si réussite * - Texte de la requête si demandé, * - False en cas d'erreur, * - Tableau de description de la requête et du temps d'exécution, si var_profile activé **/ function spip_sqlite_insert($table, $champs, $valeurs, $desc = array(), $serveur = '', $requeter = true) { $query = "INSERT INTO $table " . ($champs ? "$champs VALUES $valeurs" : "DEFAULT VALUES"); if ($r = spip_sqlite_query($query, $serveur, $requeter)) { if (!$requeter) { return $r; } $nb = spip_sqlite::last_insert_id($serveur); } else { $nb = false; } $err = spip_sqlite_error($query, $serveur); // cas particulier : ne pas substituer la reponse spip_sqlite_query si on est en profilage return isset($_GET['var_profile']) ? $r : $nb; } /** * Insère une ligne dans une table, en protégeant chaque valeur * * @param string $table * Nom de la table SQL * @param string $couples * Couples (colonne => valeur) * @param array $desc * Tableau de description des colonnes de la table SQL utilisée * (il sera calculé si nécessaire s'il n'est pas transmis). * @param string $serveur * Nom du connecteur * @param bool $requeter * Exécuter la requête, sinon la retourner * @return bool|string|int|array * - int|true identifiant de l'élément inséré (si possible), ou true, si réussite * - Texte de la requête si demandé, * - False en cas d'erreur, * - Tableau de description de la requête et du temps d'exécution, si var_profile activé **/ function spip_sqlite_insertq($table, $couples = array(), $desc = array(), $serveur = '', $requeter = true) { if (!$desc) { $desc = description_table($table, $serveur); } if (!$desc) { die("$table insertion sans description"); } $fields = isset($desc['field']) ? $desc['field'] : array(); foreach ($couples as $champ => $val) { $couples[$champ] = _sqlite_calculer_cite($val, $fields[$champ]); } // recherche de champs 'timestamp' pour mise a jour auto de ceux-ci $couples = _sqlite_ajouter_champs_timestamp($table, $couples, $desc, $serveur); $cles = $valeurs = ""; if (count($couples)) { $cles = "(" . join(',', array_keys($couples)) . ")"; $valeurs = "(" . join(',', $couples) . ")"; } return spip_sqlite_insert($table, $cles, $valeurs, $desc, $serveur, $requeter); } /** * Insère plusieurs lignes d'un coup dans une table * * @param string $table * Nom de la table SQL * @param array $tab_couples * Tableau de tableaux associatifs (colonne => valeur) * @param array $desc * Tableau de description des colonnes de la table SQL utilisée * (il sera calculé si nécessaire s'il n'est pas transmis). * @param string $serveur * Nom du connecteur * @param bool $requeter * Exécuter la requête, sinon la retourner * @return bool|string * - True en cas de succès, * - Texte de la requête si demandé, * - False en cas d'erreur. **/ function spip_sqlite_insertq_multi($table, $tab_couples = array(), $desc = array(), $serveur = '', $requeter = true) { if (!$desc) { $desc = description_table($table, $serveur); } if (!$desc) { die("$table insertion sans description"); } if (!isset($desc['field'])) { $desc['field'] = array(); } // recuperer les champs 'timestamp' pour mise a jour auto de ceux-ci $maj = _sqlite_ajouter_champs_timestamp($table, array(), $desc, $serveur); // seul le nom de la table est a traduire ici : // le faire une seule fois au debut $query_start = "INSERT INTO $table "; $query_start = spip_sqlite::traduire_requete($query_start, $serveur); // ouvrir une transaction if ($requeter) { spip_sqlite::demarrer_transaction($serveur); } while ($couples = array_shift($tab_couples)) { foreach ($couples as $champ => $val) { $couples[$champ] = _sqlite_calculer_cite($val, $desc['field'][$champ]); } // inserer les champs timestamp par defaut $couples = array_merge($maj, $couples); $champs = $valeurs = ""; if (count($couples)) { $champs = "(" . join(',', array_keys($couples)) . ")"; $valeurs = "(" . join(',', $couples) . ")"; $query = $query_start . "$champs VALUES $valeurs"; } else { $query = $query_start . "DEFAULT VALUES"; } if ($requeter) { $retour = spip_sqlite::executer_requete($query, $serveur); } // sur le dernier couple uniquement if (!count($tab_couples)) { $nb = 0; if ($requeter) { $nb = spip_sqlite::last_insert_id($serveur); } else { return $query; } } $err = spip_sqlite_error($query, $serveur); } if ($requeter) { spip_sqlite::finir_transaction($serveur); } // renvoie le dernier id d'autoincrement ajoute // cas particulier : ne pas substituer la reponse spip_sqlite_query si on est en profilage return isset($_GET['var_profile']) ? $retour : $nb; } /** * Retourne si le moteur SQL préfère utiliser des transactions. * * @param string $serveur * Nom du connecteur * @param bool $requeter * Inutilisé * @return bool * Toujours true. **/ function spip_sqlite_preferer_transaction($serveur = '', $requeter = true) { return true; } /** * Démarre une transaction * * Pratique pour des sql_updateq() dans un foreach, * parfois 100* plus rapide s'ils sont nombreux en sqlite ! * * @param string $serveur * Nom du connecteur * @param bool $requeter * true pour exécuter la requête ou false pour retourner le texte de la requête * @return bool|string * string si texte de la requête demandé, true sinon **/ function spip_sqlite_demarrer_transaction($serveur = '', $requeter = true) { if (!$requeter) { return "BEGIN TRANSACTION"; } spip_sqlite::demarrer_transaction($serveur); return true; } /** * Clôture une transaction * * @param string $serveur * Nom du connecteur * @param bool $requeter * true pour exécuter la requête ou false pour retourner le texte de la requête * @return bool|string * string si texte de la requête demandé, true sinon **/ function spip_sqlite_terminer_transaction($serveur = '', $requeter = true) { if (!$requeter) { return "COMMIT"; } spip_sqlite::finir_transaction($serveur); return true; } /** * Liste les bases de données disponibles * * @param string $serveur * Nom du connecteur * @param bool $requeter * Inutilisé * @return array * Liste des noms de bases **/ function spip_sqlite_listdbs($serveur = '', $requeter = true) { _sqlite_init(); if (!is_dir($d = substr(_DIR_DB, 0, -1))) { return array(); } include_spip('inc/flock'); $bases = preg_files($d, $pattern = '(.*)\.sqlite$'); $bds = array(); foreach ($bases as $b) { // pas de bases commencant pas sqlite // (on s'en sert pour l'installation pour simuler la presence d'un serveur) // les bases sont de la forme _sqliteX_tmp_spip_install.sqlite if (strpos($b, '_sqlite')) { continue; } $bds[] = preg_replace(";.*/$pattern;iS", '$1', $b); } return $bds; } /** * Retourne l'instruction SQL pour obtenir le texte d'un champ contenant * une balise `` dans la langue indiquée * * Cette sélection est mise dans l'alias `multi` (instruction AS multi). * * @param string $objet Colonne ayant le texte * @param string $lang Langue à extraire * @return string Texte de sélection pour la requête */ function spip_sqlite_multi($objet, $lang) { $r = "EXTRAIRE_MULTI(" . $objet . ", '" . $lang . "') AS multi"; return $r; } /** * Optimise une table SQL * * @note * Sqlite optimise TOUT un fichier sinon rien. * On évite donc 2 traitements sur la même base dans un hit. * * @param $table nom de la table a optimiser * @param $serveur nom de la connexion * @param $requeter effectuer la requete ? sinon retourner son code * @return bool|string true / false / requete **/ function spip_sqlite_optimize($table, $serveur = '', $requeter = true) { static $do = false; if ($requeter and $do) { return true; } if ($requeter) { $do = true; } return spip_sqlite_query("VACUUM", $serveur, $requeter); } /** * Échapper une valeur selon son type * mais pour SQLite avec ses spécificités * * @param string|array|number $v * Texte, nombre ou tableau à échapper * @param string $type * Description du type attendu * (par exemple description SQL de la colonne recevant la donnée) * @return string|number * Donnée prête à être utilisée par le gestionnaire SQL */ function spip_sqlite_quote($v, $type = '') { if (!is_array($v)) { return _sqlite_calculer_cite($v, $type); } // si c'est un tableau, le parcourir en propageant le type foreach ($v as $k => $r) { $v[$k] = spip_sqlite_quote($r, $type); } return join(",", $v); } /** * Tester si une date est proche de la valeur d'un champ * * @param string $champ * Nom du champ a tester * @param int $interval * Valeur de l'intervalle : -1, 4, ... * @param string $unite * Utité utilisée (DAY, MONTH, YEAR, ...) * @return string * Expression SQL **/ function spip_sqlite_date_proche($champ, $interval, $unite) { $op = (($interval <= 0) ? '>' : '<'); return "($champ $op datetime('" . date("Y-m-d H:i:s") . "', '$interval $unite'))"; } /** * Répare une table SQL * * Il n'y a pas de fonction native repair dans sqlite, mais on profite * pour vérifier que tous les champs (text|char) ont bien une clause DEFAULT * * @param string $table Nom de la table SQL * @param string $serveur Nom de la connexion * @param bool $requeter Exécuter la requête, sinon la retourner * @return string[] * Tableau avec clé 0 pouvant avoir " OK " ou " ERROR " indiquant * l'état de la table après la réparation */ function spip_sqlite_repair($table, $serveur = '', $requeter = true) { if ($desc = spip_sqlite_showtable($table, $serveur) and isset($desc['field']) and is_array($desc['field']) ) { foreach ($desc['field'] as $c => $d) { if (preg_match(",^(tinytext|mediumtext|text|longtext|varchar|char),i", $d) and stripos($d, "NOT NULL") !== false and stripos($d, "DEFAULT") === false /* pas touche aux cles primaires */ and (!isset($desc['key']['PRIMARY KEY']) or $desc['key']['PRIMARY KEY'] !== $c) ) { spip_sqlite_alter($q = "TABLE $table CHANGE $c $c $d DEFAULT ''", $serveur); spip_log("ALTER $q", "repair" . _LOG_INFO_IMPORTANTE); } if (preg_match(",^(INTEGER),i", $d) and stripos($d, "NOT NULL") !== false and stripos($d, "DEFAULT") === false /* pas touche aux cles primaires */ and (!isset($desc['key']['PRIMARY KEY']) or $desc['key']['PRIMARY KEY'] !== $c) ) { spip_sqlite_alter($q = "TABLE $table CHANGE $c $c $d DEFAULT '0'", $serveur); spip_log("ALTER $q", "repair" . _LOG_INFO_IMPORTANTE); } if (preg_match(",^(datetime),i", $d) and stripos($d, "NOT NULL") !== false and stripos($d, "DEFAULT") === false /* pas touche aux cles primaires */ and (!isset($desc['key']['PRIMARY KEY']) or $desc['key']['PRIMARY KEY'] !== $c) ) { spip_sqlite_alter($q = "TABLE $table CHANGE $c $c $d DEFAULT '0000-00-00 00:00:00'", $serveur); spip_log("ALTER $q", "repair" . _LOG_INFO_IMPORTANTE); } } return array(" OK "); } return array(" ERROR "); } /** * Insère où met à jour une entrée d’une table SQL * * La clé ou les cles primaires doivent être présentes dans les données insérés. * La fonction effectue une protection automatique des données. * * Préférer à cette fonction updateq ou insertq. * * @param string $table * Nom de la table SQL * @param array $couples * Couples colonne / valeur à modifier, * @param array $desc * Tableau de description des colonnes de la table SQL utilisée * (il sera calculé si nécessaire s'il n'est pas transmis). * @param string $serveur * Nom du connecteur * @param bool $requeter * Exécuter la requête, sinon la retourner * @return bool|string * - true si réussite * - Texte de la requête si demandé, * - False en cas d'erreur. **/ function spip_sqlite_replace($table, $couples, $desc = array(), $serveur = '', $requeter = true) { if (!$desc) { $desc = description_table($table, $serveur); } if (!$desc) { die("$table insertion sans description"); } $fields = isset($desc['field']) ? $desc['field'] : array(); foreach ($couples as $champ => $val) { $couples[$champ] = _sqlite_calculer_cite($val, $fields[$champ]); } // recherche de champs 'timestamp' pour mise a jour auto de ceux-ci $couples = _sqlite_ajouter_champs_timestamp($table, $couples, $desc, $serveur); return spip_sqlite_query("REPLACE INTO $table (" . join(',', array_keys($couples)) . ') VALUES (' . join(',', $couples) . ')', $serveur); } /** * Insère où met à jour des entrées d’une table SQL * * La clé ou les cles primaires doivent être présentes dans les données insérés. * La fonction effectue une protection automatique des données. * * Préférez insertq_multi et sql_updateq * * @param string $table * Nom de la table SQL * @param array $tab_couples * Tableau de tableau (colonne / valeur à modifier), * @param array $desc * Tableau de description des colonnes de la table SQL utilisée * (il sera calculé si nécessaire s'il n'est pas transmis). * @param string $serveur * Nom du connecteur * @param bool $requeter * Exécuter la requête, sinon la retourner * @return bool|string * - true si réussite * - Texte de la requête si demandé, * - False en cas d'erreur. **/ function spip_sqlite_replace_multi($table, $tab_couples, $desc = array(), $serveur = '', $requeter = true) { // boucler pour trainter chaque requete independemment foreach ($tab_couples as $couples) { $retour = spip_sqlite_replace($table, $couples, $desc, $serveur, $requeter); } // renvoie le dernier id return $retour; } /** * Exécute une requête de sélection avec SQLite * * Instance de sql_select (voir ses specs). * * @see sql_select() * * @param string|array $select Champs sélectionnés * @param string|array $from Tables sélectionnées * @param string|array $where Contraintes * @param string|array $groupby Regroupements * @param string|array $orderby Tris * @param string $limit Limites de résultats * @param string|array $having Contraintes posts sélections * @param string $serveur Nom de la connexion * @param bool $requeter Exécuter la requête, sinon la retourner * @return array|bool|resource|string * - string : Texte de la requête si on ne l'exécute pas * - ressource si requête exécutée, ressource pour fetch() * - false si la requête exécutée a ratée * - array : Tableau décrivant requête et temps d'exécution si var_profile actif pour tracer. */ function spip_sqlite_select( $select, $from, $where = '', $groupby = '', $orderby = '', $limit = '', $having = '', $serveur = '', $requeter = true ) { // version() n'est pas connu de sqlite $select = str_replace('version()', 'sqlite_version()', $select); // recomposer from $from = (!is_array($from) ? $from : _sqlite_calculer_select_as($from)); $query = _sqlite_calculer_expression('SELECT', $select, ', ') . _sqlite_calculer_expression('FROM', $from, ', ') . _sqlite_calculer_expression('WHERE', $where) . _sqlite_calculer_expression('GROUP BY', $groupby, ',') . _sqlite_calculer_expression('HAVING', $having) . ($orderby ? ("\nORDER BY " . _sqlite_calculer_order($orderby)) : '') . ($limit ? "\nLIMIT $limit" : ''); // dans un select, on doit renvoyer la requête en cas d'erreur $res = spip_sqlite_query($query, $serveur, $requeter); // texte de la requete demande ? if (!$requeter) { return $res; } // erreur survenue ? if ($res === false) { return spip_sqlite::traduire_requete($query, $serveur); } return $res; } /** * Sélectionne un fichier de base de données * * @param string $db * Nom de la base à utiliser * @param string $serveur * Nom du connecteur * @param bool $requeter * Inutilisé * * @return bool|string * - Nom de la base en cas de success. * - False en cas d'erreur. **/ function spip_sqlite_selectdb($db, $serveur = '', $requeter = true) { _sqlite_init(); // interdire la creation d'une nouvelle base, // sauf si on est dans l'installation if (!is_file($f = _DIR_DB . $db . '.sqlite') && (!defined('_ECRIRE_INSTALL') || !_ECRIRE_INSTALL) ) { spip_log("Il est interdit de creer la base $db", 'sqlite.' . _LOG_HS); return false; } // se connecter a la base indiquee // avec les identifiants connus $index = $serveur ? $serveur : 0; if ($link = spip_connect_db('', '', '', '', '@selectdb@' . $db, $serveur, '', '')) { if (($db == $link['db']) && $GLOBALS['connexions'][$index] = $link) { return $db; } } else { spip_log("Impossible de selectionner la base $db", 'sqlite.' . _LOG_HS); return false; } } /** * Définit un charset pour la connexion avec SQLite (aucune action ici) * * Cette fonction n'a aucune action actuellement. * * @param string $charset Charset à appliquer * @param string $serveur Nom de la connexion * @param bool $requeter inutilisé * @return void */ function spip_sqlite_set_charset($charset, $serveur = '', $requeter = true) { # spip_log("Gestion charset sql a ecrire : "."SET NAMES "._q($charset), 'sqlite.'._LOG_ERREUR); # return spip_sqlite_query("SET NAMES ". spip_sqlite_quote($charset), $serveur); //<-- Passe pas ! } /** * Retourne une ressource de la liste des tables de la base de données * * @param string $match * Filtre sur tables à récupérer * @param string $serveur * Connecteur de la base * @param bool $requeter * true pour éxecuter la requête * false pour retourner le texte de la requête. * @return ressource * Ressource à utiliser avec sql_fetch() **/ function spip_sqlite_showbase($match, $serveur = '', $requeter = true) { // type est le type d'entrée : table / index / view // on ne retourne que les tables (?) et non les vues... # ESCAPE non supporte par les versions sqlite <3 # return spip_sqlite_query("SELECT name FROM sqlite_master WHERE type='table' AND tbl_name LIKE "._q($match)." ESCAPE '\'", $serveur, $requeter); $match = preg_quote($match); $match = str_replace("\\\_", "[[TIRETBAS]]", $match); $match = str_replace("\\\%", "[[POURCENT]]", $match); $match = str_replace("_", ".", $match); $match = str_replace("%", ".*", $match); $match = str_replace("[[TIRETBAS]]", "_", $match); $match = str_replace("[[POURCENT]]", "%", $match); $match = "^$match$"; return spip_sqlite_query("SELECT name FROM sqlite_master WHERE type='table' AND tbl_name REGEXP " . _q($match), $serveur, $requeter); } define('_SQLITE_RE_SHOW_TABLE', '/^[^(),]*\(((?:[^()]*\((?:[^()]*\([^()]*\))?[^()]*\)[^()]*)*[^()]*)\)[^()]*$/'); /** * Obtient la description d'une table ou vue SQLite * * Récupère la définition d'une table ou d'une vue avec colonnes, indexes, etc. * au même format que la définition des tables SPIP, c'est à dire * un tableau avec les clés * * - `field` (tableau colonne => description SQL) et * - `key` (tableau type de clé => colonnes) * * @param string $nom_table Nom de la table SQL * @param string $serveur Nom de la connexion * @param bool $requeter Exécuter la requête, sinon la retourner * @return array|string * - chaîne vide si pas de description obtenue * - string Texte de la requête si demandé * - array description de la table sinon */ function spip_sqlite_showtable($nom_table, $serveur = '', $requeter = true) { $query = 'SELECT sql, type FROM' . ' (SELECT * FROM sqlite_master UNION ALL' . ' SELECT * FROM sqlite_temp_master)' . " WHERE tbl_name LIKE '$nom_table'" . " AND type!='meta' AND sql NOT NULL AND name NOT LIKE 'sqlite_%'" . ' ORDER BY substr(type,2,1), name'; $a = spip_sqlite_query($query, $serveur, $requeter); if (!$a) { return ""; } if (!$requeter) { return $a; } if (!($a = spip_sqlite_fetch($a, null, $serveur))) { return ""; } $vue = ($a['type'] == 'view'); // table | vue // c'est une table // il faut parser le create if (!$vue) { if (!preg_match(_SQLITE_RE_SHOW_TABLE, array_shift($a), $r)) { return ""; } else { $desc = $r[1]; // extraction d'une KEY éventuelle en prenant garde de ne pas // relever un champ dont le nom contient KEY (ex. ID_WHISKEY) if (preg_match("/^(.*?),([^,]*\sKEY[ (].*)$/s", $desc, $r)) { $namedkeys = $r[2]; $desc = $r[1]; } else { $namedkeys = ""; } $fields = array(); $keys = array(); // enlever les contenus des valeurs DEFAULT 'xxx' qui pourraient perturber // par exemple s'il contiennent une virgule. // /!\ cela peut aussi echapper le nom des champs si la table a eu des operations avec SQLite Manager ! list($desc, $echaps) = query_echappe_textes($desc); // separer toutes les descriptions de champs, separes par des virgules # /!\ explode peut exploser aussi DECIMAL(10,2) ! $k_precedent = null; foreach (explode(",", $desc) as $v) { preg_match("/^\s*([^\s]+)\s+(.*)/", $v, $r); // Les cles de champs peuvent etre entourees // de guillements doubles " , simples ', graves ` ou de crochets [ ], ou rien. // http://www.sqlite.org/lang_keywords.html $k = strtolower(query_reinjecte_textes($r[1], $echaps)); // champ, "champ", [champ]... if ($char = strpbrk($k[0], '\'"[`')) { $k = trim($k, $char); if ($char == '[') { $k = rtrim($k, ']'); } } $def = query_reinjecte_textes($r[2], $echaps); // valeur du champ # rustine pour DECIMAL(10,2) if (false !== strpos($k, ')')) { $fields[$k_precedent] .= ',' . $k . ' ' . $def; continue; } $fields[$k] = $def; $k_precedent = $k; // la primary key peut etre dans une des descriptions de champs // et non en fin de table, cas encore decouvert avec Sqlite Manager if (stripos($r[2], 'PRIMARY KEY') !== false) { $keys['PRIMARY KEY'] = $k; } } // key inclues dans la requete foreach (preg_split('/\)\s*(,|$)/', $namedkeys) as $v) { if (preg_match("/^\s*([^(]*)\(([^(]*(\(\d+\))?)$/", $v, $r)) { $k = str_replace("`", '', trim($r[1])); $t = trim(strtolower(str_replace("`", '', $r[2])), '"'); if ($k && !isset($keys[$k])) { $keys[$k] = $t; } else { $keys[] = $t; } } } // sinon ajouter les key index $query = 'SELECT name,sql FROM' . ' (SELECT * FROM sqlite_master UNION ALL' . ' SELECT * FROM sqlite_temp_master)' . " WHERE tbl_name LIKE '$nom_table'" . " AND type='index' AND name NOT LIKE 'sqlite_%'" . 'ORDER BY substr(type,2,1), name'; $a = spip_sqlite_query($query, $serveur, $requeter); while ($r = spip_sqlite_fetch($a, null, $serveur)) { $key = str_replace($nom_table . '_', '', $r['name']); // enlever le nom de la table ajoute a l'index $keytype = "KEY"; if (strpos($r['sql'], "UNIQUE INDEX") !== false) { $keytype = "UNIQUE KEY"; } $colonnes = preg_replace(',.*\((.*)\).*,', '$1', $r['sql']); $keys[$keytype . ' ' . $key] = $colonnes; } } } // c'est une vue, on liste les champs disponibles simplement else { if ($res = sql_fetsel('*', $nom_table, '', '', '', '1', '', $serveur)) { // limit 1 $fields = array(); foreach ($res as $c => $v) { $fields[$c] = ''; } $keys = array(); } else { return ""; } } return array('field' => $fields, 'key' => $keys); } /** * Met à jour des enregistrements d'une table SQL * * @param string $table * Nom de la table * @param array $champs * Couples (colonne => valeur) * @param string|array $where * Conditions a remplir (Where) * @param array $desc * Tableau de description des colonnes de la table SQL utilisée * (il sera calculé si nécessaire s'il n'est pas transmis). * @param string $serveur * Nom de la connexion * @param bool $requeter * Exécuter la requête, sinon la retourner * @return array|bool|string * - string : texte de la requête si demandé * - true si la requête a réussie, false sinon * - array Tableau décrivant la requête et son temps d'exécution si var_profile est actif */ function spip_sqlite_update($table, $champs, $where = '', $desc = '', $serveur = '', $requeter = true) { // recherche de champs 'timestamp' pour mise a jour auto de ceux-ci $champs = _sqlite_ajouter_champs_timestamp($table, $champs, $desc, $serveur); $set = array(); foreach ($champs as $champ => $val) { $set[] = $champ . "=$val"; } if (!empty($set)) { return spip_sqlite_query( _sqlite_calculer_expression('UPDATE', $table, ',') . _sqlite_calculer_expression('SET', $set, ',') . _sqlite_calculer_expression('WHERE', $where), $serveur, $requeter); } } /** * Met à jour des enregistrements d'une table SQL et protège chaque valeur * * Protège chaque valeur transmise avec sql_quote(), adapté au type * de champ attendu par la table SQL * * @param string $table * Nom de la table * @param array $champs * Couples (colonne => valeur) * @param string|array $where * Conditions a remplir (Where) * @param array $desc * Tableau de description des colonnes de la table SQL utilisée * (il sera calculé si nécessaire s'il n'est pas transmis). * @param string $serveur * Nom de la connexion * @param bool $requeter * Exécuter la requête, sinon la retourner * @return array|bool|string * - string : texte de la requête si demandé * - true si la requête a réussie, false sinon * - array Tableau décrivant la requête et son temps d'exécution si var_profile est actif */ function spip_sqlite_updateq($table, $champs, $where = '', $desc = array(), $serveur = '', $requeter = true) { if (!$champs) { return; } if (!$desc) { $desc = description_table($table, $serveur); } if (!$desc) { die("$table insertion sans description"); } $fields = $desc['field']; // recherche de champs 'timestamp' pour mise a jour auto de ceux-ci $champs = _sqlite_ajouter_champs_timestamp($table, $champs, $desc, $serveur); $set = array(); foreach ($champs as $champ => $val) { $set[] = $champ . '=' . _sqlite_calculer_cite($val, isset($fields[$champ]) ? $fields[$champ] : ''); } return spip_sqlite_query( _sqlite_calculer_expression('UPDATE', $table, ',') . _sqlite_calculer_expression('SET', $set, ',') . _sqlite_calculer_expression('WHERE', $where), $serveur, $requeter); } /* * * Ensuite les fonctions non abstraites * crees pour l'occasion de sqlite * */ /** * Initialise la première connexion à un serveur SQLite * * @return void */ function _sqlite_init() { if (!defined('_DIR_DB')) { define('_DIR_DB', _DIR_ETC . 'bases/'); } if (!defined('_SQLITE_CHMOD')) { define('_SQLITE_CHMOD', _SPIP_CHMOD); } if (!is_dir($d = _DIR_DB)) { include_spip('inc/flock'); sous_repertoire($d); } } /** * Teste la version sqlite du link en cours * * @param string $version * @param string $link * @param string $serveur * @param bool $requeter * @return bool|int */ function _sqlite_is_version($version = '', $link = '', $serveur = '', $requeter = true) { if ($link === '') { $link = _sqlite_link($serveur); } if (!$link) { return false; } if ($link instanceof PDO) { $v = 3; } else { $v = 2; } if (!$version) { return $v; } return ($version == $v); } /** * Retrouver un link d'une connexion SQLite * * @param string $serveur Nom du serveur * @return Object Information de connexion pour SQLite */ function _sqlite_link($serveur = '') { $link = &$GLOBALS['connexions'][$serveur ? $serveur : 0]['link']; return $link; } /* ordre alphabetique pour les autres */ /** * Renvoie les bons echappements (mais pas sur les fonctions comme NOW()) * * @param string|number $v Texte ou nombre à échapper * @param string $type Type de donnée attendue, description SQL de la colonne de destination * @return string|number Texte ou nombre échappé */ function _sqlite_calculer_cite($v, $type) { if ($type) { if (is_null($v) and stripos($type, "NOT NULL") === false ) { return 'NULL'; } // null php se traduit en NULL SQL if (sql_test_date($type) and preg_match('/^\w+\(/', $v)) { return $v; } if (sql_test_int($type)) { if (is_numeric($v)) { return $v; } elseif (ctype_xdigit(substr($v, 2)) and strncmp($v, '0x', 2) == 0) { return hexdec(substr($v, 2)); } else { return intval($v); } } } else { // si on ne connait pas le type on le deduit de $v autant que possible if (is_numeric($v)) { return strval($v); } } if (function_exists('sqlite_escape_string')) { return "'" . sqlite_escape_string($v) . "'"; } // trouver un link sqlite3 pour faire l'echappement foreach ($GLOBALS['connexions'] as $s) { if (_sqlite_is_version(3, $l = $s['link'])) { return $l->quote($v); } } // echapper les ' en '' spip_log("Pas de methode sqlite_escape_string ni ->quote pour echapper", "sqlite." . _LOG_INFO_IMPORTANTE); return ("'" . str_replace("'", "''", $v) . "'"); } /** * Calcule un expression pour une requête, en cumulant chaque élément * avec l'opérateur de liaison ($join) indiqué * * Renvoie grosso modo "$expression join($join, $v)" * * @param string $expression Mot clé de l'expression, tel que "WHERE" ou "ORDER BY" * @param array|string $v Données de l'expression * @param string $join Si les données sont un tableau, elles seront groupées par cette jointure * @return string Texte de l'expression, une partie donc, du texte la requête. */ function _sqlite_calculer_expression($expression, $v, $join = 'AND') { if (empty($v)) { return ''; } $exp = "\n$expression "; if (!is_array($v)) { return $exp . $v; } else { if (strtoupper($join) === 'AND') { return $exp . join("\n\t$join ", array_map('_sqlite_calculer_where', $v)); } else { return $exp . join($join, $v); } } } /** * Prépare une clause order by * * Regroupe en texte les éléments si un tableau est donné * * @note * Pas besoin de conversion pour 0+x comme il faudrait pour mysql. * * @param string|array $orderby Texte du orderby à préparer * @return string Texte du orderby préparé */ function _sqlite_calculer_order($orderby) { return (is_array($orderby)) ? join(", ", $orderby) : $orderby; } /** * Renvoie des `nom AS alias` * * @param array $args * @return string Sélection de colonnes pour une clause SELECT */ function _sqlite_calculer_select_as($args) { $res = ''; foreach ($args as $k => $v) { if (substr($k, -1) == '@') { // c'est une jointure qui se refere au from precedent // pas de virgule $res .= ' ' . $v; } else { if (!is_numeric($k)) { $p = strpos($v, " "); if ($p) { $v = substr($v, 0, $p) . " AS '$k'" . substr($v, $p); } else { $v .= " AS '$k'"; } } $res .= ', ' . $v; } } return substr($res, 2); } /** * Prépare une clause WHERE pour SQLite * * Retourne une chaîne avec les bonnes parenthèses pour la * contrainte indiquée, au format donnée par le compilateur * * @param array|string $v * Description des contraintes * - string : Texte du where * - sinon tableau : A et B peuvent être de type string ou array, * OP et C sont de type string : * - array(A) : A est le texte du where * - array(OP, A) : contrainte OP( A ) * - array(OP, A, B) : contrainte (A OP B) * - array(OP, A, B, C) : contrainte (A OP (B) : C) * @return string * Contrainte pour clause WHERE */ function _sqlite_calculer_where($v) { if (!is_array($v)) { return $v; } $op = array_shift($v); if (!($n = count($v))) { return $op; } else { $arg = _sqlite_calculer_where(array_shift($v)); if ($n == 1) { return "$op($arg)"; } else { $arg2 = _sqlite_calculer_where(array_shift($v)); if ($n == 2) { return "($arg $op $arg2)"; } else { return "($arg $op ($arg2) : $v[0])"; } } } } /** * Charger les modules SQLite * * Si possible et juste la version demandée, * ou, si aucune version, on renvoie les versions sqlite disponibles * sur ce serveur dans un tableau * * @param string $version * @return array|bool */ function _sqlite_charger_version($version = '') { $versions = array(); // version 2 if (!$version || $version == 2) { if (charger_php_extension('sqlite')) { $versions[] = 2; } } // version 3 if (!$version || $version == 3) { if (charger_php_extension('pdo') && charger_php_extension('pdo_sqlite')) { $versions[] = 3; } } if ($version) { return in_array($version, $versions); } return $versions; } /** * Gestion des requêtes ALTER non reconnues de SQLite * * Requêtes non reconnues : * * ALTER TABLE table DROP column * ALTER TABLE table CHANGE [COLUMN] columnA columnB definition * ALTER TABLE table MODIFY column definition * ALTER TABLE table ADD|DROP PRIMARY KEY * * `MODIFY` est transformé en `CHANGE columnA columnA` par spip_sqlite_alter() * * 1) Créer une table B avec le nouveau format souhaité * 2) Copier la table d'origine A vers B * 3) Supprimer la table A * 4) Renommer la table B en A * 5) Remettre les index (qui sont supprimés avec la table A) * * @param string|array $table * - string : Nom de la table table, * - array : couple (nom de la table => nom futur) * @param string|array $colonne * - string : nom de la colonne, * - array : couple (nom de la colonne => nom futur) * @param array $opt * options comme les tables SPIP, qui sera mergé à la table créee : * `array('field'=>array('nom'=>'syntaxe', ...), 'key'=>array('KEY nom'=>'colonne', ...))` * @param string $serveur * Nom de la connexion SQL en cours * @return bool * true si OK, false sinon. */ function _sqlite_modifier_table($table, $colonne, $opt = array(), $serveur = '') { if (is_array($table)) { reset($table); list($table_origine, $table_destination) = each($table); } else { $table_origine = $table_destination = $table; } // ne prend actuellement qu'un changement // mais pourra etre adapte pour changer plus qu'une colonne a la fois if (is_array($colonne)) { reset($colonne); list($colonne_origine, $colonne_destination) = each($colonne); } else { $colonne_origine = $colonne_destination = $colonne; } if (!isset($opt['field'])) { $opt['field'] = array(); } if (!isset($opt['key'])) { $opt['key'] = array(); } // si les noms de tables sont differents, pas besoin de table temporaire // on prendra directement le nom de la future table $meme_table = ($table_origine == $table_destination); $def_origine = sql_showtable($table_origine, false, $serveur); if (!$def_origine or !isset($def_origine['field'])) { spip_log("Alter table impossible sur $table_origine : table non trouvee", 'sqlite' . _LOG_ERREUR); return false; } $table_tmp = $table_origine . '_tmp'; // 1) creer une table temporaire avec les modifications // - DROP : suppression de la colonne // - CHANGE : modification de la colonne // (foreach pour conserver l'ordre des champs) // field $fields = array(); // pour le INSERT INTO plus loin // stocker la correspondance nouvelles->anciennes colonnes $fields_correspondances = array(); foreach ($def_origine['field'] as $c => $d) { if ($colonne_origine && ($c == $colonne_origine)) { // si pas DROP if ($colonne_destination) { $fields[$colonne_destination] = $opt['field'][$colonne_destination]; $fields_correspondances[$colonne_destination] = $c; } } else { $fields[$c] = $d; $fields_correspondances[$c] = $c; } } // cas de ADD sqlite2 (ajout du champ en fin de table): if (!$colonne_origine && $colonne_destination) { $fields[$colonne_destination] = $opt['field'][$colonne_destination]; } // key... $keys = array(); foreach ($def_origine['key'] as $c => $d) { $c = str_replace($colonne_origine, $colonne_destination, $c); $d = str_replace($colonne_origine, $colonne_destination, $d); // seulement si on ne supprime pas la colonne ! if ($d) { $keys[$c] = $d; } } // autres keys, on merge $keys = array_merge($keys, $opt['key']); $queries = array(); // copier dans destination (si differente de origine), sinon tmp $table_copie = ($meme_table) ? $table_tmp : $table_destination; $autoinc = (isset($keys['PRIMARY KEY']) and $keys['PRIMARY KEY'] and stripos($keys['PRIMARY KEY'], ',') === false and stripos($fields[$keys['PRIMARY KEY']], 'default') === false); if ($q = _sqlite_requete_create( $table_copie, $fields, $keys, $autoinc, $temporary = false, $ifnotexists = true, $serveur) ) { $queries[] = $q; } // 2) y copier les champs qui vont bien $champs_dest = join(', ', array_keys($fields_correspondances)); $champs_ori = join(', ', $fields_correspondances); $queries[] = "INSERT INTO $table_copie ($champs_dest) SELECT $champs_ori FROM $table_origine"; // 3) supprimer la table d'origine $queries[] = "DROP TABLE $table_origine"; // 4) renommer la table temporaire // avec le nom de la table destination // si necessaire if ($meme_table) { if (_sqlite_is_version(3, '', $serveur)) { $queries[] = "ALTER TABLE $table_copie RENAME TO $table_destination"; } else { $queries[] = _sqlite_requete_create( $table_destination, $fields, $keys, $autoinc, $temporary = false, $ifnotexists = false, // la table existe puisqu'on est dans une transaction $serveur); $queries[] = "INSERT INTO $table_destination SELECT * FROM $table_copie"; $queries[] = "DROP TABLE $table_copie"; } } // 5) remettre les index ! foreach ($keys as $k => $v) { if ($k == 'PRIMARY KEY') { } else { // enlever KEY $k = substr($k, 4); $queries[] = "CREATE INDEX $table_destination" . "_$k ON $table_destination ($v)"; } } if (count($queries)) { spip_sqlite::demarrer_transaction($serveur); // il faut les faire une par une car $query = join('; ', $queries).";"; ne fonctionne pas foreach ($queries as $q) { if (!spip_sqlite::executer_requete($q, $serveur)) { spip_log(_LOG_GRAVITE_ERREUR, "SQLite : ALTER TABLE table :" . " Erreur a l'execution de la requete : $q", 'sqlite'); spip_sqlite::annuler_transaction($serveur); return false; } } spip_sqlite::finir_transaction($serveur); } return true; } /** * Nom des fonctions * * @return array */ function _sqlite_ref_fonctions() { $fonctions = array( 'alter' => 'spip_sqlite_alter', 'count' => 'spip_sqlite_count', 'countsel' => 'spip_sqlite_countsel', 'create' => 'spip_sqlite_create', 'create_base' => 'spip_sqlite_create_base', 'create_view' => 'spip_sqlite_create_view', 'date_proche' => 'spip_sqlite_date_proche', 'delete' => 'spip_sqlite_delete', 'drop_table' => 'spip_sqlite_drop_table', 'drop_view' => 'spip_sqlite_drop_view', 'errno' => 'spip_sqlite_errno', 'error' => 'spip_sqlite_error', 'explain' => 'spip_sqlite_explain', 'fetch' => 'spip_sqlite_fetch', 'seek' => 'spip_sqlite_seek', 'free' => 'spip_sqlite_free', 'hex' => 'spip_sqlite_hex', 'in' => 'spip_sqlite_in', 'insert' => 'spip_sqlite_insert', 'insertq' => 'spip_sqlite_insertq', 'insertq_multi' => 'spip_sqlite_insertq_multi', 'listdbs' => 'spip_sqlite_listdbs', 'multi' => 'spip_sqlite_multi', 'optimize' => 'spip_sqlite_optimize', 'query' => 'spip_sqlite_query', 'quote' => 'spip_sqlite_quote', 'repair' => 'spip_sqlite_repair', 'replace' => 'spip_sqlite_replace', 'replace_multi' => 'spip_sqlite_replace_multi', 'select' => 'spip_sqlite_select', 'selectdb' => 'spip_sqlite_selectdb', 'set_charset' => 'spip_sqlite_set_charset', 'get_charset' => 'spip_sqlite_get_charset', 'showbase' => 'spip_sqlite_showbase', 'showtable' => 'spip_sqlite_showtable', 'update' => 'spip_sqlite_update', 'updateq' => 'spip_sqlite_updateq', 'preferer_transaction' => 'spip_sqlite_preferer_transaction', 'demarrer_transaction' => 'spip_sqlite_demarrer_transaction', 'terminer_transaction' => 'spip_sqlite_terminer_transaction', ); // association de chaque nom http d'un charset aux couples sqlite // SQLite supporte utf-8 et utf-16 uniquement. $charsets = array( 'utf-8' => array('charset' => 'utf8', 'collation' => 'utf8_general_ci'), //'utf-16be'=>array('charset'=>'utf16be','collation'=>'UTF-16BE'),// aucune idee de quoi il faut remplir dans es champs la //'utf-16le'=>array('charset'=>'utf16le','collation'=>'UTF-16LE') ); $fonctions['charsets'] = $charsets; return $fonctions; } /** * $query est une requete ou une liste de champs * * @param $query * @param bool $autoinc * @return mixed */ function _sqlite_remplacements_definitions_table($query, $autoinc = false) { // quelques remplacements $num = "(\s*\([0-9]*\))?"; $enum = "(\s*\([^\)]*\))?"; $remplace = array( '/enum' . $enum . '/is' => 'VARCHAR(255)', '/COLLATE \w+_bin/is' => 'COLLATE BINARY', '/COLLATE \w+_ci/is' => 'COLLATE NOCASE', '/auto_increment/is' => '', '/(timestamp .* )ON .*$/is' => '\\1', '/character set \w+/is' => '', '/((big|small|medium|tiny)?int(eger)?)' . $num . '\s*unsigned/is' => '\\1 UNSIGNED', '/(text\s+not\s+null(\s+collate\s+\w+)?)\s*$/is' => "\\1 DEFAULT ''", '/((char|varchar)' . $num . '\s+not\s+null(\s+collate\s+\w+)?)\s*$/is' => "\\1 DEFAULT ''", '/(datetime\s+not\s+null)\s*$/is' => "\\1 DEFAULT '0000-00-00 00:00:00'", '/(date\s+not\s+null)\s*$/is' => "\\1 DEFAULT '0000-00-00'", ); // pour l'autoincrement, il faut des INTEGER NOT NULL PRIMARY KEY $remplace_autocinc = array( '/(big|small|medium|tiny)?int(eger)?' . $num . '/is' => 'INTEGER' ); // pour les int non autoincrement, il faut un DEFAULT $remplace_nonautocinc = array( '/((big|small|medium|tiny)?int(eger)?' . $num . '\s+not\s+null)\s*$/is' => "\\1 DEFAULT 0", ); if (is_string($query)) { $query = preg_replace(array_keys($remplace), $remplace, $query); if ($autoinc or preg_match(',AUTO_INCREMENT,is', $query)) { $query = preg_replace(array_keys($remplace_autocinc), $remplace_autocinc, $query); } else { $query = preg_replace(array_keys($remplace_nonautocinc), $remplace_nonautocinc, $query); $query = _sqlite_collate_ci($query); } } elseif (is_array($query)) { foreach ($query as $k => $q) { $ai = ($autoinc ? $k == $autoinc : preg_match(',AUTO_INCREMENT,is', $q)); $query[$k] = preg_replace(array_keys($remplace), $remplace, $query[$k]); if ($ai) { $query[$k] = preg_replace(array_keys($remplace_autocinc), $remplace_autocinc, $query[$k]); } else { $query[$k] = preg_replace(array_keys($remplace_nonautocinc), $remplace_nonautocinc, $query[$k]); $query[$k] = _sqlite_collate_ci($query[$k]); } } } return $query; } /** * Definir la collation d'un champ en fonction de si une collation est deja explicite * et du par defaut que l'on veut NOCASE * * @param string $champ * @return string */ function _sqlite_collate_ci($champ) { if (stripos($champ, "COLLATE") !== false) { return $champ; } if (stripos($champ, "BINARY") !== false) { return str_ireplace("BINARY", "COLLATE BINARY", $champ); } if (preg_match(",^(char|varchar|(long|small|medium|tiny)?text),i", $champ)) { return $champ . " COLLATE NOCASE"; } return $champ; } /** * Creer la requete pour la creation d'une table * retourne la requete pour utilisation par sql_create() et sql_alter() * * @param $nom * @param $champs * @param $cles * @param bool $autoinc * @param bool $temporary * @param bool $_ifnotexists * @param string $serveur * @param bool $requeter * @return bool|string */ function _sqlite_requete_create( $nom, $champs, $cles, $autoinc = false, $temporary = false, $_ifnotexists = true, $serveur = '', $requeter = true ) { $query = $keys = $s = $p = ''; // certains plugins declarent les tables (permet leur inclusion dans le dump) // sans les renseigner (laisse le compilo recuperer la description) if (!is_array($champs) || !is_array($cles)) { return; } // sqlite ne gere pas KEY tout court dans une requete CREATE TABLE // il faut passer par des create index // Il gere par contre primary key ! // Soit la PK est definie dans les cles, soit dans un champs // soit faussement dans les 2 (et dans ce cas, il faut l’enlever à un des 2 endroits !) $pk = "PRIMARY KEY"; // le champ de cle primaire $c = !empty($cles[$pk]) ? $cles[$pk] : ''; foreach ($champs as $k => $v) { if (false !== stripos($v, $pk)) { $c = $k; // on n'en a plus besoin dans field, vu que defini dans key $champs[$k] = preg_replace("/$pk/is", '', $champs[$k]); break; } } if ($c) { $keys = "\n\t\t$pk ($c)"; } // Pas de DEFAULT 0 sur les cles primaires en auto-increment if (isset($champs[$c]) and stripos($champs[$c], "default 0") !== false ) { $champs[$c] = trim(str_ireplace("default 0", "", $champs[$c])); } $champs = _sqlite_remplacements_definitions_table($champs, $autoinc ? $c : false); foreach ($champs as $k => $v) { $query .= "$s\n\t\t$k $v"; $s = ","; } $ifnotexists = ""; if ($_ifnotexists) { $version = spip_sqlite_fetch(spip_sqlite_query("select sqlite_version() AS sqlite_version", $serveur), '', $serveur); if (!function_exists('spip_version_compare')) { include_spip('plugins/installer'); } if ($version and spip_version_compare($version['sqlite_version'], '3.3.0', '>=')) { $ifnotexists = ' IF NOT EXISTS'; } else { /* simuler le IF EXISTS - version 2 et sqlite < 3.3a */ $a = spip_sqlite_showtable($nom, $serveur); if (isset($a['key']['KEY ' . $nom])) { return true; } } } $temporary = $temporary ? ' TEMPORARY' : ''; $q = "CREATE$temporary TABLE$ifnotexists $nom ($query" . ($keys ? ",$keys" : '') . ")\n"; return $q; } /** * Retrouver les champs 'timestamp' * pour les ajouter aux 'insert' ou 'replace' * afin de simuler le fonctionnement de mysql * * stocke le resultat pour ne pas faire * de requetes showtable intempestives * * @param $table * @param $couples * @param string $desc * @param string $serveur * @return */ function _sqlite_ajouter_champs_timestamp($table, $couples, $desc = '', $serveur = '') { static $tables = array(); if (!isset($tables[$table])) { if (!$desc) { $trouver_table = charger_fonction('trouver_table', 'base'); $desc = $trouver_table($table, $serveur); // si pas de description, on ne fait rien, ou on die() ? if (!$desc) { return $couples; } } // recherche des champs avec simplement 'TIMESTAMP' // cependant, il faudra peut etre etendre // avec la gestion de DEFAULT et ON UPDATE // mais ceux-ci ne sont pas utilises dans le core $tables[$table] = array(); foreach ($desc['field'] as $k => $v) { if (strpos(strtolower(ltrim($v)), 'timestamp') === 0) { $tables[$table][$k] = "datetime('now')"; } } } // ajout des champs type 'timestamp' absents return array_merge($tables[$table], $couples); } /** * Renvoyer la liste des versions sqlite disponibles * sur le serveur * * @return array|bool */ function spip_versions_sqlite() { return _sqlite_charger_version(); } /** * Gère l'envoi et la réception de requêtes à SQLite, qui peuvent être * encadrées de transactions. **/ class spip_sqlite { /** @var sqlite_requeteur[] Liste des instances de requêteurs créés */ public static $requeteurs = array(); /** @var bool[] Pour chaque connexion, flag pour savoir si une transaction est en cours */ public static $transaction_en_cours = array(); /** * Retourne une unique instance du requêteur * * Retourne une instance unique du requêteur pour une connexion SQLite * donnée * * @param string $serveur * Nom du connecteur * @return sqlite_requeteur * Instance unique du requêteur **/ public static function requeteur($serveur) { if (!isset(spip_sqlite::$requeteurs[$serveur])) { spip_sqlite::$requeteurs[$serveur] = new sqlite_requeteur($serveur); } return spip_sqlite::$requeteurs[$serveur]; } /** * Prépare le texte d'une requête avant son exécution * * Adapte la requête au format plus ou moins MySQL par un format * compris de SQLite. * * Change les préfixes de tables SPIP par ceux véritables * * @param string $query Requête à préparer * @param string $serveur Nom de la connexion * @return string Requête préparée */ public static function traduire_requete($query, $serveur) { $requeteur = spip_sqlite::requeteur($serveur); $traducteur = new sqlite_traducteur($query, $requeteur->prefixe, $requeteur->sqlite_version); return $traducteur->traduire_requete(); } /** * Démarre une transaction * * @param string $serveur Nom de la connexion **/ public static function demarrer_transaction($serveur) { spip_sqlite::executer_requete("BEGIN TRANSACTION", $serveur); spip_sqlite::$transaction_en_cours[$serveur] = true; } /** * Exécute la requête donnée * * @param string $query Requête * @param string $serveur Nom de la connexion * @param null|bool $tracer Demander des statistiques (temps) ? **/ public static function executer_requete($query, $serveur, $tracer = null) { $requeteur = spip_sqlite::requeteur($serveur); return $requeteur->executer_requete($query, $tracer); } /** * Obtient l'identifiant de la dernière ligne insérée ou modifiée * * @param string $serveur Nom de la connexion * return int Identifiant **/ public static function last_insert_id($serveur) { $requeteur = spip_sqlite::requeteur($serveur); return $requeteur->last_insert_id($serveur); } /** * Annule une transaction * * @param string $serveur Nom de la connexion **/ public static function annuler_transaction($serveur) { spip_sqlite::executer_requete("ROLLBACK", $serveur); spip_sqlite::$transaction_en_cours[$serveur] = false; } /** * Termine une transaction * * @param string $serveur Nom de la connexion **/ public static function finir_transaction($serveur) { // si pas de transaction en cours, ne rien faire et le dire if (!isset(spip_sqlite::$transaction_en_cours[$serveur]) or spip_sqlite::$transaction_en_cours[$serveur] == false ) { return false; } // sinon fermer la transaction et retourner true spip_sqlite::executer_requete("COMMIT", $serveur); spip_sqlite::$transaction_en_cours[$serveur] = false; return true; } } /* * Classe pour partager les lancements de requête * * Instanciée une fois par `$serveur` : * * - peut corriger la syntaxe des requêtes pour la conformité à SQLite * - peut tracer les requêtes */ class sqlite_requeteur { /** @var string Texte de la requête */ public $query = ''; // la requete /** @var string Nom de la connexion */ public $serveur = ''; /** @var Ressource Identifiant de la connexion SQLite */ public $link = ''; /** @var string Prefixe des tables SPIP */ public $prefixe = ''; /** @var string Nom de la base de donnée */ public $db = ''; /** @var bool Doit-on tracer les requetes (var_profile) ? */ public $tracer = false; // doit-on tracer les requetes (var_profile) /** @var string Version de SQLite (2 ou 3) */ public $sqlite_version = ''; /** * Constructeur * * @param string $serveur * @return bool */ public function __construct($serveur = '') { _sqlite_init(); $this->serveur = strtolower($serveur); if (!($this->link = _sqlite_link($this->serveur)) && (!defined('_ECRIRE_INSTALL') || !_ECRIRE_INSTALL)) { spip_log("Aucune connexion sqlite (link)", 'sqlite.' . _LOG_ERREUR); return false; } $this->sqlite_version = _sqlite_is_version('', $this->link); $this->prefixe = $GLOBALS['connexions'][$this->serveur ? $this->serveur : 0]['prefixe']; $this->db = $GLOBALS['connexions'][$this->serveur ? $this->serveur : 0]['db']; // tracage des requetes ? $this->tracer = (isset($_GET['var_profile']) && $_GET['var_profile']); } /** * Lancer la requête transmise et faire le tracage si demandé * * @param string $query * Requête à exécuter * @param bool|null $tracer * true pour tracer la requête * @return bool|SQLiteResult */ public function executer_requete($query, $tracer = null) { if (is_null($tracer)) { $tracer = $this->tracer; } $err = ""; $t = 0; if ($tracer) { include_spip('public/tracer'); $t = trace_query_start(); } # spip_log("requete: $this->serveur >> $query",'sqlite.'._LOG_DEBUG); // boum ? pourquoi ? if ($this->link) { // memoriser la derniere erreur PHP vue $e = (function_exists('error_get_last') ? error_get_last() : ""); // sauver la derniere requete $GLOBALS['connexions'][$this->serveur ? $this->serveur : 0]['last'] = $query; $GLOBALS['connexions'][$this->serveur ? $this->serveur : 0]['total_requetes']++; if ($this->sqlite_version == 3) { $r = $this->link->query($query); // sauvegarde de la requete (elle y est deja dans $r->queryString) # $r->spipQueryString = $query; // comptage : oblige de compter le nombre d'entrees retournees // par une requete SELECT // aucune autre solution ne donne le nombre attendu :( ! // particulierement s'il y a des LIMIT dans la requete. if (strtoupper(substr(ltrim($query), 0, 6)) == 'SELECT') { if ($r) { // noter le link et la query pour faire le comptage *si* on en a besoin $r->spipSqliteRowCount = array($this->link, $query); } elseif ($r instanceof PDOStatement) { $r->spipSqliteRowCount = 0; } } } else { $r = sqlite_query($this->link, $query); } // loger les warnings/erreurs eventuels de sqlite remontant dans PHP if ($err = (function_exists('error_get_last') ? error_get_last() : "") and $err != $e) { $err = strip_tags($err['message']) . " in " . $err['file'] . " line " . $err['line']; spip_log("$err - " . $query, 'sqlite.' . _LOG_ERREUR); } else { $err = ""; } } else { $r = false; } if (spip_sqlite_errno($this->serveur)) { $err .= spip_sqlite_error($query, $this->serveur); } return $t ? trace_query_end($query, $t, $r, $err, $this->serveur) : $r; } /** * Obtient l'identifiant de la dernière ligne insérée ou modifiée * * @return int **/ public function last_insert_id() { if ($this->sqlite_version == 3) { return $this->link->lastInsertId(); } else { return sqlite_last_insert_rowid($this->link); } } } /** * Cette classe est presente essentiellement pour un preg_replace_callback * avec des parametres dans la fonction appelee que l'on souhaite incrementer * (fonction pour proteger les textes) */ class sqlite_traducteur { /** @var string $query Texte de la requête */ public $query = ''; /** @var string $prefixe Préfixe des tables */ public $prefixe = ''; /** @var string $sqlite_version Version de sqlite (2 ou 3) */ public $sqlite_version = ''; /** Pour les corrections à effectuer sur les requêtes : array(code=>'texte') trouvé * * @var array */ public $textes = array(); /** * Constructeur * * @param string $query Requête à préparer * @param string $prefixe Prefixe des tables à utiliser * @param string $sqlite_version Version SQLite (2 ou 3) */ public function __construct($query, $prefixe, $sqlite_version) { $this->query = $query; $this->prefixe = $prefixe; $this->sqlite_version = $sqlite_version; } /** * Transformer la requete pour SQLite * * Enlève les textes, transforme la requête pour quelle soit * bien interprétée par SQLite, puis remet les textes * la fonction affecte `$this->query` */ public function traduire_requete() { // // 1) Protection des textes en les remplacant par des codes // // enlever les 'textes' et initialiser avec list($this->query, $textes) = query_echappe_textes($this->query); // // 2) Corrections de la requete // // Correction Create Database // Create Database -> requete ignoree if (strpos($this->query, 'CREATE DATABASE') === 0) { spip_log("Sqlite : requete non executee -> $this->query", 'sqlite.' . _LOG_AVERTISSEMENT); $this->query = "SELECT 1"; } // Correction Insert Ignore // INSERT IGNORE -> insert (tout court et pas 'insert or replace') if (strpos($this->query, 'INSERT IGNORE') === 0) { spip_log("Sqlite : requete transformee -> $this->query", 'sqlite.' . _LOG_DEBUG); $this->query = 'INSERT ' . substr($this->query, '13'); } // Correction des dates avec INTERVAL // utiliser sql_date_proche() de preference if (strpos($this->query, 'INTERVAL') !== false) { $this->query = preg_replace_callback("/DATE_(ADD|SUB)(.*)INTERVAL\s+(\d+)\s+([a-zA-Z]+)\)/U", array(&$this, '_remplacerDateParTime'), $this->query); } if (strpos($this->query, 'LEFT(') !== false) { $this->query = str_replace('LEFT(', '_LEFT(', $this->query); } if (strpos($this->query, 'TIMESTAMPDIFF(') !== false) { $this->query = preg_replace('/TIMESTAMPDIFF\(\s*([^,]*)\s*,/Uims', "TIMESTAMPDIFF('\\1',", $this->query); } // Correction Using // USING (non reconnu en sqlite2) // problematique car la jointure ne se fait pas du coup. if (($this->sqlite_version == 2) && (strpos($this->query, "USING") !== false)) { spip_log("'USING (champ)' n'est pas reconnu en SQLite 2. Utilisez 'ON table1.champ = table2.champ'", 'sqlite.' . _LOG_ERREUR); $this->query = preg_replace('/USING\s*\([^\)]*\)/', '', $this->query); } // Correction Field // remplace FIELD(table,i,j,k...) par CASE WHEN table=i THEN n ... ELSE 0 END if (strpos($this->query, 'FIELD') !== false) { $this->query = preg_replace_callback('/FIELD\s*\(([^\)]*)\)/', array(&$this, '_remplacerFieldParCase'), $this->query); } // Correction des noms de tables FROM // mettre les bons noms de table dans from, update, insert, replace... if (preg_match('/\s(SET|VALUES|WHERE|DATABASE)\s/iS', $this->query, $regs)) { $suite = strstr($this->query, $regs[0]); $this->query = substr($this->query, 0, -strlen($suite)); } else { $suite = ''; } $pref = ($this->prefixe) ? $this->prefixe . "_" : ""; $this->query = preg_replace('/([,\s])spip_/S', '\1' . $pref, $this->query) . $suite; // Correction zero AS x // pg n'aime pas 0+x AS alias, sqlite, dans le meme style, // n'apprecie pas du tout SELECT 0 as x ... ORDER BY x // il dit que x ne doit pas être un integer dans le order by ! // on remplace du coup x par vide() dans ce cas uniquement // // rien que pour public/vertebrer.php ? if ((strpos($this->query, "0 AS") !== false)) { // on ne remplace que dans ORDER BY ou GROUP BY if (preg_match('/\s(ORDER|GROUP) BY\s/i', $this->query, $regs)) { $suite = strstr($this->query, $regs[0]); $this->query = substr($this->query, 0, -strlen($suite)); // on cherche les noms des x dans 0 AS x // on remplace dans $suite le nom par vide() preg_match_all('/\b0 AS\s*([^\s,]+)/', $this->query, $matches, PREG_PATTERN_ORDER); foreach ($matches[1] as $m) { $suite = str_replace($m, 'VIDE()', $suite); } $this->query .= $suite; } } // Correction possible des divisions entieres // Le standard SQL (lequel? ou?) semble indiquer que // a/b=c doit donner c entier si a et b sont entiers 4/3=1. // C'est ce que retournent effectivement SQL Server et SQLite // Ce n'est pas ce qu'applique MySQL qui retourne un reel : 4/3=1.333... // // On peut forcer la conversion en multipliant par 1.0 avant la division // /!\ SQLite 3.5.9 Debian/Ubuntu est victime d'un bug en plus ! // cf. https://bugs.launchpad.net/ubuntu/+source/sqlite3/+bug/254228 // http://www.sqlite.org/cvstrac/tktview?tn=3202 // (4*1.0/3) n'est pas rendu dans ce cas ! # $this->query = str_replace('/','* 1.00 / ',$this->query); // Correction critere REGEXP, non reconnu en sqlite2 if (($this->sqlite_version == 2) && (strpos($this->query, 'REGEXP') !== false)) { $this->query = preg_replace('/([^\s\(]*)(\s*)REGEXP(\s*)([^\s\)]*)/', 'REGEXP($4, $1)', $this->query); } // // 3) Remise en place des textes d'origine // // Correction Antiquotes et echappements // ` => rien if (strpos($this->query, '`') !== false) { $this->query = str_replace('`', '', $this->query); } $this->query = query_reinjecte_textes($this->query, $textes); return $this->query; } /** * Callback pour remplacer `DATE_` / `INTERVAL` * par `DATE ... strtotime` * * @param array $matches Captures * @return string Texte de date compris par SQLite */ public function _remplacerDateParTime($matches) { $op = strtoupper($matches[1] == 'ADD') ? '+' : '-'; return "datetime$matches[2] '$op$matches[3] $matches[4]')"; } /** * Callback pour remplacer `FIELD(table,i,j,k...)` * par `CASE WHEN table=i THEN n ... ELSE 0 END` * * @param array $matches Captures * @return string Texte de liste ordonnée compris par SQLite */ public function _remplacerFieldParCase($matches) { $fields = substr($matches[0], 6, -1); // ne recuperer que l'interieur X de field(X) $t = explode(',', $fields); $index = array_shift($t); $res = ''; $n = 0; foreach ($t as $v) { $n++; $res .= "\nWHEN $index=$v THEN $n"; } return "CASE $res ELSE 0 END "; } }