Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 113
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 / 113
0.00% covered (danger)
0.00%
0 / 6
1332
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 / 54
0.00% covered (danger)
0.00%
0 / 1
210
 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 ): string {
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    ): string {
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 IContextSource $out
84     * @param string $html to render.
85     *
86     * @return string
87     */
88    public static function domParseMobile( IContextSource $out, $html = '' ): string {
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->hasContentModel( 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        $formatter = new MobileFormatter( $html );
120
121        // https://phabricator.wikimedia.org/T232690
122        if ( !$formatter->canApply( $config->get( 'MFMobileFormatterOptions' ) ) ) {
123            // In the future, we might want to prepend a message feeding
124            // back to the user that the page is not mobile friendly.
125            return $html;
126        }
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
142            $transforms[] = new RemovableClassesTransform( $removableClasses );
143        }
144
145        if ( $enableSections ) {
146            $options = $config->get( 'MFMobileFormatterOptions' );
147            $topHeadingTags = $options['headings'];
148
149            $transforms[] = new MakeSectionsTransform(
150                $topHeadingTags,
151                true
152            );
153        }
154
155        if ( $shouldLazyTransformImages ) {
156            if ( $shouldUseParsoid ) {
157                $transforms[] = new NativeLazyImageTransform();
158            } else {
159                $transforms[] = new LazyImageTransform( $config->get( 'MFLazyLoadSkipSmallImages' ) );
160            }
161        }
162
163        if ( $showFirstParagraphBeforeInfobox ) {
164            $transforms[] = new MoveLeadParagraphTransform( $title, $title->getLatestRevID() );
165        }
166
167        $start = microtime( true );
168        $formatter->applyTransforms( $transforms );
169        $end = microtime( true );
170        $report = sprintf( "MobileFormatter took %.3f seconds", $end - $start );
171
172        return $formatter->getHtml() . "\n<!-- $report -->";
173    }
174
175    /**
176     * Return new User object based on username or IP address.
177     * @param Title $title
178     * @return User|null
179     */
180    private static function buildPageUserObject( Title $title ): ?User {
181        $titleText = $title->getText();
182
183        $usernameUtils = MediaWikiServices::getInstance()->getUserNameUtils();
184        if ( $usernameUtils->isIP( $titleText ) || IPUtils::isIPv6( $titleText ) ) {
185            return User::newFromAnyId( null, $titleText, null );
186        }
187
188        $user = User::newFromName( $titleText );
189        if ( $user && $user->isRegistered() ) {
190            return $user;
191        }
192
193        return null;
194    }
195
196    /**
197     * Generate user page content for non-existent user pages
198     *
199     * @param IContextSource $output
200     * @param User $pageUser owner of the user page
201     * @param Title $title
202     * @return string
203     */
204    protected static function getUserPageContent( IContextSource $output,
205        User $pageUser, Title $title
206    ): string {
207        /** @var MobileContext $context */
208        $context = MediaWikiServices::getInstance()->getService( 'MobileFrontend.Context' );
209        $pageUsername = $pageUser->getName();
210        // Is the current user viewing their own page?
211        $isCurrentUser = $output->getUser()->getName() === $pageUsername;
212
213        $data = [
214            'userImageClass' => 'userpage-image-placeholder',
215        ];
216        $data['ctaHeading'] = $isCurrentUser ?
217            $context->msg( 'mobile-frontend-user-page-no-owner-page-yet' )->text() :
218            $context->msg( 'mobile-frontend-user-page-no-page-yet', $pageUsername )->text();
219        $data['ctaDescription'] = $isCurrentUser ?
220            $context->msg(
221                'mobile-frontend-user-page-describe-yourself',
222                $context->msg( 'mobile-frontend-user-page-describe-yourself-editors' )->text()
223            )->text() :
224            $context->msg( 'mobile-frontend-user-page-desired-action', $pageUsername )->text();
225        $data['createPageLinkLabel'] = $isCurrentUser ?
226            $context->msg( 'mobile-frontend-user-page-create-owner-page-link-label' )->text() :
227            $context->msg(
228                'mobile-frontend-user-page-create-user-page-link-label',
229                $pageUser->getUserPage()->getBaseTitle()
230            )->text();
231        // Mobile editor has trouble when section is not specified.
232        // It doesn't matter here since the page doesn't exist.
233        $data['editUrl'] = $title->getLinkURL( [ 'action' => 'edit', 'section' => 0 ] );
234        $data['editSection'] = 0;
235        $data['createPageLinkAdditionalClasses'] = $isCurrentUser ?
236            'cdx-button cdx-button--action-progressive cdx-button--weight-primary' : '';
237
238        $templateParser = new TemplateParser( __DIR__ . '/templates' );
239        return $templateParser->processTemplate( 'UserPageCta', $data );
240    }
241
242    /**
243     * Returns a short description of a page from Wikidata
244     *
245     * @param string $item Wikibase id of the page
246     * @return string|null
247     */
248    public static function getWikibaseDescription( $item ): ?string {
249        if ( !ExtensionRegistry::getInstance()->isLoaded( 'WikibaseClient' ) ) {
250            return null;
251        }
252
253        $contLang = MediaWikiServices::getInstance()->getContentLanguage();
254        $termLookup = WikibaseClient::getTermLookup();
255        try {
256            $itemId = new ItemId( $item );
257        } catch ( InvalidArgumentException ) {
258            return null;
259        }
260
261        try {
262            return $termLookup->getDescription( $itemId, $contLang->getCode() );
263        } catch ( TermLookupException ) {
264            return null;
265        }
266    }
267}