includes/specials/SpecialMobileOptions.php
<?php
use MediaWiki\Config\Config;
use MediaWiki\Deferred\DeferredUpdates;
use MediaWiki\Html\Html;
use MediaWiki\MediaWikiServices;
use MediaWiki\Request\WebRequest;
use MediaWiki\SpecialPage\UnlistedSpecialPage;
use MediaWiki\Title\Title;
use MediaWiki\User\Options\UserOptionsManager;
use MobileFrontend\Amc\UserMode;
use MobileFrontend\Features\IFeature;
use Wikimedia\Rdbms\ReadOnlyMode;
/**
* Adds a special page with mobile specific preferences
*/
class SpecialMobileOptions extends UnlistedSpecialPage {
/** @var bool Whether this special page has a desktop version or not */
protected $hasDesktopVersion = true;
/**
* Advanced Mobile Contributions mode
* @var \MobileFrontend\Amc\Manager
*/
private $amc;
/**
* @var \MobileFrontend\Features\FeaturesManager
*/
private $featuresManager;
/** @var UserMode */
private $userMode;
/** @var UserOptionsManager */
private UserOptionsManager $userOptionsManager;
/** @var ReadOnlyMode */
private ReadOnlyMode $readOnlyMode;
/** @var MobileContext */
private $mobileContext;
/** @var Config MobileFrontend's config object */
protected Config $config;
public function __construct(
UserOptionsManager $userOptionsManager,
ReadOnlyMode $readOnlyMode,
Config $config
) {
parent::__construct( 'MobileOptions' );
$services = MediaWikiServices::getInstance();
$this->amc = $services->getService( 'MobileFrontend.AMC.Manager' );
$this->featuresManager = $services->getService( 'MobileFrontend.FeaturesManager' );
$this->userMode = $services->getService( 'MobileFrontend.AMC.UserMode' );
$this->mobileContext = $services->getService( 'MobileFrontend.Context' );
$this->userOptionsManager = $userOptionsManager;
$this->readOnlyMode = $readOnlyMode;
$this->config = $config;
}
/**
* @return bool
*/
public function doesWrites() {
return true;
}
/**
* Set the required config for the page.
*/
public function setJsConfigVars() {
$this->getOutput()->addJsConfigVars( [
'wgMFCollapseSectionsByDefault' => $this->getConfig()->get( 'MFCollapseSectionsByDefault' ),
'wgMFEnableFontChanger' => $this->featuresManager->isFeatureAvailableForCurrentUser(
'MFEnableFontChanger'
),
] );
}
/**
* Render the special page
* @param string|null $par Parameter submitted as subpage
*/
public function execute( $par = '' ) {
parent::execute( $par );
$out = $this->getOutput();
$this->setHeaders();
$out->addBodyClasses( 'mw-mf-special-page' );
$out->addModuleStyles( [
'mobile.special.styles',
'mobile.special.codex.styles',
'mobile.special.mobileoptions.styles',
] );
$out->addModules( [
'mobile.special.mobileoptions.scripts',
] );
$this->setJsConfigVars();
$this->mobileContext->setForceMobileView( true );
if ( $this->getRequest()->wasPosted() ) {
$this->submitSettingsForm();
} else {
$this->addSettingsForm();
}
}
private function buildAMCToggle() {
$amcToggle = new OOUI\CheckboxInputWidget( [
'name' => 'enableAMC',
'infusable' => true,
'selected' => $this->userMode->isEnabled(),
'id' => 'enable-amc-toggle',
'value' => '1',
] );
$layout = new OOUI\FieldLayout(
$amcToggle,
[
'label' => new OOUI\LabelWidget( [
'input' => $amcToggle,
'label' => new OOUI\HtmlSnippet(
Html::openElement( 'div' ) .
Html::rawElement( 'strong', [],
$this->msg( 'mw-mf-amc-name' )->parse() ) .
Html::rawElement( 'div', [ 'class' => 'option-description' ],
$this->msg( 'mw-mf-amc-description' )->parse()
) .
Html::closeElement( 'div' )
)
] ),
'id' => 'amc-field',
]
);
// placing links inside a label reduces usability and accessibility so
// append links to $layout and outside of label instead
// https://www.w3.org/TR/html52/sec-forms.html#example-42c5e0c5
$layout->appendContent( new OOUI\HtmlSnippet(
Html::openElement( 'ul', [ 'class' => 'hlist option-links' ] ) .
Html::openElement( 'li' ) .
Html::rawElement(
'a',
// phpcs:ignore Generic.Files.LineLength.TooLong
[ 'href' => 'https://www.mediawiki.org/wiki/Special:MyLanguage/Reading/Web/Advanced_mobile_contributions' ],
$this->msg( 'mobile-frontend-mobile-option-amc-learn-more' )->parse()
) .
Html::closeElement( 'li' ) .
Html::openElement( 'li' ) .
Html::rawElement(
'a',
// phpcs:ignore Generic.Files.LineLength.TooLong
[ 'href' => 'https://www.mediawiki.org/wiki/Special:MyLanguage/Talk:Reading/Web/Advanced_mobile_contributions' ],
$this->msg( 'mobile-frontend-mobile-option-amc-send-feedback' )->parse()
) .
Html::closeElement( 'li' ) .
Html::closeElement( 'ul' )
) );
return $layout;
}
/**
* Builds mobile user preferences field.
* @return \OOUI\FieldLayout
* @throws \OOUI\Exception
*/
private function buildMobileUserPreferences() {
$spacer = new OOUI\LabelWidget( [
'name' => 'mobile_preference_spacer',
] );
$userPreferences = new OOUI\FieldLayout(
$spacer,
[
'label' => new OOUI\LabelWidget( [
'input' => $spacer,
'label' => new OOUI\HtmlSnippet(
Html::openElement( 'div' ) .
Html::rawElement( 'strong', [],
$this->msg( 'mobile-frontend-user-pref-option' )->parse() ) .
Html::rawElement( 'div', [ 'class' => 'option-description' ],
$this->msg( 'mobile-frontend-user-pref-description' )->parse()
) .
Html::closeElement( 'div' )
)
] ),
'id' => 'mobile-user-pref',
]
);
$userPreferences->appendContent( new OOUI\HtmlSnippet(
Html::openElement( 'ul', [ 'class' => 'hlist option-links' ] ) .
Html::openElement( 'li' ) .
Html::rawElement(
'a',
[ 'href' => Title::newFromText( 'Special:Preferences' )->getLocalURL() ],
$this->msg( 'mobile-frontend-user-pref-link' )->parse()
) .
Html::closeElement( 'li' ) .
Html::closeElement( 'ul' )
) );
return $userPreferences;
}
/**
* Mark some html as being content
* @param string $html HTML content
* @param string $className additional class names
* @return string of html
*/
private static function contentElement( $html, $className = '' ) {
return Html::rawElement( 'div', [
'class' => 'content'
], $html );
}
/**
* Render the settings form (with actual set settings) and add it to the
* output as well as any supporting modules.
*/
private function addSettingsForm() {
$out = $this->getOutput();
$user = $this->getUser();
$isTemp = $user->isTemp();
$out->setPageTitleMsg( $this->msg( 'mobile-frontend-main-menu-settings-heading' ) );
$out->enableOOUI();
if ( $this->getRequest()->getCheck( 'success' ) ) {
$out->wrapWikiMsg(
self::contentElement(
Html::successBox(
$this->msg( 'savedprefs' )->parse(),
'mw-mf-mobileoptions-message'
)
)
);
}
$fields = [];
$form = new OOUI\FormLayout( [
'method' => 'POST',
'id' => 'mobile-options',
'action' => $this->getPageTitle()->getLocalURL(),
] );
$form->addClasses( [ 'mw-mf-settings' ] );
if ( $this->amc->isAvailable() && !$isTemp ) {
$fields[] = $this->buildAMCToggle();
}
// beta settings
$isInBeta = $this->mobileContext->isBetaGroupMember();
if ( $this->config->get( 'MFEnableBeta' ) ) {
$input = new OOUI\CheckboxInputWidget( [
'name' => 'enableBeta',
'infusable' => true,
'selected' => $isInBeta,
'id' => 'enable-beta-toggle',
'value' => '1',
] );
$fields[] = new OOUI\FieldLayout(
$input,
[
'label' => new OOUI\LabelWidget( [
'input' => $input,
'label' => new OOUI\HtmlSnippet(
Html::openElement( 'div' ) .
Html::rawElement( 'strong', [],
$this->msg( 'mobile-frontend-settings-beta' )->parse() ) .
Html::rawElement( 'div', [ 'class' => 'option-description' ],
$this->msg( 'mobile-frontend-opt-in-explain' )->parse()
) .
Html::closeElement( 'div' )
)
] ),
'id' => 'beta-field',
]
);
// TODO The userMode should know how to retrieve features assigned to that mode,
// we shouldn't do any special logic like this in anywhere else in the code
$features = array_diff(
$this->featuresManager->getAvailableForMode(
$this->featuresManager->getMode( IFeature::CONFIG_BETA )
),
$this->featuresManager->getAvailableForMode(
$this->featuresManager->getMode( IFeature::CONFIG_STABLE )
)
);
$classNames = [ 'mobile-options-beta-feature' ];
if ( $isInBeta ) {
$classNames[] = 'is-enabled';
$icon = 'check';
} else {
$icon = 'lock';
}
/** @var IFeature $feature */
foreach ( $features as $feature ) {
$fields[] = new OOUI\FieldLayout(
new OOUI\IconWidget( [
'icon' => $icon,
'title' => $this->msg( 'mobile-frontend-beta-only' )->text(),
] ),
[
'classes' => $classNames,
'label' => new OOUI\LabelWidget( [
'label' => new OOUI\HtmlSnippet(
Html::rawElement( 'div', [],
Html::element( 'strong', [],
$this->msg( $feature->getNameKey() )->text() ) .
Html::element( 'div', [ 'class' => 'option-description' ],
$this->msg( $feature->getDescriptionKey() )->text() )
)
),
] )
]
);
}
}
$fields[] = new OOUI\ButtonInputWidget( [
'id' => 'mw-mf-settings-save',
'infusable' => true,
'value' => $this->msg( 'mobile-frontend-save-settings' )->text(),
'label' => $this->msg( 'mobile-frontend-save-settings' )->text(),
'flags' => [ 'primary', 'progressive' ],
'type' => 'submit',
] );
if ( $user->isRegistered() && !$isTemp ) {
$fields[] = new OOUI\HiddenInputWidget( [ 'name' => 'token',
'value' => $user->getEditToken() ] );
// Special:Preferences link (https://phabricator.wikimedia.org/T327506)
$fields[] = $this->buildMobileUserPreferences();
}
$feedbackLink = $this->getConfig()->get( 'MFBetaFeedbackLink' );
if ( $feedbackLink && $isInBeta ) {
$fields[] = new OOUI\ButtonWidget( [
'framed' => false,
'href' => $feedbackLink,
'icon' => 'feedback',
'flags' => [
'progressive',
],
'classes' => [ 'mobile-options-feedback' ],
'label' => $this->msg( 'mobile-frontend-send-feedback' )->text(),
] );
}
$form->appendContent(
...$fields
);
$out->addHTML( $form );
}
/**
* @param WebRequest $request
* @return string url to redirect to
*/
private function getRedirectUrl( WebRequest $request ) {
$returnTo = $request->getText( 'returnto' );
if ( $returnTo !== '' ) {
$title = Title::newFromText( $returnTo );
if ( $title !== null ) {
return $title->getFullURL( $request->getText( 'returntoquery' ) );
}
}
return $this->mobileContext->getMobileUrl(
$this->getPageTitle()->getFullURL( 'success' )
);
}
/**
* Saves the settings submitted by the settings form
*/
private function submitSettingsForm() {
$request = $this->getRequest();
$user = $this->getUser();
if ( $user->isRegistered() && !$user->matchEditToken( $request->getVal( 'token' ) ) ) {
$errorText = __METHOD__ . '(): token mismatch';
wfDebugLog( 'mobile', $errorText );
$this->getOutput()->addHTML(
Html::errorBox(
$this->msg( "mobile-frontend-save-error" )->parse()
)
);
$this->addSettingsForm();
return;
}
// We must treat forms that only update a single field specially because if we
// don't, all the other options will be clobbered with default values
$updateSingleOption = $request->getRawVal( 'updateSingleOption' );
$enableAMC = $request->getBool( 'enableAMC' );
$enableBetaMode = $request->getBool( 'enableBeta' );
$mobileMode = $enableBetaMode ? MobileContext::MODE_BETA : '';
if ( $updateSingleOption !== 'enableAMC' ) {
$this->mobileContext->setMobileMode( $mobileMode );
}
if ( $this->amc->isAvailable() && $updateSingleOption !== 'enableBeta' ) {
$this->userMode->setEnabled( $enableAMC );
}
DeferredUpdates::addCallableUpdate( function () use (
$updateSingleOption,
$mobileMode,
$enableAMC ) {
if ( $this->readOnlyMode->isReadOnly() ) {
return;
}
$latestUser = $this->getUser()->getInstanceForUpdate();
if ( $latestUser === null || !$latestUser->isNamed() ) {
// The user is anon, temp user or could not be loaded from the database.
return;
}
if ( $updateSingleOption !== 'enableAMC' ) {
$this->userOptionsManager->setOption(
$latestUser,
MobileContext::USER_MODE_PREFERENCE_NAME,
$mobileMode
);
}
if ( $this->amc->isAvailable() && $updateSingleOption !== 'enableBeta' ) {
$this->userOptionsManager->setOption(
$latestUser,
UserMode::USER_OPTION_MODE_AMC,
$enableAMC ? UserMode::OPTION_ENABLED : UserMode::OPTION_DISABLED
);
}
$latestUser->saveSettings();
}, DeferredUpdates::PRESEND );
$this->getOutput()->redirect( $this->getRedirectUrl( $request ) );
}
}