No edit summary |
No edit summary |
||
Line 609: | Line 609: | ||
this.progressBar = new OO.ui.ProgressBarWidget(); | this.progressBar = new OO.ui.ProgressBarWidget(); | ||
this.progressLabel = new OO.ui.LabelWidget({ label: '' }); | |||
this.content = new OO.ui.PanelLayout({ | this.content = new OO.ui.PanelLayout({ | ||
Line 615: | Line 616: | ||
}); | }); | ||
this.content.$element.append(this.progressBar.$element); | this.content.$element.append(this.progressLabel.$element, this.progressBar.$element); | ||
this.$body.append(this.content.$element); | this.$body.append(this.content.$element); | ||
}; | }; | ||
Line 626: | Line 627: | ||
windowManager.openWindow('progressDialog').then(function (opened) { | windowManager.openWindow('progressDialog').then(function (opened) { | ||
opened.then(function (dialog) { | opened.then(function (dialog) { | ||
callback(dialog, function (progress) { | callback(dialog, function (progress, text) { | ||
dialog.progressBar.setProgress(progress); | dialog.progressBar.setProgress(progress); | ||
dialog.progressLabel.setLabel(text); | |||
}); | }); | ||
}); | }); | ||
Line 639: | Line 641: | ||
var totalSteps = 3; // Adjust based on the number of steps | var totalSteps = 3; // Adjust based on the number of steps | ||
function incrementProgress() { | function incrementProgress(text) { | ||
progress += 1; | progress += 1; | ||
updateProgress((progress / totalSteps) * 100); | updateProgress((progress / totalSteps) * 100, text); | ||
} | } | ||
Line 647: | Line 649: | ||
if (decisions.links === '') { | if (decisions.links === '') { | ||
promises.push(new Promise(function (resolve) { | promises.push(new Promise(function (resolve) { | ||
incrementProgress('Removing all links and redirects...'); | |||
removeAllLinksAndRedirects(pageTitle, api, function () { | removeAllLinksAndRedirects(pageTitle, api, function () { | ||
incrementProgress(); | incrementProgress('All links and redirects removed.'); | ||
resolve(); | resolve(); | ||
}); | }); | ||
Line 654: | Line 657: | ||
} else { | } else { | ||
promises.push(new Promise(function (resolve) { | promises.push(new Promise(function (resolve) { | ||
incrementProgress('Updating all links and redirects...'); | |||
updateAllLinksAndRedirects(pageTitle, decisions.links, api, function () { | updateAllLinksAndRedirects(pageTitle, decisions.links, api, function () { | ||
incrementProgress(); | incrementProgress('All links and redirects updated.'); | ||
resolve(); | resolve(); | ||
}); | }); | ||
Line 662: | Line 666: | ||
} | } | ||
promises.push(new Promise(function (resolve) { | promises.push(new Promise(function (resolve) { | ||
incrementProgress('Deleting the page...'); | |||
performDeletion(pageTitle, api, function () { | performDeletion(pageTitle, api, function () { | ||
incrementProgress(); | incrementProgress('Page deleted.'); | ||
resolve(); | resolve(); | ||
}); | }); |
Revision as of 22:14, 7 July 2024
/* Any JavaScript here will be loaded for all users on every page load. */
$(document).ready(function () {
$.get(mw.util.wikiScript('api'), {
action: 'query',
meta: 'userinfo',
format: 'json'
}).done(function (data) {
if (data.query.userinfo.id !== 0) {
var username = data.query.userinfo.name;
var userLink = mw.util.getUrl('User:' + username);
var logoutLink = mw.util.getUrl('Special:Logout');
$('#user-info').html('<a href="' + userLink + '" class="text-white">' + username + '</a>' +
' | <a href="' + logoutLink + '" class="text-white">Logout</a>');
}
});
});
document.getElementById('offcanvas-toggler').addEventListener('click', function() {
var sidebar = document.getElementById('offcanvas-menu');
if (sidebar.classList.contains('show')) {
sidebar.classList.remove('show');
} else {
sidebar.classList.add('show');
}
});
$(document).ready(function() {
$('#offcanvas-close').on('click', function() {
$('#offcanvas-menu').removeClass('show');
});
});
document.addEventListener('DOMContentLoaded', function() {
var form = document.querySelector('.namespace-search-form');
if (form) {
form.addEventListener('submit', function(e) {
var input = form.querySelector('#bs-extendedsearch-input');
if (input) {
var namespace = mw.config.get('wgCanonicalNamespace');
if (namespace && namespace.length > 0) {
input.value = namespace + ": " + input.value;
}
}
});
}
});
(function($) {
'use strict';
var css = [
'#suggestion-container {',
' position: relative;',
' width: 100%;',
'}',
'#suggestion-box {',
' position: absolute;',
' top: 100%;',
' left: 0;',
' width: 100%;',
' margin-top: 5px;',
' background-color: #fff;',
' z-index: 1000;',
'}',
'.suggestion-item {',
' padding: 8px;',
' cursor: pointer;',
'}',
'.suggestion-item:hover {',
' background-color: #e0e0e0;',
'}'
].join('\n');
$('head').append('<style type="text/css">' + css + '</style>');
$('#suggestion-container').append('<div id="suggestion-box"></div>');
function showSuggestions() {
var query = $(this).val();
if (query.length > 0) {
var apiUrl = "https://wiki.mdriven.net/api.php";
var requestData = {
action: "bs-extendedsearch-autocomplete",
format: "json",
q: JSON.stringify({
query: {
bool: {
must: {
match: {
ac_ngram: {
query: query
}
}
}
}
},
size: 8
}),
searchData: JSON.stringify({
namespace: 0,
value: query,
mainpage: ""
})
};
$.ajax({
url: apiUrl,
data: requestData,
dataType: "json",
method: "GET",
success: function(data) {
var suggestions = data.suggestions || [];
$('#suggestion-box').empty();
$.each(suggestions, function(index, suggestion) {
var item = $('<div class="suggestion-item"></div>').text(suggestion.basename);
$('#suggestion-box').append(item);
});
},
error: function(jqxhr, textStatus, error) {
console.error('Error fetching suggestions:', error);
}
});
}
}
function hideSuggestions(event) {
if (!$(event.target).closest('#suggestion-container').length) {
$('#suggestion-box').empty();
}
}
function selectSuggestion(event) {
event.preventDefault(); // Prevent the mousedown event from triggering blur on the search input
var selectedText = $(this).text();
window.location.href = '/index.php?title=' + encodeURIComponent(selectedText);
}
$('.search-input').on('input', showSuggestions);
$(document).on('click', hideSuggestions);
$('#suggestion-box').on('mousedown', '.suggestion-item', selectSuggestion);
})(jQuery);
$(document).ready(function() {
// Function to toggle a section open or closed
function toggleSection(element) {
var submenu = $(element).next('.submenu');
submenu.toggle();
}
// Function to open the submenu containing the current page link and scroll to it
function openCurrentPageSubmenuAndScroll() {
var currentPageLink = $('#navMenu .current-page');
if (currentPageLink.length) {
// Open the parent submenu(s) of the current page link
currentPageLink.parents('.submenu').show();
// Delay the scrolling to allow for any dynamic layout changes
setTimeout(function() {
// Calculate the position of the current page link
var position = currentPageLink.offset().top - $('#navMenu').offset().top + $('#navMenu').scrollTop();
// Scroll the menu to the active item
$('#navMenu').animate({
scrollTop: position
}, 500);
}, 100); // Delay of 100 milliseconds
}
}
// Event delegation for dynamically loaded content
$('#navMenu').on('click', '.menu-header', function() {
toggleSection(this);
});
// Open the submenu containing the current page link and scroll to it
openCurrentPageSubmenuAndScroll();
});
document.getElementById('menu-toggle').addEventListener('click', function() {
var navMenu = document.getElementById('navMenu');
var bodyContent = document.getElementById('bodyContent');
if (navMenu.style.display === 'none' || navMenu.style.display === '') {
navMenu.style.display = 'block';
bodyContent.style.display = 'none'; // Hide body content when nav menu is displayed
} else {
navMenu.style.display = 'none';
bodyContent.style.display = 'block'; // Show body content when nav menu is hidden
}
});
$(document).ready(function() {
$('.video__navigation .navigation-item').click(function() {
var videoID = $(this).data('video');
var startTime = $(this).data('start');
var newSrc = 'https://www.youtube.com/embed/' + videoID + '?start=' + startTime + '&autoplay=1';
$('.video__wrapper iframe').attr('src', newSrc);
});
});
$(document).ready(function() {
$('.bs-extendedsearch-filter-button-button').each(function() {
$(this).append('<span class="fa fa-chevron-down"></span>');
});
});
document.addEventListener("scroll", function() {
var sidebar = document.getElementById("navMenu");
var footerHeight = 100;
var windowHeight = window.innerHeight;
var scrollBottomPosition = window.scrollY + windowHeight;
var footerTopPosition = document.documentElement.offsetHeight - footerHeight;
if (scrollBottomPosition <= footerTopPosition) {
sidebar.style.height = "100vh";
} else {
sidebar.style.height = "calc(100vh - 110px)";
}
});
mw.loader.using('jquery', function() {
$(document).ready(function() {
var $toc = $('#toc').clone();
$('#toc').remove();
$('#tocContainer').html($toc);
$('#tocContainer').css({
position: '-webkit-sticky',
position: 'sticky',
top: '0',
padding: '15px',
margin: '0 auto',
borderRadius: '5px',
maxWidth: '280px',
maxHeight: 'max-content',
overflowY: 'auto',
height: '90vh',
zIndex: 1000
});
$('#tocContainer .toctitle').css({
fontSize: '16px',
fontWeight: '600',
color: '#555',
borderBottom: '1px solid #ccc',
paddingBottom: '10px',
marginBottom: '10px'
});
$('#tocContainer ul').css({
margin: 0,
padding: '0 0 0 20px',
listStyleType: 'none',
fontSize: '14px',
lineHeight: '1.6'
});
$('#tocContainer li').css({
marginBottom: '5px',
});
$('#tocContainer a').css({
color: '#555',
textDecoration: 'none',
}).hover(
function() { $(this).css({textDecoration: 'underline', color: '#000'}); },
function() { $(this).css({textDecoration: 'none', color: '#555'}); }
);
});
});
$(document).ready(function() {
var $inputElement = $('.oo-ui-textInputWidget-type-search .oo-ui-inputWidget-input');
if ($inputElement.length) {
$inputElement.on('keydown', function(event) {
if (event.key === 'Enter') {
event.preventDefault();
$inputElement.addClass('search-input');
}
});
}
});
$(document).ready(function() {
var $inputElement = $('.oo-ui-textInputWidget-type-search .oo-ui-inputWidget-input');
if ($inputElement.length) {
$inputElement.addClass('search-input');
console.log('Class "search-input" added to the input element.');
} else {
console.log('Input element not found.');
}
});
$(document).ready(function() {
function applyChanges() {
console.log('applyChanges function called');
var $searchInput = $('input[type="search"].oo-ui-inputWidget-input');
var $searchWidgetResults = $('.oo-ui-searchWidget-results');
if ($searchInput.length && $searchWidgetResults.length) {
console.log('Input element found:', $searchInput);
$searchInput.addClass('search-input-edit');
console.log('Class "search-input" added to input element');
if ($searchWidgetResults.next('#suggestion-container-edit').length === 0) {
$('<div id="suggestion-container-edit"></div>').insertAfter($searchWidgetResults);
console.log('Suggestion container added after .oo-ui-searchWidget-results element');
}
$searchInput.on('input', showSuggestions);
$('#suggestion-container').css('width', $searchInput.outerWidth());
} else {
console.log('Input element or .oo-ui-searchWidget-results not found, retrying...');
setTimeout(applyChanges, 100);
}
}
function checkVeActiveClass() {
console.log('Checking for ve-active class');
var $htmlTag = $('html');
if ($htmlTag.hasClass('ve-active')) {
console.log('ve-active class detected on HTML tag');
applyChanges();
} else {
console.log('ve-active class not yet detected, retrying...');
setTimeout(checkVeActiveClass, 100);
}
}
function showSuggestions() {
var query = $(this).val();
if (query.length > 0) {
var apiUrl = "https://insider.mdriven.net/api/search";
var requestData = {
q: query
};
$.ajax({
url: apiUrl,
data: requestData,
dataType: "json",
method: "GET",
success: function(data) {
var suggestions = data || [];
$('#suggestion-container-edit').empty();
$.each(suggestions, function(index, suggestion) {
var source = suggestion._source;
var displayText = source.namespace_text + ':' + source.basename;
var item = $('<div class="suggestion-item-edit"></div>').text(displayText);
item.on('click', function() {
$('input[type="search"].oo-ui-inputWidget-input').val(displayText);
$('#suggestion-container-edit').empty();
});
$('#suggestion-container-edit').append(item);
});
},
error: function(jqxhr, textStatus, error) {
console.error('Error fetching suggestions:', error);
}
});
} else {
$('#suggestion-container-edit').empty(); // Clear suggestions if query is empty
}
}
console.log('Starting to check for ve-active class');
checkVeActiveClass();
});
$('<style type="text/css">#suggestion-container-edit { position: static; border: 1px solid #ccc; background: white; z-index: 10000; width: 100%; overflow-y: auto; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); } .suggestion-item-edit { padding: 8px; cursor: pointer; } .suggestion-item-edit:hover { background: #f0f0f0; }</style>').appendTo('head');
$(document).ready(function() {
var signInLink = $('#user-info a');
var currentUrl = encodeURIComponent(window.location.pathname + window.location.search + window.location.hash);
signInLink.attr('href', signInLink.attr('href').replace('<CURRENT_URL>', currentUrl));
});
document.addEventListener('DOMContentLoaded', function() {
var images = document.querySelectorAll('.thumbimage');
var overlay = document.createElement('div');
overlay.className = 'image-overlay';
document.body.appendChild(overlay);
var overlayImage = document.createElement('img');
overlay.appendChild(overlayImage);
images.forEach(function(image) {
image.addEventListener('click', function(event) {
event.preventDefault();
event.stopPropagation();
overlayImage.src = image.src;
overlay.classList.add('show');
});
});
overlay.addEventListener('click', function() {
overlay.classList.remove('show');
});
});
// ==UserScript==
// @name MediaWiki Clean Delete
// @namespace MediaWikiScripts
// @description Adds a 'Clean Delete' action link to pages for admins to delete pages and clean up incoming links and redirects.
// ==/UserScript==
mw.loader.using(['mediawiki.api', 'mediawiki.util', 'oojs-ui'], function () {
function addCleanDeleteLink() {
if (mw.config.get('wgUserGroups').indexOf('sysop') !== -1) {
if ($('#ca-cleandelete').length === 0) {
var $link = $('<div>').attr('id', 'ca-cleandelete').attr('class', 'mw-list-item').append(
$('<a>').attr('href', '#').attr('class', 'ca-cleandelete').text('Clean Delete').click(function (e) {
e.preventDefault();
gatherAllDecisions(mw.config.get('wgPageName'));
})
);
$('#p-actions .tab-group').append($link);
}
}
}
function gatherAllDecisions(pageTitle) {
var decisions = {};
var windowManager = new OO.ui.WindowManager();
$('body').append(windowManager.$element);
askForLinkHandling(pageTitle, decisions, windowManager, function () {
confirmDeletion(pageTitle, decisions, windowManager, function () {
showProgress(windowManager, function (progressDialog, updateProgress) {
executeAllActions(pageTitle, decisions, windowManager, function () {
progressDialog.close();
location.reload();
}, updateProgress);
});
});
});
}
function askForLinkHandling(pageTitle, decisions, windowManager, callback) {
function LinkHandlingDialog(config) {
LinkHandlingDialog.super.call(this, config);
}
OO.inheritClass(LinkHandlingDialog, OO.ui.ProcessDialog);
LinkHandlingDialog.static.name = 'linkHandlingDialog';
LinkHandlingDialog.static.title = 'Handle Links and Redirects';
LinkHandlingDialog.static.actions = [
{ action: 'continue', label: 'Continue', flags: ['primary', 'progressive'] },
{ action: 'cancel', label: 'Cancel', flags: ['safe', 'close'] }
];
LinkHandlingDialog.prototype.initialize = function () {
LinkHandlingDialog.super.prototype.initialize.apply(this, arguments);
this.radioSelect = new OO.ui.RadioSelectWidget({
items: [
new OO.ui.RadioOptionWidget({ data: 'delete', label: 'Delete Links and Redirects' }),
new OO.ui.RadioOptionWidget({ data: 'change', label: 'Change Target of Links and Redirects' })
]
});
var fieldset = new OO.ui.FieldsetLayout({
items: [
new OO.ui.FieldLayout(this.radioSelect, {
label: 'Specify how you would like to handle all incoming links and redirects:',
align: 'top'
})
]
});
this.content = new OO.ui.PanelLayout({
padded: true,
expanded: false
});
this.content.$element.append(fieldset.$element);
this.$body.append(this.content.$element);
};
LinkHandlingDialog.prototype.getActionProcess = function (action) {
var dialog = this;
var process = new OO.ui.Process(function () {
if (action === 'continue') {
var choice = dialog.radioSelect.findSelectedItem();
if (choice && choice.data === 'change') {
dialog.close().then(function () {
askForNewTarget(pageTitle, decisions, windowManager, callback);
});
} else {
decisions.links = '';
dialog.close().then(function () {
callback();
});
}
} else if (action === 'cancel') {
dialog.close().then(function () {
windowManager.destroy();
});
}
});
return process;
};
windowManager.addWindows([new LinkHandlingDialog()]);
windowManager.openWindow('linkHandlingDialog');
}
function askForNewTarget(pageTitle, decisions, windowManager, callback) {
function NewTargetDialog(config) {
NewTargetDialog.super.call(this, config);
}
OO.inheritClass(NewTargetDialog, OO.ui.ProcessDialog);
NewTargetDialog.static.name = 'newTargetDialog';
NewTargetDialog.static.title = 'Specify New Target';
NewTargetDialog.static.actions = [
{ action: 'continue', label: 'Continue', flags: ['primary', 'progressive'] },
{ action: 'cancel', label: 'Cancel', flags: ['safe', 'close'] }
];
NewTargetDialog.prototype.initialize = function () {
NewTargetDialog.super.prototype.initialize.apply(this, arguments);
this.input = new OO.ui.TextInputWidget({
value: '',
placeholder: 'Enter new target'
});
var fieldset = new OO.ui.FieldsetLayout({
items: [
new OO.ui.FieldLayout(this.input, {
label: 'Enter the new target page name to update all links and redirects:',
align: 'top'
})
]
});
this.content = new OO.ui.PanelLayout({
padded: true,
expanded: false
});
this.content.$element.append(fieldset.$element);
this.$body.append(this.content.$element);
};
NewTargetDialog.prototype.getActionProcess = function (action) {
var dialog = this;
var process = new OO.ui.Process(function () {
if (action === 'continue') {
decisions.links = dialog.input.getValue();
dialog.close().then(function () {
callback();
});
} else if (action === 'cancel') {
dialog.close().then(function () {
windowManager.destroy();
});
}
});
return process;
};
windowManager.addWindows([new NewTargetDialog()]);
windowManager.openWindow('newTargetDialog');
}
function confirmDeletion(pageTitle, decisions, windowManager, callback) {
var dialog = new OO.ui.MessageDialog();
windowManager.addWindows([dialog]);
windowManager.openWindow(dialog, {
title: 'Confirm Page Deletion',
message: 'Are you sure you want to delete "' + pageTitle + '" after handling all links and redirects?',
actions: [
{ label: 'Confirm Deletion', action: 'confirm', flags: ['primary', 'destructive'] },
{ label: 'Cancel', action: 'cancel' }
]
}).closed.then(function (data) {
if (data.action === 'confirm') {
decisions.deletion = true;
callback();
} else {
windowManager.destroy();
}
});
}
function showProgress(windowManager, callback) {
function ProgressDialog(config) {
ProgressDialog.super.call(this, config);
}
OO.inheritClass(ProgressDialog, OO.ui.ProcessDialog);
ProgressDialog.static.name = 'progressDialog';
ProgressDialog.static.title = 'Deleting Links and Redirects';
ProgressDialog.prototype.initialize = function () {
ProgressDialog.super.prototype.initialize.apply(this, arguments);
this.progressBar = new OO.ui.ProgressBarWidget();
this.progressLabel = new OO.ui.LabelWidget({ label: '' });
this.content = new OO.ui.PanelLayout({
padded: true,
expanded: false
});
this.content.$element.append(this.progressLabel.$element, this.progressBar.$element);
this.$body.append(this.content.$element);
};
ProgressDialog.prototype.getActionProcess = function (action) {
return new OO.ui.Process();
};
windowManager.addWindows([new ProgressDialog()]);
windowManager.openWindow('progressDialog').then(function (opened) {
opened.then(function (dialog) {
callback(dialog, function (progress, text) {
dialog.progressBar.setProgress(progress);
dialog.progressLabel.setLabel(text);
});
});
});
}
function executeAllActions(pageTitle, decisions, windowManager, callback, updateProgress) {
var api = new mw.Api();
var promises = [];
var progress = 0;
var totalSteps = 3; // Adjust based on the number of steps
function incrementProgress(text) {
progress += 1;
updateProgress((progress / totalSteps) * 100, text);
}
if (decisions.links !== undefined) {
if (decisions.links === '') {
promises.push(new Promise(function (resolve) {
incrementProgress('Removing all links and redirects...');
removeAllLinksAndRedirects(pageTitle, api, function () {
incrementProgress('All links and redirects removed.');
resolve();
});
}));
} else {
promises.push(new Promise(function (resolve) {
incrementProgress('Updating all links and redirects...');
updateAllLinksAndRedirects(pageTitle, decisions.links, api, function () {
incrementProgress('All links and redirects updated.');
resolve();
});
}));
}
}
promises.push(new Promise(function (resolve) {
incrementProgress('Deleting the page...');
performDeletion(pageTitle, api, function () {
incrementProgress('Page deleted.');
resolve();
});
}));
Promise.all(promises).then(callback);
}
function updateAllLinksAndRedirects(pageTitle, newTarget, api, callback) {
handleLinks(pageTitle, newTarget, 'update', api, function () {
handleRedirects(pageTitle, newTarget, 'update', api, callback);
});
}
function removeAllLinksAndRedirects(pageTitle, api, callback) {
handleLinks(pageTitle, '', 'remove', api, function () {
handleRedirects(pageTitle, '', 'remove', api, callback);
});
}
function handleLinks(pageTitle, newTarget, action, api, callback) {
api.get({
action: 'query',
list: 'backlinks',
bltitle: pageTitle,
blfilterredir: 'nonredirects',
bllimit: 'max'
}).done(function (data) {
if (data.query.backlinks) {
var promises = data.query.backlinks.map(function (link) {
return new Promise(function (resolve) {
if (action === 'update') {
updateLink(link.title, newTarget, api).then(resolve);
} else {
removeLink(link.title, api).then(resolve);
}
});
});
Promise.all(promises).then(callback);
} else {
callback();
}
});
}
function handleRedirects(pageTitle, newTarget, action, api, callback) {
api.get({
action: 'query',
list: 'backlinks',
bltitle: pageTitle,
blfilterredir: 'redirects',
bllimit: 'max'
}).done(function (data) {
if (data.query.backlinks) {
var promises = data.query.backlinks.map(function (redirect) {
return new Promise(function (resolve) {
if (action === 'update') {
updateLink(redirect.title, newTarget, api).then(resolve);
} else {
removeLink(redirect.title, api).then(resolve);
}
});
});
Promise.all(promises).then(callback);
} else {
callback();
}
});
}
function updateLink(pageTitle, newTarget, api) {
return api.postWithToken('csrf', {
action: 'edit',
title: pageTitle,
text: function(text) {
return text.replace(new RegExp('\\[\\[' + mw.util.escapeRegExp(pageTitle) + '(\\|[^\\]]+)?\\]\\]', 'g'), '[[' + newTarget + '$1]]');
},
summary: 'Updated redirect to new target [[' + newTarget + ']]',
});
}
function removeLink(pageTitle, api) {
return api.postWithToken('csrf', {
action: 'edit',
title: pageTitle,
text: function(text) {
return text.replace(new RegExp('\\[\\[' + mw.util.escapeRegExp(pageTitle) + '(\\|[^\\]]+)?\\]\\]', 'g'), '');
},
summary: 'Removed link to [[' + pageTitle + ']]',
});
}
function performDeletion(pageTitle, api, callback) {
api.postWithToken('csrf', {
action: 'delete',
title: pageTitle,
reason: 'Automated clean delete by admin',
}).done(function () {
console.log('Page deleted:', pageTitle);
callback();
}).fail(function (error) {
console.error('Error during clean deletion:', error);
callback();
});
}
mw.hook('wikipage.content').add(addCleanDeleteLink);
});
This page was edited 4 days ago on 08/26/2024. What links here