Refactoring : sécurité (XSS), découpage en modules inc/* et js/admin/*, IDs résolus par slug, perf (caches, cron Gravatar, assets auto-hébergés), tests
This commit is contained in:
405
js/admin/admin-base.js
Normal file
405
js/admin/admin-base.js
Normal file
@@ -0,0 +1,405 @@
|
||||
/**
|
||||
* Socle partagé des customisations admin (namespace window.ThalimAdmin).
|
||||
* Chargé sur toutes les pages admin, avant les scripts de contexte
|
||||
* (admin-rename, admin-post-edit, admin-profile, admin-taxonomy-list,
|
||||
* admin-pods-modal) qui en dépendent.
|
||||
*/
|
||||
(function($) {
|
||||
'use strict';
|
||||
|
||||
// ── Configuration ──────────────────────────────────────────
|
||||
// Sélecteurs et identifiants Pods utilisés par les modules admin,
|
||||
// centralisés pour qu'un renommage côté Pods ne demande qu'une édition ici.
|
||||
var CONFIG = {
|
||||
// Options désactivées dans le select Pods « Type d'annonce » (term IDs)
|
||||
disabledCategoryIds: ['1', '12', '5', '20'],
|
||||
// Catégorie « Séance de séminaire » (verrouillée dans la modale Pods)
|
||||
seanceCategoryId: '12',
|
||||
// Select Pods de la catégorie
|
||||
categorySelect: '#pods-form-ui-pods-meta-categorie',
|
||||
// IDs des éditeurs TinyMCE Pods à réparer (reinitEditor)
|
||||
editors: {
|
||||
bodyEn: 'pods-form-ui-pods-meta-body-en',
|
||||
refBib: 'pods-form-ui-pods-meta-reference-bibliographique'
|
||||
},
|
||||
// Metaboxes Pods déplacées / observées
|
||||
boxes: {
|
||||
bodyEn: '#pods-meta-body-en',
|
||||
typeDannonce: '#pods-meta-type-dannonce',
|
||||
affichageAccueil: '#pods-meta-affichage-sur-laccueil',
|
||||
thematique: '#pods-meta-thematique',
|
||||
champsContextuels: '#pods-meta-champs-contextuels',
|
||||
documentsJoints: '#pods-meta-documents-joints',
|
||||
membres: '#pods-meta-membres'
|
||||
},
|
||||
// Classes des lignes Pods conditionnelles observées (MutationObserver)
|
||||
rows: {
|
||||
axes: 'pods-form-ui-row-name-axes-thematiques',
|
||||
refBib: 'pods-form-ui-row-name-reference-bibliographique'
|
||||
},
|
||||
// Taxonomies dont la page liste reçoit l'info-bulle « FR // EN »
|
||||
translateTaxonomies: ['axe_thematique', 'programme_de_recherche', 'post_tag']
|
||||
};
|
||||
|
||||
// Exécute un bloc d'init de façon isolée : une exception dans un module
|
||||
// n'empêche pas les modules suivants de s'initialiser.
|
||||
function safeRun(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
} catch (err) {
|
||||
if (window.console && console.error) {
|
||||
console.error('[thalim-admin] ' + name + ' failed:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isPostEditPage() {
|
||||
return window.pagenow === 'post'
|
||||
|| window.pagenow === 'post-new'
|
||||
// On CPTs, pagenow is the post_type slug — also catch them via the
|
||||
// body classes WP sets for any post.php / post-new.php screen.
|
||||
|| document.body.classList.contains('post-php')
|
||||
|| document.body.classList.contains('post-new-php');
|
||||
}
|
||||
|
||||
function isProfileEditPage() {
|
||||
return window.pagenow === 'profile' || window.pagenow === 'user-edit' || window.pagenow === 'user-new';
|
||||
}
|
||||
|
||||
function getProfileForm() {
|
||||
return document.querySelector('#your-profile, #createuser');
|
||||
}
|
||||
|
||||
function isPodsModal() {
|
||||
return new URLSearchParams(window.location.search).has('pods_modal');
|
||||
}
|
||||
|
||||
function updatePostboxVisibility() {
|
||||
document.querySelectorAll('.postbox').forEach(function(postBox) {
|
||||
if (postBox.id.startsWith('pods')) {
|
||||
// body-en is controlled by language tabs — never auto-hide it
|
||||
if ('#' + postBox.id === CONFIG.boxes.bodyEn) return;
|
||||
var fields = postBox.querySelectorAll('tr');
|
||||
var hasVisibleFields = Array.from(fields).some(function(field) {
|
||||
return field.style.display !== 'none';
|
||||
});
|
||||
postBox.style.display = hasVisibleFields ? 'block' : 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Force Visual (TinyMCE) mode on page load.
|
||||
// WP stores the last-used editor mode in localStorage and restores it at document.ready.
|
||||
// When Code mode is restored, TinyMCE is never initialised — tinymce.get() returns null.
|
||||
// Instead, check the wrapper's CSS class:
|
||||
// tmce-active = Visual mode (fine)
|
||||
// html-active = Code mode (switch to Visual)
|
||||
function ensureVisualMode(editorId, attempt) {
|
||||
attempt = attempt || 0;
|
||||
if (attempt > 15) return;
|
||||
var wrap = document.getElementById('wp-' + editorId + '-wrap');
|
||||
if (!wrap) {
|
||||
setTimeout(function() { ensureVisualMode(editorId, attempt + 1); }, 200);
|
||||
return;
|
||||
}
|
||||
if (wrap.classList.contains('html-active')) {
|
||||
var ed = window.tinymce && tinymce.get(editorId);
|
||||
if (!ed || !ed.initialized) {
|
||||
// TinyMCE not ready yet — retry rather than calling switchEditors.go() prematurely
|
||||
setTimeout(function() { ensureVisualMode(editorId, attempt + 1); }, 200);
|
||||
return;
|
||||
}
|
||||
if (typeof switchEditors !== 'undefined') {
|
||||
switchEditors.go(editorId, 'tmce');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!wrap.classList.contains('tmce-active')) {
|
||||
// Mode not yet determined — retry
|
||||
setTimeout(function() { ensureVisualMode(editorId, attempt + 1); }, 200);
|
||||
}
|
||||
}
|
||||
|
||||
// Rebuild a TinyMCE editor whose iframe is broken (empty/non-interactive).
|
||||
// This happens when TinyMCE is initialised on a hidden (display:none) element:
|
||||
// the iframe can't measure dimensions and its document body stays empty.
|
||||
//
|
||||
// We reinit from tinyMCEPreInit.mceInit — first trying the editor's own config
|
||||
// (registered by Pods server-side), falling back to 'content' (the native WP editor).
|
||||
//
|
||||
// Inline toolbar positioning fix:
|
||||
// TinyMCE's 'wordpress' plugin captures document.getElementById(id+'_ifr') during
|
||||
// 'preinit' — before the iframe is created — so mceIframe is always null.
|
||||
// Fix: intercept getElementById during preinit so the 'wordpress' plugin captures
|
||||
// a proxyIframe instead of null. After init, proxy delegates to the real iframe.
|
||||
function reinitEditor(editorId) {
|
||||
var ed = window.tinymce && tinymce.get(editorId);
|
||||
// Preserve existing content before destroying the instance
|
||||
var savedContent = '';
|
||||
if (ed) {
|
||||
try { savedContent = ed.getContent(); } catch (e) {}
|
||||
ed.remove();
|
||||
}
|
||||
if (!savedContent) {
|
||||
var ta = document.getElementById(editorId);
|
||||
if (ta) savedContent = ta.value || '';
|
||||
}
|
||||
|
||||
if (!window.tinyMCEPreInit || !window.tinymce) return;
|
||||
|
||||
// Use the editor's own server-side config if available, else clone from 'content'
|
||||
var baseInit = (tinyMCEPreInit.mceInit && tinyMCEPreInit.mceInit[editorId])
|
||||
|| (tinyMCEPreInit.mceInit && tinyMCEPreInit.mceInit['content']);
|
||||
if (!baseInit) return;
|
||||
|
||||
// Proxy iframe: getBoundingClientRect() falls back to the editor wrap
|
||||
var wrapId = 'wp-' + editorId + '-wrap';
|
||||
var proxyIframe = {
|
||||
getBoundingClientRect: function() {
|
||||
var el = document.getElementById(wrapId);
|
||||
return el ? el.getBoundingClientRect()
|
||||
: { top: 0, left: 0, right: window.innerWidth,
|
||||
bottom: window.innerHeight, width: window.innerWidth,
|
||||
height: window.innerHeight };
|
||||
}
|
||||
};
|
||||
|
||||
var savedGetById = document.getElementById;
|
||||
var origSetup = baseInit.setup;
|
||||
var content = savedContent;
|
||||
|
||||
tinymce.init($.extend({}, baseInit, {
|
||||
selector: '#' + editorId,
|
||||
setup: function(editor) {
|
||||
if (typeof origSetup === 'function') origSetup(editor);
|
||||
|
||||
editor.on('focus', function() {
|
||||
window.wpActiveEditor = editorId;
|
||||
});
|
||||
|
||||
editor.on('preinit', function() {
|
||||
document.getElementById = function(id) {
|
||||
if (id === editorId + '_ifr') return proxyIframe;
|
||||
return savedGetById.call(document, id);
|
||||
};
|
||||
setTimeout(function() {
|
||||
document.getElementById = savedGetById;
|
||||
}, 0);
|
||||
});
|
||||
|
||||
editor.on('init', function() {
|
||||
// Point proxy to real iframe
|
||||
var realIframe = savedGetById.call(document, editorId + '_ifr');
|
||||
if (realIframe) {
|
||||
proxyIframe.getBoundingClientRect = function() {
|
||||
return realIframe.getBoundingClientRect();
|
||||
};
|
||||
}
|
||||
// Restore content that was in the textarea
|
||||
if (content) {
|
||||
editor.setContent(content);
|
||||
}
|
||||
});
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// ── Info-popovers (post / user / taxonomy) ─────────────────
|
||||
var INFO_ICON = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M12 11v6"/><path d="M12 8v.01" stroke-width="2"/></svg>';
|
||||
var TRANSLATE_ICON = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>';
|
||||
|
||||
var TRANSLATE_LINES = [
|
||||
'Traduction en anglais après //',
|
||||
'ex : Texte en français // English text'
|
||||
];
|
||||
|
||||
// Tips without a `page` key default to 'post'.
|
||||
// type: 'translate' uses the globe icon + green button style.
|
||||
var INFO_TIPS = [
|
||||
// --- post edit page: info ---
|
||||
{
|
||||
selector: '.wp-heading-inline',
|
||||
lines: [
|
||||
'Saisir le titre anglais après //',
|
||||
'ex : Titre de l’annonce // Title of the announcement'
|
||||
]
|
||||
},
|
||||
{
|
||||
selector: '#pods-meta-documents-joints .postbox-header h2',
|
||||
lines: [
|
||||
'Ajouter les images dans les documents.',
|
||||
'Ajouter les légendes comme titre du document.'
|
||||
]
|
||||
},
|
||||
{
|
||||
selector: '#pods-meta-membres .postbox-header h2',
|
||||
lines: [
|
||||
'Le champ fonction change le libellé de la liste de personnes citées.',
|
||||
'Le champ membre permet de lister les membres de Thalim liés à l’annonce.',
|
||||
'Le champ autre personnes permet de lister des personnes extérieures à Thalim.'
|
||||
]
|
||||
},
|
||||
{
|
||||
selector: '#pods-meta-dates .postbox-header h2',
|
||||
lines: [
|
||||
'Pour entrer une date sans l’heure, régler l’heure sur 00 :00.'
|
||||
]
|
||||
},
|
||||
{
|
||||
selector: '#pods-meta-affichage-sur-laccueil .postbox-header h2',
|
||||
lines: [
|
||||
'Épingler l’annonce dans le diaporama la fait s’afficher avant les autres.'
|
||||
]
|
||||
},
|
||||
{
|
||||
selector: '#pods-meta-medias .postbox-header h2',
|
||||
lines: [
|
||||
'Pour ajouter un média Canal U, copier le lien depuis « Citer cette ressource ».',
|
||||
'ex : https://www.canal-u.tv/166564'
|
||||
]
|
||||
},
|
||||
// --- post edit page: translate ---
|
||||
{ type: 'translate', selector: '#pods-meta-documents-joints .postbox-header h2', lines: TRANSLATE_LINES },
|
||||
{ type: 'translate', selector: '.pods-form-ui-row-name-sous-titre th', lines: TRANSLATE_LINES },
|
||||
{ type: 'translate', selector: '.pods-form-ui-row-name-lieu th', lines: TRANSLATE_LINES },
|
||||
{ type: 'translate', selector: '.pods-form-ui-row-name-titre-du-lien-externe-1 th', lines: TRANSLATE_LINES },
|
||||
{ type: 'translate', selector: '.pods-form-ui-row-name-titre-du-lien-externe-2 th', lines: TRANSLATE_LINES },
|
||||
{ type: 'translate', selector: '.pods-form-ui-row-name-titre-du-lien-externe-3 th', lines: TRANSLATE_LINES },
|
||||
{ type: 'translate', selector: '.pods-form-ui-row-name-fonction-organisation th', lines: TRANSLATE_LINES },
|
||||
{ type: 'translate', selector: '.pods-form-ui-row-name-fonction-intervention th', lines: TRANSLATE_LINES },
|
||||
{ type: 'translate', selector: '.pods-form-ui-row-name-fonction-candidat th', lines: TRANSLATE_LINES },
|
||||
{ type: 'translate', selector: '.pods-form-ui-row-name-fonction-realisation th', lines: TRANSLATE_LINES },
|
||||
{ type: 'translate', selector: '.pods-form-ui-row-name-fonction-dirige th', lines: TRANSLATE_LINES },
|
||||
{ type: 'translate', selector: '.pods-form-ui-row-name-fonction-redaction th', lines: TRANSLATE_LINES },
|
||||
{ type: 'translate', selector: '.pods-form-ui-row-name-fonction-auteur th', lines: TRANSLATE_LINES },
|
||||
{ type: 'translate', selector: '.pods-form-ui-row-name-fonction-responsable th', lines: TRANSLATE_LINES },
|
||||
{ type: 'translate', selector: '.pods-form-ui-row-name-autre-fonction-autre th', lines: TRANSLATE_LINES },
|
||||
{ type: 'translate', selector: '.pods-form-ui-row-name-autre-fonction-concerne th', lines: TRANSLATE_LINES },
|
||||
{ type: 'translate', selector: '.pods-form-ui-row-name-autre-fonction-directeur th', lines: TRANSLATE_LINES },
|
||||
{ type: 'translate', selector: '.pods-form-ui-row-name-autre-fonction-direction-d-ouvrage th',lines: TRANSLATE_LINES },
|
||||
{ type: 'translate', selector: '.pods-form-ui-row-name-autre-fonction-intervenant th', lines: TRANSLATE_LINES },
|
||||
{ type: 'translate', selector: '.pods-form-ui-row-name-autre-fonction-participants th', lines: TRANSLATE_LINES },
|
||||
{ type: 'translate', selector: '.pods-form-ui-row-name-type-autre th', lines: TRANSLATE_LINES },
|
||||
// --- contenu_general edit page: translate ---
|
||||
{ type: 'translate', selector: '.pods-form-ui-row-name-umr th', lines: TRANSLATE_LINES },
|
||||
{ type: 'translate', selector: '.pods-form-ui-row-name-thalim th', lines: TRANSLATE_LINES },
|
||||
{ type: 'translate', selector: '.pods-form-ui-row-name-siecles th', lines: TRANSLATE_LINES },
|
||||
// --- user/profile edit page: translate ---
|
||||
{ type: 'translate', page: 'user', selector: '.pods-form-ui-row-name-titre-du-lien-1 th', lines: TRANSLATE_LINES },
|
||||
{ type: 'translate', page: 'user', selector: '.pods-form-ui-row-name-titre-du-lien-2 th', lines: TRANSLATE_LINES },
|
||||
{ type: 'translate', page: 'user', selector: '.pods-form-ui-row-name-titre-du-lien-3 th', lines: TRANSLATE_LINES },
|
||||
{ type: 'translate', page: 'user', selector: '.pods-form-ui-row-name-titre-du-lien-4 th', lines: TRANSLATE_LINES },
|
||||
{ type: 'translate', page: 'user', selector: '.pods-form-ui-row-name-complement-de-role-1 th', lines: TRANSLATE_LINES },
|
||||
{ type: 'translate', page: 'user', selector: '.pods-form-ui-row-name-complement-de-role-2 th', lines: TRANSLATE_LINES },
|
||||
{ type: 'translate', page: 'user', selector: '.pods-form-ui-row-name-complement-de-role-3 th', lines: TRANSLATE_LINES },
|
||||
{ type: 'translate', page: 'user', selector: '.pods-form-ui-row-name-affichage-du-statut-1 th', lines: TRANSLATE_LINES },
|
||||
{ type: 'translate', page: 'user', selector: '.pods-form-ui-row-name-affichage-du-statut-2 th', lines: TRANSLATE_LINES },
|
||||
{ type: 'translate', page: 'user', selector: '.pods-form-ui-row-name-affichage-du-statut-3 th', lines: TRANSLATE_LINES },
|
||||
{ type: 'translate', page: 'user', selector: '.pods-form-ui-row-name-affiliation-autre th', lines: TRANSLATE_LINES },
|
||||
{ type: 'translate', page: 'user', selector: '.pods-form-ui-row-name-titre-de-these th', lines: TRANSLATE_LINES },
|
||||
// --- taxonomy edit pages: translate ---
|
||||
{ type: 'translate', page: 'taxonomy', selector: 'label[for="name"]', lines: TRANSLATE_LINES },
|
||||
// --- user/profile edit page: info ---
|
||||
{
|
||||
page: 'user',
|
||||
selector: '.pods-form-ui-label-pods-meta-identifiant-hal',
|
||||
lines: [
|
||||
'Renseigner votre idHAL (en lettres), pas votre PersonId (en chiffres).'
|
||||
]
|
||||
},
|
||||
{
|
||||
page: 'user',
|
||||
selector: '.pods-form-ui-label-pods-meta-affichage-du-statut-1',
|
||||
lines: [
|
||||
'Texte de statut affiché sur le profil publique.'
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
var _popoverCloseHandlerRegistered = false;
|
||||
|
||||
function initInfoPopovers(currentPage) {
|
||||
currentPage = currentPage || 'post';
|
||||
|
||||
INFO_TIPS.forEach(function(tip) {
|
||||
if ((tip.page || 'post') !== currentPage) return;
|
||||
|
||||
var el = document.querySelector(tip.selector);
|
||||
if (!el) return;
|
||||
|
||||
var isTranslate = tip.type === 'translate';
|
||||
|
||||
var btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = isTranslate ? 'thalim-translate-btn' : 'thalim-info-btn';
|
||||
btn.setAttribute('aria-label', isTranslate ? 'Traduction bilingue' : 'Informations');
|
||||
btn.innerHTML = isTranslate ? TRANSLATE_ICON : INFO_ICON;
|
||||
|
||||
var popover = document.createElement('div');
|
||||
popover.className = 'thalim-info-popover' + (isTranslate ? ' thalim-translate-popover' : '');
|
||||
popover.innerHTML = tip.lines.map(function(line) {
|
||||
return '<p>' + line + '</p>';
|
||||
}).join('');
|
||||
|
||||
var wrapper = document.createElement('span');
|
||||
wrapper.className = 'thalim-info-wrapper';
|
||||
wrapper.appendChild(btn);
|
||||
wrapper.appendChild(popover);
|
||||
|
||||
el.appendChild(wrapper);
|
||||
|
||||
btn.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
var isOpen = popover.classList.contains('is-open');
|
||||
document.querySelectorAll('.thalim-info-popover.is-open').forEach(function(p) {
|
||||
p.classList.remove('is-open');
|
||||
});
|
||||
if (!isOpen) {
|
||||
var rect = btn.getBoundingClientRect();
|
||||
popover.style.top = (rect.bottom + 6) + 'px';
|
||||
popover.style.left = (rect.left + rect.width / 2) + 'px';
|
||||
popover.classList.add('is-open');
|
||||
}
|
||||
});
|
||||
|
||||
popover.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
});
|
||||
|
||||
if (!_popoverCloseHandlerRegistered) {
|
||||
_popoverCloseHandlerRegistered = true;
|
||||
document.addEventListener('click', function() {
|
||||
document.querySelectorAll('.thalim-info-popover.is-open').forEach(function(p) {
|
||||
p.classList.remove('is-open');
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── Reveal (#wpbody est masqué en CSS sur post/profil jusqu'à l'init) ──
|
||||
function markReady() {
|
||||
document.body.classList.add('admin-mods-ready');
|
||||
}
|
||||
|
||||
// Fallback global : force le reveal après 2 s même si le script de
|
||||
// contexte a planté ou n'a pas été chargé.
|
||||
$(document).ready(function() {
|
||||
setTimeout(markReady, 2000);
|
||||
});
|
||||
|
||||
window.ThalimAdmin = {
|
||||
CONFIG: CONFIG,
|
||||
safeRun: safeRun,
|
||||
isPostEditPage: isPostEditPage,
|
||||
isProfileEditPage: isProfileEditPage,
|
||||
getProfileForm: getProfileForm,
|
||||
isPodsModal: isPodsModal,
|
||||
ensureVisualMode: ensureVisualMode,
|
||||
reinitEditor: reinitEditor,
|
||||
updatePostboxVisibility: updatePostboxVisibility,
|
||||
initInfoPopovers: initInfoPopovers,
|
||||
markReady: markReady
|
||||
};
|
||||
|
||||
})(jQuery);
|
||||
Reference in New Issue
Block a user