2021-01-29 17:09:52 +00:00
|
|
|
<?php
|
|
|
|
/**
|
|
|
|
* DiscussionTools page hooks
|
|
|
|
*
|
|
|
|
* @file
|
|
|
|
* @ingroup Extensions
|
|
|
|
* @license MIT
|
|
|
|
*/
|
|
|
|
|
|
|
|
namespace MediaWiki\Extension\DiscussionTools\Hooks;
|
|
|
|
|
2021-07-29 06:12:10 +00:00
|
|
|
use Article;
|
2021-09-07 20:51:35 +00:00
|
|
|
use ConfigFactory;
|
2021-07-28 10:36:58 +00:00
|
|
|
use Html;
|
|
|
|
use IContextSource;
|
|
|
|
use MediaWiki\Actions\Hook\GetActionNameHook;
|
2021-01-29 18:31:27 +00:00
|
|
|
use MediaWiki\Extension\DiscussionTools\CommentFormatter;
|
2021-02-17 22:34:02 +00:00
|
|
|
use MediaWiki\Extension\DiscussionTools\SubscriptionStore;
|
2021-01-29 17:09:52 +00:00
|
|
|
use MediaWiki\Hook\BeforePageDisplayHook;
|
|
|
|
use MediaWiki\Hook\OutputPageBeforeHTMLHook;
|
2021-04-29 14:24:49 +00:00
|
|
|
use MediaWiki\Page\Hook\ArticleViewHeaderHook;
|
2021-07-29 06:12:10 +00:00
|
|
|
use MediaWiki\Page\Hook\BeforeDisplayNoArticleTextHook;
|
2021-08-25 19:10:53 +00:00
|
|
|
use MediaWiki\User\UserNameUtils;
|
2021-09-07 20:51:35 +00:00
|
|
|
use MediaWiki\User\UserOptionsLookup;
|
2021-07-29 06:12:10 +00:00
|
|
|
use OOUI\ButtonWidget;
|
2021-01-29 17:09:52 +00:00
|
|
|
use OutputPage;
|
2021-04-29 14:24:49 +00:00
|
|
|
use ParserOutput;
|
2021-07-29 06:12:10 +00:00
|
|
|
use RequestContext;
|
2021-01-29 17:09:52 +00:00
|
|
|
use Skin;
|
2021-08-25 19:10:53 +00:00
|
|
|
use SpecialPage;
|
2021-01-29 17:09:52 +00:00
|
|
|
use VisualEditorHooks;
|
|
|
|
|
|
|
|
class PageHooks implements
|
2021-04-29 14:24:49 +00:00
|
|
|
ArticleViewHeaderHook,
|
2021-07-29 06:12:10 +00:00
|
|
|
BeforeDisplayNoArticleTextHook,
|
2021-01-29 17:09:52 +00:00
|
|
|
BeforePageDisplayHook,
|
2021-07-28 10:36:58 +00:00
|
|
|
GetActionNameHook,
|
2021-01-29 17:09:52 +00:00
|
|
|
OutputPageBeforeHTMLHook
|
|
|
|
{
|
2021-09-07 20:51:35 +00:00
|
|
|
/** @var ConfigFactory */
|
|
|
|
private $configFactory;
|
|
|
|
|
2021-02-17 22:34:02 +00:00
|
|
|
/** @var SubscriptionStore */
|
2021-09-07 20:51:35 +00:00
|
|
|
private $subscriptionStore;
|
2021-02-17 22:34:02 +00:00
|
|
|
|
2021-08-25 19:10:53 +00:00
|
|
|
/** @var UserNameUtils */
|
2021-09-07 20:51:35 +00:00
|
|
|
private $userNameUtils;
|
|
|
|
|
|
|
|
/** @var UserOptionsLookup */
|
|
|
|
private $userOptionsLookup;
|
2021-08-25 19:10:53 +00:00
|
|
|
|
2021-02-17 22:34:02 +00:00
|
|
|
/**
|
2021-09-07 20:51:35 +00:00
|
|
|
* @param ConfigFactory $configFactory
|
2021-02-17 22:34:02 +00:00
|
|
|
* @param SubscriptionStore $subscriptionStore
|
2021-08-25 19:10:53 +00:00
|
|
|
* @param UserNameUtils $userNameUtils
|
2021-09-07 20:51:35 +00:00
|
|
|
* @param UserOptionsLookup $userOptionsLookup
|
2021-02-17 22:34:02 +00:00
|
|
|
*/
|
2021-09-07 20:51:35 +00:00
|
|
|
public function __construct(
|
|
|
|
ConfigFactory $configFactory,
|
|
|
|
SubscriptionStore $subscriptionStore,
|
|
|
|
UserNameUtils $userNameUtils,
|
|
|
|
UserOptionsLookup $userOptionsLookup
|
|
|
|
) {
|
|
|
|
$this->configFactory = $configFactory;
|
2021-02-17 22:34:02 +00:00
|
|
|
$this->subscriptionStore = $subscriptionStore;
|
2021-08-25 19:10:53 +00:00
|
|
|
$this->userNameUtils = $userNameUtils;
|
2021-09-07 20:51:35 +00:00
|
|
|
$this->userOptionsLookup = $userOptionsLookup;
|
2021-02-17 22:34:02 +00:00
|
|
|
}
|
|
|
|
|
2021-01-29 17:09:52 +00:00
|
|
|
/**
|
|
|
|
* Adds DiscussionTools JS to the output.
|
|
|
|
*
|
|
|
|
* This is attached to the MediaWiki 'BeforePageDisplay' hook.
|
|
|
|
*
|
|
|
|
* @param OutputPage $output
|
|
|
|
* @param Skin $skin
|
|
|
|
* @return void This hook must not abort, it must return no value
|
|
|
|
*/
|
2021-07-22 07:25:13 +00:00
|
|
|
public function onBeforePageDisplay( $output, $skin ): void {
|
2021-01-29 17:09:52 +00:00
|
|
|
$user = $output->getUser();
|
2021-07-29 06:12:10 +00:00
|
|
|
$req = $output->getRequest();
|
2021-10-26 14:02:30 +00:00
|
|
|
foreach ( HookUtils::FEATURES as $feature ) {
|
|
|
|
// Add a CSS class for each enabled feature
|
|
|
|
if ( HookUtils::isFeatureEnabledForOutput( $output, $feature ) ) {
|
|
|
|
$output->addBodyClasses( "ext-discussiontools-$feature-enabled" );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-06 12:08:50 +00:00
|
|
|
// Load style modules if the tools can be available for the title
|
2021-10-26 14:02:30 +00:00
|
|
|
// to selectively hide DT features, depending on the body classes added above.
|
2021-03-06 12:08:50 +00:00
|
|
|
if ( HookUtils::isAvailableForTitle( $output->getTitle() ) ) {
|
2021-01-29 17:09:52 +00:00
|
|
|
$output->addModuleStyles( [
|
2021-02-17 22:34:02 +00:00
|
|
|
'ext.discussionTools.init.styles',
|
2021-01-29 17:09:52 +00:00
|
|
|
] );
|
2021-03-06 12:08:50 +00:00
|
|
|
}
|
2021-10-26 14:02:30 +00:00
|
|
|
|
2021-03-06 12:08:50 +00:00
|
|
|
// Load modules if any DT feature is enabled for this user
|
|
|
|
if ( HookUtils::isFeatureEnabledForOutput( $output ) ) {
|
2021-01-29 17:09:52 +00:00
|
|
|
$output->addModules( [
|
|
|
|
'ext.discussionTools.init'
|
|
|
|
] );
|
|
|
|
|
2021-02-17 17:16:17 +00:00
|
|
|
$enabledVars = [];
|
|
|
|
foreach ( HookUtils::FEATURES as $feature ) {
|
|
|
|
$enabledVars[$feature] = HookUtils::isFeatureEnabledForOutput( $output, $feature );
|
|
|
|
}
|
|
|
|
$output->addJsConfigVars( 'wgDiscussionToolsFeaturesEnabled', $enabledVars );
|
2021-01-29 17:09:52 +00:00
|
|
|
|
2021-09-07 20:51:35 +00:00
|
|
|
$editor = $this->userOptionsLookup->getOption( $user, 'discussiontools-editmode' );
|
2021-01-29 17:09:52 +00:00
|
|
|
// User has no preferred editor yet
|
|
|
|
// If the user has a preferred editor, this will be evaluated in the client
|
|
|
|
if ( !$editor ) {
|
|
|
|
// Check which editor we would use for articles
|
|
|
|
// VE pref is 'visualeditor'/'wikitext'. Here we describe the mode,
|
|
|
|
// not the editor, so 'visual'/'source'
|
|
|
|
$editor = VisualEditorHooks::getPreferredEditor( $user, $req ) === 'visualeditor' ?
|
|
|
|
'visual' : 'source';
|
|
|
|
$output->addJsConfigVars(
|
|
|
|
'wgDiscussionToolsFallbackEditMode',
|
|
|
|
$editor
|
|
|
|
);
|
|
|
|
}
|
2021-09-07 20:51:35 +00:00
|
|
|
$dtConfig = $this->configFactory->makeConfig( 'discussiontools' );
|
2021-01-29 17:09:52 +00:00
|
|
|
$abstate = $dtConfig->get( 'DiscussionToolsABTest' ) ?
|
2021-12-10 07:21:33 +00:00
|
|
|
$this->userOptionsLookup->getOption( $user, 'discussiontools-abtest2' ) :
|
2021-01-29 17:09:52 +00:00
|
|
|
false;
|
|
|
|
if ( $abstate ) {
|
|
|
|
$output->addJsConfigVars(
|
|
|
|
'wgDiscussionToolsABTestBucket',
|
|
|
|
$abstate
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2021-07-28 10:36:58 +00:00
|
|
|
|
|
|
|
// Replace the action=edit§ion=new form with the new topic tool.
|
2021-09-20 16:20:06 +00:00
|
|
|
if ( HookUtils::shouldOpenNewTopicTool( $output->getContext() ) ) {
|
2021-07-28 10:36:58 +00:00
|
|
|
$output->addJsConfigVars( 'wgDiscussionToolsStartNewTopicTool', true );
|
|
|
|
|
|
|
|
// For no-JS compatibility, redirect to the old new section editor if JS is unavailable.
|
|
|
|
// This isn't great, because the user has to load the page twice. But making a page that is
|
|
|
|
// both a view mode and an edit mode seems difficult, so I'm cutting some corners here.
|
|
|
|
// (Code below adapted from VisualEditor.)
|
|
|
|
$params = $output->getRequest()->getValues();
|
|
|
|
$params['dtenable'] = '0';
|
|
|
|
$url = wfScript() . '?' . wfArrayToCgi( $params );
|
|
|
|
$escapedUrl = htmlspecialchars( $url );
|
|
|
|
|
|
|
|
// Redirect if the user has no JS (<noscript>)
|
|
|
|
$output->addHeadItem(
|
|
|
|
'dt-noscript-fallback',
|
|
|
|
"<noscript><meta http-equiv=\"refresh\" content=\"0; url=$escapedUrl\"></noscript>"
|
|
|
|
);
|
|
|
|
// Redirect if the user has no ResourceLoader
|
|
|
|
$output->addScript( Html::inlineScript(
|
|
|
|
"(window.NORLQ=window.NORLQ||[]).push(" .
|
|
|
|
"function(){" .
|
|
|
|
"location.href=\"$url\";" .
|
|
|
|
"}" .
|
|
|
|
");"
|
|
|
|
) );
|
|
|
|
}
|
2021-01-29 17:09:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* OutputPageBeforeHTML hook handler
|
|
|
|
* @see https://www.mediawiki.org/wiki/Manual:Hooks/OutputPageBeforeHTML
|
|
|
|
*
|
|
|
|
* @param OutputPage $output OutputPage object that corresponds to the page
|
|
|
|
* @param string &$text Text that will be displayed, in HTML
|
|
|
|
* @return bool|void This hook must not abort, it must return true or null.
|
|
|
|
*/
|
|
|
|
public function onOutputPageBeforeHTML( $output, &$text ) {
|
2021-10-26 15:42:50 +00:00
|
|
|
// ParserOutputPostCacheTransform hook would be a better place to do this,
|
|
|
|
// so that when the ParserOutput is used directly without using this hook,
|
|
|
|
// we don't leave half-baked interface elements in it (see e.g. T292345, T294168).
|
|
|
|
// But that hook doesn't provide parameters that we need to render correctly
|
|
|
|
// (including the page title, interface language, and current user).
|
|
|
|
|
2021-01-28 17:14:20 +00:00
|
|
|
$lang = $output->getLanguage();
|
2021-04-08 12:30:28 +00:00
|
|
|
if ( HookUtils::isFeatureEnabledForOutput( $output, HookUtils::TOPICSUBSCRIPTION ) ) {
|
2021-04-19 19:14:07 +00:00
|
|
|
$text = CommentFormatter::postprocessTopicSubscription(
|
|
|
|
$text, $lang, $this->subscriptionStore, $output->getUser()
|
2021-02-17 22:34:02 +00:00
|
|
|
);
|
|
|
|
}
|
2021-04-15 21:09:55 +00:00
|
|
|
if ( HookUtils::isFeatureEnabledForOutput( $output, HookUtils::REPLYTOOL ) ) {
|
|
|
|
$text = CommentFormatter::postprocessReplyTool(
|
|
|
|
$text, $lang
|
|
|
|
);
|
|
|
|
}
|
2021-02-17 22:34:02 +00:00
|
|
|
|
2021-01-29 17:09:52 +00:00
|
|
|
return true;
|
|
|
|
}
|
2021-07-28 10:36:58 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* GetActionName hook handler
|
|
|
|
*
|
|
|
|
* @param IContextSource $context Request context
|
|
|
|
* @param string &$action Default action name, reassign to change it
|
|
|
|
* @return void This hook must not abort, it must return no value
|
|
|
|
*/
|
|
|
|
public function onGetActionName( IContextSource $context, string &$action ): void {
|
2021-09-20 16:20:06 +00:00
|
|
|
if ( $action === 'edit' && (
|
|
|
|
HookUtils::shouldOpenNewTopicTool( $context ) ||
|
|
|
|
HookUtils::shouldDisplayEmptyState( $context )
|
|
|
|
) ) {
|
2021-07-28 10:36:58 +00:00
|
|
|
$action = 'view';
|
|
|
|
}
|
|
|
|
}
|
2021-07-29 06:12:10 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* BeforeDisplayNoArticleText hook handler
|
|
|
|
* @see https://www.mediawiki.org/wiki/Manual:Hooks/BeforeDisplayNoArticleText
|
|
|
|
*
|
|
|
|
* @param Article $article The (empty) article
|
|
|
|
* @return bool|void This hook can abort
|
|
|
|
*/
|
|
|
|
public function onBeforeDisplayNoArticleText( $article ) {
|
|
|
|
// We want to override the empty state for articles on which we would be enabled
|
|
|
|
$context = $article->getContext();
|
2021-09-20 16:20:06 +00:00
|
|
|
if ( !HookUtils::shouldDisplayEmptyState( $context ) ) {
|
2021-08-06 21:04:23 +00:00
|
|
|
// Our empty states are all about using the new topic tool, but
|
|
|
|
// expect to be on a talk page, so fall back if it's not
|
|
|
|
// available or if we're in a non-talk namespace that still has
|
|
|
|
// DT features enabled
|
2021-07-29 06:12:10 +00:00
|
|
|
return true;
|
|
|
|
}
|
2021-09-20 16:20:06 +00:00
|
|
|
|
|
|
|
$output = $context->getOutput();
|
2021-07-29 06:12:10 +00:00
|
|
|
$output->enableOOUI();
|
|
|
|
$output->enableClientCache( false );
|
|
|
|
|
|
|
|
$coreConfig = RequestContext::getMain()->getConfig();
|
|
|
|
$iconpath = $coreConfig->get( 'ExtensionAssetsPath' ) . '/DiscussionTools/images';
|
|
|
|
|
|
|
|
$dir = $context->getLanguage()->getDir();
|
|
|
|
$lang = $context->getLanguage()->getHtmlCode();
|
|
|
|
|
|
|
|
$output->addHTML(
|
|
|
|
// This being mw-parser-output is a lie, but makes the reply controller cope much better with everything
|
|
|
|
Html::openElement( 'div', [ 'class' => "ext-discussiontools-emptystate mw-parser-output noarticletext" ] ) .
|
|
|
|
Html::openElement( 'div', [ 'class' => "ext-discussiontools-emptystate-text" ] )
|
|
|
|
);
|
2021-08-25 19:10:53 +00:00
|
|
|
$titleMsg = false;
|
|
|
|
$descMsg = false;
|
|
|
|
$descParams = [];
|
|
|
|
$buttonMsg = 'discussiontools-emptystate-button';
|
2021-09-20 16:20:06 +00:00
|
|
|
$title = $context->getTitle();
|
2021-08-25 19:10:53 +00:00
|
|
|
if ( $title->getNamespace() == NS_USER_TALK ) {
|
|
|
|
// This is a user talk page
|
|
|
|
$isIP = $this->userNameUtils->isIP( $title->getText() );
|
|
|
|
if ( $title->equals( $output->getUser()->getTalkPage() ) ) {
|
|
|
|
// This is your own user talk page
|
|
|
|
if ( $isIP ) {
|
|
|
|
// You're an IP editor, so this is only *sort of* your talk page
|
|
|
|
$titleMsg = 'discussiontools-emptystate-title-self-anon';
|
|
|
|
$descMsg = 'discussiontools-emptystate-desc-self-anon';
|
|
|
|
$query = $context->getRequest()->getValues();
|
|
|
|
unset( $query['title'] );
|
|
|
|
$descParams = [
|
|
|
|
SpecialPage::getTitleFor( 'CreateAccount' )->getFullURL( [
|
|
|
|
'returnto' => $context->getTitle()->getFullText(),
|
|
|
|
'returntoquery' => wfArrayToCgi( $query ),
|
|
|
|
] ),
|
|
|
|
SpecialPage::getTitleFor( 'Userlogin' )->getFullURL( [
|
|
|
|
'returnto' => $context->getTitle()->getFullText(),
|
|
|
|
'returntoquery' => wfArrayToCgi( $query ),
|
|
|
|
] ),
|
|
|
|
];
|
|
|
|
} else {
|
|
|
|
// You're logged in, this is very much your talk page
|
|
|
|
$titleMsg = 'discussiontools-emptystate-title-self';
|
|
|
|
$descMsg = 'discussiontools-emptystate-desc-self';
|
|
|
|
}
|
|
|
|
$buttonMsg = false;
|
|
|
|
} elseif ( $isIP ) {
|
|
|
|
// This is an IP editor
|
|
|
|
$titleMsg = 'discussiontools-emptystate-title-user-anon';
|
|
|
|
$descMsg = 'discussiontools-emptystate-desc-user-anon';
|
|
|
|
} else {
|
|
|
|
// This is any other user
|
|
|
|
$titleMsg = 'discussiontools-emptystate-title-user';
|
|
|
|
$descMsg = 'discussiontools-emptystate-desc-user';
|
|
|
|
}
|
2021-07-29 06:12:10 +00:00
|
|
|
} else {
|
2021-08-25 19:10:53 +00:00
|
|
|
// This is any other page on which DT is enabled
|
|
|
|
$titleMsg = 'discussiontools-emptystate-title';
|
|
|
|
$descMsg = 'discussiontools-emptystate-desc';
|
|
|
|
}
|
|
|
|
$output->addHTML( Html::rawElement( 'h3', [],
|
|
|
|
$context->msg( $titleMsg )->parse()
|
|
|
|
) );
|
|
|
|
$output->addHTML( Html::rawElement( 'div', [ 'class' => 'plainlinks' ],
|
|
|
|
$context->msg( $descMsg, $descParams )->parseAsBlock()
|
|
|
|
) );
|
|
|
|
if ( $buttonMsg ) {
|
|
|
|
$output->addHTML( new ButtonWidget( [
|
|
|
|
'label' => $context->msg( $buttonMsg )->text(),
|
|
|
|
'href' => $title->getLocalURL( 'action=edit§ion=new' ),
|
|
|
|
'flags' => [ 'primary', 'progressive' ]
|
|
|
|
] ) );
|
2021-07-29 06:12:10 +00:00
|
|
|
}
|
|
|
|
$output->addHTML(
|
|
|
|
Html::closeElement( 'div' ) .
|
|
|
|
Html::element( 'img', [
|
|
|
|
'src' => $iconpath . '/emptystate.svg',
|
|
|
|
'class' => "ext-discussiontools-emptystate-logo",
|
|
|
|
// This is a purely decorative element
|
|
|
|
'alt' => "",
|
|
|
|
] ) .
|
|
|
|
Html::closeElement( 'div' )
|
|
|
|
);
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
2021-04-29 14:24:49 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @param Article $article
|
|
|
|
* @param bool|ParserOutput &$outputDone
|
|
|
|
* @param bool &$pcache
|
|
|
|
* @return bool|void
|
|
|
|
*/
|
|
|
|
public function onArticleViewHeader( $article, &$outputDone, &$pcache ) {
|
|
|
|
$context = $article->getContext();
|
|
|
|
$output = $context->getOutput();
|
|
|
|
if (
|
|
|
|
$output->getSkin()->getSkinName() === 'minerva' &&
|
|
|
|
HookUtils::isFeatureEnabledForOutput( $output, HookUtils::NEWTOPICTOOL ) &&
|
|
|
|
// No need to show the button when the empty state banner is shown
|
|
|
|
!HookUtils::shouldDisplayEmptyState( $context )
|
|
|
|
) {
|
|
|
|
// Minerva doesn't show a new topic button by default, unless the MobileFrontend
|
|
|
|
// talk page feature is enabled, but we shouldn't depend on code from there.
|
|
|
|
$output->enableOOUI();
|
|
|
|
$output->addHTML(
|
|
|
|
new ButtonWidget( [
|
|
|
|
'href' => $article->getTitle()->getLinkURL( [ 'action' => 'edit', 'section' => 'new' ] ),
|
|
|
|
// TODO: Make this a local message if the Minerva feature goes away
|
|
|
|
'label' => $context->msg( 'minerva-talk-add-topic' )->text(),
|
|
|
|
'flags' => [ 'progressive', 'primary' ],
|
|
|
|
'classes' => [ 'ext-discussiontools-init-new-topic' ]
|
|
|
|
] )
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2021-01-29 17:09:52 +00:00
|
|
|
}
|