diff --git a/ecrire/inc/math.php b/ecrire/inc/math.php
index 68a4e22884da2f77743ee3f2736b239fe050f787..048d6e2dc9bf4071c6fd39e991a0a5021122e750 100644
--- a/ecrire/inc/math.php
+++ b/ecrire/inc/math.php
@@ -117,7 +117,7 @@ function traiter_math($letexte, $source = '', $defaire_amp = false) {
 		];
 		foreach ($traitements as $t) {
 			while (
-				strpos($texte_milieu, $t['str']) !== false
+				str_contains($texte_milieu, $t['str'])
 				and (preg_match($t['preg'], $texte_milieu, $regs))) {
 				$expression = $regs[1];
 				if ($defaire_amp) {
diff --git a/ecrire/inc/notifications.php b/ecrire/inc/notifications.php
index b8f7f64aa748bd4ac59a6bae78c073646d460f50..572143ec2707bec322a375604945e8b27d3a9b76 100644
--- a/ecrire/inc/notifications.php
+++ b/ecrire/inc/notifications.php
@@ -92,10 +92,10 @@ function notifications_envoyer_mails($emails, $texte, $sujet = '', $from = '', $
 
 	// tester si le mail est deja en html
 	if (
-		strpos($texte, '<') !== false // eviter les tests suivants si possible
+		str_contains($texte, '<') // eviter les tests suivants si possible
 		and $ttrim = trim($texte)
-		and substr($ttrim, 0, 1) == '<'
-		and substr($ttrim, -1, 1) == '>'
+		and str_starts_with($ttrim, '<')
+		and str_ends_with($ttrim, '>')
 		and stripos($ttrim, '</html>') !== false
 	) {
 		if (!strlen($sujet)) {
diff --git a/ecrire/inc/pipelines_ecrire.php b/ecrire/inc/pipelines_ecrire.php
index 7a2b1bc55572a7b5455eb666916eb562adb77d85..7ba3d3fff858f4d37cf7d29cefcebca6e15a5bc9 100644
--- a/ecrire/inc/pipelines_ecrire.php
+++ b/ecrire/inc/pipelines_ecrire.php
@@ -81,7 +81,7 @@ function f_jQuery_prive($texte) {
  */
 function affichage_final_prive_title_auto($texte) {
 	if (
-		strpos($texte, '<title>') === false
+		!str_contains($texte, '<title>')
 		and
 		(preg_match(',<h1[^>]*>(.+)</h1>,Uims', $texte, $match)
 			or preg_match(',<h[23][^>]*>(.+)</h[23]>,Uims', $texte, $match))
@@ -210,7 +210,7 @@ function f_afficher_blocs_ecrire($flux) {
 				['args' => $flux['args']['contexte'], 'data' => $flux['data']['texte']]
 			);
 		} elseif (
-			strncmp($fond, 'prive/objets/contenu/', 21) == 0
+			str_starts_with($fond, 'prive/objets/contenu/')
 			and $objet = basename($fond)
 			and $objet == substr($fond, 21)
 			and isset($o[$objet])
diff --git a/ecrire/inc/plugin.php b/ecrire/inc/plugin.php
index ee19a622a05ed12e5e58fb9a27aec732958d7fa4..3fff24a72dd4aa0075f8a0f272a48e51bdb7dae1 100644
--- a/ecrire/inc/plugin.php
+++ b/ecrire/inc/plugin.php
@@ -213,7 +213,7 @@ function plugin_version_compatible($intervalle, $version, $avec_quoi = '') {
 	}
 
 	$minimum_inc = $intervalle[0] == '[';
-	$maximum_inc = substr($intervalle, -1) == ']';
+	$maximum_inc = str_ends_with($intervalle, ']');
 
 	if (strlen($minimum)) {
 		if ($minimum_inc and spip_version_compare($version, $minimum, '<')) {
@@ -708,7 +708,7 @@ function plugin_message_incompatibilite($intervalle, $version, $nom, $balise) {
 		$type = 'spip';
 	} elseif ($nom === 'PHP') {
 		$type = 'php';
-	} elseif (strncmp($nom, 'PHP:', 4) === 0) {
+	} elseif (str_starts_with($nom, 'PHP:')) {
 		$type = 'extension_php';
 		[, $nom] = explode(':', $nom, 2);
 	}
@@ -718,7 +718,7 @@ function plugin_message_incompatibilite($intervalle, $version, $nom, $balise) {
 		$maximum = $regs[2];
 
 		$minimum_inclus = $intervalle[0] == '[';
-		$maximum_inclus = substr($intervalle, -1) == ']';
+		$maximum_inclus = str_ends_with($intervalle, ']');
 
 		if (strlen($minimum)) {
 			if ($minimum_inclus and spip_version_compare($version, $minimum, '<')) {
@@ -1198,7 +1198,7 @@ function pipeline_matrice_precompile($plugin_valides, $ordre, $pipe_recherche) {
 					$GLOBALS['spip_pipeline'][$nom] = '';
 					}
 					if ($action) {
-						if (strpos($GLOBALS['spip_pipeline'][$nom], (string) "|$prefix$action") === false) {
+						if (!str_contains($GLOBALS['spip_pipeline'][$nom], (string) "|$prefix$action")) {
 							$GLOBALS['spip_pipeline'][$nom] = preg_replace(
 								',(\|\||$),',
 								"|$prefix$action\\1",
@@ -1468,7 +1468,7 @@ function ecrire_fichier_php($nom, $contenu, $comment = '') {
 	// si un fichier existe deja on verifie que son contenu change avant de l'ecraser
 	// si pas de modif on ne touche pas au fichier initial
 	if (file_exists($nom)) {
-		if (substr($nom, -4) == '.php') {
+		if (str_ends_with($nom, '.php')) {
 			$fichier_tmp = substr($nom, 0, -4) . '.tmp.php';
 		}
 		else {
diff --git a/ecrire/inc/queue.php b/ecrire/inc/queue.php
index e44d5859f8d2055c783e69ba3aaad20bb888ead3..03062a66cd13bcec53d242affabfd82b47581097 100644
--- a/ecrire/inc/queue.php
+++ b/ecrire/inc/queue.php
@@ -245,7 +245,7 @@ function queue_start_job($row) {
 
 	$fonction = $row['fonction'];
 	if (strlen($inclure = trim($row['inclure']))) {
-		if (substr($inclure, -1) == '/') { // c'est un chemin pour charger_fonction
+		if (str_ends_with($inclure, '/')) { // c'est un chemin pour charger_fonction
 			$f = charger_fonction($fonction, rtrim($inclure, '/'), false);
 			if ($f) {
 				$fonction = $f;
@@ -437,7 +437,7 @@ function queue_error_handler() {
  */
 function queue_is_cron_job($function, $inclure) {
 	static $taches = null;
-	if (strncmp($inclure, 'genie/', 6) == 0) {
+	if (str_starts_with($inclure, 'genie/')) {
 		if (is_null($taches)) {
 			include_spip('inc/genie');
 			$taches = taches_generales();
diff --git a/ecrire/inc/texte_mini.php b/ecrire/inc/texte_mini.php
index 3c6134d84a14c65f91e2de01dfe478f477770fdb..4e5aabbf409b2d77fedec25a4586079faf776ecd 100644
--- a/ecrire/inc/texte_mini.php
+++ b/ecrire/inc/texte_mini.php
@@ -138,7 +138,7 @@ function traiter_echap_pre_dist($regs, $options = []) {
 	$pre = $regs['innerHtml'];
 
 	// echapper les < dans <code>
-	if (strpos($pre, '<') !== false) {
+	if (str_contains($pre, '<')) {
 		$collecteurCode = new CollecteurHtmlTag('code');
 		$collections = $collecteurCode->collecter($pre);
 		$collections = array_reverse($collections);
@@ -156,7 +156,7 @@ function traiter_echap_code_dist($regs, $options = []) {
 	$att = $regs['attributs'];
 
 	// ne pas mettre le <div...> s'il n'y a qu'une ligne
-	if (strpos($corps, "\n") !== false) {
+	if (str_contains($corps, "\n")) {
 		// supprimer les sauts de ligne debut/fin
 		// (mais pas les espaces => ascii art).
 		$corps = preg_replace("/^[\n\r]+|[\n\r]+$/s", '', $corps);
@@ -426,7 +426,7 @@ function couper($texte, $taille = 50, $suite = null) {
 
 
 function protege_js_modeles($texte) {
-	if (isset($GLOBALS['visiteur_session']) and strpos($texte, '<') !== false) {
+	if (isset($GLOBALS['visiteur_session']) and str_contains($texte, '<')) {
 		$tags = [
 			'javascript' => ['tag' => 'script', 'preg' => ',<script.*?($|</script.),isS', 'c' => '_PROTEGE_JS_MODELES'],
 			'php' => ['tag' => '?php', 'preg' => ',<\?php.*?($|\?' . '>),isS', 'c' => '_PROTEGE_PHP_MODELES'],
@@ -447,7 +447,7 @@ function protege_js_modeles($texte) {
 
 
 function echapper_faux_tags($letexte) {
-	if (strpos($letexte, '<') === false) {
+	if (!str_contains($letexte, '<')) {
 		return $letexte;
 	}
 	$textMatches = preg_split(',(</?[a-z!][^<>]*>),', $letexte, -1, PREG_SPLIT_DELIM_CAPTURE);
@@ -508,7 +508,7 @@ function echapper_html_suspect($texte, $options = [], $connect = null, $env = []
 
 	// pas de balise html ou pas d'attribut sur les balises ? c'est OK
 	if (
-		strpos($texte, '<') === false
+		!str_contains($texte, '<')
 		or !str_contains($texte, '=')
 	) {
 		return $texte;
@@ -524,7 +524,7 @@ function echapper_html_suspect($texte, $options = [], $connect = null, $env = []
 		// car sinon on declenche sur les modeles ou ressources
 		if (
 			!$strict and
-			(strpos($texte, 'on') === false or !preg_match(",<\w+.*\bon\w+\s*=,UimsS", $texte))
+			(!str_contains($texte, 'on') or !preg_match(",<\w+.*\bon\w+\s*=,UimsS", $texte))
 		) {
 			return $texte;
 		}
@@ -608,7 +608,7 @@ function safehtml($t) {
 		return $t;
 	}
 	# attention safehtml nettoie deux ou trois caracteres de plus. A voir
-	if (strpos($t, '<') === false) {
+	if (!str_contains($t, '<')) {
 		return str_replace("\x00", '', $t);
 	}
 
diff --git a/ecrire/inc/urls.php b/ecrire/inc/urls.php
index 846e1a2db767420648ceae1644133b16c7900be9..61293cefe51d6a894cbe3ed92ed5c046a9116277 100644
--- a/ecrire/inc/urls.php
+++ b/ecrire/inc/urls.php
@@ -73,7 +73,7 @@ function urls_decoder_url($url, $fond = '', $contexte = [], $assembler = false)
 		// le decodage des urls se fait toujours par rapport au site public
 		$current_base = url_absolue(_DIR_RACINE ?: './');
 	}
-	if (strncmp($url, $current_base, strlen($current_base)) == 0) {
+	if (str_starts_with($url, $current_base)) {
 		$url = substr($url, strlen($current_base));
 	}
 
@@ -101,7 +101,7 @@ function urls_decoder_url($url, $fond = '', $contexte = [], $assembler = false)
 	unset($_SERVER['REDIRECT_url_propre']);
 	unset($_ENV['url_propre']);
 	include_spip('inc/filtres_mini');
-	if (strpos($url, '://') === false) {
+	if (!str_contains($url, '://')) {
 		$GLOBALS['profondeur_url'] = substr_count(ltrim(resolve_path("/$url"), '/'), '/');
 	} else {
 		$GLOBALS['profondeur_url'] = max(0, substr_count($url, '/') - substr_count($current_base, '/'));
diff --git a/ecrire/inc/utils.php b/ecrire/inc/utils.php
index 4c09330851940bf92a70506da2e736af501f69c2..21a9b8b30f630697723475a5d6805e15bc7093a1 100644
--- a/ecrire/inc/utils.php
+++ b/ecrire/inc/utils.php
@@ -49,7 +49,7 @@ if (!defined('_ECRIRE_INC_VERSION')) {
 function charger_fonction($nom, $dossier = 'exec', $continue = false) {
 	static $echecs = [];
 
-	if (strlen($dossier) and substr($dossier, -1) != '/') {
+	if (strlen($dossier) and !str_ends_with($dossier, '/')) {
 		$dossier .= '/';
 	}
 	$f = str_replace('/', '_', $dossier) . $nom;
@@ -572,7 +572,7 @@ function tester_url_absolue($url) {
  */
 function parametre_url($url, $c, $v = null, $sep = '&amp;') {
 	// requete erronnee : plusieurs variable dans $c et aucun $v
-	if (strpos($c, '|') !== false and is_null($v)) {
+	if (str_contains($c, '|') and is_null($v)) {
 		return null;
 	}
 
@@ -609,7 +609,7 @@ function parametre_url($url, $c, $v = null, $sep = '&amp;') {
 			$r = array_pad($r, 3, null);
 			if ($v === null) {
 				// c'est un tableau, on memorise les valeurs
-				if (substr($r[1], -2) == '[]') {
+				if (str_ends_with($r[1], '[]')) {
 					if (!$v_read) {
 						$v_read = [];
 					}
@@ -624,7 +624,7 @@ function parametre_url($url, $c, $v = null, $sep = '&amp;') {
 			}
 			// Ajout. Pour une variable, remplacer au meme endroit,
 			// pour un tableau ce sera fait dans la prochaine boucle
-			elseif (substr($r[1], -2) != '[]') {
+			elseif (!str_ends_with($r[1], '[]')) {
 				$url[$n] = $r[1] . '=' . $u;
 				unset($ajouts[$r[1]]);
 			}
@@ -648,7 +648,7 @@ function parametre_url($url, $c, $v = null, $sep = '&amp;') {
 			if (!is_array($v)) {
 				$url[] = $k . '=' . $u;
 			} else {
-				$id = (substr($k, -2) == '[]') ? $k : ($k . '[]');
+				$id = (str_ends_with($k, '[]')) ? $k : ($k . '[]');
 				foreach ($v as $w) {
 					$url[] = $id . '=' . (is_array($w) ? 'Array' : rawurlencode($w));
 				}
@@ -761,7 +761,7 @@ function self($amp = '&amp;', $root = false) {
 	}
 	// ajouter le cas echeant les variables _POST['id_...']
 	foreach ($_POST as $v => $c) {
-		if (substr($v, 0, 3) == 'id_') {
+		if (str_starts_with($v, 'id_')) {
 			$url = parametre_url($url, $v, $c, '&');
 		}
 	}
@@ -936,7 +936,7 @@ function _L($text, $args = [], $options = []) {
 			include_spip('inc/texte_mini');
 		}
 		foreach ($args as $name => $value) {
-			if (strpos($text, (string) "@$name@") !== false) {
+			if (str_contains($text, (string) "@$name@")) {
 				if ($options['sanitize']) {
 					$value = echapper_html_suspect($value);
 					$value = interdire_scripts($value, -1);
@@ -1365,7 +1365,7 @@ function _chemin($dir_path = null) {
 			$path = _DIR_RACINE . 'squelettes/:' . $path;
 		}
 		foreach (explode(':', $path) as $dir) {
-			if (strlen($dir) and substr($dir, -1) != '/') {
+			if (strlen($dir) and !str_ends_with($dir, '/')) {
 				$dir .= '/';
 			}
 			$path_base[] = $dir;
@@ -1391,7 +1391,7 @@ function _chemin($dir_path = null) {
 		$dirs = (is_array($dir_path) ? $dir_path : explode(':', $dir_path));
 		$dirs = array_reverse($dirs);
 		foreach ($dirs as $dir_path) {
-			if (substr($dir_path, -1) != '/') {
+			if (!str_ends_with($dir_path, '/')) {
 				$dir_path .= '/';
 			}
 			if (!in_array($dir_path, $path_base)) {
@@ -1530,7 +1530,7 @@ function chemin_image($icone) {
 		$icone = substr($icone, 0, $p);
 	}
 	// gerer le cas d'un double appel en evitant de refaire le travail inutilement
-	if (strpos($icone, '/') !== false and file_exists($icone)) {
+	if (str_contains($icone, '/') and file_exists($icone)) {
 		return $icone;
 	}
 
@@ -2086,7 +2086,7 @@ function url_de_base($profondeur = null) {
 
 	if (
 		isset($_SERVER['SCRIPT_URI'])
-		and substr($_SERVER['SCRIPT_URI'], 0, 5) == 'https'
+		and str_starts_with($_SERVER['SCRIPT_URI'], 'https')
 	) {
 		$http = 'https';
 	} elseif (
@@ -2113,7 +2113,7 @@ function url_de_base($profondeur = null) {
 	if (
 		isset($_SERVER['SERVER_PORT'])
 		and $port = $_SERVER['SERVER_PORT']
-		and strpos($host, ':') == false
+		and !str_contains($host, ':')
 	) {
 		if (!defined('_PORT_HTTP_STANDARD')) {
 			define('_PORT_HTTP_STANDARD', '80');
@@ -2168,7 +2168,7 @@ function url_de_($http, $host, $request, $prof = 0) {
 	[$myself] = explode('?', $myself);
 	// vieux mode HTTP qui envoie après le nom de la methode l'URL compléte
 	// protocole, "://", nom du serveur avant le path dans _SERVER["REQUEST_URI"]
-	if (strpos($myself, '://') !== false) {
+	if (str_contains($myself, '://')) {
 		$myself = explode('://', $myself);
 		array_shift($myself);
 		$myself = implode('://', $myself);
@@ -2316,7 +2316,7 @@ function generer_url_public($script = '', $args = '', $no_entities = false, $rel
 			$action = parametre_url($action, _SPIP_PAGE, $script, '&');
 		}
 		if ($args) {
-			$action .= (strpos($action, '?') !== false ? '&' : '?') . $args;
+			$action .= (str_contains($action, '?') ? '&' : '?') . $args;
 		}
 		// ne pas generer une url avec /./?page= en cas d'url absolue et de _SPIP_SCRIPT vide
 		$url = ($rel ? _DIR_RACINE . $action : rtrim(url_de_base(), '/') . preg_replace(',^/[.]/,', '/', "/$action"));
@@ -2456,7 +2456,7 @@ function generer_url_api(string $script, string $path, string $args, bool $no_en
 	if (is_null($public)) {
 		$public = (_DIR_RACINE ? false : true);
 	}
-	if (substr($script, -4) !== '.api') {
+	if (!str_ends_with($script, '.api')) {
 		$script .= '.api';
 	}
 	$url =
@@ -3115,7 +3115,7 @@ function init_var_mode() {
 						and $_SERVER['REQUEST_METHOD'] === 'GET'
 					) {
 						$self = self('&', true);
-						if (strpos($self, 'page=login') === false) {
+						if (!str_contains($self, 'page=login')) {
 							include_spip('inc/headers');
 							$redirect = parametre_url(self('&', true), 'var_mode', $_GET['var_mode'], '&');
 							redirige_par_entete(generer_url_public('login', 'url=' . rawurlencode($redirect), true));
diff --git a/ecrire/plugins/afficher_plugin.php b/ecrire/plugins/afficher_plugin.php
index 438862e9d3868a5b1e8babdc14efefc6896dca8f..cbdb65f39845633066ed0e4b1fb858af5907a6ac 100644
--- a/ecrire/plugins/afficher_plugin.php
+++ b/ecrire/plugins/afficher_plugin.php
@@ -244,7 +244,7 @@ function plugin_etat_en_clair($etat) {
 
 function plugin_propre($texte, $module = '', $propre = 'propre') {
 	// retirer le retour a la racine du module, car le find_in_path se fait depuis la racine
-	if (_DIR_RACINE and strncmp($module, _DIR_RACINE, strlen(_DIR_RACINE)) == 0) {
+	if (_DIR_RACINE and str_starts_with($module, _DIR_RACINE)) {
 		$module = substr($module, strlen(_DIR_RACINE));
 	}
 	if (preg_match('|^\w+_[\w_]+$|', $texte)) {
diff --git a/ecrire/public/aiguiller.php b/ecrire/public/aiguiller.php
index 63e5100d8b87d92c2147c281a0841b895315995e..fb71c5203881a3cae66efd78a75daba1ee48077f 100644
--- a/ecrire/public/aiguiller.php
+++ b/ecrire/public/aiguiller.php
@@ -20,7 +20,7 @@ if (!defined('_ECRIRE_INC_VERSION')) {
 function securiser_redirect_action($redirect) {
 	$redirect ??= '';
 	// cas d'un double urlencode : si un urldecode de l'url n'est pas secure, on retient ca comme redirect
-	if (strpos($redirect, '%') !== false) {
+	if (str_contains($redirect, '%')) {
 		$r2 = urldecode($redirect);
 		if (($r3 = securiser_redirect_action($r2)) !== $r2) {
 			return $r3;
@@ -33,11 +33,11 @@ function securiser_redirect_action($redirect) {
 		// si l'url est une url du site, on la laisse passer sans rien faire
 		// c'est encore le plus simple
 		$base = $GLOBALS['meta']['adresse_site'] . '/';
-		if (strlen($base) and strncmp($redirect, $base, strlen($base)) == 0) {
+		if (strlen($base) and str_starts_with($redirect, $base)) {
 			return $redirect;
 		}
 		$base = url_de_base();
-		if (strlen($base) and strncmp($redirect, $base, strlen($base)) == 0) {
+		if (strlen($base) and str_starts_with($redirect, $base)) {
 			return $redirect;
 		}
 
diff --git a/ecrire/public/assembler.php b/ecrire/public/assembler.php
index 03110300544cedbf42392d96375c678c53727110..de780d1cf143074f15a41b1a45e70478dd337f30 100644
--- a/ecrire/public/assembler.php
+++ b/ecrire/public/assembler.php
@@ -755,7 +755,7 @@ function page_base_href(&$texte) {
 		$head = substr($texte, 0, $poshead);
 		$insert = false;
 		$href_base = false;
-		if (strpos($head, '<base') === false) {
+		if (!str_contains($head, '<base')) {
 			$insert = true;
 		} else {
 			// si aucun <base ...> n'a de href il faut en inserer un
@@ -790,10 +790,10 @@ function page_base_href(&$texte) {
 			if (strpos($base, "'") or strpos($base, '"') or strpos($base, '<')) {
 				$base = str_replace(["'",'"','<'], ['%27','%22','%3C'], $base);
 			}
-			if (strpos($texte, "href='#") !== false) {
+			if (str_contains($texte, "href='#")) {
 				$texte = str_replace("href='#", "href='$base#", $texte);
 			}
-			if (strpos($texte, 'href="#') !== false) {
+			if (str_contains($texte, 'href="#')) {
 				$texte = str_replace('href="#', "href=\"$base#", $texte);
 			}
 		}
diff --git a/ecrire/public/compiler.php b/ecrire/public/compiler.php
index bc71f683f2d93069af1c59128866cae08be8402b..b4de4c4a5aceec82468f229df555101d8205ac0a 100644
--- a/ecrire/public/compiler.php
+++ b/ecrire/public/compiler.php
@@ -535,7 +535,7 @@ function calculer_boucle_nonrec($id_boucle, &$boucles, $trace) {
 		$fin_lang_select_public = '';
 		// sortir les appels au traducteur (invariants de boucle)
 		if (
-			strpos($return, '?php') === false
+			!str_contains($return, '?php')
 			and preg_match_all("/\W(_T[(]'[^']*'[)])/", $return, $r)
 		) {
 			$i = 1;
@@ -558,7 +558,7 @@ function calculer_boucle_nonrec($id_boucle, &$boucles, $trace) {
 				(($return === "''") ? '' :
 					("\n\t\t" . '$t0 .= ' . $return . ';'))) :
 			("\n\t\t\$t1 " .
-				((strpos($return, '$t1.') === 0) ?
+				((str_starts_with($return, '$t1.')) ?
 					('.=' . substr($return, 4)) :
 					('= ' . $return)) .
 				";\n\t\t" .
@@ -680,7 +680,7 @@ function calculer_requete_sql($boucle) {
 	$init[] = calculer_dec(
 		'limit',
 		(
-			strpos($boucle->limit, 'intval') === false ?
+			!str_contains($boucle->limit, 'intval') ?
 			"'" . ($boucle->limit) . "'" :
 			$boucle->limit
 		)
@@ -947,7 +947,7 @@ function calculer_liste($tableau, $descr, &$boucles, $id_boucle = '') {
 			foreach ($codes as $code) {
 				if (
 					!preg_match("/^'[^']*'$/", $code)
-					or substr($res, -1, 1) !== "'"
+					or !str_ends_with($res, "'")
 				) {
 					$res .= " .\n$tab$code";
 				} else {
diff --git a/ecrire/public/composer.php b/ecrire/public/composer.php
index b12114f326ef7e2f0bbb4ef879706d0cc869db29..af0c950f462fcbab047d2af6c3bdffddb445d549 100644
--- a/ecrire/public/composer.php
+++ b/ecrire/public/composer.php
@@ -224,10 +224,10 @@ function analyse_resultat_skel($nom, $cache, $corps, $source = '') {
 	}
 	// S'agit-il d'un resultat constant ou contenant du code php
 	$process_ins = (
-		strpos($corps, '<' . '?') === false
+		!str_contains($corps, '<' . '?')
 		or
-		(strpos($corps, '<' . '?xml') !== false and
-			strpos(str_replace('<' . '?xml', '', $corps), '<' . '?') === false)
+		(str_contains($corps, '<' . '?xml') and
+			!str_contains(str_replace('<' . '?xml', '', $corps), '<' . '?'))
 	)
 		? 'html'
 		: 'php';
@@ -256,10 +256,10 @@ function analyse_resultat_skel($nom, $cache, $corps, $source = '') {
 
 		if ($process_ins == 'html') {
 			$skel['process_ins'] = (
-				strpos($corps, '<' . '?') === false
+				!str_contains($corps, '<' . '?')
 				or
-				(strpos($corps, '<' . '?xml') !== false and
-					strpos(str_replace('<' . '?xml', '', $corps), '<' . '?') === false)
+				(str_contains($corps, '<' . '?xml') and
+					!str_contains(str_replace('<' . '?xml', '', $corps), '<' . '?'))
 			)
 				? 'html'
 				: 'php';
@@ -304,7 +304,7 @@ if ($lang_select) lang_select();
  **/
 function synthetiser_balise_dynamique($nom, $args, $file, $context_compil) {
 	if (
-		strncmp($file, '/', 1) !== 0
+		!str_starts_with($file, '/')
 		// pas de lien symbolique sous Windows
 		and !(stristr(PHP_OS, 'WIN') and str_contains($file, ':'))
 	) {
diff --git a/ecrire/public/phraser_html.php b/ecrire/public/phraser_html.php
index df7e1a19a65ebf830e311a28bb45988baf62595b..46a215ae8a90b313b28c4b01d6fde3c6bd9114c4 100644
--- a/ecrire/public/phraser_html.php
+++ b/ecrire/public/phraser_html.php
@@ -221,7 +221,7 @@ function phraser_idiomes(string $texte, int $ligne, array $result): array {
 		// Stocker les arguments de la balise de traduction
 		$args = [];
 		$largs = (string) $match[5];
-		while (strpos($largs, '=') !== false
+		while (str_contains($largs, '=')
 			&& preg_match(BALISE_IDIOMES_ARGS, $largs, $r)) {
 			$args[$r[1]] = phraser_champs($r[2], 0, []);
 			$largs = substr($largs, strlen($r[0]));
diff --git a/ecrire/req/mysql.php b/ecrire/req/mysql.php
index 0a22e83635f9bb185292b8b53168105afeab07e4..99a17643507b7bb04ce0260defd1109b115ed54e 100644
--- a/ecrire/req/mysql.php
+++ b/ecrire/req/mysql.php
@@ -328,7 +328,7 @@ function spip_mysql_optimize($table, $serveur = '', $requeter = true) {
  * @return array           Tableau de l'explication
  */
 function spip_mysql_explain($query, $serveur = '', $requeter = true) {
-	if (strpos(ltrim($query), 'SELECT') !== 0) {
+	if (!str_starts_with(ltrim($query), 'SELECT')) {
 		return [];
 	}
 	$connexion = &$GLOBALS['connexions'][$serveur ? strtolower($serveur) : 0];
@@ -497,7 +497,7 @@ function calculer_mysql_expression($expression, $v, $join = 'AND') {
 function spip_mysql_select_as($args) {
 	$res = '';
 	foreach ($args as $k => $v) {
-		if (substr($k, -1) == '@') {
+		if (str_ends_with($k, '@')) {
 			// c'est une jointure qui se refere au from precedent
 			// pas de virgule
 			$res .= '  ' . $v;
diff --git a/ecrire/src/Texte/Collecteur/AbstractCollecteur.php b/ecrire/src/Texte/Collecteur/AbstractCollecteur.php
index b45441bb33e45cfd6612f08fdeecd664fc2c45d7..ffa7881f04a0f73dc2319ed308523c3f4edab78f 100644
--- a/ecrire/src/Texte/Collecteur/AbstractCollecteur.php
+++ b/ecrire/src/Texte/Collecteur/AbstractCollecteur.php
@@ -183,7 +183,7 @@ abstract class AbstractCollecteur {
 		if ($pregBalisesBloc === null) {
 			$pregBalisesBloc = ',</?(' . implode('|', static::$listeBalisesBloc) . ')[>[:space:]],iS';
 		}
-		return (strpos($texte, '<') !== false and preg_match($pregBalisesBloc, $texte)) ? true : false;
+		return (str_contains($texte, '<') and preg_match($pregBalisesBloc, $texte)) ? true : false;
 	}
 
 	/**
@@ -249,7 +249,7 @@ abstract class AbstractCollecteur {
 			# spip_log(spip_htmlspecialchars($texte));  ## pour les curieux
 			$max_prof = 5;
 			$encore = true;
-			while ($encore and strpos($texte, 'base64' . $source) !== false and $max_prof--) {
+			while ($encore and str_contains($texte, 'base64' . $source) and $max_prof--) {
 				$encore = false;
 				foreach (['span', 'div'] as $tag) {
 					$htmlTagCollecteur = new HtmlTag($tag,
diff --git a/ecrire/src/Texte/Collecteur/HtmlTag.php b/ecrire/src/Texte/Collecteur/HtmlTag.php
index e3adb152b6b2d36e5dedae9fb01752e97983f587..c352e7f032748d583f320f0dae6b7a62e4e7f55c 100644
--- a/ecrire/src/Texte/Collecteur/HtmlTag.php
+++ b/ecrire/src/Texte/Collecteur/HtmlTag.php
@@ -51,7 +51,7 @@ class HtmlTag extends AbstractCollecteur {
 		}
 
 		$upperTag = strtoupper($this->tag);
-		$hasUpperCaseTags = ($upperTag !== $this->tag and (strpos($texte, '<' . $upperTag) !== false or strpos($texte, '</' . $upperTag) !== false));
+		$hasUpperCaseTags = ($upperTag !== $this->tag and (str_contains($texte, '<' . $upperTag) or str_contains($texte, '</' . $upperTag)));
 
 		// collecter les balises ouvrantes
 		$opening = static::collecteur($texte, '', $hasUpperCaseTags ? '<' : '<' . $this->tag, $this->preg_openingtag, empty($options['detecter_presence']) ? 0 : 1);
@@ -72,7 +72,7 @@ class HtmlTag extends AbstractCollecteur {
 		while (!empty($opening)) {
 			$first_opening = array_shift($opening);
 			// self closing ?
-			if (strpos($first_opening['raw'], '/>', -2) !== false) {
+			if (str_contains($first_opening['raw'], '/>')) {
 				$tag = $first_opening;
 				$tag['opening'] = $tag['raw'];
 				$tag['closing'] = '';
@@ -102,7 +102,7 @@ class HtmlTag extends AbstractCollecteur {
 					while ($next_opening and $next_closing and $next_opening['pos'] < $next_closing['pos']) {
 						while ($next_opening and $next_opening['pos'] < $next_closing['pos']) {
 							// si pas self closing, il faut un closing de plus
-							if (strpos($next_opening['raw'], '/>', -2) === false) {
+							if (!str_contains($next_opening['raw'], '/>')) {
 								$need_closing++;
 							}
 							array_shift($opening);
diff --git a/ecrire/tests/Constraint/IsOk.php b/ecrire/tests/Constraint/IsOk.php
index 8f6241bb340460f7a83f424de1360eff4f474b92..3fa9c5b6c89768f0d6cc14fc04e32c7e58048dd1 100644
--- a/ecrire/tests/Constraint/IsOk.php
+++ b/ecrire/tests/Constraint/IsOk.php
@@ -23,7 +23,7 @@ final class IsOk extends Constraint
 	 */
 	protected function matches($other): bool
 	{
-		return substr(strtolower(trim($other)), 0, 2) === 'ok';
+		return str_starts_with(strtolower(trim($other)), 'ok');
 	}
 
 	/**
diff --git a/ecrire/tests/LegacyUnitHtmlTest.php b/ecrire/tests/LegacyUnitHtmlTest.php
index f41b67cbb867355c1e62f4913202c28f27d043a6..bb86d73685b1513f39fd0e5e0b646c9b5bd16ecb 100644
--- a/ecrire/tests/LegacyUnitHtmlTest.php
+++ b/ecrire/tests/LegacyUnitHtmlTest.php
@@ -65,7 +65,7 @@ class LegacyUnitHtmlTest extends TestCase
 		}
 
 		$result = rtrim(implode("\n", $output));
-		if (substr($result, 0, 2) === 'NA') {
+		if (str_starts_with($result, 'NA')) {
 			$this->markTestSkipped($result);
 		} elseif (preg_match("#^OK \(?\d+\)?$#", $result)) {
 			$result = 'OK';
diff --git a/ecrire/tests/LegacyUnitPhpTest.php b/ecrire/tests/LegacyUnitPhpTest.php
index 63e410d1ada7ca55c02179f744b5f4343df35cf6..b63434731c3f610a5d55d96b53c917ff185c0f71 100644
--- a/ecrire/tests/LegacyUnitPhpTest.php
+++ b/ecrire/tests/LegacyUnitPhpTest.php
@@ -73,7 +73,7 @@ class LegacyUnitPhpTest extends TestCase
 		}
 
 		$result = rtrim(implode("\n", $output));
-		if (substr($result, 0, 2) === 'NA') {
+		if (str_starts_with($result, 'NA')) {
 			$this->markTestSkipped($result);
 		} elseif (preg_match("#^OK \(?\d+\)?$#", $result)) {
 			$result = 'OK';
diff --git a/ecrire/tests/SquelettesTestCase.php b/ecrire/tests/SquelettesTestCase.php
index 827ce018c7e86476c5c006cb1cbe7dde8e4b8264..26268e5aeb0803f1a8c86eefa2b8bab4671a42d9 100644
--- a/ecrire/tests/SquelettesTestCase.php
+++ b/ecrire/tests/SquelettesTestCase.php
@@ -15,7 +15,7 @@ abstract class SquelettesTestCase extends TestCase
 	 */
 	public static function isNa(string $chaine): bool
 	{
-		return substr(strtolower(trim($chaine)), 0, 2) === 'na';
+		return str_starts_with(strtolower(trim($chaine)), 'na');
 	}
 
 	/**
diff --git a/ecrire/tests/legacy/test_fonctions.php b/ecrire/tests/legacy/test_fonctions.php
index c0aedbed1f1fe033c0e856672febf6814c9ba7f4..b20cbd92ab4030253744fef4d44af5ba4cddb149 100644
--- a/ecrire/tests/legacy/test_fonctions.php
+++ b/ecrire/tests/legacy/test_fonctions.php
@@ -231,7 +231,7 @@ function tests_legacy_lister($extension = null)
 
 		foreach ($tests as $test) {
 			$t = (string) _request('rech');
-			if (strlen($t) && (strpos($test, $t) === false)) {
+			if (strlen($t) && (!str_contains($test, $t))) {
 				continue;
 			}
 
@@ -256,18 +256,14 @@ function tests_legacy_lister($extension = null)
 
 			$testbasename = basename($test);
 			// ignorer les vrais tests PHPUnit
-			if (strlen($testbasename) > 8 && substr($testbasename, -8) === 'Test.php') {
+			if (strlen($testbasename) > 8 && str_ends_with($testbasename, 'Test.php')) {
 				continue;
 			}
 
-			if (strncmp($testbasename, 'inclus_', 7) !== 0 && substr($testbasename, -14) !== '_fonctions.php' && (strncmp(
-				$testbasename,
-				'NA_',
-				3
-			) !== 0 || _request('var_mode') === 'dev')) {
+			if (!str_starts_with($testbasename, 'inclus_') && !str_ends_with($testbasename, '_fonctions.php') && (!str_starts_with($testbasename, 'NA_') || _request('var_mode') === 'dev')) {
 				$joli = preg_replace('#\.(php|html)$#', '', basename($test));
 				$section = dirname($test);
-				if (strpos($base, _DIR_TESTS) === 0) {
+				if (str_starts_with($base, _DIR_TESTS)) {
 					$section = substr($section, strlen(_DIR_TESTS . '/tests'));
 				}
 
diff --git a/ecrire/tests/legacy/unit/cache/cache_sessions.php b/ecrire/tests/legacy/unit/cache/cache_sessions.php
index 8c6ac804c99b7a4b82f9be142962613b45907031..4b4909874e7f130e684bbf9a5e12330a0a310489 100644
--- a/ecrire/tests/legacy/unit/cache/cache_sessions.php
+++ b/ecrire/tests/legacy/unit/cache/cache_sessions.php
@@ -146,12 +146,12 @@ function salt_contexte()
 function trace_contexte($contexte)
 {
 	foreach ($contexte as $k => $v) {
-		if (strpos($k, 'date_') === 0 || $k === 'salt') {
+		if (str_starts_with($k, 'date_') || $k === 'salt') {
 			unset($contexte[$k]);
 		}
 	}
 
-	if (isset($contexte['caller']) && strpos($contexte['caller'], 'tests/squelettes/') === 0) {
+	if (isset($contexte['caller']) && str_starts_with($contexte['caller'], 'tests/squelettes/')) {
 		$contexte['caller'] = substr($contexte['caller'], 17);
 	}
 
diff --git a/ecrire/tests/legacy/unit/filtres/extraire_attribut.php b/ecrire/tests/legacy/unit/filtres/extraire_attribut.php
index 85c1e4ca1cd852530dc97d242a3d91d13f570b99..3f6d668f82d34aaf07455778645e0d22112a1acc 100644
--- a/ecrire/tests/legacy/unit/filtres/extraire_attribut.php
+++ b/ecrire/tests/legacy/unit/filtres/extraire_attribut.php
@@ -122,8 +122,7 @@ $err = tester_fun('extraire_attribut', $essais);
 // ... pour pouvoir le remplacer correctement
 
 if (
-	strpos('x', (string) inserer_attribut('<a><img src="x"/></a>', 'src', 'y'))
-	!== false) {
+	str_contains('x', (string) inserer_attribut('<a><img src="x"/></a>', 'src', 'y'))) {
 	$err[] = 'erreur remplacement tag interne';
 }
 
diff --git a/ecrire/tests/legacy/unit/plugin/dir_plugins_suppl.php b/ecrire/tests/legacy/unit/plugin/dir_plugins_suppl.php
index b3b4d8c669276b08f2d4cb48048ea178f11771cb..3b1fb4cf64e8db8dcb960ef99d45a1640dbeb9bf 100644
--- a/ecrire/tests/legacy/unit/plugin/dir_plugins_suppl.php
+++ b/ecrire/tests/legacy/unit/plugin/dir_plugins_suppl.php
@@ -29,7 +29,7 @@ function test_dir_plugins_suppl()
 		return 'NA : la constante _DIR_PLUGINS_SUPPL definie dans mes_options.php ne doit contenir qu\'un seul chemin supplementaire ; actuellement sa valeur est "' . _DIR_PLUGINS_SUPPL . '"';
 	}
 
-	if (substr(_DIR_PLUGINS_SUPPL, -1) !== '/') {
+	if (!str_ends_with(_DIR_PLUGINS_SUPPL, '/')) {
 		return 'NA : la constante _DIR_PLUGINS_SUPPL doit terminer par un / ; actuellement sa valeur est "' . _DIR_PLUGINS_SUPPL . '"';
 	}
 	// preparation: verifier qu'il existe au moins un dossier plugin par rep suppl (i.e. contenant un fichier paquet.xml)?
diff --git a/ecrire/tests/legacy/unit/propre/liens_classes.php b/ecrire/tests/legacy/unit/propre/liens_classes.php
index 22ca4a452a6711e1eed0bbf0ffab84580b8ce878..9e2d7fad816b16e6521197ee19e3fd14565a52ac 100644
--- a/ecrire/tests/legacy/unit/propre/liens_classes.php
+++ b/ecrire/tests/legacy/unit/propre/liens_classes.php
@@ -26,10 +26,10 @@ if (! $id) {
 
 $p0 = "[->art{$id}]";
 
-if (! ($c = extraire_attribut(propre($p0), 'class')) || strpos($c, 'spip_in') === false || strpos(
+if (! ($c = extraire_attribut(propre($p0), 'class')) || !str_contains($c, 'spip_in') || str_contains(
 	$c,
 	'spip_out'
-) !== false) {
+)) {
 	$err[] = "Classe {$c} errone dans {$p0} : " . PtoBR(propre($p0));
 }
 
@@ -41,10 +41,10 @@ if (! $id) {
 
 $p0 = "[->rub{$id}]";
 
-if (! $c = extraire_attribut(propre($p0), 'class') || strpos($c, 'spip_in') === false || strpos(
+if (! $c = extraire_attribut(propre($p0), 'class') || !str_contains($c, 'spip_in') || str_contains(
 	$c,
 	'spip_out'
-) !== false) {
+)) {
 	$err[] = "Classe {$c} errone dans {$p0} : " . PtoBR(propre($p0));
 }
 
@@ -56,10 +56,10 @@ if (! $id) {
 
 $p0 = "[->site{$id}]";
 
-if (! $c = extraire_attribut(propre($p0), 'class') || strpos($c, 'spip_in') !== false || strpos(
+if (! $c = extraire_attribut(propre($p0), 'class') || str_contains($c, 'spip_in') || !str_contains(
 	$c,
 	'spip_out'
-) === false) {
+)) {
 	$err[] = "Classe {$c} errone dans {$p0} : " . PtoBR(propre($p0));
 }