Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
87.14% |
271 / 311 |
|
65.00% |
13 / 20 |
CRAP | |
0.00% |
0 / 1 |
| ZObjectContentHandler | |
87.14% |
271 / 311 |
|
65.00% |
13 / 20 |
58.98 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| canBeUsedOn | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| makeEmptyContent | |
100.00% |
25 / 25 |
|
100.00% |
1 / 1 |
1 | |||
| makeContent | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getContentClass | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| serializeContent | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
| unserializeContent | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
| getExternalRepresentation | |
83.33% |
30 / 36 |
|
0.00% |
0 / 1 |
6.17 | |||
| getSecondaryDataUpdates | |
83.33% |
15 / 18 |
|
0.00% |
0 / 1 |
3.04 | |||
| getDeletionUpdates | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
| supportsDirectEditing | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getActionOverrides | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
| generateHTMLOnEdit | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| fillParserOutput | |
78.35% |
76 / 97 |
|
0.00% |
0 / 1 |
11.01 | |||
| createZObjectViewHeader | |
100.00% |
77 / 77 |
|
100.00% |
1 / 1 |
9 | |||
| createZObjectViewTitle | |
88.89% |
8 / 9 |
|
0.00% |
0 / 1 |
4.02 | |||
| validateSave | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| preSaveTransform | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 | |||
| createDifferenceEngine | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
| getSlotDiffRendererWithOptions | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * WikiLambda content handler for ZObjects |
| 4 | * |
| 5 | * @file |
| 6 | * @ingroup Extensions |
| 7 | * @copyright 2020– Abstract Wikipedia team; see AUTHORS.txt |
| 8 | * @license MIT |
| 9 | */ |
| 10 | |
| 11 | namespace MediaWiki\Extension\WikiLambda; |
| 12 | |
| 13 | use GuzzleHttp\Client; |
| 14 | use InvalidArgumentException; |
| 15 | use MediaWiki\Config\Config; |
| 16 | use MediaWiki\Content\Content; |
| 17 | use MediaWiki\Content\ContentHandler; |
| 18 | use MediaWiki\Content\ContentSerializationException; |
| 19 | use MediaWiki\Content\Renderer\ContentParseParams; |
| 20 | use MediaWiki\Content\Transform\PreSaveTransformParams; |
| 21 | use MediaWiki\Content\ValidationParams; |
| 22 | use MediaWiki\Context\IContextSource; |
| 23 | use MediaWiki\Context\RequestContext; |
| 24 | use MediaWiki\Diff\TextSlotDiffRenderer; |
| 25 | use MediaWiki\Extension\WikiLambda\Cache\MemcachedWrapper; |
| 26 | use MediaWiki\Extension\WikiLambda\Diff\ZObjectContentDifferenceEngine; |
| 27 | use MediaWiki\Extension\WikiLambda\Registry\ZErrorTypeRegistry; |
| 28 | use MediaWiki\Extension\WikiLambda\Registry\ZLangRegistry; |
| 29 | use MediaWiki\Extension\WikiLambda\Registry\ZTypeRegistry; |
| 30 | use MediaWiki\Html\Html; |
| 31 | use MediaWiki\Json\FormatJson; |
| 32 | use MediaWiki\Language\Language; |
| 33 | use MediaWiki\Logger\LoggerFactory; |
| 34 | use MediaWiki\MainConfigNames; |
| 35 | use MediaWiki\MediaWikiServices; |
| 36 | use MediaWiki\Parser\ParserOutput; |
| 37 | use MediaWiki\Revision\SlotRenderingProvider; |
| 38 | use MediaWiki\Title\Title; |
| 39 | use StatusValue; |
| 40 | |
| 41 | class ZObjectContentHandler extends ContentHandler { |
| 42 | use ZObjectEditingPageTrait; |
| 43 | |
| 44 | /** |
| 45 | * @param string $modelId |
| 46 | * @param Config $config |
| 47 | * @param ZObjectStore $zObjectStore |
| 48 | * @param MemcachedWrapper $zObjectCache |
| 49 | */ |
| 50 | public function __construct( |
| 51 | $modelId, |
| 52 | private readonly Config $config, |
| 53 | private readonly ZObjectStore $zObjectStore, |
| 54 | private readonly MemcachedWrapper $zObjectCache |
| 55 | ) { |
| 56 | if ( $modelId !== CONTENT_MODEL_ZOBJECT ) { |
| 57 | throw new InvalidArgumentException( __CLASS__ . " initialised for invalid content model" ); |
| 58 | } |
| 59 | |
| 60 | // Triggers use of message content-model-zobject |
| 61 | parent::__construct( CONTENT_MODEL_ZOBJECT, [ CONTENT_FORMAT_TEXT ] ); |
| 62 | } |
| 63 | |
| 64 | /** |
| 65 | * @param Title $title Page to check |
| 66 | * @return bool |
| 67 | */ |
| 68 | public function canBeUsedOn( Title $title ) { |
| 69 | return $title->inNamespace( NS_MAIN ); |
| 70 | } |
| 71 | |
| 72 | /** |
| 73 | * @return ZObjectContent |
| 74 | */ |
| 75 | public function makeEmptyContent() { |
| 76 | $class = $this->getContentClass(); |
| 77 | return new $class( |
| 78 | '{' . "\n" |
| 79 | . '"' . ZTypeRegistry::Z_OBJECT_TYPE . '": "' . ZTypeRegistry::Z_PERSISTENTOBJECT . '",' . "\n" |
| 80 | . '"' . ZTypeRegistry::Z_PERSISTENTOBJECT_ID . '": {' . "\n" |
| 81 | . '"' . ZTypeRegistry::Z_OBJECT_TYPE . '": "' . ZTypeRegistry::Z_STRING . '",' . "\n" |
| 82 | . '"' . ZTypeRegistry::Z_STRING_VALUE . '": "' . ZTypeRegistry::Z_NULL_REFERENCE . '"},' . "\n" |
| 83 | . '"' . ZTypeRegistry::Z_PERSISTENTOBJECT_VALUE . '": "",' . "\n" |
| 84 | . '"' . ZTypeRegistry::Z_PERSISTENTOBJECT_LABEL . '": {' . "\n" |
| 85 | . '"' . ZTypeRegistry::Z_OBJECT_TYPE . '": "' . ZTypeRegistry::Z_MULTILINGUALSTRING . '",' . "\n" |
| 86 | . '"' . ZTypeRegistry::Z_MULTILINGUALSTRING_VALUE . '":' |
| 87 | . '["' . ZTypeRegistry::Z_MONOLINGUALSTRING . '"]' . "\n" |
| 88 | . '},' . "\n" |
| 89 | . '"' . ZTypeRegistry::Z_PERSISTENTOBJECT_ALIASES . '": {' . "\n" |
| 90 | . '"' . ZTypeRegistry::Z_OBJECT_TYPE . '": "' . ZTypeRegistry::Z_MULTILINGUALSTRINGSET . '",' . "\n" |
| 91 | . '"' . ZTypeRegistry::Z_MULTILINGUALSTRINGSET_VALUE . '":' |
| 92 | . '["' . ZTypeRegistry::Z_MONOLINGUALSTRINGSET . '"]' . "\n" |
| 93 | . '},' . "\n" |
| 94 | . '"' . ZTypeRegistry::Z_PERSISTENTOBJECT_DESCRIPTION . '": {' . "\n" |
| 95 | . '"' . ZTypeRegistry::Z_OBJECT_TYPE . '": "' . ZTypeRegistry::Z_MULTILINGUALSTRING . '",' . "\n" |
| 96 | . '"' . ZTypeRegistry::Z_MULTILINGUALSTRING_VALUE . '":' |
| 97 | . '["' . ZTypeRegistry::Z_MONOLINGUALSTRING . '"]' . "\n" |
| 98 | . '}' . "\n" |
| 99 | . '}' |
| 100 | ); |
| 101 | } |
| 102 | |
| 103 | /** |
| 104 | * @param string $data |
| 105 | * @param Title|null $title |
| 106 | * @param string|null $modelId |
| 107 | * @param string|null $format |
| 108 | * @return ZObjectContent |
| 109 | */ |
| 110 | public static function makeContent( $data, ?Title $title = null, $modelId = null, $format = null ) { |
| 111 | // @phan-suppress-next-line PhanTypeMismatchReturnSuperType |
| 112 | return parent::makeContent( $data, $title, $modelId, $format ); |
| 113 | } |
| 114 | |
| 115 | /** |
| 116 | * @return string |
| 117 | */ |
| 118 | protected function getContentClass() { |
| 119 | return ZObjectContent::class; |
| 120 | } |
| 121 | |
| 122 | /** |
| 123 | * @param Content $content |
| 124 | * @param string|null $format |
| 125 | * @return string |
| 126 | */ |
| 127 | public function serializeContent( Content $content, $format = null ) { |
| 128 | $this->checkFormat( $format ); |
| 129 | |
| 130 | if ( !( $content instanceof ZObjectContent ) ) { |
| 131 | // Throw? |
| 132 | return ''; |
| 133 | } |
| 134 | |
| 135 | return $content->getText(); |
| 136 | } |
| 137 | |
| 138 | /** |
| 139 | * @param string $text |
| 140 | * @param string|null $format |
| 141 | * @return ZObjectContent |
| 142 | * @throws ContentSerializationException if input causes an error |
| 143 | */ |
| 144 | public function unserializeContent( $text, $format = null ) { |
| 145 | $this->checkFormat( $format ); |
| 146 | |
| 147 | $class = $this->getContentClass(); |
| 148 | try { |
| 149 | return new $class( $text ); |
| 150 | } catch ( ZErrorException $zerror ) { |
| 151 | // (T381115) If the passed user input isn't valid, we're expected to throw this particular MW error |
| 152 | throw new ContentSerializationException( $zerror->getZError()->getMessage() ); |
| 153 | } |
| 154 | } |
| 155 | |
| 156 | /** |
| 157 | * @param Title $zObjectTitle The page to fetch. |
| 158 | * @param string|null $languageCode The language in which to return results. If unset, all results are returned. |
| 159 | * @param int|null $revision The revision ID of the page to fetch. If unset, the latest is returned. |
| 160 | * @return string The external JSON form of the given title. |
| 161 | * @throws ZErrorException |
| 162 | */ |
| 163 | public static function getExternalRepresentation( |
| 164 | Title $zObjectTitle, ?string $languageCode = null, ?int $revision = null |
| 165 | ): string { |
| 166 | if ( $zObjectTitle->getNamespace() !== NS_MAIN ) { |
| 167 | throw new ZErrorException( |
| 168 | ZErrorFactory::createZErrorInstance( |
| 169 | ZErrorTypeRegistry::Z_ERROR_WRONG_NAMESPACE, |
| 170 | [ 'title' => (string)$zObjectTitle ] |
| 171 | ) |
| 172 | ); |
| 173 | } |
| 174 | |
| 175 | if ( $zObjectTitle->getContentModel() !== CONTENT_MODEL_ZOBJECT ) { |
| 176 | throw new ZErrorException( |
| 177 | ZErrorFactory::createZErrorInstance( |
| 178 | ZErrorTypeRegistry::Z_ERROR_WRONG_CONTENT_TYPE, |
| 179 | [ 'title' => (string)$zObjectTitle ] |
| 180 | ) |
| 181 | ); |
| 182 | } |
| 183 | |
| 184 | $zObjectStore = WikiLambdaServices::getZObjectStore(); |
| 185 | $zObject = $zObjectStore->fetchZObjectByTitle( $zObjectTitle, $revision ); |
| 186 | |
| 187 | if ( $zObject === false ) { |
| 188 | throw new ZErrorException( |
| 189 | ZErrorFactory::createZErrorInstance( |
| 190 | ZErrorTypeRegistry::Z_ERROR_ZID_NOT_FOUND, |
| 191 | [ 'data' => (string)$zObjectTitle ] |
| 192 | ) |
| 193 | ); |
| 194 | } |
| 195 | |
| 196 | // Cautionary canonicalization: all objects should be fully canonicalized in the DB |
| 197 | $object = ZObjectUtils::canonicalize( $zObject->getObject() ); |
| 198 | |
| 199 | if ( $languageCode ) { |
| 200 | // TODO (T362246): Dependency-inject |
| 201 | $services = MediaWikiServices::getInstance(); |
| 202 | |
| 203 | // If language code is not valid, throws ZErrorException of Z540/Invalid language code |
| 204 | if ( !$services->getLanguageNameUtils()->isValidCode( $languageCode ) ) { |
| 205 | throw new ZErrorException( |
| 206 | ZErrorFactory::createZErrorInstance( |
| 207 | ZErrorTypeRegistry::Z_ERROR_INVALID_LANG_CODE, |
| 208 | [ 'lang' => $languageCode ] |
| 209 | ) |
| 210 | ); |
| 211 | } |
| 212 | |
| 213 | // If language doesn't have a Zid, throws ZErrorException of Z541/Language code not found |
| 214 | $languageZid = ZLangRegistry::singleton()->getLanguageZidFromCode( $languageCode ); |
| 215 | |
| 216 | // Filter all Multilingual Strings and Stringsets if language is present and valid |
| 217 | $object = ZObjectUtils::filterZMultilingualStringsToLanguage( $object, [ $languageZid ] ); |
| 218 | } |
| 219 | |
| 220 | return FormatJson::encode( $object, true, FormatJson::UTF8_OK ); |
| 221 | } |
| 222 | |
| 223 | /** |
| 224 | * @inheritDoc |
| 225 | */ |
| 226 | public function getSecondaryDataUpdates( |
| 227 | Title $title, |
| 228 | Content $content, |
| 229 | $role, |
| 230 | SlotRenderingProvider $slotOutput |
| 231 | ) { |
| 232 | $orchestrator = null; |
| 233 | |
| 234 | if ( $this->config->get( 'WikiLambdaPersistBackendCache' ) ) { |
| 235 | $orchestratorHost = $this->config->get( 'WikiLambdaOrchestratorLocation' ); |
| 236 | $client = new Client( [ "base_uri" => $orchestratorHost ] ); |
| 237 | $orchestrator = new OrchestratorRequest( $client ); |
| 238 | } |
| 239 | |
| 240 | $ourUpdate = []; |
| 241 | if ( ( $content instanceof ZObjectContent ) ) { |
| 242 | $ourUpdate[] = new ZObjectSecondaryDataUpdate( |
| 243 | $title, |
| 244 | $content, |
| 245 | $this->zObjectStore, |
| 246 | $this->zObjectCache, |
| 247 | $orchestrator |
| 248 | ); |
| 249 | } |
| 250 | |
| 251 | return array_merge( |
| 252 | parent::getSecondaryDataUpdates( $title, $content, $role, $slotOutput ), |
| 253 | $ourUpdate |
| 254 | ); |
| 255 | } |
| 256 | |
| 257 | /** |
| 258 | * @inheritDoc |
| 259 | */ |
| 260 | public function getDeletionUpdates( Title $title, $role ) { |
| 261 | return array_merge( |
| 262 | parent::getDeletionUpdates( $title, $role ), |
| 263 | [ new ZObjectSecondaryDataRemoval( |
| 264 | $title, |
| 265 | $this->zObjectStore, |
| 266 | $this->zObjectCache |
| 267 | ) ] |
| 268 | ); |
| 269 | } |
| 270 | |
| 271 | /** |
| 272 | * @inheritDoc |
| 273 | */ |
| 274 | public function supportsDirectEditing() { |
| 275 | return false; |
| 276 | } |
| 277 | |
| 278 | /** |
| 279 | * @inheritDoc |
| 280 | */ |
| 281 | public function getActionOverrides() { |
| 282 | return [ |
| 283 | 'edit' => ZObjectEditAction::class, |
| 284 | 'history' => ZObjectHistoryAction::class |
| 285 | ]; |
| 286 | } |
| 287 | |
| 288 | /** |
| 289 | * Do not render HTML on edit (T285987) |
| 290 | * |
| 291 | * @return bool |
| 292 | */ |
| 293 | public function generateHTMLOnEdit(): bool { |
| 294 | return false; |
| 295 | } |
| 296 | |
| 297 | /** |
| 298 | * Set the HTML and add the appropriate styles. |
| 299 | * |
| 300 | * @inheritDoc |
| 301 | * @param Content $content |
| 302 | * @param ContentParseParams $cpoParams |
| 303 | * @param ParserOutput &$parserOutput The output object to fill (reference). |
| 304 | */ |
| 305 | protected function fillParserOutput( |
| 306 | Content $content, |
| 307 | ContentParseParams $cpoParams, |
| 308 | ParserOutput &$parserOutput |
| 309 | ) { |
| 310 | $userLang = RequestContext::getMain()->getLanguage(); |
| 311 | $logger = LoggerFactory::getInstance( 'WikiLambda' ); |
| 312 | |
| 313 | // Ensure the stored content is a valid ZObject; this also populates $this->getZObject() for us |
| 314 | if ( !( $content instanceof ZObjectContent ) || !$content->isValid() ) { |
| 315 | $parserOutput->setContentHolderText( |
| 316 | Html::element( |
| 317 | 'div', |
| 318 | [ |
| 319 | 'class' => [ 'ext-wikilambda-view-invalidcontent', 'warning' ], |
| 320 | ], |
| 321 | wfMessage( 'wikilambda-invalidzobject' )->inLanguage( $userLang )->text() |
| 322 | ) |
| 323 | ); |
| 324 | // Exit early, as the rest of the code relies on the stored content being well-formed and valid. |
| 325 | return; |
| 326 | } |
| 327 | |
| 328 | // Add the ZObject's labels in each language as a page property, for cheaper reading |
| 329 | $zLangRegistry = ZLangRegistry::singleton(); |
| 330 | $labels = $content->getLabels()->getValueAsList(); |
| 331 | foreach ( $labels as $langZid => $label ) { |
| 332 | if ( !$langZid ) { |
| 333 | // (T402670) Something's wrong with this label entry; skip it |
| 334 | $logger->debug( |
| 335 | 'Skipping setting page property for label in blank language ZID when displaying "{page}"', |
| 336 | [ |
| 337 | 'page' => $content->getZObject()->getZid() |
| 338 | ] |
| 339 | ); |
| 340 | continue; |
| 341 | } |
| 342 | |
| 343 | try { |
| 344 | $lang = $zLangRegistry->getLanguageCodeFromZid( $langZid ); |
| 345 | $parserOutput->setPageProperty( "wikilambda-label-$lang", $label ); |
| 346 | } catch ( ZErrorException ) { |
| 347 | // The language code is somehow not recognised; don't set a property, but log it for review |
| 348 | $logger->warning( |
| 349 | 'Skipping setting page property for label in unknown language "{langZid}" when displaying "{page}"', |
| 350 | [ |
| 351 | 'langZid' => $langZid, |
| 352 | 'page' => $content->getZObject()->getZid() |
| 353 | ] |
| 354 | ); |
| 355 | } |
| 356 | } |
| 357 | |
| 358 | // Add links to other ZObjects |
| 359 | // (T361701) Do this ahead of the early return, as LinkUpdater asks for the non-HTML version |
| 360 | foreach ( $content->getInnerZObject()->getLinkedZObjects() as $link ) { |
| 361 | if ( !$link ) { |
| 362 | // (T402670) Something's wrong with this link entry; skip it |
| 363 | $logger->debug( |
| 364 | 'Skipping setting page property for link to blank page when displaying "{page}"', |
| 365 | [ |
| 366 | 'page' => $content->getZObject()->getZid() |
| 367 | ] |
| 368 | ); |
| 369 | continue; |
| 370 | } |
| 371 | |
| 372 | $title = Title::newFromText( $link, NS_MAIN ); |
| 373 | |
| 374 | // (T400521) Don't try to write a null Title, but log that it happened |
| 375 | if ( !$title ) { |
| 376 | $logger->warning( |
| 377 | 'Skipping setting page property for link to unknown page "{link}" when displaying "{page}"', |
| 378 | [ |
| 379 | 'link' => $link, |
| 380 | 'page' => $content->getZObject()->getZid(), |
| 381 | ] |
| 382 | ); |
| 383 | } else { |
| 384 | $parserOutput->addLink( $title, $title->getArticleID() ); |
| 385 | } |
| 386 | } |
| 387 | |
| 388 | // Don't do further work if the requester doesn't want the HTML version generated. |
| 389 | if ( !$cpoParams->getGenerateHtml() ) { |
| 390 | $parserOutput->setContentHolderText( '' ); |
| 391 | return; |
| 392 | } |
| 393 | |
| 394 | $pageIdentity = $cpoParams->getPage(); |
| 395 | |
| 396 | // TODO (T362245): Re-work our code to use PageReferences rather than Titles |
| 397 | $title = Title::castFromPageReference( $pageIdentity ); |
| 398 | '@phan-var Title $title'; |
| 399 | |
| 400 | $parserOutput->addModuleStyles( [ 'ext.wikilambda.viewpage.styles' ] ); |
| 401 | $parserOutput->addModules( [ 'ext.wikilambda.app' ] ); |
| 402 | |
| 403 | $userLangCode = $userLang->getCode(); |
| 404 | |
| 405 | // If the userLang isn't recognised (e.g. it's qqx, or a language we don't support yet, or it's |
| 406 | // nonsense), then fall back to English. |
| 407 | $userLangZid = $zLangRegistry->getLanguageZidFromCode( $userLangCode, true ); |
| 408 | // Normalise our used language code from what the Language object says |
| 409 | $userLangCode = $zLangRegistry->getLanguageCodeFromZid( $userLangZid ); |
| 410 | |
| 411 | // Add the canonical page link to /view/<lang>/<zid> |
| 412 | $output = RequestContext::getMain()->getOutput(); |
| 413 | |
| 414 | // Set page header |
| 415 | $header = static::createZObjectViewHeader( $content, $title, $userLang ); |
| 416 | $output->setPageTitle( $header ); |
| 417 | |
| 418 | // (T360169) Set page title meta tag |
| 419 | $metaTitle = static::createZObjectViewTitle( $content, $title, $userLang ); |
| 420 | $output->setHTMLTitle( $metaTitle ); |
| 421 | |
| 422 | $output->addLink( [ |
| 423 | 'rel' => 'canonical', |
| 424 | 'hreflang' => $userLangCode, |
| 425 | 'href' => "/view/$userLangCode/" . $title->getDBkey(), |
| 426 | ] ); |
| 427 | |
| 428 | $editingData = [ |
| 429 | // The following paramether may be the same now, |
| 430 | // but will surely change in the future as we remove the Zds from the UI |
| 431 | 'title' => $title->getBaseText(), |
| 432 | 'zId' => $title->getBaseText(), |
| 433 | 'page' => $title->getPrefixedDBkey(), |
| 434 | 'zlang' => $userLangCode, |
| 435 | 'zlangZid' => $userLangZid, |
| 436 | 'createNewPage' => false, |
| 437 | 'viewmode' => true |
| 438 | ]; |
| 439 | |
| 440 | $parserOutput->setJsConfigVar( 'wgWikiLambda', $editingData ); |
| 441 | |
| 442 | $loadingMessage = wfMessage( 'wikilambda-loading' )->inLanguage( $userLang )->text(); |
| 443 | $parserOutput->setContentHolderText( |
| 444 | // Placeholder div for the Vue template with Codex progress indicator. |
| 445 | Html::rawElement( |
| 446 | 'div', |
| 447 | [ 'id' => 'ext-wikilambda-app' ], |
| 448 | UIUtils::createCodexProgressIndicator( $loadingMessage ) |
| 449 | ) |
| 450 | // Fallback message for users without JavaScript. |
| 451 | . Html::rawElement( |
| 452 | 'noscript', |
| 453 | [], |
| 454 | wfMessage( 'wikilambda-nojs' )->inLanguage( $userLang )->parse() |
| 455 | ) |
| 456 | ); |
| 457 | } |
| 458 | |
| 459 | /** |
| 460 | * Generate the special "title" shown on view pages |
| 461 | * |
| 462 | * <span lang="es" class="ext-wikilambda-viewpage-header"> |
| 463 | * <span data-title="English" class="ext-wikilambda-viewpage-header__bcp47-code">en</span> |
| 464 | * <span class="ext-wikilambda-viewpage-header__title ext-wikilambda-viewpage-header__title--function-name"> |
| 465 | * multiply |
| 466 | * </span> |
| 467 | * <span class="ext-wikilambda-viewpage-header__zid">Z12345</span> |
| 468 | * <div class="ext-wikilambda-viewpage-header__type"> |
| 469 | * <span data-title="English" class="ext-wikilambda-viewpage-header__bcp47-code">en</span> |
| 470 | * <span class="ext-wikilambda-viewpage-header__type-label">Function</span> |
| 471 | * </div> |
| 472 | * </span> |
| 473 | * |
| 474 | * @param ZObjectContent $content |
| 475 | * @param Title $title |
| 476 | * @param Language $userLang |
| 477 | * @return string |
| 478 | */ |
| 479 | public static function createZObjectViewHeader( |
| 480 | ZObjectContent $content, Title $title, Language $userLang |
| 481 | ): string { |
| 482 | // TODO (T362246): Dependency-inject |
| 483 | $services = MediaWikiServices::getInstance(); |
| 484 | |
| 485 | $zobject = $content->getZObject(); |
| 486 | |
| 487 | if ( !$zobject || !$zobject->isValid() ) { |
| 488 | // Something's bad, let's give up. |
| 489 | return ''; |
| 490 | } |
| 491 | |
| 492 | // Get best-available label (and its language code) for the target object's type, given the request language. |
| 493 | [ |
| 494 | 'title' => $targetTypeLabel, |
| 495 | 'languageCode' => $targetTypeLabelLanguage |
| 496 | ] = $content->getTypeStringAndLanguage( $userLang ); |
| 497 | |
| 498 | // OBJECT TYPE Language code, which is usually a BCP47 code (e.g. 'en') but sometimes tests inject it as a |
| 499 | // Language object(!) |
| 500 | $targetTypeDisplayCode = gettype( $targetTypeLabelLanguage ) === 'string' |
| 501 | ? $targetTypeLabelLanguage : $targetTypeLabelLanguage->getCode(); |
| 502 | // OBJECT TYPE language label (e.g. 'Function') of the language currently being rendered |
| 503 | $targetTypeDisplayLabelLanguageName = $services->getLanguageNameUtils()->getLanguageName( |
| 504 | $targetTypeDisplayCode |
| 505 | ); |
| 506 | |
| 507 | // Get best-available label (and its language code) for the target object's name, given the request language. |
| 508 | |
| 509 | // OBJECT NAME label (e.g. 'My function' or 'Unknown') and language code (e.g. 'en') |
| 510 | [ |
| 511 | 'title' => $targetLabel, |
| 512 | 'languageCode' => $targetLabelLanguageCode |
| 513 | ] = $zobject->getLabels()->buildStringForLanguage( $userLang ) |
| 514 | ->fallbackWithEnglish() |
| 515 | ->placeholderForTitle() |
| 516 | ->getStringAndLanguageCode(); |
| 517 | |
| 518 | // OBJECT NAME language label (e.g. 'English') of the language currently being rendered |
| 519 | $targetDisplayLabelLanguageName = $services->getLanguageNameUtils()->getLanguageName( |
| 520 | $targetLabelLanguageCode |
| 521 | ); |
| 522 | |
| 523 | $bcp47CodeClassName = 'ext-wikilambda-viewpage-header__bcp47-code'; |
| 524 | |
| 525 | $targetDisplayLabelWidget = ''; |
| 526 | // If the object type label (e.g. 'Function') is not in the user's language, show a BCP47 code widget |
| 527 | // for the language used instead |
| 528 | if ( $targetLabelLanguageCode !== $userLang->getCode() ) { |
| 529 | $targetDisplayLabelWidget = UIUtils::wrapBCP47CodeInFakeCodexChip( |
| 530 | $targetLabelLanguageCode, $targetDisplayLabelLanguageName, $bcp47CodeClassName |
| 531 | ); |
| 532 | } |
| 533 | |
| 534 | $targetDisplayTypeWidget = ''; |
| 535 | // If the object label (e.g. 'Echo') is not in the user's language, show a BCP47 code widget |
| 536 | // for the language used instead |
| 537 | if ( $targetTypeDisplayCode !== $userLang->getCode() ) { |
| 538 | $targetDisplayTypeWidget = UIUtils::wrapBCP47CodeInFakeCodexChip( |
| 539 | $targetTypeDisplayCode, $targetTypeDisplayLabelLanguageName, $bcp47CodeClassName |
| 540 | ); |
| 541 | } |
| 542 | |
| 543 | $untitledStyle = $targetLabel === wfMessage( 'wikilambda-editor-default-name' )->text() ? |
| 544 | 'ext-wikilambda-viewpage-header__title--untitled' : null; |
| 545 | |
| 546 | $labelSpan = Html::element( |
| 547 | 'span', |
| 548 | [ |
| 549 | 'class' => [ |
| 550 | 'ext-wikilambda-viewpage-header__title ext-wikilambda-viewpage-header__title--function-name', |
| 551 | $untitledStyle |
| 552 | ] |
| 553 | ], |
| 554 | $targetLabel |
| 555 | ); |
| 556 | |
| 557 | $zidSpan = Html::element( |
| 558 | 'span', |
| 559 | [ |
| 560 | 'class' => 'ext-wikilambda-viewpage-header__zid', |
| 561 | 'role' => 'button', |
| 562 | 'tabindex' => '0', |
| 563 | 'aria-live' => 'polite' |
| 564 | ], |
| 565 | $title->getText() |
| 566 | ); |
| 567 | |
| 568 | $labelTitle = |
| 569 | // (T356731) When $targetDisplayLabelWidget is an empty string, colon-separator already |
| 570 | // adds/removes the needed/unneeded whitespace for languages. Always adding a |
| 571 | // space would unexpectedly add unneeded extra whitespace for languages including |
| 572 | // zh-hans, zh-hant, etc. |
| 573 | ( $targetDisplayLabelWidget === '' ? '' : $targetDisplayLabelWidget . ' ' ) |
| 574 | . $labelSpan . ' ' . $zidSpan; |
| 575 | |
| 576 | $typeSubtitle = Html::rawElement( |
| 577 | 'div', [ 'class' => 'ext-wikilambda-viewpage-header__type' ], |
| 578 | $targetDisplayTypeWidget . ' ' . Html::element( |
| 579 | 'span', |
| 580 | [ |
| 581 | 'class' => 'ext-wikilambda-viewpage-header__type-label' |
| 582 | ], |
| 583 | $targetTypeLabel |
| 584 | ) |
| 585 | ); |
| 586 | |
| 587 | return Html::rawElement( |
| 588 | 'span', |
| 589 | [ |
| 590 | // Mark the header in the correct language, regardless of the rest of the page |
| 591 | // … but mark it back into their requested language if it's actually untitled |
| 592 | 'lang' => ( $untitledStyle === null ? $userLang->getCode() : $targetTypeDisplayCode ), |
| 593 | 'class' => 'ext-wikilambda-viewpage-header' |
| 594 | ], |
| 595 | $labelTitle . $typeSubtitle |
| 596 | ); |
| 597 | } |
| 598 | |
| 599 | /** |
| 600 | * Generate the HTML "title" tag for the view page |
| 601 | * |
| 602 | * @param ZObjectContent $content |
| 603 | * @param Title $title |
| 604 | * @param Language $userLang |
| 605 | * @return string |
| 606 | */ |
| 607 | public static function createZObjectViewTitle( |
| 608 | ZObjectContent $content, Title $title, Language $userLang |
| 609 | ): string { |
| 610 | $zobject = $content->getZObject(); |
| 611 | if ( !$zobject || !$zobject->isValid() ) { |
| 612 | // Something's bad, let's give up. |
| 613 | return ''; |
| 614 | } |
| 615 | |
| 616 | $sitename = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::Sitename ); |
| 617 | |
| 618 | // Get label, english fallback, or zid if no available option |
| 619 | $label = $zobject->getLabels()->buildStringForLanguage( $userLang ) |
| 620 | ->fallbackWithEnglish() |
| 621 | ->getString(); |
| 622 | |
| 623 | // Return Label/Zid - Sitename |
| 624 | return ( $label ?: $title->getBaseText() ) . ' - ' . |
| 625 | $sitename; |
| 626 | } |
| 627 | |
| 628 | /** |
| 629 | * @param Content $content |
| 630 | * @param ValidationParams $validationParams |
| 631 | * @return StatusValue |
| 632 | */ |
| 633 | public function validateSave( $content, $validationParams ) { |
| 634 | if ( $content->isValid() ) { |
| 635 | return StatusValue::newGood(); |
| 636 | } |
| 637 | return StatusValue::newFatal( "wikilambda-invalidzobject" ); |
| 638 | } |
| 639 | |
| 640 | /** |
| 641 | * @inheritDoc |
| 642 | */ |
| 643 | public function preSaveTransform( Content $content, PreSaveTransformParams $pstParams ): Content { |
| 644 | '@phan-var ZObjectContent $content'; |
| 645 | |
| 646 | if ( !$content->isValid() ) { |
| 647 | return $content; |
| 648 | } |
| 649 | |
| 650 | $json = ZObjectUtils::canonicalize( $content->getObject() ); |
| 651 | $encoded = FormatJson::encode( $json, true, FormatJson::UTF8_OK ); |
| 652 | $encodedCleanedWhitespace = str_replace( [ "\r\n", "\r" ], "\n", rtrim( $encoded ) ); |
| 653 | |
| 654 | if ( $content->getText() !== $encodedCleanedWhitespace ) { |
| 655 | $contentClass = $this->getContentClass(); |
| 656 | return new $contentClass( $encodedCleanedWhitespace ); |
| 657 | } |
| 658 | |
| 659 | return $content; |
| 660 | } |
| 661 | |
| 662 | /** |
| 663 | * @inheritDoc |
| 664 | */ |
| 665 | public function createDifferenceEngine( |
| 666 | IContextSource $context, |
| 667 | $oldContentRevisionId = 0, |
| 668 | $newContentRevisionId = 0, |
| 669 | $recentChangesId = 0, |
| 670 | $refreshCache = false, |
| 671 | $unhide = false |
| 672 | ) { |
| 673 | return new ZObjectContentDifferenceEngine( |
| 674 | $context, $oldContentRevisionId, $newContentRevisionId, $recentChangesId, $refreshCache, $unhide |
| 675 | ); |
| 676 | } |
| 677 | |
| 678 | /** |
| 679 | * @inheritDoc |
| 680 | * |
| 681 | * Access level widened to public for use in ZObjectContentDifferenceEngine |
| 682 | */ |
| 683 | public function getSlotDiffRendererWithOptions( IContextSource $context, $options = [] ) { |
| 684 | // TODO (T362246): Dependency-inject (if we haven't replaced this by then) |
| 685 | $slotDiffRenderer = MediaWikiServices::getInstance() |
| 686 | ->getContentHandlerFactory() |
| 687 | ->getContentHandler( CONTENT_MODEL_TEXT ) |
| 688 | ->getSlotDiffRenderer( $context ); |
| 689 | '@phan-var TextSlotDiffRenderer $slotDiffRenderer'; |
| 690 | return $slotDiffRenderer; |
| 691 | } |
| 692 | |
| 693 | } |