Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 113 |
|
0.00% |
0 / 6 |
CRAP | |
0.00% |
0 / 1 |
| ExtMobileFrontend | |
0.00% |
0 / 113 |
|
0.00% |
0 / 6 |
1332 | |
0.00% |
0 / 1 |
| blankUserPageHTML | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
42 | |||
| domParseWithContentProvider | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
| domParseMobile | |
0.00% |
0 / 54 |
|
0.00% |
0 / 1 |
210 | |||
| buildPageUserObject | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
30 | |||
| getUserPageContent | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
30 | |||
| getWikibaseDescription | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
20 | |||
| 1 | <?php |
| 2 | |
| 3 | use MediaWiki\Context\IContextSource; |
| 4 | use MediaWiki\Html\TemplateParser; |
| 5 | use MediaWiki\MediaWikiServices; |
| 6 | use MediaWiki\Output\OutputPage; |
| 7 | use MediaWiki\Registration\ExtensionRegistry; |
| 8 | use MediaWiki\Title\Title; |
| 9 | use MediaWiki\User\User; |
| 10 | use MobileFrontend\Api\ApiParseExtender; |
| 11 | use MobileFrontend\ContentProviders\IContentProvider; |
| 12 | use MobileFrontend\Features\FeaturesManager; |
| 13 | use MobileFrontend\Hooks\HookRunner; |
| 14 | use MobileFrontend\Transforms\LazyImageTransform; |
| 15 | use MobileFrontend\Transforms\MakeSectionsTransform; |
| 16 | use MobileFrontend\Transforms\MoveLeadParagraphTransform; |
| 17 | use MobileFrontend\Transforms\NativeLazyImageTransform; |
| 18 | use MobileFrontend\Transforms\RemovableClassesTransform; |
| 19 | use Wikibase\Client\WikibaseClient; |
| 20 | use Wikibase\DataModel\Entity\ItemId; |
| 21 | use Wikibase\DataModel\Services\Lookup\TermLookupException; |
| 22 | use Wikimedia\IPUtils; |
| 23 | |
| 24 | /** |
| 25 | * Implements additional functions to use in MobileFrontend |
| 26 | */ |
| 27 | class 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 | } |