wikimedia/mediawiki-extensions-MobileFrontend

View on GitHub
includes/ExtMobileFrontend.php

Summary

Maintainability
B
4 hrs
Test Coverage
<?php

use MediaWiki\Context\IContextSource;
use MediaWiki\Html\TemplateParser;
use MediaWiki\MediaWikiServices;
use MediaWiki\Output\OutputPage;
use MediaWiki\Registration\ExtensionRegistry;
use MediaWiki\Title\Title;
use MediaWiki\User\User;
use MobileFrontend\Api\ApiParseExtender;
use MobileFrontend\ContentProviders\IContentProvider;
use MobileFrontend\Features\FeaturesManager;
use MobileFrontend\Hooks\HookRunner;
use MobileFrontend\Transforms\LazyImageTransform;
use MobileFrontend\Transforms\MakeSectionsTransform;
use MobileFrontend\Transforms\MoveLeadParagraphTransform;
use MobileFrontend\Transforms\RemovableClassesTransform;
use Wikibase\Client\WikibaseClient;
use Wikibase\DataModel\Entity\ItemId;
use Wikibase\DataModel\Services\Lookup\TermLookupException;
use Wikimedia\IPUtils;

/**
 * Implements additional functions to use in MobileFrontend
 */
class ExtMobileFrontend {
    /**
     * Provide alternative HTML for a user page which has not been created.
     * Let the user know about it with pretty graphics and different texts depending
     * on whether the user is the owner of the page or not.
     * @internal Only for use inside MobileFrontend.
     * @param OutputPage $out
     * @param Title $title
     * @return string that is empty if the transform does not apply.
     */
    public static function blankUserPageHTML( OutputPage $out, Title $title ) {
        $pageUser = self::buildPageUserObject( $title );
        $isHidden = $pageUser && $pageUser->isHidden();
        $canViewHidden = !$isHidden || $out->getAuthority()->isAllowed( 'hideuser' );

        $out->addModuleStyles( [
            'mobile.userpage.styles', 'mobile.userpage.images'
        ] );

        if ( $pageUser && !$title->exists() && $canViewHidden ) {
            return self::getUserPageContent(
                $out, $pageUser, $title );
        } else {
            return '';
        }
    }

    /**
     * Obtains content using the given content provider and routes it to the mobile formatter
     * if required.
     *
     * @param IContentProvider $provider
     * @param OutputPage $out
     * @param bool $mobileFormatHtml whether content should be run through the MobileFormatter
     *
     * @return string
     */
    public static function domParseWithContentProvider(
        IContentProvider $provider,
        OutputPage $out,
        $mobileFormatHtml = true
    ) {
        $html = $provider->getHTML();

        // If we're not running the formatter we can exit earlier
        if ( !$mobileFormatHtml ) {
            return $html;
        } else {
            return self::domParseMobile( $out, $html );
        }
    }

    /**
     * Transforms content to be mobile friendly version.
     * Filters out various elements and runs the MobileFormatter.
     *
     * @param OutputPage $out
     * @param string $html to render.
     *
     * @return string
     */
    public static function domParseMobile( OutputPage $out, $html = '' ) {
        $services = MediaWikiServices::getInstance();
        /** @var FeaturesManager $featuresManager */
        $featuresManager = $services->getService( 'MobileFrontend.FeaturesManager' );
        /** @var MobileContext $context */
        $context = $services->getService( 'MobileFrontend.Context' );
        $config = $services->getService( 'MobileFrontend.Config' );

        $title = $out->getTitle();
        $ns = $title->getNamespace();
        $action = $context->getRequest()->getText( 'action', 'view' );
        $isView = $action === 'view' || ApiParseExtender::isParseAction( $action );

        $shouldUseParsoid = false;
        if ( ExtensionRegistry::getInstance()->isLoaded( 'ParserMigration' ) ) {
            $oracle = MediaWikiServices::getInstance()->getService( 'ParserMigration.Oracle' );
            $shouldUseParsoid =
                $oracle->shouldUseParsoid( $context->getUser(), $context->getRequest(), $title );
        }

        $enableSections = (
            // Don't collapse sections e.g. on JS pages
            $title->canExist()
            && $title->getContentModel() == CONTENT_MODEL_WIKITEXT
            // And not in certain namespaces
            && !in_array( $ns, $config->get( 'MFNamespacesWithoutCollapsibleSections' ) )
            // And not when what's shown is not actually article text
            && $isView
            && !$shouldUseParsoid
        );

        // https://phabricator.wikimedia.org/T232690
        if ( !MobileFormatter::canApply( $html, $config->get( 'MFMobileFormatterOptions' ) ) ) {
            // In future we might want to prepend a message feeding
            // back to the user that the page is not mobile friendly.
            return $html;
        }

        $formatter = new MobileFormatter( $html );

        $hookRunner = new HookRunner( $services->getHookContainer() );
        $hookRunner->onMobileFrontendBeforeDOM( $context, $formatter );

        $shouldLazyTransformImages = $featuresManager->isFeatureAvailableForCurrentUser( 'MFLazyLoadImages' );
        $leadParagraphEnabled = in_array( $ns, $config->get( 'MFNamespacesWithLeadParagraphs' ) );
        $showFirstParagraphBeforeInfobox = $leadParagraphEnabled &&
            $featuresManager->isFeatureAvailableForCurrentUser( 'MFShowFirstParagraphBeforeInfobox' );

        $transforms = [];
        // Remove specified content in content namespaces
        if ( in_array( $title->getNamespace(), $config->get( 'ContentNamespaces' ), true ) ) {
            $mfRemovableClasses = $config->get( 'MFRemovableClasses' );
            $removableClasses = $mfRemovableClasses['base'];
            if ( $context->isBetaGroupMember() ) {
                $removableClasses = array_unique(
                    array_merge( $removableClasses, $mfRemovableClasses['beta'] )
                );
            }

            $transforms[] = new RemovableClassesTransform( $removableClasses );
        }

        if ( $enableSections ) {
            $options = $config->get( 'MFMobileFormatterOptions' );
            $topHeadingTags = $options['headings'];

            $transforms[] = new MakeSectionsTransform(
                $topHeadingTags,
                true
            );
        }

        if ( $shouldLazyTransformImages ) {
            $transforms[] = new LazyImageTransform( $config->get( 'MFLazyLoadSkipSmallImages' ) );
        }

        if ( $showFirstParagraphBeforeInfobox ) {
            $transforms[] = new MoveLeadParagraphTransform( $title, $title->getLatestRevID() );
        }

        $start = microtime( true );
        $formatter->applyTransforms( $transforms );
        $end = microtime( true );
        $report = sprintf( "MobileFormatter took %.3f seconds", $end - $start );

        return $formatter->getText() . "\n<!-- $report -->";
    }

    /**
     * Return new User object based on username or IP address.
     * @param Title $title
     * @return User|null
     */
    private static function buildPageUserObject( Title $title ) {
        $titleText = $title->getText();

        $usernameUtils = MediaWikiServices::getInstance()->getUserNameUtils();
        if ( $usernameUtils->isIP( $titleText ) || IPUtils::isIPv6( $titleText ) ) {
            return User::newFromAnyId( null, $titleText, null );
        }

        $user = User::newFromName( $titleText );
        if ( $user && $user->isRegistered() ) {
            return $user;
        }

        return null;
    }

    /**
     * Generate user page content for non-existent user pages
     *
     * @param IContextSource $output
     * @param User $pageUser owner of the user page
     * @param Title $title
     * @return string
     */
    protected static function getUserPageContent( IContextSource $output,
        User $pageUser, Title $title
    ) {
        /** @var MobileContext $context */
        $context = MediaWikiServices::getInstance()->getService( 'MobileFrontend.Context' );
        $pageUsername = $pageUser->getName();
        // Is the current user viewing their own page?
        $isCurrentUser = $output->getUser()->getName() === $pageUsername;

        $data = [
            'userImageClass' => 'userpage-image-placeholder',
        ];
        $data['ctaHeading'] = $isCurrentUser ?
            $context->msg( 'mobile-frontend-user-page-no-owner-page-yet' )->text() :
            $context->msg( 'mobile-frontend-user-page-no-page-yet', $pageUsername )->parse();
        $data['ctaDescription'] = $isCurrentUser ?
            $context->msg(
                'mobile-frontend-user-page-describe-yourself',
                $context->msg( 'mobile-frontend-user-page-describe-yourself-editors' )->text()
            )->text() :
            $context->msg( 'mobile-frontend-user-page-desired-action', $pageUsername )->parse();
        $data['createPageLinkLabel'] = $isCurrentUser ?
            $context->msg( 'mobile-frontend-user-page-create-owner-page-link-label' )->text() :
            $context->msg(
                'mobile-frontend-user-page-create-user-page-link-label',
                $pageUser->getUserPage()->getBaseTitle()
            )->parse();
        // Mobile editor has trouble when section is not specified.
        // It doesn't matter here since the page doesn't exist.
        $data['editUrl'] = $title->getLinkURL( [ 'action' => 'edit', 'section' => 0 ] );
        $data['editSection'] = 0;
        $data['createPageLinkAdditionalClasses'] = $isCurrentUser ?
            'cdx-button cdx-button--action-progressive cdx-button--weight-primary' : '';

        $templateParser = new TemplateParser( __DIR__ . '/templates' );
        return $templateParser->processTemplate( 'UserPageCta', $data );
    }

    /**
     * Returns a short description of a page from Wikidata
     *
     * @param string $item Wikibase id of the page
     * @return string|null
     */
    public static function getWikibaseDescription( $item ) {
        if ( !ExtensionRegistry::getInstance()->isLoaded( 'WikibaseClient' ) ) {
            return null;
        }

        $contLang = MediaWikiServices::getInstance()->getContentLanguage();
        $termLookup = WikibaseClient::getTermLookup();
        try {
            $itemId = new ItemId( $item );
        } catch ( InvalidArgumentException $exception ) {
            return null;
        }

        try {
            return $termLookup->getDescription( $itemId, $contLang->getCode() );
        } catch ( TermLookupException $exception ) {
            return null;
        }
    }
}