Program: CWE-79
Project: CVE-2022-28959
Buggy source:
<?php
/***************************************************************************\
 *  SPIP, Systeme de publication pour l'internet                           *
 *                                                                         *
 *  Copyright (c) 2001-2019                                                *
 *  Arnaud Martin, Antoine Pitrou, Philippe Riviere, Emmanuel Saint-James  *
 *                                                                         *
 *  Ce programme est un logiciel libre distribue sous licence GNU/GPL.     *
 *  Pour plus de details voir le fichier COPYING.txt ou l'aide en ligne.   *
\***************************************************************************/

/**
 * Gestion du formulaire d'édition de liens
 *
 * @package SPIP\Core\Formulaires
 **/
if (!defined('_ECRIRE_INC_VERSION')) {
	return;
}


/**
 * Retrouve la source et l'objet de la liaison
 *
 * À partir des 3 premiers paramètres transmis au formulaire,
 * la fonction retrouve :
 * - l'objet dont on utilise sa table de liaison (table_source)
 * - l'objet et id_objet sur qui on lie des éléments (objet, id_objet)
 * - l'objet que l'on veut lier dessus (objet_lien)
 *
 * @param string $a
 * @param string|int $b
 * @param int|string $c
 * @return array
 *   ($table_source,$objet,$id_objet,$objet_lien)
 */
function determine_source_lien_objet($a, $b, $c) {
	$table_source = $objet_lien = $objet = $id_objet = null;
	// auteurs, article, 23 :
	// associer des auteurs à l'article 23, sur la table pivot spip_auteurs_liens
	if (is_numeric($c) and !is_numeric($b)) {
		$table_source = table_objet($a);
		$objet_lien = objet_type($a);
		$objet = objet_type($b);
		$id_objet = $c;
	}
	// article, 23, auteurs
	// associer des auteurs à l'article 23, sur la table pivot spip_articles_liens
	if (is_numeric($b) and !is_numeric($c)) {
		$table_source = table_objet($c);
		$objet_lien = objet_type($a);
		$objet = objet_type($a);
		$id_objet = $b;
	}

	return array($table_source, $objet, $id_objet, $objet_lien);
}

/**
 * Chargement du formulaire d'édition de liens
 *
 * #FORMULAIRE_EDITER_LIENS{auteurs,article,23}
 *   pour associer des auteurs à l'article 23, sur la table pivot spip_auteurs_liens
 * #FORMULAIRE_EDITER_LIENS{article,23,auteurs}
 *   pour associer des auteurs à l'article 23, sur la table pivot spip_articles_liens
 * #FORMULAIRE_EDITER_LIENS{articles,auteur,12}
 *   pour associer des articles à l'auteur 12, sur la table pivot spip_articles_liens
 * #FORMULAIRE_EDITER_LIENS{auteur,12,articles}
 *   pour associer des articles à l'auteur 12, sur la table pivot spip_auteurs_liens
 *
 * @param string $a
 * @param string|int $b
 * @param int|string $c
 * @param array|bool $options
 *    - Si array, tableau d'options
 *    - Si bool : valeur de l'option 'editable' uniquement
 *
 * @return array
 */
function formulaires_editer_liens_charger_dist($a, $b, $c, $options = array()) {

	// compat avec ancienne signature ou le 4eme argument est $editable
	if (!is_array($options)) {
		$options = array('editable' => $options);
	} elseif (!isset($options['editable'])) {
		$options['editable'] = true;
	}

	$editable = $options['editable'];

	list($table_source, $objet, $id_objet, $objet_lien) = determine_source_lien_objet($a, $b, $c);
	if (!$table_source or !$objet or !$objet_lien or !$id_objet) {
		return false;
	}

	$objet_source = objet_type($table_source);
	$table_sql_source = table_objet_sql($objet_source);

	// verifier existence de la table xxx_liens
	include_spip('action/editer_liens');
	if (!objet_associable($objet_lien)) {
		return false;
	}

	// L'éditabilité :) est définie par un test permanent (par exemple "associermots") ET le 4ème argument
	include_spip('inc/autoriser');
	$editable = ($editable and autoriser('associer' . $table_source, $objet, $id_objet)
		and autoriser('modifier', $objet, $id_objet));

	if (!$editable and !count(objet_trouver_liens(
		array($objet_lien => '*'),
		array(($objet_lien == $objet_source ? $objet : $objet_source) => $id_objet)
	))) {
		return false;
	}

	// squelettes de vue et de d'association
	// ils sont différents si des rôles sont définis.
	$skel_vue = $table_source . '_lies';
	$skel_ajout = $table_source . '_associer';

	// description des roles
	include_spip('inc/roles');
	if ($roles = roles_presents($objet_source, $objet)) {
		// on demande de nouveaux squelettes en conséquence
		$skel_vue = $table_source . '_roles_lies';
		$skel_ajout = $table_source . '_roles_associer';
	}

	$valeurs = array(
		'id' => "$table_source-$objet-$id_objet-$objet_lien", // identifiant unique pour les id du form
		'_vue_liee' => $skel_vue,
		'_vue_ajout' => $skel_ajout,
		'_objet_lien' => $objet_lien,
		'id_lien_ajoute' => _request('id_lien_ajoute'),
		'objet' => $objet,
		'id_objet' => $id_objet,
		'objet_source' => $objet_source,
		'table_source' => $table_source,
		'recherche' => '',
		'visible' => 0,
		'ajouter_lien' => '',
		'supprimer_lien' => '',
		'qualifier_lien' => '',
		'ordonner_lien' => '',
		'desordonner_liens' => '',
		'_roles' => $roles, # description des roles
		'_oups' => _request('_oups'),
		'editable' => $editable,
	);

	// les options non definies dans $valeurs sont passees telles quelles au formulaire html
	$valeurs = array_merge($options, $valeurs);

	return $valeurs;
}

/**
 * Traiter le post des informations d'édition de liens
 *
 * Les formulaires peuvent poster dans quatre variables
 * - ajouter_lien et supprimer_lien
 * - remplacer_lien
 * - qualifier_lien
 * - ordonner_lien
 * - desordonner_liens
 *
 * Les deux premières peuvent être de trois formes différentes :
 * ajouter_lien[]="objet1-id1-objet2-id2"
 * ajouter_lien[objet1-id1-objet2-id2]="nimportequoi"
 * ajouter_lien['clenonnumerique']="objet1-id1-objet2-id2"
 * Dans ce dernier cas, la valeur ne sera prise en compte
 * que si _request('clenonnumerique') est vrai (submit associé a l'input)
 *
 * remplacer_lien doit être de la forme
 * remplacer_lien[objet1-id1-objet2-id2]="objet3-id3-objet2-id2"
 * ou objet1-id1 est celui qu'on enleve et objet3-id3 celui qu'on ajoute
 *
 * qualifier_lien doit être de la forme, et sert en complément de ajouter_lien
 * qualifier_lien[objet1-id1-objet2-id2][role] = array("role1", "autre_role")
 * qualifier_lien[objet1-id1-objet2-id2][valeur] = array("truc", "chose")
 * produira 2 liens chacun avec array("role"=>"role1","valeur"=>"truc") et array("role"=>"autre_role","valeur"=>"chose")
 *
 * ordonner_lien doit être de la forme, et sert pour trier les liens
 * ordonner_lien[objet1-id1-objet2-id2] = nouveau_rang
 *
 * desordonner_liens n'a pas de forme précise, il doit simplement être non nul/non vide
 *
 * @param string $a
 * @param string|int $b
 * @param int|string $c
 * @param array|bool $options
 *    - Si array, tableau d'options
 *    - Si bool : valeur de l'option 'editable' uniquement
 *
 * @return array
 */
function formulaires_editer_liens_traiter_dist($a, $b, $c, $options = array()) {
	// compat avec ancienne signature ou le 4eme argument est $editable
	if (!is_array($options)) {
		$options = array('editable' => $options);
	} elseif (!isset($options['editable'])) {
		$options['editable'] = true;
	}

	$editable = $options['editable'];

	$res = array('editable' => $editable ? true : false);
	list($table_source, $objet, $id_objet, $objet_lien) = determine_source_lien_objet($a, $b, $c);
	if (!$table_source or !$objet or !$objet_lien) {
		return $res;
	}


	if (_request('tout_voir')) {
		set_request('recherche', '');
	}

	include_spip('inc/autoriser');
	if (autoriser('modifier', $objet, $id_objet)) {
		// annuler les suppressions du coup d'avant !
		if (_request('annuler_oups')
			and $oups = _request('_oups')
			and $oups = unserialize($oups)
		) {
			if ($oups_objets = charger_fonction("editer_liens_oups_{$table_source}_{$objet}_{$objet_lien}", 'action', true)) {
				$oups_objets($oups);
			} else {
				$objet_source = objet_type($table_source);
				include_spip('action/editer_liens');
				foreach ($oups as $oup) {
					if ($objet_lien == $objet_source) {
						objet_associer(array($objet_source => $oup[$objet_source]), array($objet => $oup[$objet]), $oup);
					} else {
						objet_associer(array($objet => $oup[$objet]), array($objet_source => $oup[$objet_source]), $oup);
					}
				}
			}
			# oups ne persiste que pour la derniere action, si suppression
			set_request('_oups');
		}

		$supprimer = _request('supprimer_lien');
		$ajouter = _request('ajouter_lien');
		$ordonner = _request('ordonner_lien');

		if (_request('desordonner_liens')) {
			include_spip('action/editer_liens');
			objet_qualifier_liens(array($objet_lien => '*'), array($objet => $id_objet), array('rang_lien' => 0));
		}

		// il est possible de preciser dans une seule variable un remplacement :
		// remplacer_lien[old][new]
		if ($remplacer = _request('remplacer_lien')) {
			foreach ($remplacer as $k => $v) {
				if ($old = lien_verifier_action($k, '')) {
					foreach (is_array($v) ? $v : array($v) as $kn => $vn) {
						if ($new = lien_verifier_action($kn, $vn)) {
							$supprimer[$old] = 'x';
							$ajouter[$new] = '+';
						}
					}
				}
			}
		}

		if ($supprimer) {
			if ($supprimer_objets = charger_fonction(
				"editer_liens_supprimer_{$table_source}_{$objet}_{$objet_lien}",
				'action',
				true
			)) {
				$oups = $supprimer_objets($supprimer);
			} else {
				include_spip('action/editer_liens');
				$oups = array();

				foreach ($supprimer as $k => $v) {
					if ($lien = lien_verifier_action($k, $v)) {
						$lien = explode('-', $lien);
						list($objet_source, $ids, $objet_lie, $idl, $role) = array_pad($lien, 5, null);
						// appliquer une condition sur le rôle si défini ('*' pour tous les roles)
						$cond = (!is_null($role) ? array('role' => $role) : array());
						if ($objet_lien == $objet_source) {
							$oups = array_merge(
								$oups,
								objet_trouver_liens(array($objet_source => $ids), array($objet_lie => $idl), $cond)
							);
							objet_dissocier(array($objet_source => $ids), array($objet_lie => $idl), $cond);
						} else {
							$oups = array_merge(
								$oups,
								objet_trouver_liens(array($objet_lie => $idl), array($objet_source => $ids), $cond)
							);
							objet_dissocier(array($objet_lie => $idl), array($objet_source => $ids), $cond);
						}
					}
				}
			}
			set_request('_oups', $oups ? serialize($oups) : null);
		}

		if ($ajouter) {
			if ($ajouter_objets = charger_fonction("editer_liens_ajouter_{$table_source}_{$objet}_{$objet_lien}", 'action', true)
			) {
				$ajout_ok = $ajouter_objets($ajouter);
			} else {
				$ajout_ok = false;
				include_spip('action/editer_liens');
				foreach ($ajouter as $k => $v) {
					if ($lien = lien_verifier_action($k, $v)) {
						$ajout_ok = true;
						list($objet1, $ids, $objet2, $idl) = explode('-', $lien);
						$qualifs = lien_retrouver_qualif($objet_lien, $lien);
						if ($objet_lien == $objet1) {
							lien_ajouter_liaisons($objet1, $ids, $objet2, $idl, $qualifs);
						} else {
							lien_ajouter_liaisons($objet2, $idl, $objet1, $ids, $qualifs);
						}
						set_request('id_lien_ajoute', $ids);
					}
				}
			}
			# oups ne persiste que pour la derniere action, si suppression
			# une suppression suivie d'un ajout dans le meme hit est un remplacement
			# non annulable !
			if ($ajout_ok) {
				set_request('_oups');
			}
		}

		if ($ordonner) {
			include_spip('action/editer_liens');
			foreach ($ordonner as $k => $rang_lien) {
				if ($lien = lien_verifier_action($k, '')) {
					list($objet1, $ids, $objet2, $idl) = explode('-', $lien);
					$qualif = array('rang_lien' => $rang_lien);

					if ($objet_lien == $objet1) {
						objet_qualifier_liens(array($objet1 => $ids), array($objet2 => $idl), $qualif);
					} else {
						objet_qualifier_liens(array($objet2 => $idl), array($objet1 => $ids), $qualif);
					}
					set_request('id_lien_ajoute', $ids);
					set_request('_oups');
				}
			}
		}
	}


	return $res;
}


/**
 * Retrouver l'action de liaision demandée
 *
 * Les formulaires envoient une action dans un tableau ajouter_lien
 * ou supprimer_lien
 *
 * L'action est de la forme : objet1-id1-objet2-id2
 * ou de la forme : objet1-id1-objet2-id2-role
 *
 * L'action peut-être indiquée dans la clé ou dans la valeur.
 * Si elle est indiquee dans la valeur et que la clé est non numérique,
 * on ne la prend en compte que si un submit avec la clé a été envoyé
 *
 * @internal
 * @param string $k Clé du tableau
 * @param string $v Valeur du tableau
 * @return string Action demandée si trouvée, sinon ''
 */
function lien_verifier_action($k, $v) {
	$action = '';
	if (preg_match(',^\w+-[\w*]+-[\w*]+-[\w*]+(-[\w*])?,', $k)) {
		$action = $k;
	}
	if (preg_match(',^\w+-[\w*]+-[\w*]+-[\w*]+(-[\w*])?,', $v)) {
		if (is_numeric($k)) {
			$action = $v;
		}
		if (_request($k)) {
			$action = $v;
		}
	}
	// ajout un role null fictif (plus pratique) si pas défini
	if ($action and count(explode('-', $action)) == 4) {
		$action .= '-';
	}

	return $action;
}


/**
 * Retrouve le ou les qualificatifs postés avec une liaison demandée
 *
 * @internal
 * @param string $objet_lien
 *    objet qui porte le lien
 * @param string $lien
 *   Action du lien
 * @return array
 *   Liste des qualifs pour chaque lien. Tableau vide s'il n'y en a pas.
 **/
function lien_retrouver_qualif($objet_lien, $lien) {
	// un role est défini dans la liaison
	$defs = explode('-', $lien);
	list($objet1, , $objet2, , $role) = array_pad($defs, 5, null);
	if ($objet_lien == $objet1) {
		$colonne_role = roles_colonne($objet1, $objet2);
	} else {
		$colonne_role = roles_colonne($objet2, $objet1);
	}

	// cas ou le role est defini en 5e argument de l'action sur le lien (suppression, ajout rapide sans autre attribut)
	if ($role) {
		return array(
			// un seul lien avec ce role
			array($colonne_role => $role)
		);
	}

	// retrouver les rôles postés pour cette liaison, s'il y en a.
	$qualifier_lien = _request('qualifier_lien');
	if (!$qualifier_lien or !is_array($qualifier_lien)) {
		return array();
	}

	// pas avec l'action complete (incluant le role)
	$qualif = array();
	if ((!isset($qualifier_lien[$lien]) or !$qualif = $qualifier_lien[$lien])
		and count($defs) == 5
	) {
		// on tente avec l'action sans le role
		array_pop($defs);
		$lien = implode('-', $defs);
		if (!isset($qualifier_lien[$lien]) or !$qualif = $qualifier_lien[$lien]) {
			$qualif = array();
		}
	}

	// $qualif de la forme array(role=>array(...),valeur=>array(...),....)
	// on le reforme en array(array(role=>..,valeur=>..,..),array(role=>..,valeur=>..,..),...)
	$qualifs = array();
	while (count($qualif)) {
		$q = array();
		foreach ($qualif as $att => $values) {
			if (is_array($values)) {
				$q[$att] = array_shift($qualif[$att]);
				if (!count($qualif[$att])) {
					unset($qualif[$att]);
				}
			} else {
				$q[$att] = $values;
				unset($qualif[$att]);
			}
		}
		// pas de rôle vide
		if (!$colonne_role or !isset($q[$colonne_role]) or $q[$colonne_role]) {
			$qualifs[] = $q;
		}
	}

	return $qualifs;
}

/**
 * Ajoute les liens demandés en prenant éventuellement en compte le rôle
 *
 * Appelle la fonction objet_associer. L'appelle autant de fois qu'il y
 * a de rôles demandés pour cette liaison.
 *
 * @internal
 * @param string $objet_source Objet source de la liaison (qui a la table de liaison)
 * @param array|string $ids Identifiants pour l'objet source
 * @param string $objet_lien Objet à lier
 * @param array|string $idl Identifiants pour l'objet lié
 * @param array $qualifs
 * @return void
 **/
function lien_ajouter_liaisons($objet_source, $ids, $objet_lien, $idl, $qualifs) {

	// retrouver la colonne de roles s'il y en a a lier
	if (is_array($qualifs) and count($qualifs)) {
		foreach ($qualifs as $qualif) {
			objet_associer(array($objet_source => $ids), array($objet_lien => $idl), $qualif);
		}
	} else {
		objet_associer(array($objet_source => $ids), array($objet_lien => $idl));
	}
}


Fixed version (ground truth):
<?php
/***************************************************************************\
 *  SPIP, Systeme de publication pour l'internet                           *
 *                                                                         *
 *  Copyright (c) 2001-2019                                                *
 *  Arnaud Martin, Antoine Pitrou, Philippe Riviere, Emmanuel Saint-James  *
 *                                                                         *
 *  Ce programme est un logiciel libre distribue sous licence GNU/GPL.     *
 *  Pour plus de details voir le fichier COPYING.txt ou l'aide en ligne.   *
\***************************************************************************/

/**
 * Gestion du formulaire d'édition de liens
 *
 * @package SPIP\Core\Formulaires
 **/
if (!defined('_ECRIRE_INC_VERSION')) {
	return;
}


/**
 * Retrouve la source et l'objet de la liaison
 *
 * À partir des 3 premiers paramètres transmis au formulaire,
 * la fonction retrouve :
 * - l'objet dont on utilise sa table de liaison (table_source)
 * - l'objet et id_objet sur qui on lie des éléments (objet, id_objet)
 * - l'objet que l'on veut lier dessus (objet_lien)
 *
 * @param string $a
 * @param string|int $b
 * @param int|string $c
 * @return array
 *   ($table_source,$objet,$id_objet,$objet_lien)
 */
function determine_source_lien_objet($a, $b, $c) {
	$table_source = $objet_lien = $objet = $id_objet = null;
	// auteurs, article, 23 :
	// associer des auteurs à l'article 23, sur la table pivot spip_auteurs_liens
	if (is_numeric($c) and !is_numeric($b)) {
		$table_source = table_objet($a);
		$objet_lien = objet_type($a);
		$objet = objet_type($b);
		$id_objet = $c;
	}
	// article, 23, auteurs
	// associer des auteurs à l'article 23, sur la table pivot spip_articles_liens
	if (is_numeric($b) and !is_numeric($c)) {
		$table_source = table_objet($c);
		$objet_lien = objet_type($a);
		$objet = objet_type($a);
		$id_objet = $b;
	}

	return array($table_source, $objet, $id_objet, $objet_lien);
}

/**
 * Chargement du formulaire d'édition de liens
 *
 * #FORMULAIRE_EDITER_LIENS{auteurs,article,23}
 *   pour associer des auteurs à l'article 23, sur la table pivot spip_auteurs_liens
 * #FORMULAIRE_EDITER_LIENS{article,23,auteurs}
 *   pour associer des auteurs à l'article 23, sur la table pivot spip_articles_liens
 * #FORMULAIRE_EDITER_LIENS{articles,auteur,12}
 *   pour associer des articles à l'auteur 12, sur la table pivot spip_articles_liens
 * #FORMULAIRE_EDITER_LIENS{auteur,12,articles}
 *   pour associer des articles à l'auteur 12, sur la table pivot spip_auteurs_liens
 *
 * @param string $a
 * @param string|int $b
 * @param int|string $c
 * @param array|bool $options
 *    - Si array, tableau d'options
 *    - Si bool : valeur de l'option 'editable' uniquement
 *
 * @return array
 */
function formulaires_editer_liens_charger_dist($a, $b, $c, $options = array()) {

	// compat avec ancienne signature ou le 4eme argument est $editable
	if (!is_array($options)) {
		$options = array('editable' => $options);
	} elseif (!isset($options['editable'])) {
		$options['editable'] = true;
	}

	$editable = $options['editable'];

	list($table_source, $objet, $id_objet, $objet_lien) = determine_source_lien_objet($a, $b, $c);
	if (!$table_source or !$objet or !$objet_lien or !$id_objet) {
		return false;
	}

	$objet_source = objet_type($table_source);
	$table_sql_source = table_objet_sql($objet_source);

	// verifier existence de la table xxx_liens
	include_spip('action/editer_liens');
	if (!objet_associable($objet_lien)) {
		return false;
	}

	// L'éditabilité :) est définie par un test permanent (par exemple "associermots") ET le 4ème argument
	include_spip('inc/autoriser');
	$editable = ($editable and autoriser('associer' . $table_source, $objet, $id_objet)
		and autoriser('modifier', $objet, $id_objet));

	if (!$editable and !count(objet_trouver_liens(
		array($objet_lien => '*'),
		array(($objet_lien == $objet_source ? $objet : $objet_source) => $id_objet)
	))) {
		return false;
	}

	// squelettes de vue et de d'association
	// ils sont différents si des rôles sont définis.
	$skel_vue = $table_source . '_lies';
	$skel_ajout = $table_source . '_associer';

	// description des roles
	include_spip('inc/roles');
	if ($roles = roles_presents($objet_source, $objet)) {
		// on demande de nouveaux squelettes en conséquence
		$skel_vue = $table_source . '_roles_lies';
		$skel_ajout = $table_source . '_roles_associer';
	}

	$oups = _request('_oups');
	if (unserialize(base64_decode($oups))) {
		// on est bon, rien a faire
	}
	elseif(unserialize($oups)) {
		// il faut encoder
		$oups = base64_encode($oups);
	}
	else {
		$oups = '';
	}
	$valeurs = array(
		'id' => "$table_source-$objet-$id_objet-$objet_lien", // identifiant unique pour les id du form
		'_vue_liee' => $skel_vue,
		'_vue_ajout' => $skel_ajout,
		'_objet_lien' => $objet_lien,
		'id_lien_ajoute' => _request('id_lien_ajoute'),
		'objet' => $objet,
		'id_objet' => $id_objet,
		'objet_source' => $objet_source,
		'table_source' => $table_source,
		'recherche' => '',
		'visible' => 0,
		'ajouter_lien' => '',
		'supprimer_lien' => '',
		'qualifier_lien' => '',
		'ordonner_lien' => '',
		'desordonner_liens' => '',
		'_roles' => $roles, # description des roles
		'_oups' => $oups,
		'editable' => $editable,
	);

	// les options non definies dans $valeurs sont passees telles quelles au formulaire html
	$valeurs = array_merge($options, $valeurs);

	return $valeurs;
}

/**
 * Traiter le post des informations d'édition de liens
 *
 * Les formulaires peuvent poster dans quatre variables
 * - ajouter_lien et supprimer_lien
 * - remplacer_lien
 * - qualifier_lien
 * - ordonner_lien
 * - desordonner_liens
 *
 * Les deux premières peuvent être de trois formes différentes :
 * ajouter_lien[]="objet1-id1-objet2-id2"
 * ajouter_lien[objet1-id1-objet2-id2]="nimportequoi"
 * ajouter_lien['clenonnumerique']="objet1-id1-objet2-id2"
 * Dans ce dernier cas, la valeur ne sera prise en compte
 * que si _request('clenonnumerique') est vrai (submit associé a l'input)
 *
 * remplacer_lien doit être de la forme
 * remplacer_lien[objet1-id1-objet2-id2]="objet3-id3-objet2-id2"
 * ou objet1-id1 est celui qu'on enleve et objet3-id3 celui qu'on ajoute
 *
 * qualifier_lien doit être de la forme, et sert en complément de ajouter_lien
 * qualifier_lien[objet1-id1-objet2-id2][role] = array("role1", "autre_role")
 * qualifier_lien[objet1-id1-objet2-id2][valeur] = array("truc", "chose")
 * produira 2 liens chacun avec array("role"=>"role1","valeur"=>"truc") et array("role"=>"autre_role","valeur"=>"chose")
 *
 * ordonner_lien doit être de la forme, et sert pour trier les liens
 * ordonner_lien[objet1-id1-objet2-id2] = nouveau_rang
 *
 * desordonner_liens n'a pas de forme précise, il doit simplement être non nul/non vide
 *
 * @param string $a
 * @param string|int $b
 * @param int|string $c
 * @param array|bool $options
 *    - Si array, tableau d'options
 *    - Si bool : valeur de l'option 'editable' uniquement
 *
 * @return array
 */
function formulaires_editer_liens_traiter_dist($a, $b, $c, $options = array()) {
	// compat avec ancienne signature ou le 4eme argument est $editable
	if (!is_array($options)) {
		$options = array('editable' => $options);
	} elseif (!isset($options['editable'])) {
		$options['editable'] = true;
	}

	$editable = $options['editable'];

	$res = array('editable' => $editable ? true : false);
	list($table_source, $objet, $id_objet, $objet_lien) = determine_source_lien_objet($a, $b, $c);
	if (!$table_source or !$objet or !$objet_lien) {
		return $res;
	}


	if (_request('tout_voir')) {
		set_request('recherche', '');
	}

	include_spip('inc/autoriser');
	if (autoriser('modifier', $objet, $id_objet)) {
		// annuler les suppressions du coup d'avant !
		if (_request('annuler_oups')
			and $oups = _request('_oups')
			and $oups = base64_decode($oups)
			and $oups = unserialize($oups)
		) {
			if ($oups_objets = charger_fonction("editer_liens_oups_{$table_source}_{$objet}_{$objet_lien}", 'action', true)) {
				$oups_objets($oups);
			} else {
				$objet_source = objet_type($table_source);
				include_spip('action/editer_liens');
				foreach ($oups as $oup) {
					if ($objet_lien == $objet_source) {
						objet_associer(array($objet_source => $oup[$objet_source]), array($objet => $oup[$objet]), $oup);
					} else {
						objet_associer(array($objet => $oup[$objet]), array($objet_source => $oup[$objet_source]), $oup);
					}
				}
			}
			# oups ne persiste que pour la derniere action, si suppression
			set_request('_oups');
		}

		$supprimer = _request('supprimer_lien');
		$ajouter = _request('ajouter_lien');
		$ordonner = _request('ordonner_lien');

		if (_request('desordonner_liens')) {
			include_spip('action/editer_liens');
			objet_qualifier_liens(array($objet_lien => '*'), array($objet => $id_objet), array('rang_lien' => 0));
		}

		// il est possible de preciser dans une seule variable un remplacement :
		// remplacer_lien[old][new]
		if ($remplacer = _request('remplacer_lien')) {
			foreach ($remplacer as $k => $v) {
				if ($old = lien_verifier_action($k, '')) {
					foreach (is_array($v) ? $v : array($v) as $kn => $vn) {
						if ($new = lien_verifier_action($kn, $vn)) {
							$supprimer[$old] = 'x';
							$ajouter[$new] = '+';
						}
					}
				}
			}
		}

		if ($supprimer) {
			if ($supprimer_objets = charger_fonction(
				"editer_liens_supprimer_{$table_source}_{$objet}_{$objet_lien}",
				'action',
				true
			)) {
				$oups = $supprimer_objets($supprimer);
			} else {
				include_spip('action/editer_liens');
				$oups = array();

				foreach ($supprimer as $k => $v) {
					if ($lien = lien_verifier_action($k, $v)) {
						$lien = explode('-', $lien);
						list($objet_source, $ids, $objet_lie, $idl, $role) = array_pad($lien, 5, null);
						// appliquer une condition sur le rôle si défini ('*' pour tous les roles)
						$cond = (!is_null($role) ? array('role' => $role) : array());
						if ($objet_lien == $objet_source) {
							$oups = array_merge(
								$oups,
								objet_trouver_liens(array($objet_source => $ids), array($objet_lie => $idl), $cond)
							);
							objet_dissocier(array($objet_source => $ids), array($objet_lie => $idl), $cond);
						} else {
							$oups = array_merge(
								$oups,
								objet_trouver_liens(array($objet_lie => $idl), array($objet_source => $ids), $cond)
							);
							objet_dissocier(array($objet_lie => $idl), array($objet_source => $ids), $cond);
						}
					}
				}
			}
			set_request('_oups', $oups ? base64_encode(serialize($oups)) : null);
		}

		if ($ajouter) {
			if ($ajouter_objets = charger_fonction("editer_liens_ajouter_{$table_source}_{$objet}_{$objet_lien}", 'action', true)
			) {
				$ajout_ok = $ajouter_objets($ajouter);
			} else {
				$ajout_ok = false;
				include_spip('action/editer_liens');
				foreach ($ajouter as $k => $v) {
					if ($lien = lien_verifier_action($k, $v)) {
						$ajout_ok = true;
						list($objet1, $ids, $objet2, $idl) = explode('-', $lien);
						$qualifs = lien_retrouver_qualif($objet_lien, $lien);
						if ($objet_lien == $objet1) {
							lien_ajouter_liaisons($objet1, $ids, $objet2, $idl, $qualifs);
						} else {
							lien_ajouter_liaisons($objet2, $idl, $objet1, $ids, $qualifs);
						}
						set_request('id_lien_ajoute', $ids);
					}
				}
			}
			# oups ne persiste que pour la derniere action, si suppression
			# une suppression suivie d'un ajout dans le meme hit est un remplacement
			# non annulable !
			if ($ajout_ok) {
				set_request('_oups');
			}
		}

		if ($ordonner) {
			include_spip('action/editer_liens');
			foreach ($ordonner as $k => $rang_lien) {
				if ($lien = lien_verifier_action($k, '')) {
					list($objet1, $ids, $objet2, $idl) = explode('-', $lien);
					$qualif = array('rang_lien' => $rang_lien);

					if ($objet_lien == $objet1) {
						objet_qualifier_liens(array($objet1 => $ids), array($objet2 => $idl), $qualif);
					} else {
						objet_qualifier_liens(array($objet2 => $idl), array($objet1 => $ids), $qualif);
					}
					set_request('id_lien_ajoute', $ids);
					set_request('_oups');
				}
			}
		}
	}


	return $res;
}


/**
 * Retrouver l'action de liaision demandée
 *
 * Les formulaires envoient une action dans un tableau ajouter_lien
 * ou supprimer_lien
 *
 * L'action est de la forme : objet1-id1-objet2-id2
 * ou de la forme : objet1-id1-objet2-id2-role
 *
 * L'action peut-être indiquée dans la clé ou dans la valeur.
 * Si elle est indiquee dans la valeur et que la clé est non numérique,
 * on ne la prend en compte que si un submit avec la clé a été envoyé
 *
 * @internal
 * @param string $k Clé du tableau
 * @param string $v Valeur du tableau
 * @return string Action demandée si trouvée, sinon ''
 */
function lien_verifier_action($k, $v) {
	$action = '';
	if (preg_match(',^\w+-[\w*]+-[\w*]+-[\w*]+(-[\w*])?,', $k)) {
		$action = $k;
	}
	if (preg_match(',^\w+-[\w*]+-[\w*]+-[\w*]+(-[\w*])?,', $v)) {
		if (is_numeric($k)) {
			$action = $v;
		}
		if (_request($k)) {
			$action = $v;
		}
	}
	// ajout un role null fictif (plus pratique) si pas défini
	if ($action and count(explode('-', $action)) == 4) {
		$action .= '-';
	}

	return $action;
}


/**
 * Retrouve le ou les qualificatifs postés avec une liaison demandée
 *
 * @internal
 * @param string $objet_lien
 *    objet qui porte le lien
 * @param string $lien
 *   Action du lien
 * @return array
 *   Liste des qualifs pour chaque lien. Tableau vide s'il n'y en a pas.
 **/
function lien_retrouver_qualif($objet_lien, $lien) {
	// un role est défini dans la liaison
	$defs = explode('-', $lien);
	list($objet1, , $objet2, , $role) = array_pad($defs, 5, null);
	if ($objet_lien == $objet1) {
		$colonne_role = roles_colonne($objet1, $objet2);
	} else {
		$colonne_role = roles_colonne($objet2, $objet1);
	}

	// cas ou le role est defini en 5e argument de l'action sur le lien (suppression, ajout rapide sans autre attribut)
	if ($role) {
		return array(
			// un seul lien avec ce role
			array($colonne_role => $role)
		);
	}

	// retrouver les rôles postés pour cette liaison, s'il y en a.
	$qualifier_lien = _request('qualifier_lien');
	if (!$qualifier_lien or !is_array($qualifier_lien)) {
		return array();
	}

	// pas avec l'action complete (incluant le role)
	$qualif = array();
	if ((!isset($qualifier_lien[$lien]) or !$qualif = $qualifier_lien[$lien])
		and count($defs) == 5
	) {
		// on tente avec l'action sans le role
		array_pop($defs);
		$lien = implode('-', $defs);
		if (!isset($qualifier_lien[$lien]) or !$qualif = $qualifier_lien[$lien]) {
			$qualif = array();
		}
	}

	// $qualif de la forme array(role=>array(...),valeur=>array(...),....)
	// on le reforme en array(array(role=>..,valeur=>..,..),array(role=>..,valeur=>..,..),...)
	$qualifs = array();
	while (count($qualif)) {
		$q = array();
		foreach ($qualif as $att => $values) {
			if (is_array($values)) {
				$q[$att] = array_shift($qualif[$att]);
				if (!count($qualif[$att])) {
					unset($qualif[$att]);
				}
			} else {
				$q[$att] = $values;
				unset($qualif[$att]);
			}
		}
		// pas de rôle vide
		if (!$colonne_role or !isset($q[$colonne_role]) or $q[$colonne_role]) {
			$qualifs[] = $q;
		}
	}

	return $qualifs;
}

/**
 * Ajoute les liens demandés en prenant éventuellement en compte le rôle
 *
 * Appelle la fonction objet_associer. L'appelle autant de fois qu'il y
 * a de rôles demandés pour cette liaison.
 *
 * @internal
 * @param string $objet_source Objet source de la liaison (qui a la table de liaison)
 * @param array|string $ids Identifiants pour l'objet source
 * @param string $objet_lien Objet à lier
 * @param array|string $idl Identifiants pour l'objet lié
 * @param array $qualifs
 * @return void
 **/
function lien_ajouter_liaisons($objet_source, $ids, $objet_lien, $idl, $qualifs) {

	// retrouver la colonne de roles s'il y en a a lier
	if (is_array($qualifs) and count($qualifs)) {
		foreach ($qualifs as $qualif) {
			objet_associer(array($objet_source => $ids), array($objet_lien => $idl), $qualif);
		}
	} else {
		objet_associer(array($objet_source => $ids), array($objet_lien => $idl));
	}
}