<?php

/***************************************************************************\
 *  SPIP, Système de publication pour l'internet                           *
 *                                                                         *
 *  Copyright © avec tendresse depuis 2001                                 *
 *  Arnaud Martin, Antoine Pitrou, Philippe Rivière, Emmanuel Saint-James  *
 *                                                                         *
 *  Ce programme est un logiciel libre distribué sous licence GNU/GPL.     *
\***************************************************************************/

use function SpipLeague\Component\Kernel\param;

if (!defined('_ECRIRE_INC_VERSION')) {
	return;
} // securiser

# donner un exemple d'url pour le formulaire de choix
defined('URLS_ARBO_EXEMPLE') || define('URLS_ARBO_EXEMPLE', '/article/titre');
# specifier le form de config utilise pour ces urls
defined('URLS_ARBO_CONFIG') || define('URLS_ARBO_CONFIG', 'arbo');

// TODO: une interface permettant de verifier qu'on veut effectivment modifier
// une adresse existante
defined('CONFIRMER_MODIFIER_URL') || define('CONFIRMER_MODIFIER_URL', false);

/**
 * - Comment utiliser ce jeu d'URLs ?
 * Recopiez le fichier "htaccess.txt" du repertoire de base du site SPIP sous
 * le sous le nom ".htaccess" (attention a ne pas ecraser d'autres reglages
 * que vous pourriez avoir mis dans ce fichier) ; si votre site est en
 * "sous-repertoire", vous devrez aussi editer la ligne "RewriteBase" ce fichier.
 * Les URLs definies seront alors redirigees vers les fichiers de SPIP.
 *
 * Choisissez "arbo" dans les pages de configuration d'URL
 *
 * SPIP calculera alors ses liens sous la forme "Mon-titre-d-article".
 * Variantes :
 *
 * Terminaison :
 * les terminaisons ne *sont pas* stockees en base, elles servent juste
 * a rendre les url jolies ou conformes a un usage
 * pour avoir des url terminant par html
 * define ('_terminaison_urls_arbo', '.html');
 *
 * pour preciser des terminaisons particulieres pour certains types
 * $GLOBALS['url_arbo_terminaisons']=array(
 * 'rubrique' => '/',
 * 'mot' => '',
 * 'groupe' => '/',
 * 'defaut' => '.html');
 *
 * pour avoir des url numeriques (id) du type 12/5/4/article/23
 * define ('_URLS_ARBO_MIN',255);
 *
 *
 * pour conserver la casse des titres dans les url
 * define ('_url_arbo_minuscules',0);
 *
 * pour choisir le caractere de separation titre-id en cas de doublon
 * (ne pas utiliser '/')
 * define ('_url_arbo_sep_id','-');
 *
 * pour modifier la hierarchie apparente dans la constitution des urls
 * ex pour que les mots soient classes par groupes
 * $GLOBALS['url_arbo_parents']=array(
 *        'article'=>array('id_rubrique','rubrique'),
 *        'rubrique'=>array('id_parent','rubrique'),
 *        'breve'=>array('id_rubrique','rubrique'),
 *        'site'=>array('id_rubrique','rubrique'),
 *        'mot'=>array('id_groupe','groupes_mot'));
 *
 * pour personaliser les types
 * $GLOBALS['url_arbo_types']=array(
 * 'rubrique'=>'', // pas de type pour les rubriques
 * 'article'=>'a',
 * 'mot'=>'tags'
 * );
 *
 */

$config_urls_arbo = isset($GLOBALS['meta']['urls_arbo']) ? unserialize($GLOBALS['meta']['urls_arbo']) : [];
if (!defined('_debut_urls_arbo')) {
	define('_debut_urls_arbo', '');
}
if (!defined('_terminaison_urls_arbo')) {
	define('_terminaison_urls_arbo', '');
}
// pour choisir le caractere de separation titre-id en cas de doublon
// (ne pas utiliser '/')
if (!defined('_url_arbo_sep_id')) {
	define('_url_arbo_sep_id', $config_urls_arbo['url_arbo_sep_id'] ?? '-');
}
// option pour tout passer en minuscules
if (!defined('_url_arbo_minuscules')) {
	define('_url_arbo_minuscules', $config_urls_arbo['url_arbo_minuscules'] ?? 1);
}
if (!defined('_URLS_ARBO_MAX')) {
	define('_URLS_ARBO_MAX', $config_urls_arbo['URLS_ARBO_MAX'] ?? 80);
}
if (!defined('_URLS_ARBO_MIN')) {
	define('_URLS_ARBO_MIN', $config_urls_arbo['URLS_ARBO_MIN'] ?? 3);
}

if (!defined('_url_sep_id')) {
	define('_url_sep_id', \_url_arbo_sep_id);
}

// peut prendre plusieurs valeurs :
// - false pour ne pas gerer le multilinguisme => fonctionnement historique
//    define('_url_arbo_multilang',false);
// - la valeur d'une langue pour forcer le calcul des URLs dans une langue donnee
//    define('_url_arbo_multilang','en');
// - true pour forcer la gestion complete du multilinguisme :
//   calcul des URLs dans toutes les langues possibles,
//   ajout d'un premier segment fr/ dans les URLs pour definir la langue
//   prise en compte de l'argument lang=xx dans $args au moment de generer l'URL
//    define('_url_arbo_multilang',true);

if (!defined('_url_arbo_multilang')) {
	define('_url_arbo_multilang', false);
}


// Ces chaines servaient de marqueurs a l'epoque ou les URL propres devaient
// indiquer la table ou les chercher (articles, auteurs etc),
// et elles etaient retirees par les preg_match dans la fonction ci-dessous.
// Elles sont a present definies a "" pour avoir des URL plus jolies
// mais les preg_match restent necessaires pour gerer les anciens signets.

#define('_MARQUEUR_URL', serialize(array('rubrique1' => '-', 'rubrique2' => '-', 'breve1' => '+', 'breve2' => '+', 'site1' => '@', 'site2' => '@', 'auteur1' => '_', 'auteur2' => '_', 'mot1' => '+-', 'mot2' => '-+')));
if (!defined('_MARQUEUR_URL')) {
	define('_MARQUEUR_URL', false);
}

/**
 * Definir les parentees utilisees pour construire des urls arborescentes
 *
 * @param string $type
 * @return string
 */
function url_arbo_parent($type) {
	static $parents = null;
	if (is_null($parents)) {
		$parents = [
			'article' => ['id_rubrique', 'rubrique'],
			'rubrique' => ['id_parent', 'rubrique'],
			'breve' => ['id_rubrique', 'rubrique'],
			'site' => ['id_rubrique', 'rubrique']
		];
		if (isset($GLOBALS['url_arbo_parents']) and !isset($_REQUEST['url_arbo_parents'])) {
			$parents = array_merge($parents, $GLOBALS['url_arbo_parents']);
		}
	}

	return ($parents[$type] ?? '');
}

/**
 * Definir les terminaisons des urls :
 * / pour une rubrique
 * .html pour une page etc..
 *
 * @param string $type
 * @return string
 */
function url_arbo_terminaison(string $type): string {
	$terminaison_types = url_arbo_terminaisons_par_type();

	if (isset($terminaison_types[$type])) {
		return $terminaison_types[$type];
	} elseif (isset($terminaison_types['defaut'])) {
		return $terminaison_types['defaut'];
	}

	return '';
}

/**
 * La liste des terminaisons par type, eventuellement personalisee par la globale url_arbo_terminaisons
 * @return array
 */
function url_arbo_terminaisons_par_type(): array {
	static $terminaison_types = null;
	if ($terminaison_types == null) {
		$terminaison_types = [
			'rubrique' => '/',
			'mot' => '',
			'defaut' => defined('_terminaison_urls_arbo') ? \_terminaison_urls_arbo : '.html'
		];
		if (isset($GLOBALS['url_arbo_terminaisons'])) {
			$terminaison_types = array_merge($terminaison_types, $GLOBALS['url_arbo_terminaisons']);
		}
	}

	return $terminaison_types;
}


/**
 * Definir le prefixe qui designe le type et qu'on utilise pour chaque objet
 * ex : "article"/truc
 * par defaut les rubriques ne sont pas typees, mais le reste oui
 *
 * @param string $type
 * @return array|string
 */
function url_arbo_type($type) {
	static $synonymes_types = null;
	if (!$synonymes_types) {
		$synonymes_types = ['rubrique' => ''];
		if (isset($GLOBALS['url_arbo_types']) and is_array($GLOBALS['url_arbo_types'])) {
			$synonymes_types = array_merge($synonymes_types, $GLOBALS['url_arbo_types']);
		}
	}
	// si c'est un appel avec type='' c'est pour avoir la liste inversee des synonymes
	if (!$type) {
		return array_flip($synonymes_types);
	}

	return
		($t = ($synonymes_types[$type] ?? $type))  // le type ou son synonyme
		. ($t ? '/' : ''); // le / eventuel pour separer, si le synonyme n'est pas vide
}

/**
 * Pipeline pour creation d'une adresse : il recoit l'url propose par le
 * precedent, un tableau indiquant le titre de l'objet, son type, son id,
 * et doit donner en retour une chaine d'url, sans se soucier de la
 * duplication eventuelle, qui sera geree apres
 *
 * @param array $x
 * @return array
 */
function urls_arbo_creer_chaine_url($x) {
	// NB: ici url_old ne sert pas, mais un plugin qui ajouterait une date
	// pourrait l'utiliser pour juste ajouter la
	$url_old = $x['data'];
	$objet = $x['objet'];
	include_spip('inc/filtres');

	include_spip('action/editer_url');
	if (
		!$url = url_nettoyer(
			$objet['titre'],
			_URLS_ARBO_MAX,
			_URLS_ARBO_MIN,
			'-',
			\_url_arbo_minuscules ? 'spip_strtolower' : ''
		)
	) {
		$url = $objet['id_objet'];
	}

	// le type ou son synonyme
	$prefixe = url_arbo_type($objet['type']);
	if (strpos($prefixe, '<') !== false) {
		$prefixe = extraire_multi($prefixe);
		$prefixe = textebrut($prefixe);
	}
	$x['data'] = $prefixe . $url; // le titre

	return $x;
}

/**
 * Boucler sur le parent pour construire l'url complete a partir des segments
 *
 * @param string $url
 * @param string $type
 * @param string $parent
 * @param string $type_parent
 * @param array $contexte
 * @return string
 */
function declarer_url_arbo_rec($url, $type, $parent, $type_parent, $contexte = []) {
	if (is_null($parent)) {
		return $url;
	}
	// le contexte parent ne se transmet pas
	if (isset($contexte['id_parent'])) {
		unset($contexte['id_parent']);
	}
	// Si pas de parent ou si son URL est vide, on ne renvoit que l'URL de l'objet en court
	if ($parent == 0 or !($url_parent = declarer_url_arbo($type_parent ?: 'rubrique', $parent, $contexte))) {
		return rtrim($url, '/');
	} // Sinon on renvoit l'URL de l'objet concaténée avec celle du parent
	else {
		return rtrim($url_parent, '/') . '/' . rtrim($url, '/');
	}
}

/**
 * Renseigner les infos les plus recentes de l'url d'un objet
 * et de quoi la (re)construire si besoin
 *
 * @param string $type
 * @param int $id_objet
 * @param array $contexte
 *   id_parent : rubrique parent
 * @return bool|null|array
 */
function renseigner_url_arbo($type, $id_objet, $contexte = []) {
	$urls = [];
	$trouver_table = charger_fonction('trouver_table', 'base');
	$desc = $trouver_table(table_objet($type));
	// Quand $type ne reference pas une table
	if (!$desc) {
		return false;
	}
	$table = $desc['table'];
	$col_id = $desc['key']['PRIMARY KEY'] ?? null;
	if (!$col_id) {
		return false;
	}

	$id_objet = intval($id_objet);

	$id_parent = ($contexte['id_parent'] ?? null);
	$langue = ($contexte['langue'] ?? '');

	$champ_titre = $desc['titre'] ?: 'titre';

	// parent
	$champ_parent = url_arbo_parent($type);
	$sel_parent = ', 0 as parent';
	$order_by_parent = '';
	if ($champ_parent) {
		// si un parent est fourni est qu'il est legitime, on recherche une URL pour ce parent
		if (
			$id_parent
			and $type_parent = end($champ_parent)
			and $url_verifier_parent_objet = charger_fonction('url_verifier_parent_objet', 'inc', true)
			and $url_verifier_parent_objet($type, $id_objet, $type_parent, $id_parent)
		) {
			$sel_parent = ', ' . intval($id_parent) . ' as parent';
			// trouver l'url qui matche le parent en premier
			$order_by_parent = 'U.id_parent=' . intval($id_parent) . ' DESC, ';
		} else {
			// sinon on prend son parent direct fourni par $champ_parent
			$sel_parent = ', O.' . reset($champ_parent) . ' as parent';
			// trouver l'url qui matche le parent en premier
			$order_by_parent = 'O.' . reset($champ_parent) . '=U.id_parent DESC, ';
		}
	}
	$order_by_langue = "U.langue='' DESC, ";
	if ($langue) {
		$order_by_langue = 'U.langue=' . sql_quote($langue) . ' DESC, ' . $order_by_langue;
	}

	//  Recuperer une URL propre correspondant a l'objet.
	$row = sql_fetsel(
		"U.url, U.date, U.id_parent, U.perma, U.langue, $champ_titre $sel_parent",
		"$table AS O LEFT JOIN spip_urls AS U ON (U.type='$type' AND U.id_objet=O.$col_id)",
		"O.$col_id=$id_objet",
		'',
		$order_by_parent . 'U.perma DESC, ' . $order_by_langue . 'U.date DESC',
		1
	);
	if ($row) {
		$urls[$type][$id_objet] = $row;
		$urls[$type][$id_objet]['type_parent'] = $champ_parent ? end($champ_parent) : '';
	}

	return $urls[$type][$id_objet] ?? null;
}

/**
 * Retrouver/Calculer l'ensemble des segments d'url d'un objet
 *
 * @param string $type
 * @param int $id_objet
 * @param array $contexte
 *   id_parent : rubrique parent
 *   langue : langue courante pour laquelle on veut l'URL
 * @return string
 */
function declarer_url_arbo($type, $id_objet, $contexte = []) {
	static $urls = [];

	// contexte de langue si pas defini, en fonction de la configuration
	if (!isset($contexte['langue'])) {
		if (!\_url_arbo_multilang) {
			$contexte['langue'] = '';
		} elseif (\_url_arbo_multilang === true) {
			$contexte['langue'] = $GLOBALS['spip_lang'];
		} else {
			$contexte['langue'] = \_url_arbo_multilang;
		}
	}
	ksort($contexte);
	$hash = json_encode($contexte, JSON_THROW_ON_ERROR);

	// Se contenter de cette URL si elle existe ;
	// sauf si on invoque par "voir en ligne" avec droit de modifier l'url

	// l'autorisation est verifiee apres avoir calcule la nouvelle url propre
	// car si elle ne change pas, cela ne sert a rien de verifier les autorisations
	// qui requetent en base
	$modifier_url = (defined('_VAR_URLS') and _VAR_URLS);

	if (!isset($urls[$type][$id_objet][$hash]) or $modifier_url) {
		$r = renseigner_url_arbo($type, $id_objet, $contexte);
		// Quand $type ne reference pas une table
		if ($r === false) {
			return false;
		}

		if (!is_null($r)) {
			$urls[$type][$id_objet][$hash] = $r;
		}
	}

	if (!isset($urls[$type][$id_objet][$hash])) {
		return '';
	} # objet inexistant

	$u = &$urls[$type][$id_objet][$hash];
	$url_propre = $u['url'];

	// si on a trouve l'url
	// et que le parent est bon
	// et (permanente ou pas de demande de modif)
	if (
		!is_null($url_propre)
		and $u['id_parent'] == $u['parent']
		and ($u['perma'] or !$modifier_url)
	) {
		return declarer_url_arbo_rec(
			$url_propre,
			$type,
			$u['parent'] ?? 0,
			$u['type_parent'] ?? null,
			$contexte
		);
	}

	// Si URL inconnue ou maj forcee sur une url non permanente, recreer une url
	$url = $url_propre;
	$urls_langues = [];
	if (is_null($url_propre) or ($modifier_url and !$u['perma'])) {
		$langues = [];
		if (\_url_arbo_multilang === true) {
			include_spip('inc/lang');
			$langues = ($GLOBALS['meta']['langues_multilingue'] ?? '');
			$langues = explode(',', $langues);
			if ($k = array_search(_LANGUE_PAR_DEFAUT, $langues)) {
				unset($langues[$k]);
				array_unshift($langues, _LANGUE_PAR_DEFAUT);
			}
		}
		if (!in_array($contexte['langue'], $langues)) {
			$langues[] = $contexte['langue'];
		}

		// on calcule l'URL de chaque langue utile (langue courante, langue forcee ou toutes les langues utilises)
		$langue_courante = $GLOBALS['spip_lang'];

		include_spip('inc/urls');
		$objets = urls_liste_objets();

		foreach ($langues as $l) {
			if ($l) {
				changer_langue($l);
			}
			$urls_langues[$l] = pipeline(
				'arbo_creer_chaine_url',
				[
					'data' => $url_propre,  // le vieux url_propre
					'objet' => array_merge($u, ['type' => $type, 'id_objet' => $id_objet])
				]
			);

			// Eviter de tamponner les URLs a l'ancienne (cas d'un article
			// intitule "auteur2")
			if (
				preg_match(',^(' . $objets . ')[0-9]*$,', $urls_langues[$l], $r)
				and $r[1] != $type
			) {
				$urls_langues[$l] = $urls_langues[$l] . \_url_arbo_sep_id . $id_objet;
			}
		}
		// retablir la $langue_courante par securite, au cas ou on a change de langue
		changer_langue($langue_courante);

		$url = $urls_langues[$contexte['langue']];
	}


	// Pas de changement d'url ni de parent
	if (
		$url == $url_propre
		and $u['id_parent'] == $u['parent']
	) {
		return declarer_url_arbo_rec($url_propre, $type, $u['parent'], $u['type_parent'], $contexte);
	}

	// verifier l'autorisation, maintenant qu'on est sur qu'on va agir
	if ($modifier_url) {
		include_spip('inc/autoriser');
		$modifier_url = autoriser('modifierurl', $type, $id_objet);
	}
	// Verifier si l'utilisateur veut effectivement changer l'URL
	if (
		$modifier_url
		and CONFIRMER_MODIFIER_URL
		and $url_propre
		// on essaye pas de regenerer une url en -xxx (suffixe id anti collision)
		and $url != preg_replace('/' . preg_quote(\_url_propres_sep_id, '/') . '.*/', '', $url_propre)
	) {
		$confirmer = true;
	} else {
		$confirmer = false;
	}

	if ($confirmer and !_request('ok')) {
		die("vous changez d'url ? $url_propre -&gt; $url");
	}

	// on enregistre toutes les langues
	include_spip('action/editer_url');
	foreach ($urls_langues as $langue => $url) {
		$set = [
			'url' => $url,
			'type' => $type,
			'id_objet' => $id_objet,
			'id_parent' => $u['parent'],
			'langue' => $langue,
			'perma' => intval($u['perma'])
		];
		$res = url_insert($set, $confirmer, \_url_arbo_sep_id);
		if ($langue == $contexte['langue']) {
			if ($res) {
				$u['url'] = $set['url'];
				$u['id_parent'] = $set['id_parent'];
			} else {
				// l'insertion a echoue,
				//serveur out ? retourner au mieux
				$u['url'] = $url_propre;
			}
		}
	}

	return declarer_url_arbo_rec($u['url'] ?? '', $type, $u['parent'], $u['type_parent'], $contexte);
}


/**
 * Generer l'url d'un objet SPIP
 * @param int $id
 * @param string $objet
 * @param string $args
 * @param string $ancre
 * @return string
 */
function urls_arbo_generer_url_objet_dist(int $id, string $objet, string $args = '', string $ancre = ''): string {
	if ($generer_url_externe = charger_fonction_url($objet, 'defaut')) {
		$url = $generer_url_externe($id, $args, $ancre);
		// une url === null indique "je ne traite pas cette url, appliquez le calcul standard"
		// une url vide est une url vide, ne rien faire de plus
		if (!is_null($url)) {
			return $url;
		}
	}

	$debut_langue = '';

	// Mode propre
	$c = [];

	parse_str($args, $contexte);
	// choisir le contexte de langue en fonction de la configuration
	$c['langue'] = '';
	if (\_url_arbo_multilang === true) {
		if (isset($contexte['lang']) and $contexte['lang']) {
			$c['langue'] = $contexte['lang'];
			$debut_langue = $c['langue'] . '/';
			unset($contexte['lang']);
			$args = http_build_query($contexte);
		} elseif (isset($GLOBALS['spip_lang']) and $GLOBALS['spip_lang']) {
			$c['langue'] = $GLOBALS['spip_lang'];
			$debut_langue = $c['langue'] . '/';
		}
	} elseif (\_url_arbo_multilang) {
		$c['langue'] = \_url_arbo_multilang;
	}
	$propre = declarer_url_arbo($objet, $id, $c);

	// si le parent est fourni en contexte dans le $args, verifier si l'URL relative a ce parent est la meme ou non
	$champ_parent = url_arbo_parent($objet);
	if (
		$champ_parent
		and $champ_parent = reset($champ_parent)
		and isset($contexte[$champ_parent]) and $contexte[$champ_parent]
	) {
		$c['id_parent'] = $contexte[$champ_parent];
		$propre_contexte = declarer_url_arbo($objet, $id, $c);
		// si l'URL est differente on la prend et on enleve l'argument de l'URL (redondance puisque parent defini par l'URL elle meme)
		if ($propre_contexte !== $propre) {
			$propre = $propre_contexte;
			unset($contexte[$champ_parent]);
			$args = http_build_query($contexte);
		}
	}


	if ($propre === false) {
		return '';
	} // objet inconnu. raccourci ?

	if ($propre) {
		$url = \_debut_urls_arbo
			. $debut_langue
			. rtrim($propre, '/')
			. url_arbo_terminaison($objet);
	} else {
		// objet connu mais sans possibilite d'URL lisible, revenir au defaut
		include_spip('base/connect_sql');
		$id_type = id_table_objet($objet);
		$url = get_spip_script('./') . '?' . _SPIP_PAGE . "=$objet&$id_type=$id";
	}

	// Ajouter les args
	if ($args) {
		$url .= ((strpos($url, '?') === false) ? '?' : '&') . $args;
	}

	// Ajouter l'ancre
	if ($ancre) {
		$url .= "#$ancre";
	}

	return _DIR_RACINE . $url;
}


/**
 * Decoder une url propres
 * retrouve le fond et les parametres d'une URL abregee
 * le contexte deja existant est fourni dans args sous forme de tableau
 *
 * @param string $url
 * @param string $entite
 * @param array $contexte
 * @return array([contexte],[type],[url_redirect],[fond]) : url decodee
 */
function urls_arbo_decoder_url_dist(string $url, string $entite, array $contexte = []): array {

	// traiter les injections du type domaine.org/spip.php/cestnimportequoi/ou/encore/plus/rubrique23
	if ($GLOBALS['profondeur_url'] > 0 and $entite == 'sommaire') {
		$entite = 'type_urls';
	}

	$id_objet = $type = 0;
	$url_redirect = null;

	// Migration depuis anciennes URLs ?
	if (($_SERVER['REQUEST_METHOD'] ?? null) !== 'POST') {
		$res = urls_transition_retrouver_anciennes_url_html($url, $entite, $contexte);
		if ($res) {
			return $res;
		}
	}
	/* Fin compatibilite anciennes urls */

	// Chercher les valeurs d'environnement qui indiquent l'url-propre
	$url_propre = preg_replace(',[?].*,', '', $url);

	// Mode Query-String ?
	if (
		!$url_propre
		and preg_match(',[?]([^=/?&]+)(&.*)?$,', $url, $r)
	) {
		$url_propre = $r[1];
	}

	if (
		!$url_propre
		or $url_propre == param('spip.routes.back_office')
		or $url_propre == _SPIP_SCRIPT
	) {
		// qu'est-ce qu'il veut ???
		return [];
	}

	include_spip('base/abstract_sql'); // chercher dans la table des URLS

	// Revenir en utf-8 si encodage type %D8%A7 (farsi)
	$url_propre = rawurldecode($url_propre);

	// Compatibilite avec .htm/.html et autres terminaisons
	$t = array_diff(array_unique(array_merge(['.html', '.htm', '/'], url_arbo_terminaisons_par_type())), ['']);
	if (count($t)) {
		$url_propre = preg_replace('{('
			. implode('|', array_map('preg_quote', $t)) . ')$}i', '', $url_propre);
	}

	if (strlen($url_propre) and !preg_match(',^[^/]*[.]php,', $url_propre)) {
		$parents_vus = [];
		$args = http_build_query($contexte); // va servir pour d'eventuelles redirections

		// recuperer tous les objets de larbo xxx/article/yyy/mot/zzzz
		// on parcourt les segments de gauche a droite
		// pour pouvoir contextualiser un segment par son parent
		$url_arbo = explode('/', $url_propre);
		$url_arbo_new = [];
		$dernier_parent_vu = false;
		$objet_segments = 0;

		$langue = '';
		if (\_url_arbo_multilang === true) {
			// la langue : si fourni en QS prioritaire car vient du skel ou de forcer_lang
			if (isset($contexte['lang'])) {
				$langue = $contexte['lang'];
			}
			// le premier segment peut etre la langue : l'extraire
			// on le prend en compte si lang non fournie par la QS sinon on l'ignore
			include_spip('action/editer_url'); // pour url_verifier_langue
			if (
				count($url_arbo) > 1
				and $first = reset($url_arbo)
				and url_verifier_langue($first)
			) {
				array_shift($url_arbo);
				if (!$langue) {
					$contexte['lang'] = $langue = $first;
				}
			}
		} elseif (\_url_arbo_multilang) {
			$langue = \_url_arbo_multilang;
		}

		while (count($url_arbo) > 0) {
			$type = null;
			if (count($url_arbo) > 1) {
				$type = array_shift($url_arbo);
			}
			$url_segment = array_shift($url_arbo);
			// Rechercher le segment de candidat
			// si on est dans un contexte de parent, donne par le segment precedent,
			// prefixer le segment recherche avec ce contexte
			$cp = '0'; // par defaut : parent racine, id=0
			if ($dernier_parent_vu) {
				$cp = $parents_vus[$dernier_parent_vu];
			}
			// d'abord recherche avec prefixe parent, en une requete car aucun risque de colision
			$row = sql_fetsel(
				'id_objet, type, url',
				'spip_urls',
				is_null($type)
					? 'url=' . sql_quote($url_segment, '', 'TEXT')
					: sql_in_quote('url', ["$type/$url_segment", $type], '', '', 'TEXT'),
				'',
				// en priorite celui qui a le bon parent
				// puis la bonne langue puis la langue ''
				// puis les deux segments puis 1 seul segment
				//
				// si parent indefini on privilegie id_parent=0 avec la derniere clause du order
				(intval($cp) ? 'id_parent=' . intval($cp) . ' DESC, ' : 'id_parent>=0 DESC, ')
				. ($langue ? 'langue=' . sql_quote($langue) . ' DESC, ' : '') . "langue='' DESC,"
				. 'segments DESC, id_parent'
			);
			if ($row) {
				if (!is_null($type) and $row['url'] == $type) {
					array_unshift($url_arbo, $url_segment);
					$url_segment = $type;
					$type = null;
				}
				$type = $row['type'];
				$col_id = id_table_objet($type);

				// le plus a droite l'emporte pour des objets presents plusieurs fois dans l'url (ie rubrique)
				$contexte[$col_id] = $row['id_objet'];

				$type_parent = '';
				if ($p = url_arbo_parent($type)) {
					$type_parent = end($p);
				}
				// l'entite la plus a droite l'emporte, si le type de son parent a ete vu
				// sinon c'est un segment contextuel supplementaire a ignorer
				// ex : rub1/article/art1/mot1 : il faut ignorer le mot1, la vrai url est celle de l'article
				if (
					!$entite
					or $dernier_parent_vu == $type_parent
				) {
					if ($objet_segments == 0) {
						$entite = $type;
					}
				} // sinon on change d'objet concerne
				else {
					$objet_segments++;
				}

				$url_arbo_new[$objet_segments]['id_objet'] = $row['id_objet'];
				$url_arbo_new[$objet_segments]['objet'] = $type;
				$url_arbo_new[$objet_segments]['segment'][] = $row['url'];

				// on note le dernier parent vu de chaque type
				$parents_vus[$dernier_parent_vu = $type] = $row['id_objet'];
			} else {
				// un segment est inconnu
				if ($entite == '' or $entite == 'type_urls') {
					// on genere une 404 comme il faut si on ne sait pas ou aller
					return [[], '404'];
				}
				// ici on a bien reconnu un segment en amont, mais le segment en cours est inconnu
				// on pourrait renvoyer sur le dernier segment identifie
				// mais de fait l'url entiere est inconnu : 404 aussi
				// mais conserver le contexte qui peut contenir un fond d'ou venait peut etre $entite (reecriture urls)
				return [$contexte, '404'];
			}
		}

		if (count($url_arbo_new)) {
			$caller = debug_backtrace();
			$caller = $caller[1]['function'];
			// si on est appele par un autre module d'url c'est du decodage d'une ancienne URL
			// ne pas regenerer des segments arbo, mais rediriger vers la nouvelle URL
			// dans la nouvelle forme
			if (str_starts_with($caller, 'urls_') && !in_array($caller, ['urls_decoder_url', 'urls_arbo_decoder_url'])) {
				// en absolue, car assembler ne gere pas ce cas particulier
				include_spip('inc/filtres_mini');
				$col_id = id_table_objet($entite);
				$url_new = generer_objet_url($contexte[$col_id], $entite, $args);
				// securite contre redirection infinie
				if (
					$url_new !== $url_propre
					and rtrim($url_new, '/') !== rtrim($url_propre, '/')
				) {
					$url_redirect = url_absolue($url_new);
				}
			} else {
				foreach ($url_arbo_new as $k => $o) {
					$c = [ 'langue' => $langue ];
					if (isset($parents_vus['rubrique'])) {
						$c['id_parent'] = $parents_vus['rubrique'];
					}
					if ($s = declarer_url_arbo($o['objet'], $o['id_objet'], $c)) {
						$url_arbo_new[$k] = $s;
					} else {
						$url_arbo_new[$k] = implode('/', $o['segment']);
					}
				}
				$url_arbo_new = ltrim(implode('/', $url_arbo_new), '/');
				if ($langue and \_url_arbo_multilang === true) {
					$url_arbo_new = "$langue/" . $url_arbo_new;
					if (strpos($args, 'lang=') !== false) {
						parse_str($args, $cl);
						unset($cl['lang']);
						$args = http_build_query($cl);
					}
				}
				if ($url_arbo_new !== $url_propre) {
					//var_dump($url_arbo_new,$url_propre);
					$url_redirect = \_debut_urls_arbo
						. $url_arbo_new
						. url_arbo_terminaison($entite)
						. ($args ? "?$args" : '');
					// en absolue, car assembler ne gere pas ce cas particulier
					include_spip('inc/filtres_mini');
					$url_redirect = url_absolue($url_redirect);
				}
			}
		}

		// gerer le retour depuis des urls propres
		if (
			($entite == '' or $entite == 'type_urls')
			and $GLOBALS['profondeur_url'] <= 0
		) {
			return urls_transition_retrouver_anciennes_url_propres($url, $entite, $contexte);
		}
	}
	if ($entite == '' or $entite == 'type_urls' /* compat .htaccess 2.0 */) {
		if ($type) {
			$entite = objet_type($type);
		} else {
			// Si ca ressemble a une URL d'objet, ce n'est pas la home
			// et on provoque un 404
			if (preg_match(',^[^\.]+(\.html)?$,', $url)) {
				$entite = '404';
				$contexte['erreur'] = ''; // qu'afficher ici ?  l'url n'existe pas... on ne sait plus dire de quel type d'objet il s'agit
			}
		}
	}
	if (!defined('_SET_HTML_BASE')) {
		define('_SET_HTML_BASE', 1);
	}

	return [$contexte, $entite, $url_redirect, null];
}
