Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 117
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
ExtMobileFrontend
0.00% covered (danger)
0.00%
0 / 117
0.00% covered (danger)
0.00%
0 / 6
1406
0.00% covered (danger)
0.00%
0 / 1
 blankUserPageHTML
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
42
 domParseWithContentProvider
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 domParseMobile
0.00% covered (danger)
0.00%
0 / 58
0.00% covered (danger)
0.00%
0 / 1
240
 buildPageUserObject
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
 getUserPageContent
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
30
 getWikibaseDescription
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2
3use MediaWiki\Context\IContextSource;
4use MediaWiki\Html\TemplateParser;
5use MediaWiki\MediaWikiServices;
6use MediaWiki\Output\OutputPage;
7use MediaWiki\Registration\ExtensionRegistry;
8use MediaWiki\Title\Title;
9use MediaWiki\User\User;
10use MobileFrontend\Api\ApiParseExtender;
11use MobileFrontend\ContentProviders\IContentProvider;
12use MobileFrontend\Features\FeaturesManager;
13use MobileFrontend\Hooks\HookRunner;
14use MobileFrontend\Transforms\LazyImageTransform;
15use MobileFrontend\Transforms\MakeSectionsTransform;
16use MobileFrontend\Transforms\MoveLeadParagraphTransform;
17use MobileFrontend\Transforms\NativeLazyImageTransform;
18use MobileFrontend\Transforms\RemovableClassesTransform;
19use Wikibase\Client\WikibaseClient;
20use Wikibase\DataModel\Entity\ItemId;
21use Wikibase\DataModel\Services\Lookup\TermLookupException;
22use Wikimedia\IPUtils;
23
24/**
25 * Implements additional functions to use in MobileFrontend
26 */
27class ExtMobileFrontend {
28    /**
29     * Provide alternative HTML for a user page which has not been created.
30     * Let the user know about it with pretty graphics and different texts depending
31     * on whether the user is the owner of the page or not.
32     * @internal Only for use inside MobileFrontend.
33     * @param OutputPage $out
34     * @param Title $title
35     * @return string that is empty if the transform does not apply.
36     */
37    public static function blankUserPageHTML( OutputPage $out, Title $title ) {
38        $pageUser = self::buildPageUserObject( $title );
39        $isHidden = $pageUser && $pageUser->isHidden();
40        $canViewHidden = !$isHidden || $out->getAuthority()->isAllowed( 'hideuser' );
41
42        $out->addModuleStyles( [
43            'mobile.userpage.styles', 'mobile.userpage.images'
44        ] );
45
46        if ( $pageUser && !$title->exists() && $canViewHidden ) {
47            return self::getUserPageContent(
48                $out, $pageUser, $title );
49        } else {
50            return '';
51        }
52    }
53
54    /**
55     * Obtains content using the given content provider and routes it to the mobile formatter
56     * if required.
57     *
58     * @param IContentProvider $provider
59     * @param OutputPage $out
60     * @param bool $mobileFormatHtml whether content should be run through the MobileFormatter
61     *
62     * @return string
63     */
64    public static function domParseWithContentProvider(
65        IContentProvider $provider,
66        OutputPage $out,
67        $mobileFormatHtml = true
68    ) {
69        $html = $provider->getHTML();
70
71        // If we're not running the formatter we can exit earlier
72        if ( !$mobileFormatHtml ) {
73            return $html;
74        } else {
75            return self::domParseMobile( $out, $html );
76        }
77    }
78
79    /**
80     * Transforms content to be mobile friendly version.
81     * Filters out various elements and runs the MobileFormatter.
82     *
83     * @param OutputPage $out
84     * @param string $html to render.
85     *
86     * @return string
87     */
88    public static function domParseMobile( OutputPage $out, $html = '' ) {
89        $services = MediaWikiServices::getInstance();
90        /** @var FeaturesManager $featuresManager */
91        $featuresManager = $services->getService( 'MobileFrontend.FeaturesManager' );
92        /** @var MobileContext $context */
93        $context = $services->getService( 'MobileFrontend.Context' );
94        $config = $services->getService( 'MobileFrontend.Config' );
95
96        $title = $out->getTitle();
97        $ns = $title->getNamespace();
98        $action = $context->getRequest()->getText( 'action', 'view' );
99        $isView = $action === 'view' || ApiParseExtender::isParseAction( $action );
100
101        $shouldUseParsoid = false;
102        if ( ExtensionRegistry::getInstance()->isLoaded( 'ParserMigration' ) ) {
103            $oracle = MediaWikiServices::getInstance()->getService( 'ParserMigration.Oracle' );
104            $shouldUseParsoid =
105                $oracle->shouldUseParsoid( $context->getUser(), $context->getRequest(), $title );
106        }
107
108        $enableSections = (
109            // Don't collapse sections e.g. on JS pages
110            $title->canExist()
111            && $title->getContentModel() == CONTENT_MODEL_WIKITEXT
112            // And not in certain namespaces
113            && !in_array( $ns, $config->get( 'MFNamespacesWithoutCollapsibleSections' ) )
114            // And not when what's shown is not actually article text
115            && $isView
116            && !$shouldUseParsoid
117        );
118
119        // https://phabricator.wikimedia.org/T232690
120        if ( !MobileFormatter::canApply( $html, $config->get( 'MFMobileFormatterOptions' ) ) ) {
121            // In future we might want to prepend a message feeding
122            // back to the user that the page is not mobile friendly.
123            return $html;
124        }
125
126        $formatter = new MobileFormatter( $html );
127
128        $hookRunner = new HookRunner( $services->getHookContainer() );
129        $hookRunner->onMobileFrontendBeforeDOM( $context, $formatter );
130
131        $shouldLazyTransformImages = $featuresManager->isFeatureAvailableForCurrentUser( 'MFLazyLoadImages' );
132        $leadParagraphEnabled = in_array( $ns, $config->get( 'MFNamespacesWithLeadParagraphs' ) );
133        $showFirstParagraphBeforeInfobox = $leadParagraphEnabled &&
134            $featuresManager->isFeatureAvailableForCurrentUser( 'MFShowFirstParagraphBeforeInfobox' );
135
136        $transforms = [];
137        // Remove specified content in content namespaces
138        if ( in_array( $title->getNamespace(), $config->get( 'ContentNamespaces' ), true ) ) {
139            $mfRemovableClasses = $config->get( 'MFRemovableClasses' );
140            $removableClasses = $mfRemovableClasses['base'];
141            if ( $context->isBetaGroupMember() ) {
142                $removableClasses = array_unique(
143                    array_merge( $removableClasses, $mfRemovableClasses['beta'] )
144                );
145            }
146
147            $transforms[] = new RemovableClassesTransform( $removableClasses );
148        }
149
150        if ( $enableSections ) {
151            $options = $config->get( 'MFMobileFormatterOptions' );
152            $topHeadingTags = $options['headings'];
153
154            $transforms[] = new MakeSectionsTransform(
155                $topHeadingTags,
156                true
157            );
158        }
159
160        if ( $shouldLazyTransformImages ) {
161            if ( $shouldUseParsoid ) {
162                $transforms[] = new NativeLazyImageTransform();
163            } else {
164                $transforms[] = new LazyImageTransform( $config->get( 'MFLazyLoadSkipSmallImages' ) );
165            }
166        }
167
168        if ( $showFirstParagraphBeforeInfobox ) {
169            $transforms[] = new MoveLeadParagraphTransform( $title, $title->getLatestRevID() );
170        }
171
172        $start = microtime( true );
173        $formatter->applyTransforms( $transforms );
174        $end = microtime( true );
175        $report = sprintf( "MobileFormatter took %.3f seconds", $end - $start );
176
177        return $formatter->getText() . "\n<!-- $report -->";
178    }
179
180    /**
181     * Return new User object based on username or IP address.
182     * @param Title $title
183     * @return User|null
184     */
185    private static function buildPageUserObject( Title $title ) {
186        $titleText = $title->getText();
187
188        $usernameUtils = MediaWikiServices::getInstance()->getUserNameUtils();
189        if ( $usernameUtils->isIP( $titleText ) || IPUtils::isIPv6( $titleText ) ) {
190            return User::newFromAnyId( null, $titleText, null );
191        }
192
193        $user = User::newFromName( $titleText );
194        if ( $user && $user->isRegistered() ) {
195            return $user;
196        }
197
198        return null;
199    }
200
201    /**
202     * Generate user page content for non-existent user pages
203     *
204     * @param IContextSource $output
205     * @param User $pageUser owner of the user page
206     * @param Title $title
207     * @return string
208     */
209    protected static function getUserPageContent( IContextSource $output,
210        User $pageUser, Title $title
211    ) {
212        /** @var MobileContext $context */
213        $context = MediaWikiServices::getInstance()->getService( 'MobileFrontend.Context' );
214        $pageUsername = $pageUser->getName();
215        // Is the current user viewing their own page?
216        $isCurrentUser = $output->getUser()->getName() === $pageUsername;
217
218        $data = [
219            'userImageClass' => 'userpage-image-placeholder',
220        ];
221        $data['ctaHeading'] = $isCurrentUser ?
222            $context->msg( 'mobile-frontend-user-page-no-owner-page-yet' )->text() :
223            $context->msg( 'mobile-frontend-user-page-no-page-yet', $pageUsername )->parse();
224        $data['ctaDescription'] = $isCurrentUser ?
225            $context->msg(
226                'mobile-frontend-user-page-describe-yourself',
227                $context->msg( 'mobile-frontend-user-page-describe-yourself-editors' )->text()
228            )->text() :
229            $context->msg( 'mobile-frontend-user-page-desired-action', $pageUsername )->parse();
230        $data['createPageLinkLabel'] = $isCurrentUser ?
231            $context->msg( 'mobile-frontend-user-page-create-owner-page-link-label' )->text() :
232            $context->msg(
233                'mobile-frontend-user-page-create-user-page-link-label',
234                $pageUser->getUserPage()->getBaseTitle()
235            )->parse();
236        // Mobile editor has trouble when section is not specified.
237        // It doesn't matter here since the page doesn't exist.
238        $data['editUrl'] = $title->getLinkURL( [ 'action' => 'edit', 'section' => 0 ] );
239        $data['editSection'] = 0;
240        $data['createPageLinkAdditionalClasses'] = $isCurrentUser ?
241            'cdx-button cdx-button--action-progressive cdx-button--weight-primary' : '';
242
243        $templateParser = new TemplateParser( __DIR__ . '/templates' );
244        return $templateParser->processTemplate( 'UserPageCta', $data );
245    }
246
247    /**
248     * Returns a short description of a page from Wikidata
249     *
250     * @param string $item Wikibase id of the page
251     * @return string|null
252     */
253    public static function getWikibaseDescription( $item ) {
254        if ( !ExtensionRegistry::getInstance()->isLoaded( 'WikibaseClient' ) ) {
255            return null;
256        }
257
258        $contLang = MediaWikiServices::getInstance()->getContentLanguage();
259        $termLookup = WikibaseClient::getTermLookup();
260        try {
261            $itemId = new ItemId( $item );
262        } catch ( InvalidArgumentException $exception ) {
263            return null;
264        }
265
266        try {
267            return $termLookup->getDescription( $itemId, $contLang->getCode() );
268        } catch ( TermLookupException $exception ) {
269            return null;
270        }
271    }
272}